From e07c15bd3b82dd0073c266e6ce19e631dbd458be Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 2 Apr 2025 00:44:14 +0900 Subject: [PATCH 001/357] =?UTF-8?q?issue=20template=20,=20pr=20template=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 --- .github/ISSUE_TEMPLATE/issue_template.md | 20 ++++++++++++++ .github/pull_request_template.md | 35 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/issue_template.md create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md new file mode 100644 index 00000000..e1972966 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -0,0 +1,20 @@ +--- +name: "\bIssue 생성 템플릿" +about: 해당 Issue 생성 템플릿을 통하여 Issue를 생성해주세요. +title: 'Feat: Issue 제목' +labels: '' +assignees: '' + +--- + +### 📝 Description + +- 구현할 내용 1 +- 구현할 내용 2 + +--- + +### 📝 Todo + +- [ ] 구현할 내용 1 +- [ ] 구현할 내용 2 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..893f7db7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ +## ✅ PR 유형 +어떤 변경 사항이 있었나요? + +- [ ] 새로운 기능 추가 +- [ ] 버그 수정 +- [ ] 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경) +- [ ] 코드 리팩토링 +- [ ] 주석 추가 및 수정 +- [ ] 문서 수정 +- [ ] 빌드 부분 혹은 패키지 매니저 수정 +- [ ] 파일 혹은 폴더명 수정 +- [ ] 파일 혹은 폴더 삭제 + +--- + +## 📝 작업 내용 +이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) + +- 작업한 내용 1 +- 작업한 내용 2 + +--- + +## ✏️ 관련 이슈 +본인이 작업한 내용이 어떤 Issue Number와 관련이 있는지만 작성해주세요 + +ex) +- Fixes : #00 (수정중인 이슈) +- Resolves : #100 (무슨 이슈를 해결했는지) +- Ref : #00 #01 (참고할 이슈) +- Related to : #00 #01 (해당 커밋과 관련) + +--- + +## 🎸 기타 사항 or 추가 코멘트 From 73e8085fc02480fc688fc4c279dd63815b24c002 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 2 Apr 2025 00:55:50 +0900 Subject: [PATCH 002/357] =?UTF-8?q?chore:=20=ED=8F=B4=EB=8D=94=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=98=88=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/gomushin/backend/auth/application/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/domain/entity/.gitkeep | 0 .../kotlin/gomushin/backend/auth/domain/repository/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/domain/service/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/domain/value/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/infrastructure/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/presentation/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt | 4 ++++ .../gomushin/backend/auth/presentation/request/.gitkeep | 0 .../gomushin/backend/auth/presentation/response/.gitkeep | 0 10 files changed, 4 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/auth/application/.gitkeep create mode 100644 src/main/kotlin/gomushin/backend/auth/domain/entity/.gitkeep create mode 100644 src/main/kotlin/gomushin/backend/auth/domain/repository/.gitkeep create mode 100644 src/main/kotlin/gomushin/backend/auth/domain/service/.gitkeep create mode 100644 src/main/kotlin/gomushin/backend/auth/domain/value/.gitkeep create mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/.gitkeep create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/.gitkeep create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/request/.gitkeep create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/response/.gitkeep diff --git a/src/main/kotlin/gomushin/backend/auth/application/.gitkeep b/src/main/kotlin/gomushin/backend/auth/application/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/domain/entity/.gitkeep b/src/main/kotlin/gomushin/backend/auth/domain/entity/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/domain/repository/.gitkeep b/src/main/kotlin/gomushin/backend/auth/domain/repository/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/domain/service/.gitkeep b/src/main/kotlin/gomushin/backend/auth/domain/service/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/domain/value/.gitkeep b/src/main/kotlin/gomushin/backend/auth/domain/value/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/.gitkeep b/src/main/kotlin/gomushin/backend/auth/infrastructure/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/.gitkeep b/src/main/kotlin/gomushin/backend/auth/presentation/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt new file mode 100644 index 00000000..dcc9b8fb --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt @@ -0,0 +1,4 @@ +package gomushin.backend.auth.presentation + +object ApiPath { +} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/request/.gitkeep b/src/main/kotlin/gomushin/backend/auth/presentation/request/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/response/.gitkeep b/src/main/kotlin/gomushin/backend/auth/presentation/response/.gitkeep new file mode 100644 index 00000000..e69de29b From 774f5ff4239169b6614a7cddfbdf575fdfba1d13 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:07:01 +0900 Subject: [PATCH 003/357] =?UTF-8?q?feat:=20gitkeep=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/gomushin/backend/auth/application/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/infrastructure/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/presentation/.gitkeep | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/auth/application/.gitkeep delete mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/.gitkeep delete mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/.gitkeep diff --git a/src/main/kotlin/gomushin/backend/auth/application/.gitkeep b/src/main/kotlin/gomushin/backend/auth/application/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/.gitkeep b/src/main/kotlin/gomushin/backend/auth/infrastructure/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/.gitkeep b/src/main/kotlin/gomushin/backend/auth/presentation/.gitkeep deleted file mode 100644 index e69de29b..00000000 From fe8aa1c717124f7f3eb5d90d9c4632ab1e1163e2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:08:18 +0900 Subject: [PATCH 004/357] =?UTF-8?q?chore:=20configuration=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 --- .../restclient/RestClientConfiguration.kt | 22 +++++++ .../security/CustomCorsConfiguration.kt | 24 +++++++ .../security/SecurityConfiguration.kt | 64 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/restclient/RestClientConfiguration.kt create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt diff --git a/src/main/kotlin/gomushin/backend/core/configuration/restclient/RestClientConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/restclient/RestClientConfiguration.kt new file mode 100644 index 00000000..f5a35024 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/restclient/RestClientConfiguration.kt @@ -0,0 +1,22 @@ +package gomushin.backend.core.configuration.restclient + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpStatusCode +import org.springframework.web.client.RestClient + +@Configuration +class RestClientConfiguration { + + @Bean + fun restClient(): RestClient { + return RestClient.builder() + .defaultStatusHandler(HttpStatusCode::is4xxClientError) { request, response -> + throw IllegalStateException("4xx 에러 발생 ${response.statusCode}") + } + .defaultStatusHandler(HttpStatusCode::is5xxServerError) { request, response -> + throw IllegalStateException("5xx 에러 발생 ${response.statusCode}") + } + .build(); + } +} diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt new file mode 100644 index 00000000..14dc9e6c --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -0,0 +1,24 @@ +package gomushin.backend.core.configuration.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.UrlBasedCorsConfigurationSource + +@Configuration +class CustomCorsConfiguration { + + @Bean + fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = listOf("http://localhost:5173", "http://localhost:8080") + configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") + configuration.allowedHeaders = listOf("*") + configuration.allowCredentials = true + configuration.maxAge = 3600 + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source + } +} diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt new file mode 100644 index 00000000..b8ac1af6 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -0,0 +1,64 @@ +package gomushin.backend.core.configuration.security + +import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.CustomUserDetailsService +import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter +import gomushin.backend.member.domain.repository.MemberRepository +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +@EnableWebSecurity +class SecurityConfiguration( + private val jwtTokenProvider: JwtTokenProvider, + private val memberRepository: MemberRepository +) { + + @Bean + fun filterChain(http: HttpSecurity, corsConfiguration: CustomCorsConfiguration): SecurityFilterChain { + http + .csrf { + it.disable() + } + .cors { + it.configurationSource( + corsConfiguration.corsConfigurationSource() + ) + } + .authorizeHttpRequests { + it.requestMatchers( + "/v1/auth/**", + "/v1/oauth/**", + "/swagger-ui.html", + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-resources/**", + "/webjars/**", + "/health", + "/swagger-ui/index.html", + "/favicon.ico", + "/error" + ).permitAll() + } + .addFilterBefore( + JwtAuthenticationFilter( + jwtTokenProvider, + CustomUserDetailsService( + memberRepository + ) + ), + UsernamePasswordAuthenticationFilter::class.java + ) + .formLogin { + it.disable() + } + .httpBasic { + it.disable() + } + return http.build() + } +} From 596d9794fe3c542dee4b2f828283f3a9d75f22e1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:11:19 +0900 Subject: [PATCH 005/357] =?UTF-8?q?chore:=20security=20,=20jwt,=20oauth2,?= =?UTF-8?q?=20configuration=20=EC=9D=98=EC=A1=B4=EC=84=B1=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 --- build.gradle.kts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index bbc871c8..7fd67fa9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,20 @@ dependencies { // logging implementation("io.github.microutils:kotlin-logging:2.0.11") + //security + implementation("org.springframework.boot:spring-boot-starter-security") + + //jwt + implementation("io.jsonwebtoken:jjwt-api:0.12.6") + implementation("io.jsonwebtoken:jjwt-impl:0.12.6") + implementation("io.jsonwebtoken:jjwt-jackson:0.12.6") + + // oauth2 + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + + // configuration processor + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + implementation("org.springframework.boot:spring-boot-starter-validation") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") From f7d78d70469daab36b9f6a50dbdda27998cf6c40 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:11:44 +0900 Subject: [PATCH 006/357] =?UTF-8?q?feat:=20Security=20CustomUserDetails=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 --- .../backend/core/CustomUserDetails.kt | 41 +++++++++++++++++++ .../backend/core/CustomUserDetailsService.kt | 28 +++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt create mode 100644 src/main/kotlin/gomushin/backend/core/CustomUserDetailsService.kt diff --git a/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt b/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt new file mode 100644 index 00000000..34958acb --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt @@ -0,0 +1,41 @@ +package gomushin.backend.core + +import gomushin.backend.member.domain.entity.Member +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails + +class CustomUserDetails( + private val member: Member, +) : UserDetails { + override fun getAuthorities(): MutableCollection { + return mutableListOf() + } + + override fun getPassword(): String { + return "" + } + + override fun getUsername(): String { + return member.name + } + + override fun isAccountNonExpired(): Boolean { + return true + } + + override fun isAccountNonLocked(): Boolean { + return true + } + + override fun isCredentialsNonExpired(): Boolean { + return true + } + + override fun isEnabled(): Boolean { + return true + } + + fun getId(): Long { + return member.id ?: throw IllegalStateException("존재하지 않는 멤버입니다.") + } +} diff --git a/src/main/kotlin/gomushin/backend/core/CustomUserDetailsService.kt b/src/main/kotlin/gomushin/backend/core/CustomUserDetailsService.kt new file mode 100644 index 00000000..786cfc9c --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/CustomUserDetailsService.kt @@ -0,0 +1,28 @@ +package gomushin.backend.core + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.repository.MemberRepository +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.stereotype.Service + +@Service +class CustomUserDetailsService(private val memberRepository: MemberRepository) : UserDetailsService { + override fun loadUserByUsername(username: String?): UserDetails { + if (username == null) { + throw BadRequestException("saranggun.member.not-provided-name") + } + + return memberRepository.findByName(username!!)?.let { createUserDetail(it) } + ?: throw BadRequestException("sarangggun.member.not-exist-member") + } + + fun loadUserById(id: Long): UserDetails = + memberRepository.findById(id).map { createUserDetail(it) } + .orElseThrow { BadRequestException("sarangggun.member.not-exist-member") } + + private fun createUserDetail(member: Member): UserDetails { + return CustomUserDetails(member) + } +} From 8fcfae1a5f54913f018e1ea4e693d947a7c127c4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:13:08 +0900 Subject: [PATCH 007/357] =?UTF-8?q?feat:=20Member=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/entity/Member.kt | 39 +++++++++++++++++++ .../domain/repository/MemberRepository.kt | 9 +++++ .../backend/member/domain/value/Gender.kt | 6 +++ .../backend/member/domain/value/Provider.kt | 7 ++++ 4 files changed, 61 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt create mode 100644 src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt create mode 100644 src/main/kotlin/gomushin/backend/member/domain/value/Gender.kt create mode 100644 src/main/kotlin/gomushin/backend/member/domain/value/Provider.kt diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt new file mode 100644 index 00000000..19b6191d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -0,0 +1,39 @@ +package gomushin.backend.member.domain.entity + +import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import gomushin.backend.member.domain.value.Provider +import jakarta.persistence.* + +@Entity +@Table(name = "member") +class Member( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + val name: String, + + @Column(unique = true) + val email: String, + + val profileImageUrl: String, + + @Enumerated(EnumType.STRING) + val provider: Provider, +): BaseEntity() { + companion object { + fun create( + name: String, + email: String, + profileImageUrl: String?, + provider: Provider + ): Member { + return Member( + name = name, + email = email, + profileImageUrl = profileImageUrl ?: "", + provider = provider, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt b/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt new file mode 100644 index 00000000..7f11dcee --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt @@ -0,0 +1,9 @@ +package gomushin.backend.member.domain.repository + +import gomushin.backend.member.domain.entity.Member +import org.springframework.data.jpa.repository.JpaRepository + +interface MemberRepository : JpaRepository { + fun findByName(name: String): Member? + fun findByEmail(email: String): Member? +} diff --git a/src/main/kotlin/gomushin/backend/member/domain/value/Gender.kt b/src/main/kotlin/gomushin/backend/member/domain/value/Gender.kt new file mode 100644 index 00000000..ccd41f2c --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/value/Gender.kt @@ -0,0 +1,6 @@ +package gomushin.backend.member.domain.value + +enum class Gender { + MALE, + FEMALE +} diff --git a/src/main/kotlin/gomushin/backend/member/domain/value/Provider.kt b/src/main/kotlin/gomushin/backend/member/domain/value/Provider.kt new file mode 100644 index 00000000..7be07207 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/value/Provider.kt @@ -0,0 +1,7 @@ +package gomushin.backend.member.domain.value + +enum class Provider { + GOOGLE, + KAKAO, + NAVER +} From 5b680b64b8b89734aa954290027c7115c1f19ac0 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:13:41 +0900 Subject: [PATCH 008/357] =?UTF-8?q?feat:=20Jwt=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/jwt/JwtTokenProvider.kt | 7 ++ .../core/jwt/infrastructrue/JwtProperties.kt | 13 +++ .../infrastructrue/JwtTokenProviderImpl.kt | 93 +++++++++++++++++++ .../backend/core/jwt/infrastructrue/Type.kt | 6 ++ 4 files changed, 119 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt create mode 100644 src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtProperties.kt create mode 100644 src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtTokenProviderImpl.kt create mode 100644 src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt diff --git a/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt b/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt new file mode 100644 index 00000000..5c4dbe13 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt @@ -0,0 +1,7 @@ +package gomushin.backend.core.jwt + +interface JwtTokenProvider { + fun provideAccessToken(userId: Long): String + fun getMemberIdFromToken(token: String): Long + fun validateToken(token: String): Boolean +} diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtProperties.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtProperties.kt new file mode 100644 index 00000000..61db4cba --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtProperties.kt @@ -0,0 +1,13 @@ +package gomushin.backend.core.jwt.infrastructrue + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("jwt") +data class JwtProperties( + val issuer: String, + val audience: String, + val secretKey: String, + val accessTokenExpiration: Long, + val refreshTokenExpiration: Long +) + diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtTokenProviderImpl.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtTokenProviderImpl.kt new file mode 100644 index 00000000..0b337101 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtTokenProviderImpl.kt @@ -0,0 +1,93 @@ +package gomushin.backend.core.jwt.infrastructrue + +import gomushin.backend.core.jwt.JwtTokenProvider +import io.jsonwebtoken.* +import io.jsonwebtoken.security.Keys +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import org.springframework.stereotype.Component +import java.util.* + +@Component +class JwtTokenProviderImpl( + jwtProperties: JwtProperties +) : JwtTokenProvider { + companion object { + private val logger: Logger = LogManager.getLogger(JwtTokenProviderImpl::class.java) + } + + val ISSUER = jwtProperties.issuer + val AUDIENCE = jwtProperties.audience + val SECRET_KEY = Keys.hmacShaKeyFor(jwtProperties.secretKey.toByteArray(Charsets.UTF_8)) + val ACCESS_TOKEN_EXPIRATION = jwtProperties.accessTokenExpiration + val REFRESH_TOKEN_EXPIRATION = jwtProperties.refreshTokenExpiration + + override fun provideAccessToken(userId: Long): String { + return createToken(userId, ACCESS_TOKEN_EXPIRATION, Type.ACCESS) + } + + override fun getMemberIdFromToken(token: String): Long { + val subject = getSubject(token) + return subject.toLong() + } + + override fun validateToken(token: String): Boolean { + try { + Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token) + return true + } catch (e: Exception) { + when (e) { + is ExpiredJwtException, + is UnsupportedJwtException, + is MalformedJwtException, + is IllegalArgumentException -> return false + + else -> throw e + } + } + } + + private fun createToken(userId: Long, expiration: Long, type: Type): String { + val expirationMs = expiration * 60 * 1000 + val expiryDate = Date(System.currentTimeMillis() + expirationMs) + + return Jwts.builder() + .issuer(ISSUER) + .audience().add(AUDIENCE).and() + .subject(userId.toString()) + .claim("type", type.name) + .issuedAt(Date()) + .expiration(expiryDate) + .signWith(SECRET_KEY) + .compact() + } + + fun getSubject(token: String): String { + return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).payload.subject + } + +// private fun getClaimsFromToken(token: String): Claims { +// try { +// return Jwts.parserBuilder() +// .setSigningKey(SECRET_KEY) +// .build() +// .parseClaimsJws(token) +// .body +// } catch (e: ExpiredJwtException) { +// logger.warn("만료된 JWT 토큰입니다: {}", e.message) +// throw e +// } catch (e: UnsupportedJwtException) { +// logger.warn("지원되지 않는 JWT 토큰입니다: {}", e.message) +// throw e +// } catch (e: MalformedJwtException) { +// logger.warn("잘못된 형식의 JWT 토큰입니다: {}", e.message) +// throw e +// } catch (e: io.jsonwebtoken.security.SignatureException) { +// logger.warn("유효하지 않은 JWT 서명입니다: {}", e.message) +// throw e +// } catch (e: Exception) { +// logger.error("JWT 토큰 파싱 중 오류 발생: {}", e.message, e) +// throw e +// } +// } +} diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt new file mode 100644 index 00000000..3f7bec9f --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt @@ -0,0 +1,6 @@ +package gomushin.backend.core.jwt.infrastructrue + +enum class Type { + ACCESS, REFRESH +} + From 15e449c775ac4e5ddd003a8a0ed3a8d57f3bcf73 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:13:56 +0900 Subject: [PATCH 009/357] =?UTF-8?q?chore:=20=EC=9D=B8=EC=A6=9D=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=97=90=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/infrastructure/exception/UnauthorizedException.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt new file mode 100644 index 00000000..057feac7 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt @@ -0,0 +1,7 @@ +package gomushin.backend.core.infrastructure.exception + +import gomushin.backend.core.common.web.response.ExtendedHttpStatus +import gomushin.backend.core.common.web.response.exception.ErrorCodeResolvingApiErrorException + +class UnauthorizedException(code: String = "unauthorized", cause: Throwable?) : + ErrorCodeResolvingApiErrorException(ExtendedHttpStatus.UNAUTHORIZED, code, cause) From 8dc5bee6b45a57a58ffbf732e32f821bbaaa7c4e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:14:22 +0900 Subject: [PATCH 010/357] =?UTF-8?q?feat:=20OAuth=20=EC=B6=94=EC=83=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/domain/oauth/OAuthProvider.kt | 7 +++++++ .../backend/auth/domain/oauth/OAuthToken.kt | 20 +++++++++++++++++++ .../auth/domain/oauth/OAuthUserInfo.kt | 8 ++++++++ .../auth/infrastructure/OAuthProperties.kt | 18 +++++++++++++++++ .../infrastructure/OAuthProviderFactory.kt | 18 +++++++++++++++++ 5 files changed, 71 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthProvider.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthToken.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthUserInfo.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProperties.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProviderFactory.kt diff --git a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthProvider.kt b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthProvider.kt new file mode 100644 index 00000000..5b810109 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthProvider.kt @@ -0,0 +1,7 @@ +package gomushin.backend.auth.domain.oauth + +interface OAuthProvider { + fun getAuthorizationUrl(): String + fun getToken(code: String): OAuthToken + fun getUserInfo(accessToken: String): OAuthUserInfo +} diff --git a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthToken.kt b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthToken.kt new file mode 100644 index 00000000..a0ea73a7 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthToken.kt @@ -0,0 +1,20 @@ +package gomushin.backend.auth.domain.oauth + +import com.fasterxml.jackson.annotation.JsonProperty + +data class OAuthToken( + @JsonProperty("access_token") + val accessToken: String, + + @JsonProperty("token_type") + val tokenType: String, + + @JsonProperty("refresh_token") + val refreshToken: String, + + @JsonProperty("expires_in") + val expiresIn: Int, + + @JsonProperty("refresh_token_expires_in") + val refreshTokenExpiresIn: Int, +) diff --git a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthUserInfo.kt b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthUserInfo.kt new file mode 100644 index 00000000..ed67c7e0 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthUserInfo.kt @@ -0,0 +1,8 @@ +package gomushin.backend.auth.domain.oauth + +data class OAuthUserInfo( + val id: String, + val email: String?, + val name: String?, + val profileImageUrl: String? +) diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProperties.kt b/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProperties.kt new file mode 100644 index 00000000..ee8db0bc --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProperties.kt @@ -0,0 +1,18 @@ +package gomushin.backend.auth.infrastructure + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("oauth2") +data class OAuthProperties( + val kakao: OAuthProvider +) { + data class OAuthProvider( + val authUri: String, + val clientId: String, + val clientSecret: String, + val redirectUri: String, + val tokenUri: String, + val resourceUri: String, + val scope: List + ) +} diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProviderFactory.kt b/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProviderFactory.kt new file mode 100644 index 00000000..2080af61 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProviderFactory.kt @@ -0,0 +1,18 @@ +package gomushin.backend.auth.infrastructure + +import gomushin.backend.auth.domain.oauth.OAuthProvider +import gomushin.backend.auth.infrastructure.kakao.KakaoOAuthProvider +import gomushin.backend.core.infrastructure.exception.BadRequestException +import org.springframework.stereotype.Component + +@Component +class OAuthProviderFactory( + private val kakaoOAuthProvider: KakaoOAuthProvider, +) { + fun getOAuthProvider(provider: String): OAuthProvider { + return when (provider) { + "kakao" -> kakaoOAuthProvider + else -> throw BadRequestException("sarangggun.oauth.invalid-provider") + } + } +} From 3ebd815960a8fc5e253c38ac6d1b52d70556bc5a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:14:40 +0900 Subject: [PATCH 011/357] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20O?= =?UTF-8?q?Auth=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kakao/KakaoOAuthProvider.kt | 86 +++++++++++++++++++ .../kakao/KakaoOAuthUserInfo.kt | 72 ++++++++++++++++ .../response/KakaoOAuthResponse.kt | 26 ++++++ 3 files changed, 184 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthProvider.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthUserInfo.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/response/KakaoOAuthResponse.kt diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthProvider.kt b/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthProvider.kt new file mode 100644 index 00000000..571a3f24 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthProvider.kt @@ -0,0 +1,86 @@ +package gomushin.backend.auth.infrastructure.kakao + +import gomushin.backend.auth.domain.oauth.OAuthProvider +import gomushin.backend.auth.domain.oauth.OAuthToken +import gomushin.backend.auth.domain.oauth.OAuthUserInfo +import gomushin.backend.auth.infrastructure.OAuthProperties +import gomushin.backend.core.infrastructure.exception.BadRequestException +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.client.RestClient +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@Component +class KakaoOAuthProvider(private val oAuthProperties: OAuthProperties) : OAuthProvider { + override fun getAuthorizationUrl(): String { + val kakaoProperties = oAuthProperties.kakao + return "https://kauth.kakao.com/oauth/authorize?" + + "client_id=${kakaoProperties.clientId}" + + "&redirect_uri=${kakaoProperties.redirectUri}" + + "&response_type=code" + } + + + override fun getUserInfo(accessToken: String): OAuthUserInfo { + val resourceUri = oAuthProperties.kakao.resourceUri + val restClient = RestClient.builder() + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer $accessToken") + .build() + + val responseString = restClient.get() + .uri(resourceUri) + .retrieve() + .toEntity(String::class.java) + + println("responseString = $responseString") + + val response = restClient.get() + .uri(resourceUri) + .retrieve() + .toEntity(KakaoOAuthUserInfo::class.java) + + val kakaoUserInfo = response.body ?: throw BadRequestException("saranggun.oauth.failed-to-fetch-user-info") + + return kakaoUserInfo.toOAuthUserInfo() + } + + override fun getToken(code: String): OAuthToken { + val kakaoProperties = oAuthProperties.kakao + + val params = mapOf( + "grant_type" to "authorization_code", + "client_id" to kakaoProperties.clientId, + "redirect_uri" to kakaoProperties.redirectUri, + "code" to code, + "client_secret" to kakaoProperties.clientSecret + ) + + val restClient = RestClient.builder() + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=utf-8") + .build() + + val tokenUrl = kakaoProperties.tokenUri + + val response = restClient.post() + .uri(tokenUrl) + .body(params.toFormData()) + .retrieve() + .toEntity(OAuthToken::class.java) + + return response.body ?: throw IllegalStateException("카카오 토큰 페칭 실패") + } + + private fun Map.toFormData(): String { + return this.entries.joinToString("&") { + "${ + URLEncoder.encode( + it.key, + StandardCharsets.UTF_8 + ) + }=${URLEncoder.encode(it.value, StandardCharsets.UTF_8)}" + } + } +} diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthUserInfo.kt b/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthUserInfo.kt new file mode 100644 index 00000000..890683bd --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthUserInfo.kt @@ -0,0 +1,72 @@ +package gomushin.backend.auth.infrastructure.kakao + +import com.fasterxml.jackson.annotation.JsonProperty +import gomushin.backend.auth.domain.oauth.OAuthUserInfo + +data class KakaoOAuthUserInfo( + val id: Long, + @JsonProperty("connected_at") + val connectedAt: String, + val properties: KakaoProperties, + @JsonProperty("kakao_account") + val kakaoAccount: KakaoAccount +) { + fun toOAuthUserInfo(): OAuthUserInfo { + return OAuthUserInfo( + id = this.id.toString(), + email = this.kakaoAccount.email, + name = this.properties.nickname, + profileImageUrl = this.properties.profileImage + ) + } +} + +data class KakaoProperties( + val nickname: String, + + @JsonProperty("profile_image") + val profileImage: String, + + @JsonProperty("thumbnail_image") + val thumbnailImage: String +) + +data class KakaoAccount( + @JsonProperty("profile_nickname_needs_agreement") + val profileNicknameNeedsAgreement: Boolean, + + @JsonProperty("profile_image_needs_agreement") + val profileImageNeedsAgreement: Boolean, + + @JsonProperty("has_email") + val hasEmail: Boolean, + + @JsonProperty("email_needs_agreement") + val emailNeedsAgreement: Boolean, + + @JsonProperty("is_email_valid") + val isEmailValid: Boolean, + + @JsonProperty("is_email_verified") + val isEmailVerified: Boolean, + + val email: String, + + val profile: KakaoProfile +) + +data class KakaoProfile( + val nickname: String, + + @JsonProperty("thumbnail_image_url") + val thumbnailImageUrl: String, + + @JsonProperty("profile_image_url") + val profileImageUrl: String, + + @JsonProperty("is_default_image") + val isDefaultImage: Boolean, + + @JsonProperty("is_default_nickname") + val isDefaultNickname: Boolean +) diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/response/KakaoOAuthResponse.kt b/src/main/kotlin/gomushin/backend/auth/presentation/response/KakaoOAuthResponse.kt new file mode 100644 index 00000000..e5f12e18 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/presentation/response/KakaoOAuthResponse.kt @@ -0,0 +1,26 @@ +package gomushin.backend.auth.presentation.response + +import com.fasterxml.jackson.annotation.JsonProperty + +data class KakaoOAuthResponse( + @JsonProperty("access_token") + val accessToken: String, + + @JsonProperty("token_type") + val tokenType: String, + + @JsonProperty("id_token") + val idToken: String, + + @JsonProperty("refresh_token") + val refreshToken: String, + + @JsonProperty("expires_in") + val expiresIn: Int, + + @JsonProperty("refresh_token_expires_in") + val refreshTokenExpiresIn: Int, + + @JsonProperty("scope") + val scope: String +) From a289c09044139a6c142bd2379e6c3e02cfcbc8ff Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:15:21 +0900 Subject: [PATCH 012/357] =?UTF-8?q?chore:=20OAuth=20URI=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/gomushin/backend/auth/presentation/ApiPath.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt index dcc9b8fb..30d0fb9a 100644 --- a/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt @@ -1,4 +1,8 @@ package gomushin.backend.auth.presentation object ApiPath { + object OAuth { + const val AUTHORIZE_OAUTH = "/v1/oauth/{provider}" + const val LOGIN_OAUTH = "/v1/oauth/login/{provider}" + } } From b9f56a51b3cf230ff72f92aba58309d33b2fd459 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:15:34 +0900 Subject: [PATCH 013/357] =?UTF-8?q?feat:=20OAuth=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/AuthorizeOAuthUseCase.kt | 15 ++++ .../auth/application/LoginOAuthUseCase.kt | 71 +++++++++++++++++++ .../presentation/AuthorizeOAuthController.kt | 32 +++++++++ .../auth/presentation/LoginOAuthController.kt | 26 +++++++ .../response/LoginOAuthResponse.kt | 19 +++++ 5 files changed, 163 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/auth/application/AuthorizeOAuthUseCase.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/application/LoginOAuthUseCase.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/AuthorizeOAuthController.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/LoginOAuthController.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/response/LoginOAuthResponse.kt diff --git a/src/main/kotlin/gomushin/backend/auth/application/AuthorizeOAuthUseCase.kt b/src/main/kotlin/gomushin/backend/auth/application/AuthorizeOAuthUseCase.kt new file mode 100644 index 00000000..92415d04 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/application/AuthorizeOAuthUseCase.kt @@ -0,0 +1,15 @@ +package gomushin.backend.auth.application + +import gomushin.backend.auth.infrastructure.OAuthProviderFactory +import org.springframework.stereotype.Service + +@Service +class AuthorizeOAuthUseCase( + private val oAuthProviderFactory: OAuthProviderFactory +) { + + fun getRedirectUrl(provider: String): String { + val oAuthProvider = oAuthProviderFactory.getOAuthProvider(provider) + return oAuthProvider.getAuthorizationUrl() + } +} diff --git a/src/main/kotlin/gomushin/backend/auth/application/LoginOAuthUseCase.kt b/src/main/kotlin/gomushin/backend/auth/application/LoginOAuthUseCase.kt new file mode 100644 index 00000000..babb9ce7 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/application/LoginOAuthUseCase.kt @@ -0,0 +1,71 @@ +package gomushin.backend.auth.application + +import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.auth.infrastructure.OAuthProviderFactory +import gomushin.backend.auth.presentation.response.LoginOAuthResponse +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Provider +import org.apache.coyote.BadRequestException +import org.springframework.stereotype.Service +import java.util.* + +@Service +class LoginOAuthUseCase( + private val oAuthProviderFactory: OAuthProviderFactory, + private val memberRepository: MemberRepository, + private val jwtTokenProvider: JwtTokenProvider + +) { + fun execute( + provider: String, + code: String?, + state: String?, + error: String?, + errorDescription: String? + ): LoginOAuthResponse { + val oAuthProvider = oAuthProviderFactory.getOAuthProvider(provider) + when { + code != null -> { + val token = oAuthProvider.getToken(code) + val userInfo = oAuthProvider.getUserInfo(token.accessToken) + + memberRepository.findByEmail( + userInfo.email ?: throw BadRequestException("sarangggun.member.not-exist-email") + )?.let { + val provideAccessToken = jwtTokenProvider.provideAccessToken(it.id!!) + + return LoginOAuthResponse.success( + provideAccessToken, + LoginOAuthResponse.UserInfo(it.id.toString(), it.email, it.name, it.profileImageUrl) + ) + } + + val savedMember = memberRepository.save( + Member.create( + userInfo.name ?: throw BadRequestException("sarangggun.member.not-exist-name"), + userInfo.email, + userInfo.profileImageUrl, + Provider.valueOf(provider.uppercase(Locale.getDefault())) + ) + ) + + val provideAccessToken = jwtTokenProvider.provideAccessToken(savedMember.id!!) + + return LoginOAuthResponse.success( + provideAccessToken, + LoginOAuthResponse.UserInfo( + savedMember.id.toString(), + savedMember.email, + savedMember.name, + savedMember.profileImageUrl + ) + ) + } + + else -> { + return LoginOAuthResponse.error("로그인 실패"); + } + } + } +} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/AuthorizeOAuthController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/AuthorizeOAuthController.kt new file mode 100644 index 00000000..112f00df --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/presentation/AuthorizeOAuthController.kt @@ -0,0 +1,32 @@ +package gomushin.backend.auth.presentation + +import gomushin.backend.auth.application.AuthorizeOAuthUseCase +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.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import java.net.URI + +@Tag(name = "OAuth", description = "OAuth 로그인 API") +@Controller +class AuthorizeOAuthController( + private val authorizeOAuthUseCase: AuthorizeOAuthUseCase +) { + @Operation(summary = "OAuth 로그인", description = "OAuth 로그인 하는 url.") + @ApiResponses( + ApiResponse(responseCode = "201", description = "로그인 성공"), + ApiResponse(responseCode = "400", description = "잘못된 요청"), + ApiResponse(responseCode = "500", description = "서버 오류") + ) + @GetMapping(ApiPath.OAuth.AUTHORIZE_OAUTH) + fun authorizeOAuth(@PathVariable provider: String): ResponseEntity { + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(authorizeOAuthUseCase.getRedirectUrl(provider))) + .build() + } +} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/LoginOAuthController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/LoginOAuthController.kt new file mode 100644 index 00000000..13912ba8 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/presentation/LoginOAuthController.kt @@ -0,0 +1,26 @@ +package gomushin.backend.auth.presentation + +import gomushin.backend.auth.application.LoginOAuthUseCase +import gomushin.backend.auth.presentation.response.LoginOAuthResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "OAuth", description = "OAuth 로그인 API") +@RestController +class LoginOAuthController( + private val loginOAuthUseCase: LoginOAuthUseCase +) { + @Operation(summary = "OAuth 로그인", description = "OAuth 코드 보내는 URL") + @GetMapping(ApiPath.OAuth.LOGIN_OAUTH) + fun loginOAuth( + @PathVariable provider: String, + @RequestParam("code", required = false) code: String?, + @RequestParam("state", required = false) state: String?, + @RequestParam("error", required = false) error: String?, + @RequestParam("error_description", required = false) errorDescription: String? + ): LoginOAuthResponse = loginOAuthUseCase.execute(provider, code, state, error, errorDescription) +} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/response/LoginOAuthResponse.kt b/src/main/kotlin/gomushin/backend/auth/presentation/response/LoginOAuthResponse.kt new file mode 100644 index 00000000..6074767c --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/presentation/response/LoginOAuthResponse.kt @@ -0,0 +1,19 @@ +package gomushin.backend.auth.presentation.response + +data class LoginOAuthResponse( + val success: Boolean, + val userInfo: UserInfo? = null, + val token: String?, +) { + data class UserInfo( + val id: String, + val email: String?, + val name: String?, + val profileImageUrl: String? + ) + + companion object { + fun success(token: String, userInfo: UserInfo): LoginOAuthResponse = LoginOAuthResponse(true, userInfo, token) + fun error(message: String): LoginOAuthResponse = LoginOAuthResponse(false, null, null) + } +} From 2d762d3424b286a4bcf8df5049b3d7cb0a4d8590 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:15:46 +0900 Subject: [PATCH 014/357] =?UTF-8?q?feat:=20JwtAuthenticationFilter=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 --- .../filter/JwtAuthenticationFilter.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt new file mode 100644 index 00000000..4629e10d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -0,0 +1,56 @@ +package gomushin.backend.core.infrastructure.filter + +import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.CustomUserDetailsService +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtAuthenticationFilter( + private val tokenProvider: JwtTokenProvider, + private val customUserDetailsService: CustomUserDetailsService +) : OncePerRequestFilter() { + private val AUTHORIZATION_HEADER = "Authorization" + private val BEARER_PREFIX = "Bearer " + + override fun shouldNotFilter(request: HttpServletRequest): Boolean { + return request.requestURI.contains("/v1/auth") || request.requestURI.contains("/v1/oauth") + } + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + try { + val token = resolveToken(request) + if (token.isNotEmpty()) { + val auth = createAuthentication(token) + SecurityContextHolder.getContext().authentication = auth + } + filterChain.doFilter(request, response) + } catch (e: Exception) { + SecurityContextHolder.clearContext() + response.sendError(401, "로그인이 필요한 서비스입니다.") + } + } + + private fun resolveToken(request: HttpServletRequest): String { + val authHeader = request.getHeader(AUTHORIZATION_HEADER) + if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) { + return authHeader.substring(7) + } + return "" + } + + private fun createAuthentication(token: String): Authentication { + val userDetails = customUserDetailsService.loadUserById(tokenProvider.getMemberIdFromToken(token)) + return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities) + } +} From bba6fbedfd262d9b7ef8c7698e991a2f86549617 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 6 Apr 2025 17:29:00 +0900 Subject: [PATCH 015/357] =?UTF-8?q?refactor:=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EB=AA=85=20=EC=98=A4=ED=83=80=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/infrastructure/filter/JwtAuthenticationFilter.kt | 2 +- .../kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt | 6 ------ .../jwt/{infrastructrue => infrastructure}/JwtProperties.kt | 2 +- .../JwtTokenProviderImpl.kt | 2 +- .../kotlin/gomushin/backend/core/jwt/infrastructure/Type.kt | 6 ++++++ 5 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt rename src/main/kotlin/gomushin/backend/core/jwt/{infrastructrue => infrastructure}/JwtProperties.kt (85%) rename src/main/kotlin/gomushin/backend/core/jwt/{infrastructrue => infrastructure}/JwtTokenProviderImpl.kt (98%) create mode 100644 src/main/kotlin/gomushin/backend/core/jwt/infrastructure/Type.kt diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index 4629e10d..654ba739 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -44,7 +44,7 @@ class JwtAuthenticationFilter( private fun resolveToken(request: HttpServletRequest): String { val authHeader = request.getHeader(AUTHORIZATION_HEADER) if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) { - return authHeader.substring(7) + return authHeader.substring(BEARER_PREFIX.length) } return "" } diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt deleted file mode 100644 index 3f7bec9f..00000000 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/Type.kt +++ /dev/null @@ -1,6 +0,0 @@ -package gomushin.backend.core.jwt.infrastructrue - -enum class Type { - ACCESS, REFRESH -} - diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtProperties.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtProperties.kt similarity index 85% rename from src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtProperties.kt rename to src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtProperties.kt index 61db4cba..bdcfcb12 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtProperties.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtProperties.kt @@ -1,4 +1,4 @@ -package gomushin.backend.core.jwt.infrastructrue +package gomushin.backend.core.jwt.infrastructure import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtTokenProviderImpl.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt similarity index 98% rename from src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtTokenProviderImpl.kt rename to src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt index 0b337101..738211af 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructrue/JwtTokenProviderImpl.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt @@ -1,4 +1,4 @@ -package gomushin.backend.core.jwt.infrastructrue +package gomushin.backend.core.jwt.infrastructure import gomushin.backend.core.jwt.JwtTokenProvider import io.jsonwebtoken.* diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/Type.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/Type.kt new file mode 100644 index 00000000..09b7f57a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/Type.kt @@ -0,0 +1,6 @@ +package gomushin.backend.core.jwt.infrastructure + +enum class Type { + ACCESS, REFRESH +} + From 6ec6379b97f300f080d802c7b3bbe7c4f585d50b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 01:57:31 +0900 Subject: [PATCH 016/357] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20OAuth2?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/application/AuthorizeOAuthUseCase.kt | 15 ---- .../auth/application/LoginOAuthUseCase.kt | 71 --------------- .../auth/domain/oauth/OAuthProvider.kt | 7 -- .../backend/auth/domain/oauth/OAuthToken.kt | 20 ----- .../auth/domain/oauth/OAuthUserInfo.kt | 8 -- .../auth/infrastructure/OAuthProperties.kt | 18 ---- .../infrastructure/OAuthProviderFactory.kt | 18 ---- .../kakao/KakaoOAuthProvider.kt | 86 ------------------- .../kakao/KakaoOAuthUserInfo.kt | 72 ---------------- .../presentation/AuthorizeOAuthController.kt | 32 ------- .../auth/presentation/LoginOAuthController.kt | 26 ------ .../response/KakaoOAuthResponse.kt | 26 ------ .../response/LoginOAuthResponse.kt | 19 ---- 13 files changed, 418 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/auth/application/AuthorizeOAuthUseCase.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/application/LoginOAuthUseCase.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthProvider.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthToken.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthUserInfo.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProperties.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProviderFactory.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthProvider.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthUserInfo.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/AuthorizeOAuthController.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/LoginOAuthController.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/response/KakaoOAuthResponse.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/response/LoginOAuthResponse.kt diff --git a/src/main/kotlin/gomushin/backend/auth/application/AuthorizeOAuthUseCase.kt b/src/main/kotlin/gomushin/backend/auth/application/AuthorizeOAuthUseCase.kt deleted file mode 100644 index 92415d04..00000000 --- a/src/main/kotlin/gomushin/backend/auth/application/AuthorizeOAuthUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package gomushin.backend.auth.application - -import gomushin.backend.auth.infrastructure.OAuthProviderFactory -import org.springframework.stereotype.Service - -@Service -class AuthorizeOAuthUseCase( - private val oAuthProviderFactory: OAuthProviderFactory -) { - - fun getRedirectUrl(provider: String): String { - val oAuthProvider = oAuthProviderFactory.getOAuthProvider(provider) - return oAuthProvider.getAuthorizationUrl() - } -} diff --git a/src/main/kotlin/gomushin/backend/auth/application/LoginOAuthUseCase.kt b/src/main/kotlin/gomushin/backend/auth/application/LoginOAuthUseCase.kt deleted file mode 100644 index babb9ce7..00000000 --- a/src/main/kotlin/gomushin/backend/auth/application/LoginOAuthUseCase.kt +++ /dev/null @@ -1,71 +0,0 @@ -package gomushin.backend.auth.application - -import gomushin.backend.core.jwt.JwtTokenProvider -import gomushin.backend.auth.infrastructure.OAuthProviderFactory -import gomushin.backend.auth.presentation.response.LoginOAuthResponse -import gomushin.backend.member.domain.entity.Member -import gomushin.backend.member.domain.repository.MemberRepository -import gomushin.backend.member.domain.value.Provider -import org.apache.coyote.BadRequestException -import org.springframework.stereotype.Service -import java.util.* - -@Service -class LoginOAuthUseCase( - private val oAuthProviderFactory: OAuthProviderFactory, - private val memberRepository: MemberRepository, - private val jwtTokenProvider: JwtTokenProvider - -) { - fun execute( - provider: String, - code: String?, - state: String?, - error: String?, - errorDescription: String? - ): LoginOAuthResponse { - val oAuthProvider = oAuthProviderFactory.getOAuthProvider(provider) - when { - code != null -> { - val token = oAuthProvider.getToken(code) - val userInfo = oAuthProvider.getUserInfo(token.accessToken) - - memberRepository.findByEmail( - userInfo.email ?: throw BadRequestException("sarangggun.member.not-exist-email") - )?.let { - val provideAccessToken = jwtTokenProvider.provideAccessToken(it.id!!) - - return LoginOAuthResponse.success( - provideAccessToken, - LoginOAuthResponse.UserInfo(it.id.toString(), it.email, it.name, it.profileImageUrl) - ) - } - - val savedMember = memberRepository.save( - Member.create( - userInfo.name ?: throw BadRequestException("sarangggun.member.not-exist-name"), - userInfo.email, - userInfo.profileImageUrl, - Provider.valueOf(provider.uppercase(Locale.getDefault())) - ) - ) - - val provideAccessToken = jwtTokenProvider.provideAccessToken(savedMember.id!!) - - return LoginOAuthResponse.success( - provideAccessToken, - LoginOAuthResponse.UserInfo( - savedMember.id.toString(), - savedMember.email, - savedMember.name, - savedMember.profileImageUrl - ) - ) - } - - else -> { - return LoginOAuthResponse.error("로그인 실패"); - } - } - } -} diff --git a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthProvider.kt b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthProvider.kt deleted file mode 100644 index 5b810109..00000000 --- a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthProvider.kt +++ /dev/null @@ -1,7 +0,0 @@ -package gomushin.backend.auth.domain.oauth - -interface OAuthProvider { - fun getAuthorizationUrl(): String - fun getToken(code: String): OAuthToken - fun getUserInfo(accessToken: String): OAuthUserInfo -} diff --git a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthToken.kt b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthToken.kt deleted file mode 100644 index a0ea73a7..00000000 --- a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthToken.kt +++ /dev/null @@ -1,20 +0,0 @@ -package gomushin.backend.auth.domain.oauth - -import com.fasterxml.jackson.annotation.JsonProperty - -data class OAuthToken( - @JsonProperty("access_token") - val accessToken: String, - - @JsonProperty("token_type") - val tokenType: String, - - @JsonProperty("refresh_token") - val refreshToken: String, - - @JsonProperty("expires_in") - val expiresIn: Int, - - @JsonProperty("refresh_token_expires_in") - val refreshTokenExpiresIn: Int, -) diff --git a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthUserInfo.kt b/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthUserInfo.kt deleted file mode 100644 index ed67c7e0..00000000 --- a/src/main/kotlin/gomushin/backend/auth/domain/oauth/OAuthUserInfo.kt +++ /dev/null @@ -1,8 +0,0 @@ -package gomushin.backend.auth.domain.oauth - -data class OAuthUserInfo( - val id: String, - val email: String?, - val name: String?, - val profileImageUrl: String? -) diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProperties.kt b/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProperties.kt deleted file mode 100644 index ee8db0bc..00000000 --- a/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProperties.kt +++ /dev/null @@ -1,18 +0,0 @@ -package gomushin.backend.auth.infrastructure - -import org.springframework.boot.context.properties.ConfigurationProperties - -@ConfigurationProperties("oauth2") -data class OAuthProperties( - val kakao: OAuthProvider -) { - data class OAuthProvider( - val authUri: String, - val clientId: String, - val clientSecret: String, - val redirectUri: String, - val tokenUri: String, - val resourceUri: String, - val scope: List - ) -} diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProviderFactory.kt b/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProviderFactory.kt deleted file mode 100644 index 2080af61..00000000 --- a/src/main/kotlin/gomushin/backend/auth/infrastructure/OAuthProviderFactory.kt +++ /dev/null @@ -1,18 +0,0 @@ -package gomushin.backend.auth.infrastructure - -import gomushin.backend.auth.domain.oauth.OAuthProvider -import gomushin.backend.auth.infrastructure.kakao.KakaoOAuthProvider -import gomushin.backend.core.infrastructure.exception.BadRequestException -import org.springframework.stereotype.Component - -@Component -class OAuthProviderFactory( - private val kakaoOAuthProvider: KakaoOAuthProvider, -) { - fun getOAuthProvider(provider: String): OAuthProvider { - return when (provider) { - "kakao" -> kakaoOAuthProvider - else -> throw BadRequestException("sarangggun.oauth.invalid-provider") - } - } -} diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthProvider.kt b/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthProvider.kt deleted file mode 100644 index 571a3f24..00000000 --- a/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package gomushin.backend.auth.infrastructure.kakao - -import gomushin.backend.auth.domain.oauth.OAuthProvider -import gomushin.backend.auth.domain.oauth.OAuthToken -import gomushin.backend.auth.domain.oauth.OAuthUserInfo -import gomushin.backend.auth.infrastructure.OAuthProperties -import gomushin.backend.core.infrastructure.exception.BadRequestException -import org.springframework.http.HttpHeaders -import org.springframework.http.MediaType -import org.springframework.stereotype.Component -import org.springframework.web.client.RestClient -import java.net.URLEncoder -import java.nio.charset.StandardCharsets - -@Component -class KakaoOAuthProvider(private val oAuthProperties: OAuthProperties) : OAuthProvider { - override fun getAuthorizationUrl(): String { - val kakaoProperties = oAuthProperties.kakao - return "https://kauth.kakao.com/oauth/authorize?" + - "client_id=${kakaoProperties.clientId}" + - "&redirect_uri=${kakaoProperties.redirectUri}" + - "&response_type=code" - } - - - override fun getUserInfo(accessToken: String): OAuthUserInfo { - val resourceUri = oAuthProperties.kakao.resourceUri - val restClient = RestClient.builder() - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer $accessToken") - .build() - - val responseString = restClient.get() - .uri(resourceUri) - .retrieve() - .toEntity(String::class.java) - - println("responseString = $responseString") - - val response = restClient.get() - .uri(resourceUri) - .retrieve() - .toEntity(KakaoOAuthUserInfo::class.java) - - val kakaoUserInfo = response.body ?: throw BadRequestException("saranggun.oauth.failed-to-fetch-user-info") - - return kakaoUserInfo.toOAuthUserInfo() - } - - override fun getToken(code: String): OAuthToken { - val kakaoProperties = oAuthProperties.kakao - - val params = mapOf( - "grant_type" to "authorization_code", - "client_id" to kakaoProperties.clientId, - "redirect_uri" to kakaoProperties.redirectUri, - "code" to code, - "client_secret" to kakaoProperties.clientSecret - ) - - val restClient = RestClient.builder() - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=utf-8") - .build() - - val tokenUrl = kakaoProperties.tokenUri - - val response = restClient.post() - .uri(tokenUrl) - .body(params.toFormData()) - .retrieve() - .toEntity(OAuthToken::class.java) - - return response.body ?: throw IllegalStateException("카카오 토큰 페칭 실패") - } - - private fun Map.toFormData(): String { - return this.entries.joinToString("&") { - "${ - URLEncoder.encode( - it.key, - StandardCharsets.UTF_8 - ) - }=${URLEncoder.encode(it.value, StandardCharsets.UTF_8)}" - } - } -} diff --git a/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthUserInfo.kt b/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthUserInfo.kt deleted file mode 100644 index 890683bd..00000000 --- a/src/main/kotlin/gomushin/backend/auth/infrastructure/kakao/KakaoOAuthUserInfo.kt +++ /dev/null @@ -1,72 +0,0 @@ -package gomushin.backend.auth.infrastructure.kakao - -import com.fasterxml.jackson.annotation.JsonProperty -import gomushin.backend.auth.domain.oauth.OAuthUserInfo - -data class KakaoOAuthUserInfo( - val id: Long, - @JsonProperty("connected_at") - val connectedAt: String, - val properties: KakaoProperties, - @JsonProperty("kakao_account") - val kakaoAccount: KakaoAccount -) { - fun toOAuthUserInfo(): OAuthUserInfo { - return OAuthUserInfo( - id = this.id.toString(), - email = this.kakaoAccount.email, - name = this.properties.nickname, - profileImageUrl = this.properties.profileImage - ) - } -} - -data class KakaoProperties( - val nickname: String, - - @JsonProperty("profile_image") - val profileImage: String, - - @JsonProperty("thumbnail_image") - val thumbnailImage: String -) - -data class KakaoAccount( - @JsonProperty("profile_nickname_needs_agreement") - val profileNicknameNeedsAgreement: Boolean, - - @JsonProperty("profile_image_needs_agreement") - val profileImageNeedsAgreement: Boolean, - - @JsonProperty("has_email") - val hasEmail: Boolean, - - @JsonProperty("email_needs_agreement") - val emailNeedsAgreement: Boolean, - - @JsonProperty("is_email_valid") - val isEmailValid: Boolean, - - @JsonProperty("is_email_verified") - val isEmailVerified: Boolean, - - val email: String, - - val profile: KakaoProfile -) - -data class KakaoProfile( - val nickname: String, - - @JsonProperty("thumbnail_image_url") - val thumbnailImageUrl: String, - - @JsonProperty("profile_image_url") - val profileImageUrl: String, - - @JsonProperty("is_default_image") - val isDefaultImage: Boolean, - - @JsonProperty("is_default_nickname") - val isDefaultNickname: Boolean -) diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/AuthorizeOAuthController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/AuthorizeOAuthController.kt deleted file mode 100644 index 112f00df..00000000 --- a/src/main/kotlin/gomushin/backend/auth/presentation/AuthorizeOAuthController.kt +++ /dev/null @@ -1,32 +0,0 @@ -package gomushin.backend.auth.presentation - -import gomushin.backend.auth.application.AuthorizeOAuthUseCase -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.HttpStatus -import org.springframework.http.ResponseEntity -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import java.net.URI - -@Tag(name = "OAuth", description = "OAuth 로그인 API") -@Controller -class AuthorizeOAuthController( - private val authorizeOAuthUseCase: AuthorizeOAuthUseCase -) { - @Operation(summary = "OAuth 로그인", description = "OAuth 로그인 하는 url.") - @ApiResponses( - ApiResponse(responseCode = "201", description = "로그인 성공"), - ApiResponse(responseCode = "400", description = "잘못된 요청"), - ApiResponse(responseCode = "500", description = "서버 오류") - ) - @GetMapping(ApiPath.OAuth.AUTHORIZE_OAUTH) - fun authorizeOAuth(@PathVariable provider: String): ResponseEntity { - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create(authorizeOAuthUseCase.getRedirectUrl(provider))) - .build() - } -} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/LoginOAuthController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/LoginOAuthController.kt deleted file mode 100644 index 13912ba8..00000000 --- a/src/main/kotlin/gomushin/backend/auth/presentation/LoginOAuthController.kt +++ /dev/null @@ -1,26 +0,0 @@ -package gomushin.backend.auth.presentation - -import gomushin.backend.auth.application.LoginOAuthUseCase -import gomushin.backend.auth.presentation.response.LoginOAuthResponse -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.tags.Tag -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController - -@Tag(name = "OAuth", description = "OAuth 로그인 API") -@RestController -class LoginOAuthController( - private val loginOAuthUseCase: LoginOAuthUseCase -) { - @Operation(summary = "OAuth 로그인", description = "OAuth 코드 보내는 URL") - @GetMapping(ApiPath.OAuth.LOGIN_OAUTH) - fun loginOAuth( - @PathVariable provider: String, - @RequestParam("code", required = false) code: String?, - @RequestParam("state", required = false) state: String?, - @RequestParam("error", required = false) error: String?, - @RequestParam("error_description", required = false) errorDescription: String? - ): LoginOAuthResponse = loginOAuthUseCase.execute(provider, code, state, error, errorDescription) -} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/response/KakaoOAuthResponse.kt b/src/main/kotlin/gomushin/backend/auth/presentation/response/KakaoOAuthResponse.kt deleted file mode 100644 index e5f12e18..00000000 --- a/src/main/kotlin/gomushin/backend/auth/presentation/response/KakaoOAuthResponse.kt +++ /dev/null @@ -1,26 +0,0 @@ -package gomushin.backend.auth.presentation.response - -import com.fasterxml.jackson.annotation.JsonProperty - -data class KakaoOAuthResponse( - @JsonProperty("access_token") - val accessToken: String, - - @JsonProperty("token_type") - val tokenType: String, - - @JsonProperty("id_token") - val idToken: String, - - @JsonProperty("refresh_token") - val refreshToken: String, - - @JsonProperty("expires_in") - val expiresIn: Int, - - @JsonProperty("refresh_token_expires_in") - val refreshTokenExpiresIn: Int, - - @JsonProperty("scope") - val scope: String -) diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/response/LoginOAuthResponse.kt b/src/main/kotlin/gomushin/backend/auth/presentation/response/LoginOAuthResponse.kt deleted file mode 100644 index 6074767c..00000000 --- a/src/main/kotlin/gomushin/backend/auth/presentation/response/LoginOAuthResponse.kt +++ /dev/null @@ -1,19 +0,0 @@ -package gomushin.backend.auth.presentation.response - -data class LoginOAuthResponse( - val success: Boolean, - val userInfo: UserInfo? = null, - val token: String?, -) { - data class UserInfo( - val id: String, - val email: String?, - val name: String?, - val profileImageUrl: String? - ) - - companion object { - fun success(token: String, userInfo: UserInfo): LoginOAuthResponse = LoginOAuthResponse(true, userInfo, token) - fun error(message: String): LoginOAuthResponse = LoginOAuthResponse(false, null, null) - } -} From 203566e9388837b57476d01746842ebd55724185 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 01:58:49 +0900 Subject: [PATCH 017/357] =?UTF-8?q?refactor:=20ApiPath=EC=97=90=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20OAuth=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/gomushin/backend/auth/application/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt | 4 ---- 2 files changed, 4 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/auth/application/.gitkeep diff --git a/src/main/kotlin/gomushin/backend/auth/application/.gitkeep b/src/main/kotlin/gomushin/backend/auth/application/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt index 30d0fb9a..dcc9b8fb 100644 --- a/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt @@ -1,8 +1,4 @@ package gomushin.backend.auth.presentation object ApiPath { - object OAuth { - const val AUTHORIZE_OAUTH = "/v1/oauth/{provider}" - const val LOGIN_OAUTH = "/v1/oauth/login/{provider}" - } } From 0009319fd489b27b1de56becc166db2dabfae0eb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 01:59:56 +0900 Subject: [PATCH 018/357] =?UTF-8?q?refactor:=20Member=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20name,=20email,=20profileImageUrl=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=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 --- .../backend/auth/presentation/MainController.kt | 16 ++++++++++++++++ .../backend/member/domain/entity/Member.kt | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt new file mode 100644 index 00000000..9f22723a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt @@ -0,0 +1,16 @@ +package gomushin.backend.auth.presentation + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/") +class MainController { + + @GetMapping + fun index(): String { + return "곰신 서버" + } + +} diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index 19b6191d..ce03922e 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -11,12 +11,12 @@ class Member( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L, - val name: String, + var name: String, @Column(unique = true) - val email: String, + var email: String, - val profileImageUrl: String, + var profileImageUrl: String?, @Enumerated(EnumType.STRING) val provider: Provider, From bcac371b53806b27eac1203f7ed09aaddeb68154 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 02:00:17 +0900 Subject: [PATCH 019/357] =?UTF-8?q?refactor:=20OAuth2=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=EB=8D=94=20=EC=9D=B4=EB=84=98=20value=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 --- .../backend/member/domain/value/Provider.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/domain/value/Provider.kt b/src/main/kotlin/gomushin/backend/member/domain/value/Provider.kt index 7be07207..2c0291fe 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/value/Provider.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/value/Provider.kt @@ -1,7 +1,18 @@ package gomushin.backend.member.domain.value -enum class Provider { - GOOGLE, - KAKAO, - NAVER +import gomushin.backend.core.infrastructure.exception.BadRequestException + +enum class Provider( + private val value: String +) { + GOOGLE("google"), + KAKAO("kakao"), + NAVER("naver"); + + companion object { + fun getProviderByValue(value: String): Provider { + return entries.firstOrNull { it.value == value } + ?: throw BadRequestException("sarangggun.oauth.invalid-provider") + } + } } From c6835767c5634c8b1d08b1e2b1076d3ecdc33da4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 02:02:42 +0900 Subject: [PATCH 020/357] =?UTF-8?q?refactor:=20OAuth2=20Response=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20Response?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/oauth/dto/KakaoResponse.kt | 34 +++++++++++++++++++ .../backend/core/oauth/dto/OAuth2Response.kt | 9 +++++ 2 files changed, 43 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/core/oauth/dto/OAuth2Response.kt diff --git a/src/main/kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt b/src/main/kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt new file mode 100644 index 00000000..7f363083 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt @@ -0,0 +1,34 @@ +package gomushin.backend.core.oauth.dto + +import org.apache.coyote.BadRequestException + +class KakaoResponse(private val attributes: Map) : OAuth2Response { + override fun getProviderId(): String { + return attributes["id"].toString() + } + + override fun getEmail(): String { + val kakaoAccount = attributes["kakao_account"] as? Map<*, *> + + return kakaoAccount?.get("email")?.toString() + ?: throw BadRequestException("sarangggun.oauth.missing-email") + } + + override fun getName(): String { + val properties = attributes["properties"] as? Map<*, *> + properties?.get("nickname")?.let { + return it.toString() + } + + val kakaoAccount = attributes["kakao_account"] as? Map<*, *> + val profile = kakaoAccount?.get("profile") as? Map<*, *> + return profile?.get("nickname")?.toString() + ?: throw BadRequestException("sarangggun.oauth.missing-nickname") + } + + fun getProfileImage(): String? { + val kakaoAccount = attributes["kakao_account"] as? Map<*, *> + val profile = kakaoAccount?.get("profile") as? Map<*, *> + return profile?.get("profile_image_url") as? String + } +} diff --git a/src/main/kotlin/gomushin/backend/core/oauth/dto/OAuth2Response.kt b/src/main/kotlin/gomushin/backend/core/oauth/dto/OAuth2Response.kt new file mode 100644 index 00000000..2f24454a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/oauth/dto/OAuth2Response.kt @@ -0,0 +1,9 @@ +package gomushin.backend.core.oauth.dto + +interface OAuth2Response { + fun getProviderId(): String + + fun getEmail(): String + + fun getName(): String +} From ffd6f47a35a9044cd5cee2a6b51441935db9cdfd Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 02:03:25 +0900 Subject: [PATCH 021/357] =?UTF-8?q?refactor:=20OAuth2=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B1=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=20CustomOAuth2User=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/oauth/CustomOAuth2User.kt | 31 ++++++++ .../backend/core/oauth/dto/UserDTO.kt | 17 ++++ .../oauth/service/CustomOAuth2UserService.kt | 77 +++++++++++++++++++ .../{ => service}/CustomUserDetailsService.kt | 3 +- 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt create mode 100644 src/main/kotlin/gomushin/backend/core/oauth/dto/UserDTO.kt create mode 100644 src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt rename src/main/kotlin/gomushin/backend/core/{ => service}/CustomUserDetailsService.kt (93%) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt b/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt new file mode 100644 index 00000000..e2304404 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt @@ -0,0 +1,31 @@ +package gomushin.backend.core.oauth + +import gomushin.backend.core.oauth.dto.UserDTO +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.oauth2.core.user.OAuth2User + +class CustomOAuth2User( + private val userDto: UserDTO +) : OAuth2User { + + override fun getName(): String { + return userDto.name + } + + override fun getAttributes(): MutableMap { + return mutableMapOf() + } + + override fun getAuthorities(): MutableCollection { + return mutableListOf() + } + + fun getEmail(): String { + return userDto.email ?: "" + } + + fun getUserId(): Long { + return userDto.userId + } + +} diff --git a/src/main/kotlin/gomushin/backend/core/oauth/dto/UserDTO.kt b/src/main/kotlin/gomushin/backend/core/oauth/dto/UserDTO.kt new file mode 100644 index 00000000..f45178db --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/oauth/dto/UserDTO.kt @@ -0,0 +1,17 @@ +package gomushin.backend.core.oauth.dto + +data class UserDTO( + val registrationId: String, + + val role: String, + + val name: String, + + val username: String, + + val email: String? = null, + + val profileImage: String? = null, + + val userId: Long, +) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt b/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt new file mode 100644 index 00000000..29dd7c1d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt @@ -0,0 +1,77 @@ +package gomushin.backend.core.oauth.service + +import gomushin.backend.core.oauth.dto.KakaoResponse +import gomushin.backend.core.oauth.dto.UserDTO +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.core.oauth.CustomOAuth2User +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Provider +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CustomOAuth2UserService( + private val memberRepository: MemberRepository +) : DefaultOAuth2UserService() { + + @Transactional + override fun loadUser(oauth2UserRequest: OAuth2UserRequest): OAuth2User? { + val oAuth2User = super.loadUser(oauth2UserRequest) + + val registrationId = oauth2UserRequest.clientRegistration.registrationId + val oAuth2Response = when (registrationId) { + "kakao" -> KakaoResponse(oAuth2User.attributes) + else -> throw BadRequestException("sarangggun.oauth.invalid-provider") + } + + val email = oAuth2Response.getEmail() + + getMemberByEmail(email)?.let { + it.email = oAuth2Response.getEmail() + it.name = oAuth2Response.getName() + it.profileImageUrl = oAuth2Response.getProfileImage() + + val savedMember = memberRepository.save(it) + + val userDto = UserDTO( + username = oAuth2Response.getProviderId(), + name = oAuth2Response.getName(), + email = oAuth2Response.getEmail(), + profileImage = oAuth2Response.getProfileImage(), + role = "ROLE_USER", + registrationId = registrationId, + userId = savedMember.id, + ) + + return CustomOAuth2User(userDto) + } ?: run { + val newMember = Member.create( + name = oAuth2Response.getName(), + email = oAuth2Response.getEmail(), + profileImageUrl = oAuth2Response.getProfileImage(), + provider = Provider.getProviderByValue(registrationId) + ) + + val savedMember = memberRepository.save(newMember) + + val userDto = UserDTO( + username = oAuth2Response.getProviderId(), + name = oAuth2Response.getName(), + email = oAuth2Response.getEmail(), + profileImage = oAuth2Response.getProfileImage(), + role = "ROLE_USER", + registrationId = registrationId, + userId = savedMember.id, + ) + return CustomOAuth2User(userDto) + } + } + + private fun getMemberByEmail(email: String): Member? { + return memberRepository.findByEmail(email) + } +} diff --git a/src/main/kotlin/gomushin/backend/core/CustomUserDetailsService.kt b/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt similarity index 93% rename from src/main/kotlin/gomushin/backend/core/CustomUserDetailsService.kt rename to src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt index 786cfc9c..84f68d87 100644 --- a/src/main/kotlin/gomushin/backend/core/CustomUserDetailsService.kt +++ b/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt @@ -1,5 +1,6 @@ -package gomushin.backend.core +package gomushin.backend.core.service +import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository From 7b6a3471fd7f28b666fcaa6fb53a14c0952f6996 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 02:04:04 +0900 Subject: [PATCH 022/357] =?UTF-8?q?refactor:=20OAuth2=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20Cookie=EB=A1=9C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=B0=8F=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth/handler/CustomSuccessHandler.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt new file mode 100644 index 00000000..1d758934 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -0,0 +1,52 @@ +package gomushin.backend.core.oauth.handler + +import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.oauth.CustomOAuth2User +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.repository.MemberRepository +import io.jsonwebtoken.io.IOException +import jakarta.servlet.ServletException +import jakarta.servlet.http.Cookie +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.Authentication +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler +import org.springframework.stereotype.Component + +@Component +class CustomSuccessHandler( + private val jwtTokenProvider: JwtTokenProvider, + private val memberRepository: MemberRepository, +) : SimpleUrlAuthenticationSuccessHandler() { + + @Throws(IOException::class, ServletException::class) + override fun onAuthenticationSuccess( + request: HttpServletRequest?, + response: HttpServletResponse?, + authentication: Authentication + ) { + val oAuth2User = authentication.principal as CustomOAuth2User + var accessToken = "" + getMemberByEmail(oAuth2User.getEmail())?.let { + accessToken = jwtTokenProvider.provideAccessToken(it.id) + } ?: run { + accessToken = jwtTokenProvider.provideAccessToken(oAuth2User.getUserId()) + } + + response!!.addCookie(creatCookie("access_token", accessToken)) + response.sendRedirect("http://localhost:8080") // TODO: 프론트엔드 주소로 변경 , 환경변수 처리 + } + + private fun creatCookie(key: String, value: String): Cookie { + val cookie = Cookie(key, value) + cookie.path = "/" + cookie.isHttpOnly = true + cookie.secure = true + cookie.maxAge = 1800 + return cookie + } + + private fun getMemberByEmail(email: String): Member? { + return memberRepository.findByEmail(email) + } +} From b69061652a13d66dceb0d186349db5c4b980f465 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 02:04:29 +0900 Subject: [PATCH 023/357] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=94=84=EB=A7=81?= =?UTF-8?q?=20OAuth=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SecurityConfiguration.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index b8ac1af6..1945add3 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -1,8 +1,10 @@ package gomushin.backend.core.configuration.security -import gomushin.backend.core.jwt.JwtTokenProvider -import gomushin.backend.core.CustomUserDetailsService +import gomushin.backend.core.oauth.handler.CustomSuccessHandler import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter +import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.oauth.service.CustomOAuth2UserService +import gomushin.backend.core.service.CustomUserDetailsService import gomushin.backend.member.domain.repository.MemberRepository import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -19,7 +21,10 @@ class SecurityConfiguration( ) { @Bean - fun filterChain(http: HttpSecurity, corsConfiguration: CustomCorsConfiguration): SecurityFilterChain { + fun filterChain( + http: HttpSecurity, corsConfiguration: CustomCorsConfiguration, + customOAuth2UserService: CustomOAuth2UserService + ): SecurityFilterChain { http .csrf { it.disable() @@ -29,8 +34,23 @@ class SecurityConfiguration( corsConfiguration.corsConfigurationSource() ) } + .formLogin { + it.disable() + } + .httpBasic { + it.disable() + } + .oauth2Login { oAuth2LoginConfigurer -> + oAuth2LoginConfigurer + .userInfoEndpoint { userInfoEndpointConfigurer -> + userInfoEndpointConfigurer + .userService(customOAuth2UserService) + } + .successHandler(CustomSuccessHandler(jwtTokenProvider, memberRepository)) + } .authorizeHttpRequests { it.requestMatchers( + "/", "/v1/auth/**", "/v1/oauth/**", "/swagger-ui.html", @@ -51,14 +71,9 @@ class SecurityConfiguration( memberRepository ) ), - UsernamePasswordAuthenticationFilter::class.java + UsernamePasswordAuthenticationFilter:: + class.java ) - .formLogin { - it.disable() - } - .httpBasic { - it.disable() - } return http.build() } } From 8c8d18ee9bcb7366931cc59a92e62f60aebe0e32 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 02:04:44 +0900 Subject: [PATCH 024/357] =?UTF-8?q?refactor:=20Jwt=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=BF=A0=ED=82=A4=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/JwtAuthenticationFilter.kt | 40 +++++++++---------- .../infrastructure/JwtTokenProviderImpl.kt | 26 ------------ 2 files changed, 18 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index 654ba739..3a201fec 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -1,12 +1,11 @@ package gomushin.backend.core.infrastructure.filter import gomushin.backend.core.jwt.JwtTokenProvider -import gomushin.backend.core.CustomUserDetailsService +import gomushin.backend.core.service.CustomUserDetailsService import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter @@ -16,8 +15,6 @@ class JwtAuthenticationFilter( private val tokenProvider: JwtTokenProvider, private val customUserDetailsService: CustomUserDetailsService ) : OncePerRequestFilter() { - private val AUTHORIZATION_HEADER = "Authorization" - private val BEARER_PREFIX = "Bearer " override fun shouldNotFilter(request: HttpServletRequest): Boolean { return request.requestURI.contains("/v1/auth") || request.requestURI.contains("/v1/oauth") @@ -28,29 +25,28 @@ class JwtAuthenticationFilter( response: HttpServletResponse, filterChain: FilterChain ) { - try { - val token = resolveToken(request) - if (token.isNotEmpty()) { - val auth = createAuthentication(token) - SecurityContextHolder.getContext().authentication = auth + + val accessToken = getCookieValue(request, "access_token") + + when { + accessToken != null && tokenProvider.validateToken(accessToken) -> { + applyAuthentication(accessToken) } - filterChain.doFilter(request, response) - } catch (e: Exception) { - SecurityContextHolder.clearContext() - response.sendError(401, "로그인이 필요한 서비스입니다.") } } - private fun resolveToken(request: HttpServletRequest): String { - val authHeader = request.getHeader(AUTHORIZATION_HEADER) - if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) { - return authHeader.substring(BEARER_PREFIX.length) - } - return "" + private fun applyAuthentication(token: String) { + val userId = tokenProvider.getMemberIdFromToken(token) + val userDetails = customUserDetailsService.loadUserById(userId) + val auth = UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.authorities + ) + SecurityContextHolder.getContext().authentication = auth } - private fun createAuthentication(token: String): Authentication { - val userDetails = customUserDetailsService.loadUserById(tokenProvider.getMemberIdFromToken(token)) - return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities) + private fun getCookieValue(request: HttpServletRequest, name: String): String? { + return request.cookies?.firstOrNull { it.name == name }?.value } } diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt index 738211af..926bacd1 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt @@ -41,7 +41,6 @@ class JwtTokenProviderImpl( is UnsupportedJwtException, is MalformedJwtException, is IllegalArgumentException -> return false - else -> throw e } } @@ -65,29 +64,4 @@ class JwtTokenProviderImpl( fun getSubject(token: String): String { return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).payload.subject } - -// private fun getClaimsFromToken(token: String): Claims { -// try { -// return Jwts.parserBuilder() -// .setSigningKey(SECRET_KEY) -// .build() -// .parseClaimsJws(token) -// .body -// } catch (e: ExpiredJwtException) { -// logger.warn("만료된 JWT 토큰입니다: {}", e.message) -// throw e -// } catch (e: UnsupportedJwtException) { -// logger.warn("지원되지 않는 JWT 토큰입니다: {}", e.message) -// throw e -// } catch (e: MalformedJwtException) { -// logger.warn("잘못된 형식의 JWT 토큰입니다: {}", e.message) -// throw e -// } catch (e: io.jsonwebtoken.security.SignatureException) { -// logger.warn("유효하지 않은 JWT 서명입니다: {}", e.message) -// throw e -// } catch (e: Exception) { -// logger.error("JWT 토큰 파싱 중 오류 발생: {}", e.message, e) -// throw e -// } -// } } From 13cb26a258a23bf77c20cde7d664428a13c28f86 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 02:14:23 +0900 Subject: [PATCH 025/357] =?UTF-8?q?refactor:=20KakaoResponse=20BadRequestE?= =?UTF-8?q?xception=20=EC=9E=84=ED=8F=AC=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt b/src/main/kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt index 7f363083..d25bb717 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/dto/KakaoResponse.kt @@ -1,6 +1,7 @@ package gomushin.backend.core.oauth.dto -import org.apache.coyote.BadRequestException +import gomushin.backend.core.infrastructure.exception.BadRequestException + class KakaoResponse(private val attributes: Map) : OAuth2Response { override fun getProviderId(): String { From 7664a1718e192ea6e7813a9e593d25d66d396be5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 8 Apr 2025 02:18:38 +0900 Subject: [PATCH 026/357] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20principal=20=ED=9A=8D=EB=93=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/JwtAuthenticationFilter.kt | 2 ++ .../backend/core/oauth/CustomOAuth2User.kt | 1 - .../core/oauth/handler/CustomSuccessHandler.kt | 16 +++++++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index 3a201fec..40f6fffe 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -33,6 +33,8 @@ class JwtAuthenticationFilter( applyAuthentication(accessToken) } } + + filterChain.doFilter(request, response) } private fun applyAuthentication(token: String) { diff --git a/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt b/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt index e2304404..e7285a07 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt @@ -27,5 +27,4 @@ class CustomOAuth2User( fun getUserId(): Long { return userDto.userId } - } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 1d758934..8040a07f 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -1,5 +1,6 @@ package gomushin.backend.core.oauth.handler +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.core.jwt.JwtTokenProvider import gomushin.backend.core.oauth.CustomOAuth2User import gomushin.backend.member.domain.entity.Member @@ -25,19 +26,24 @@ class CustomSuccessHandler( response: HttpServletResponse?, authentication: Authentication ) { - val oAuth2User = authentication.principal as CustomOAuth2User + val principal = authentication.principal + + if (principal !is CustomOAuth2User) { + throw BadRequestException("sarangggun.oauth.invalid-principal") + } + var accessToken = "" - getMemberByEmail(oAuth2User.getEmail())?.let { + getMemberByEmail(principal.getEmail())?.let { accessToken = jwtTokenProvider.provideAccessToken(it.id) } ?: run { - accessToken = jwtTokenProvider.provideAccessToken(oAuth2User.getUserId()) + accessToken = jwtTokenProvider.provideAccessToken(principal.getUserId()) } - response!!.addCookie(creatCookie("access_token", accessToken)) + response!!.addCookie(createCookie("access_token", accessToken)) response.sendRedirect("http://localhost:8080") // TODO: 프론트엔드 주소로 변경 , 환경변수 처리 } - private fun creatCookie(key: String, value: String): Cookie { + private fun createCookie(key: String, value: String): Cookie { val cookie = Cookie(key, value) cookie.path = "/" cookie.isHttpOnly = true From af855223f408ef72adf70fce03bdc32ef37dc2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 10 Apr 2025 21:48:35 +0900 Subject: [PATCH 027/357] =?UTF-8?q?feat=20:=20dockerfile=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e00bc4d9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM openjdk:17.0.1-jdk-slim +WORKDIR /app +COPY ./build/libs/backend-0.0.1-SNAPSHOT.jar /app/backend.jar +EXPOSE 8080 +ENTRYPOINT ["java"] +CMD ["-jar", "backend.jar"] \ No newline at end of file From 9f6b6adda6cd001cf43e0510c16bd2c23930ebd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 10 Apr 2025 21:48:44 +0900 Subject: [PATCH 028/357] =?UTF-8?q?feat=20:=20deploy.yml=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 84 ++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..1bedf19c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,84 @@ +name: Sarang Backend CI/CD + +on: + push: + branches: [main] + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Gradle 캐시 적용 + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle + + - name: JDK 17 세팅 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: YML 파일 세팅 + env: + APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }} + # TEST_APPLICATION_PROPERTIES: ${{ secrets.TEST_APPLICATION_PROPERTIES }} + run: | + cd ./src + rm -rf main/resources/application.yml + mkdir -p test/resources + echo "$APPLICATION_PROPERTIES" > main/resources/application.yml + # echo "$TEST_APPLICATION_PROPERTIES" > test/resources/application.yml + + - name: gradlew 권한 부여 + run: chmod +x gradlew + + - name: 테스트 수행 + run: ./gradlew test + + - name: 스프링부트 빌드 + run: ./gradlew build + + - name: Docker Buildx 세팅 + uses: docker/setup-buildx-action@v3 + + - name: NCP 레지스트리 로그인 + uses: docker/login-action@v3 + with: + registry: ${{ secrets.NCP_CONTAINER_REGISTRY }} + username: ${{ secrets.NCP_ACCESS_KEY }} + password: ${{ secrets.NCP_SECRET_KEY }} + + - name: 도커 이미지 빌드 후 푸시 + if: success() + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/sarang-backend:${{ github.sha }} + platforms: linux/amd64,linux/arm64 + + - name: NCP 접속 후 이미지 다운로드 및 배포 + if: success() + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.NCP_HOST }} + username: ${{ secrets.NCP_USERNAME }} + password: ${{ secrets.NCP_PASSWORD }} + port: ${{ secrets.NCP_PORT }} + script: | + docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/sarang-backend:${{ github.sha }} + sudo chmod +x ./deploy.sh + export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} + export GITHUB_SHA=${{ github.sha }} + ./deploy.sh \ No newline at end of file From f48b5f7da91c1e927193af166474e47212c9b32b Mon Sep 17 00:00:00 2001 From: kimyeoungrok <127182406+kimyeoungrok@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:53:28 +0900 Subject: [PATCH 029/357] =?UTF-8?q?deploy.yml=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main/resource가 없다고 해서 생성로직 추가 --- .github/workflows/deploy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1bedf19c..084840d5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,6 +36,7 @@ jobs: cd ./src rm -rf main/resources/application.yml mkdir -p test/resources + mkdir -p main/resources echo "$APPLICATION_PROPERTIES" > main/resources/application.yml # echo "$TEST_APPLICATION_PROPERTIES" > test/resources/application.yml @@ -81,4 +82,4 @@ jobs: sudo chmod +x ./deploy.sh export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} export GITHUB_SHA=${{ github.sha }} - ./deploy.sh \ No newline at end of file + ./deploy.sh From bad52b37f351e6cac075a77b19acf422e3d376b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 10 Apr 2025 23:03:57 +0900 Subject: [PATCH 030/357] =?UTF-8?q?fix=20:=20docker=20EXPOSE=20=EB=B0=94?= =?UTF-8?q?=EA=BE=B8=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e00bc4d9..fa87115a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM openjdk:17.0.1-jdk-slim WORKDIR /app COPY ./build/libs/backend-0.0.1-SNAPSHOT.jar /app/backend.jar -EXPOSE 8080 +EXPOSE 80 ENTRYPOINT ["java"] CMD ["-jar", "backend.jar"] \ No newline at end of file From 5e0aef7d3c395d1a7be970c7cbcc145041d28e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 11 Apr 2025 18:02:43 +0900 Subject: [PATCH 031/357] =?UTF-8?q?feat=20:=20blue-green=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=EB=9D=BC=20=EB=8F=84=EC=BB=A4=20=EB=91=90=20=EA=B0=9C=20?= =?UTF-8?q?=EB=91=98=EA=B1=B0=EC=9E=84=20->=20docker-compose=EB=A1=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=ED=95=98=EB=8A=94=EA=B2=8C=20=EC=9A=A9?= =?UTF-8?q?=EC=9D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9ebf81fb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + blue: + image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" + container_name: sarang-backend-blue + env_file: + - .env + environment: + TZ: Asia/Seoul + ports: + - '8080:8080' + networks: + - sarang-backend-network + + green: + image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" + container_name: sarang-backend-green + env_file: + - .env + environment: + TZ: Asia/Seoul + ports: + - '8081:8080' + networks: + - sarang-backend-network From da9c4c09c653742d09a3ed672167e4af86e47063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 11 Apr 2025 18:03:37 +0900 Subject: [PATCH 032/357] =?UTF-8?q?feat=20:=20docker-compose=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20ncp=EB=A1=9C=20=EC=A0=84=EC=86=A1=ED=95=98=EA=B8=B0?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=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 --- .github/workflows/deploy.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 084840d5..4f799909 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,6 +12,11 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Set up SSH key + uses: webfactory/ssh-agent@v0.5.3 + with: + ssh-private-key: ${{ secrets.NCP_SSH_PRIVATE_KEY }} + - name: Gradle 캐시 적용 uses: actions/cache@v3 with: @@ -69,6 +74,9 @@ jobs: tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/sarang-backend:${{ github.sha }} platforms: linux/amd64,linux/arm64 + - name: Docker Compose 파일 NCP 서버로 전송 + run: scp -o StrictHostKeyChecking=no -P ${{ secrets.NCP_PORT }} docker-compose.yml ${{ secrets.NCP_USERNAME }}@${{ secrets.NCP_HOST }}:./ + - name: NCP 접속 후 이미지 다운로드 및 배포 if: success() uses: appleboy/ssh-action@master From 2fa49e23ae3b9f56461f9b2b75be83ee093ca021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 11 Apr 2025 18:32:21 +0900 Subject: [PATCH 033/357] =?UTF-8?q?feat=20:=20docker-compose=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=97=90=20=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=EA=B0=80=20=EC=A0=95=EC=9D=98=EB=90=98=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=95=84=EC=84=9C=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EC=A0=95=EC=9D=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 9ebf81fb..b3886cc0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,3 +22,7 @@ services: - '8081:8080' networks: - sarang-backend-network + +networks: + cnergy-backend-network: + driver: bridge From 1609d47c4511d55a6c0067902ed40bda12de15d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 11 Apr 2025 18:35:54 +0900 Subject: [PATCH 034/357] =?UTF-8?q?fix=20:=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EC=9D=98=EB=90=98=EC=96=B4?= =?UTF-8?q?=EC=9E=88=EC=96=B4=EC=84=9C=20=EC=88=98=EC=A0=95=ED=95=A8(cnerg?= =?UTF-8?q?y=EA=B0=80=20=EC=95=84=EB=8B=88=EB=9D=BC=20sarang=EC=9D=B8?= =?UTF-8?q?=EB=8D=B0...)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b3886cc0..7df270b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,5 +24,5 @@ services: - sarang-backend-network networks: - cnergy-backend-network: + sarang-backend-network: driver: bridge From cc4d9537cd53fa547c9e7234183e64f08e9b14e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 11 Apr 2025 18:42:11 +0900 Subject: [PATCH 035/357] =?UTF-8?q?fix=20:=20env=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EB=8A=94=20=EC=A0=95=EC=9D=98=EB=90=9C?= =?UTF-8?q?=EA=B2=8C=20=EC=97=86=EC=96=B4=EC=84=9C=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7df270b4..91edd78d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,6 @@ services: blue: image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" container_name: sarang-backend-blue - env_file: - - .env environment: TZ: Asia/Seoul ports: @@ -14,8 +12,6 @@ services: green: image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" container_name: sarang-backend-green - env_file: - - .env environment: TZ: Asia/Seoul ports: From feaa8d8e22d969c1152006165c8e26381ca08b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 11 Apr 2025 18:46:47 +0900 Subject: [PATCH 036/357] =?UTF-8?q?fix=20:=20image=EB=AA=85=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=98=EC=96=B4=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 91edd78d..5fdc1a5b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: blue: - image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" + image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" container_name: sarang-backend-blue environment: TZ: Asia/Seoul @@ -10,7 +10,7 @@ services: - sarang-backend-network green: - image: "${NCP_CONTAINER_REGISTRY}/cnergy-backend:${GITHUB_SHA}" + image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" container_name: sarang-backend-green environment: TZ: Asia/Seoul From bff5cb9d7214725697d83da4643aa337a80bf1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 11 Apr 2025 18:58:06 +0900 Subject: [PATCH 037/357] =?UTF-8?q?feath=20:=20=ED=97=AC=EC=8A=A4=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80(=EC=9D=B4?= =?UTF-8?q?=EA=B1=B0=EC=95=88=ED=95=B4=EC=84=9C=20deploy.sh=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=97=AC=EC=8A=A4=EC=B2=B4=ED=81=AC=EA=B0=80=20?= =?UTF-8?q?=EC=95=88=EB=90=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/auth/presentation/MainController.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt index 9f22723a..8fe0355d 100644 --- a/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt +++ b/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt @@ -13,4 +13,9 @@ class MainController { return "곰신 서버" } + @GetMapping("/health") + fun healthCheck() : String { + return "OK" + } + } From 17b36f61c581cde97d83faa97c81393702c2697a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 11 Apr 2025 19:01:57 +0900 Subject: [PATCH 038/357] =?UTF-8?q?fix=20:=20=ED=97=AC=EC=8A=A4=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C(=EC=95=8C?= =?UTF-8?q?=EA=B3=A0=EB=B3=B4=EB=8B=88=20=EC=9D=B4=EB=AF=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=90=98=EC=96=B4=EC=9E=88=EC=97=88=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/auth/presentation/MainController.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt index 8fe0355d..9f22723a 100644 --- a/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt +++ b/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt @@ -13,9 +13,4 @@ class MainController { return "곰신 서버" } - @GetMapping("/health") - fun healthCheck() : String { - return "OK" - } - } From 68ad043f2281e45b7c592aa7daf58ef63720b0c7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Apr 2025 15:41:26 +0900 Subject: [PATCH 039/357] =?UTF-8?q?chore:=20Spring=20Security=20SessionCre?= =?UTF-8?q?ationPolicy=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/SecurityConfiguration.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 1945add3..0e9e03c4 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -10,6 +10,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter @@ -40,6 +41,9 @@ class SecurityConfiguration( .httpBasic { it.disable() } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } .oauth2Login { oAuth2LoginConfigurer -> oAuth2LoginConfigurer .userInfoEndpoint { userInfoEndpointConfigurer -> From 3f48b0b49824ab5b635a904cff6c6935c4147f86 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Apr 2025 15:45:49 +0900 Subject: [PATCH 040/357] =?UTF-8?q?chore:=20redis=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=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 --- build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 7fd67fa9..386f76db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,10 @@ dependencies { // logging implementation("io.github.microutils:kotlin-logging:2.0.11") + // redis + implementation("org.springframework.boot:spring-boot-starter-data-redis:3.3.10") + implementation("org.redisson:redisson:3.44.0") + //security implementation("org.springframework.boot:spring-boot-starter-security") From d969bfed039cc050c0bef1e1225d06da541def98 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Apr 2025 18:10:53 +0900 Subject: [PATCH 041/357] =?UTF-8?q?chore:=20Cache=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=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 --- build.gradle.kts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 386f76db..c7bc739f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,8 @@ plugins { - kotlin("jvm") version "1.9.25" - kotlin("plugin.spring") version "1.9.25" - kotlin("plugin.jpa") version "1.9.25" + kotlin("jvm") version "2.1.0" + kotlin("plugin.spring") version "2.1.0" + kotlin("plugin.jpa") version "2.1.0" + kotlin("plugin.allopen") version "2.1.0" id("org.springframework.boot") version "3.4.3" id("io.spring.dependency-management") version "1.1.7" } @@ -41,6 +42,10 @@ dependencies { // logging implementation("io.github.microutils:kotlin-logging:2.0.11") + // cache + implementation("org.springframework.boot:spring-boot-starter-cache:3.3.10") + implementation("com.github.ben-manes.caffeine:caffeine:3.1.8") + // redis implementation("org.springframework.boot:spring-boot-starter-data-redis:3.3.10") implementation("org.redisson:redisson:3.44.0") @@ -73,8 +78,9 @@ kotlin { allOpen { annotation("jakarta.persistence.Entity") - annotation("jakarta.persistence.MappedSuperclass") annotation("jakarta.persistence.Embeddable") + annotation("jakarta.persistence.MappedSuperclass") + annotation("org.springframework.stereotype.Component") } tasks.withType { From 4917811837ecd034af278d7ca6e0aa1d30d53938 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Apr 2025 18:11:08 +0900 Subject: [PATCH 042/357] =?UTF-8?q?feat:=20Local=20=EC=BA=90=EC=8B=9C=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 --- .../annotation/cacheable/LocalCacheOnly.kt | 8 ++++++ .../cache/annotation/evict/LocalCacheEvict.kt | 8 ++++++ .../configuration/LocalCacheConfiguration.kt | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/LocalCacheOnly.kt create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/LocalCacheEvict.kt create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/configuration/LocalCacheConfiguration.kt diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/LocalCacheOnly.kt b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/LocalCacheOnly.kt new file mode 100644 index 00000000..9fde046e --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/LocalCacheOnly.kt @@ -0,0 +1,8 @@ +package gomushin.backend.core.common.cache.annotation.cacheable + +import org.springframework.cache.annotation.Cacheable + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@Cacheable("localCache", cacheManager = "localCacheManager") +annotation class LocalCacheOnly diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/LocalCacheEvict.kt b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/LocalCacheEvict.kt new file mode 100644 index 00000000..0dd162ff --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/LocalCacheEvict.kt @@ -0,0 +1,8 @@ +package gomushin.backend.core.common.cache.annotation.evict + +import org.springframework.cache.annotation.CacheEvict + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@CacheEvict("localCache", cacheManager = "localCacheManager", allEntries = true) +annotation class LocalCacheEvict diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/configuration/LocalCacheConfiguration.kt b/src/main/kotlin/gomushin/backend/core/common/cache/configuration/LocalCacheConfiguration.kt new file mode 100644 index 00000000..4a9ec4ea --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/configuration/LocalCacheConfiguration.kt @@ -0,0 +1,27 @@ +package gomushin.backend.core.common.cache.configuration + +import com.github.benmanes.caffeine.cache.Caffeine +import org.springframework.cache.annotation.EnableCaching +import org.springframework.context.annotation.Configuration +import org.springframework.cache.CacheManager +import org.springframework.cache.caffeine.CaffeineCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import java.time.Duration + +@Configuration +@EnableCaching +class LocalCacheConfiguration { + + @Primary + @Bean + fun localCacheManager(): CacheManager { + val caffeineCacheManager = CaffeineCacheManager() + caffeineCacheManager.setCaffeine( + Caffeine.newBuilder() + .maximumSize(100) + .expireAfterWrite(Duration.ofMinutes(5)) + ) + return caffeineCacheManager + } +} From 1488e23b268481eed77fac1331ba5a836cac4cd1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Apr 2025 18:11:16 +0900 Subject: [PATCH 043/357] =?UTF-8?q?feat:=20=EB=B6=84=EC=82=B0=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cacheable/DistributedCacheOnly.kt | 8 ++++ .../annotation/evict/DistributedCacheEvict.kt | 8 ++++ .../DistributedCacheConfiguration.kt | 40 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/DistributedCacheOnly.kt create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/DistributedCacheEvict.kt create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/configuration/DistributedCacheConfiguration.kt diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/DistributedCacheOnly.kt b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/DistributedCacheOnly.kt new file mode 100644 index 00000000..102952ac --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/DistributedCacheOnly.kt @@ -0,0 +1,8 @@ +package gomushin.backend.core.common.cache.annotation.cacheable + +import org.springframework.cache.annotation.Cacheable + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@Cacheable("distributedCache", cacheManager = "distributedCacheManager") +annotation class DistributedCacheOnly diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/DistributedCacheEvict.kt b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/DistributedCacheEvict.kt new file mode 100644 index 00000000..e7e70133 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/DistributedCacheEvict.kt @@ -0,0 +1,8 @@ +package gomushin.backend.core.common.cache.annotation.evict + +import org.springframework.cache.annotation.CacheEvict + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@CacheEvict("distributedCache", cacheManager = "distributedCacheManager", allEntries = true) +annotation class DistributedCacheEvict diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/configuration/DistributedCacheConfiguration.kt b/src/main/kotlin/gomushin/backend/core/common/cache/configuration/DistributedCacheConfiguration.kt new file mode 100644 index 00000000..3c1b10f0 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/configuration/DistributedCacheConfiguration.kt @@ -0,0 +1,40 @@ +package gomushin.backend.core.common.cache.configuration + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.cache.CacheManager +import org.springframework.cache.annotation.EnableCaching +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.cache.RedisCacheConfiguration +import org.springframework.data.redis.cache.RedisCacheManager +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer +import org.springframework.data.redis.serializer.RedisSerializationContext +import org.springframework.data.redis.serializer.StringRedisSerializer +import java.time.Duration + +@Configuration +@EnableCaching +class DistributedCacheConfiguration { + + @Bean + fun distributedCacheManager( + redisConnectionFactory: RedisConnectionFactory, + redisCacheConfiguration: RedisCacheConfiguration + ): CacheManager = RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build() + + @Bean + fun redisCacheConfiguration(objectMapper: ObjectMapper): RedisCacheConfiguration { + val serializer = GenericJackson2JsonRedisSerializer(objectMapper) + return RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(serializer) + ) + .entryTtl(Duration.ofMinutes(10)) + } +} From 00b951971383fe68cea0b0f013036ee928f52e23 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 10 Apr 2025 18:11:20 +0900 Subject: [PATCH 044/357] =?UTF-8?q?feat:=202=EA=B3=84=EC=B8=B5=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cacheable/MultiLayerCacheApply.kt | 8 +++ .../annotation/evict/MultiLayerCacheEvict.kt | 8 +++ .../cache/configuration/MultiLayerCache.kt | 65 +++++++++++++++++++ .../configuration/MultiLayerCacheManager.kt | 26 ++++++++ 4 files changed, 107 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/MultiLayerCacheApply.kt create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/MultiLayerCacheEvict.kt create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/configuration/MultiLayerCache.kt create mode 100644 src/main/kotlin/gomushin/backend/core/common/cache/configuration/MultiLayerCacheManager.kt diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/MultiLayerCacheApply.kt b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/MultiLayerCacheApply.kt new file mode 100644 index 00000000..d96218e3 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/cacheable/MultiLayerCacheApply.kt @@ -0,0 +1,8 @@ +package gomushin.backend.core.common.cache.annotation.cacheable + +import org.springframework.cache.annotation.Cacheable + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@Cacheable("multiLayerCache", cacheManager = "multiLayerCacheManager") +annotation class MultiLayerCacheApply diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/MultiLayerCacheEvict.kt b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/MultiLayerCacheEvict.kt new file mode 100644 index 00000000..170cd504 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/annotation/evict/MultiLayerCacheEvict.kt @@ -0,0 +1,8 @@ +package gomushin.backend.core.common.cache.annotation.evict + +import org.springframework.cache.annotation.CacheEvict + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@CacheEvict("multiLayerCache", cacheManager = "multiLayerCacheManager", allEntries = true) +annotation class MultiLayerCacheEvict diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/configuration/MultiLayerCache.kt b/src/main/kotlin/gomushin/backend/core/common/cache/configuration/MultiLayerCache.kt new file mode 100644 index 00000000..a6f71cfc --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/configuration/MultiLayerCache.kt @@ -0,0 +1,65 @@ +package gomushin.backend.core.common.cache.configuration + +import org.springframework.cache.Cache +import java.util.concurrent.Callable + +class MultiLayerCache( + val localCache: Cache, + val distributedCache: Cache, +) : Cache { + override fun getName(): String { + return localCache.name + } + + override fun getNativeCache(): Any { + return localCache.nativeCache + } + + override fun get(key: Any): Cache.ValueWrapper? { + var value = localCache.get(key) + if (value == null) { + value = distributedCache.get(key) + if (value != null) { + localCache.put(key, value.get()) + } + } + return value + } + + override fun get(key: Any, type: Class?): T? { + var value = localCache.get(key, type) + if (value == null) { + value = distributedCache.get(key, type) + if (value != null) { + localCache.put(key, value) + } + } + return value + } + + override fun get(key: Any, valueLoader: Callable): T? { + var value = localCache.get(key, valueLoader) + if (value == null) { + value = distributedCache.get(key, valueLoader) + if (value != null) { + localCache.put(key, value) + } + } + return value + } + + override fun put(key: Any, value: Any?) { + distributedCache.put(key, value) + localCache.put(key, value) + } + + override fun evict(key: Any) { + localCache.evict(key) + distributedCache.evict(key) + } + + override fun clear() { + localCache.clear() + distributedCache.clear() + } +} diff --git a/src/main/kotlin/gomushin/backend/core/common/cache/configuration/MultiLayerCacheManager.kt b/src/main/kotlin/gomushin/backend/core/common/cache/configuration/MultiLayerCacheManager.kt new file mode 100644 index 00000000..c28c7dec --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/cache/configuration/MultiLayerCacheManager.kt @@ -0,0 +1,26 @@ +package gomushin.backend.core.common.cache.configuration + +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.cache.Cache +import org.springframework.cache.CacheManager +import org.springframework.stereotype.Component +import java.util.stream.Collectors +import java.util.stream.Stream + +@Component +class MultiLayerCacheManager( + @Qualifier("localCacheManager") private val localCacheManager: CacheManager, + @Qualifier("distributedCacheManager") private val distributedCacheManager: CacheManager +) : CacheManager { + + override fun getCache(name: String): Cache = MultiLayerCache( + localCacheManager.getCache("localCache")!!, + distributedCacheManager.getCache("distributedCache")!! + ) + + override fun getCacheNames(): MutableCollection = Stream.concat( + localCacheManager.cacheNames.stream(), + distributedCacheManager.cacheNames.stream() + ).collect(Collectors.toList()) + +} From 47a25d0eed59a1aa32f4051fbe911304d90af49e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:28:47 +0900 Subject: [PATCH 045/357] =?UTF-8?q?chore:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- .../swagger/SwaggerConfiguration.kt | 42 ------------------- .../exception/UnauthorizedException.kt | 7 ---- 3 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt delete mode 100644 src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt diff --git a/build.gradle.kts b/build.gradle.kts index c7bc739f..a6ff3033 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ dependencies { runtimeOnly("mysql:mysql-connector-java:8.0.33") // swagger - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0") // mail implementation("org.springframework.boot:spring-boot-starter-mail") diff --git a/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt deleted file mode 100644 index b9ccf2d5..00000000 --- a/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt +++ /dev/null @@ -1,42 +0,0 @@ -package gomushin.backend.core.configuration.swagger - -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.springdoc.core.models.GroupedOpenApi -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -@Configuration -class SwaggerConfiguration { - @Bean - fun publicApi(): GroupedOpenApi { - return GroupedOpenApi.builder() - .group("base-service") - .pathsToMatch("/**") - .build() - } - - @Bean - fun customOpenAPI( - @Value("\${application-description}") appDescription: String?, @Value( - "\${application-version}" - ) appVersion: String? - ): OpenAPI { - val contact = Contact() - contact.email = "abc29887@naver.com" - contact.name = "HOYEONG JEON" - return OpenAPI() - .info( - Info() - .title("사랑하는군 API") - .version(appVersion) - .description(appDescription) - .termsOfService("http://swagger.io/terms/") - .license(License().name("Apache 2.0").url("http://springdoc.org")) - .contact(contact) - ) - } -} diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt deleted file mode 100644 index 057feac7..00000000 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt +++ /dev/null @@ -1,7 +0,0 @@ -package gomushin.backend.core.infrastructure.exception - -import gomushin.backend.core.common.web.response.ExtendedHttpStatus -import gomushin.backend.core.common.web.response.exception.ErrorCodeResolvingApiErrorException - -class UnauthorizedException(code: String = "unauthorized", cause: Throwable?) : - ErrorCodeResolvingApiErrorException(ExtendedHttpStatus.UNAUTHORIZED, code, cause) From 0854993ded386799aff40db20580cb3e25687db8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:29:31 +0900 Subject: [PATCH 046/357] =?UTF-8?q?refactor:=20Member=EC=97=90=20role=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 --- .../backend/member/domain/entity/Member.kt | 22 ++++++++++++++----- .../domain/repository/MemberRepository.kt | 1 - .../backend/member/domain/value/Role.kt | 6 +++++ 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/member/domain/value/Role.kt diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index ce03922e..ae41d7d2 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -2,7 +2,9 @@ package gomushin.backend.member.domain.entity import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity import gomushin.backend.member.domain.value.Provider +import gomushin.backend.member.domain.value.Role import jakarta.persistence.* +import java.time.LocalDate @Entity @Table(name = "member") @@ -11,25 +13,35 @@ class Member( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L, - var name: String, + @Column(name = "nickname", nullable = false) + var nickname: String, - @Column(unique = true) + @Column(name = "email", unique = true, nullable = false) var email: String, + @Column(name = "birth_date") + var birthDate: LocalDate? = null, + + @Column(name = "profile_image_url") var profileImageUrl: String?, @Enumerated(EnumType.STRING) + @Column(name = "provider", nullable = false) val provider: Provider, -): BaseEntity() { + + @Enumerated(EnumType.STRING) + @Column(name = "role", nullable = false) + var role: Role = Role.GUEST, +) : BaseEntity() { companion object { fun create( - name: String, + nickname: String, email: String, profileImageUrl: String?, provider: Provider ): Member { return Member( - name = name, + nickname = nickname, email = email, profileImageUrl = profileImageUrl ?: "", provider = provider, diff --git a/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt b/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt index 7f11dcee..7e86a7ab 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt @@ -4,6 +4,5 @@ import gomushin.backend.member.domain.entity.Member import org.springframework.data.jpa.repository.JpaRepository interface MemberRepository : JpaRepository { - fun findByName(name: String): Member? fun findByEmail(email: String): Member? } diff --git a/src/main/kotlin/gomushin/backend/member/domain/value/Role.kt b/src/main/kotlin/gomushin/backend/member/domain/value/Role.kt new file mode 100644 index 00000000..7225670e --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/value/Role.kt @@ -0,0 +1,6 @@ +package gomushin.backend.member.domain.value + +enum class Role { + GUEST, + MEMBER, +} From 650835764609b1498f4e714d45167bc9896ea1a5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:30:42 +0900 Subject: [PATCH 047/357] =?UTF-8?q?refactor:=20Member=EC=97=90=20role=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 --- .../gomushin/backend/core/CustomUserDetails.kt | 5 +++-- .../security/CustomCorsConfiguration.kt | 3 ++- .../filter/JwtAuthenticationFilter.kt | 5 ++++- .../backend/core/jwt/JwtTokenProvider.kt | 2 +- .../jwt/infrastructure/JwtTokenProviderImpl.kt | 8 +++++--- .../backend/core/oauth/CustomOAuth2User.kt | 4 ++++ .../core/oauth/handler/CustomSuccessHandler.kt | 6 +++--- .../oauth/service/CustomOAuth2UserService.kt | 9 +++++---- .../core/service/CustomUserDetailsService.kt | 17 +++++------------ 9 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt b/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt index 34958acb..5cd70851 100644 --- a/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt +++ b/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt @@ -2,13 +2,14 @@ package gomushin.backend.core import gomushin.backend.member.domain.entity.Member import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.UserDetails class CustomUserDetails( private val member: Member, ) : UserDetails { override fun getAuthorities(): MutableCollection { - return mutableListOf() + return mutableListOf(SimpleGrantedAuthority("ROLE_${member.role.name}")) } override fun getPassword(): String { @@ -16,7 +17,7 @@ class CustomUserDetails( } override fun getUsername(): String { - return member.name + return member.nickname } override fun isAccountNonExpired(): Boolean { diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index 14dc9e6c..65d7b840 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -12,7 +12,8 @@ class CustomCorsConfiguration { @Bean fun corsConfigurationSource(): CorsConfigurationSource { val configuration = CorsConfiguration() - configuration.allowedOrigins = listOf("http://localhost:5173", "http://localhost:8080") + configuration.allowedOrigins = + listOf("http://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app/") configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") configuration.allowCredentials = true diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index 40f6fffe..72cf3d2e 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -17,7 +17,9 @@ class JwtAuthenticationFilter( ) : OncePerRequestFilter() { override fun shouldNotFilter(request: HttpServletRequest): Boolean { - return request.requestURI.contains("/v1/auth") || request.requestURI.contains("/v1/oauth") + return request.requestURI.contains("/v1/auth") || request.requestURI.contains("/v1/oauth") || request.requestURI.contains( + "/swagger" + ) || request.requestURI.contains("/v3/api-docs") || request.requestURI.contains("/api-docs") } override fun doFilterInternal( @@ -40,6 +42,7 @@ class JwtAuthenticationFilter( private fun applyAuthentication(token: String) { val userId = tokenProvider.getMemberIdFromToken(token) val userDetails = customUserDetailsService.loadUserById(userId) + val auth = UsernamePasswordAuthenticationToken( userDetails, null, diff --git a/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt b/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt index 5c4dbe13..dd6b927c 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt @@ -1,7 +1,7 @@ package gomushin.backend.core.jwt interface JwtTokenProvider { - fun provideAccessToken(userId: Long): String + fun provideAccessToken(userId: Long, role: String): String fun getMemberIdFromToken(token: String): Long fun validateToken(token: String): Boolean } diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt index 926bacd1..19f11bde 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt @@ -22,8 +22,8 @@ class JwtTokenProviderImpl( val ACCESS_TOKEN_EXPIRATION = jwtProperties.accessTokenExpiration val REFRESH_TOKEN_EXPIRATION = jwtProperties.refreshTokenExpiration - override fun provideAccessToken(userId: Long): String { - return createToken(userId, ACCESS_TOKEN_EXPIRATION, Type.ACCESS) + override fun provideAccessToken(userId: Long, role: String): String { + return createToken(userId, role, ACCESS_TOKEN_EXPIRATION, Type.ACCESS) } override fun getMemberIdFromToken(token: String): Long { @@ -41,12 +41,13 @@ class JwtTokenProviderImpl( is UnsupportedJwtException, is MalformedJwtException, is IllegalArgumentException -> return false + else -> throw e } } } - private fun createToken(userId: Long, expiration: Long, type: Type): String { + private fun createToken(userId: Long, role: String, expiration: Long, type: Type): String { val expirationMs = expiration * 60 * 1000 val expiryDate = Date(System.currentTimeMillis() + expirationMs) @@ -55,6 +56,7 @@ class JwtTokenProviderImpl( .audience().add(AUDIENCE).and() .subject(userId.toString()) .claim("type", type.name) + .claim("role", role) .issuedAt(Date()) .expiration(expiryDate) .signWith(SECRET_KEY) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt b/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt index e7285a07..9688431a 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt @@ -27,4 +27,8 @@ class CustomOAuth2User( fun getUserId(): Long { return userDto.userId } + + fun getRole() : String { + return userDto.role + } } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 8040a07f..cdcf8436 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -34,13 +34,13 @@ class CustomSuccessHandler( var accessToken = "" getMemberByEmail(principal.getEmail())?.let { - accessToken = jwtTokenProvider.provideAccessToken(it.id) + accessToken = jwtTokenProvider.provideAccessToken(it.id, it.role.name) } ?: run { - accessToken = jwtTokenProvider.provideAccessToken(principal.getUserId()) + accessToken = jwtTokenProvider.provideAccessToken(principal.getUserId(), principal.getRole()) } response!!.addCookie(createCookie("access_token", accessToken)) - response.sendRedirect("http://localhost:8080") // TODO: 프론트엔드 주소로 변경 , 환경변수 처리 + response.sendRedirect("https://frontend-sarang.vercel.app") } private fun createCookie(key: String, value: String): Cookie { diff --git a/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt b/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt index 29dd7c1d..417f49ae 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt @@ -7,6 +7,7 @@ import gomushin.backend.core.oauth.CustomOAuth2User import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Provider +import gomushin.backend.member.domain.value.Role import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest import org.springframework.security.oauth2.core.user.OAuth2User @@ -32,7 +33,7 @@ class CustomOAuth2UserService( getMemberByEmail(email)?.let { it.email = oAuth2Response.getEmail() - it.name = oAuth2Response.getName() + it.nickname = oAuth2Response.getName() it.profileImageUrl = oAuth2Response.getProfileImage() val savedMember = memberRepository.save(it) @@ -42,7 +43,7 @@ class CustomOAuth2UserService( name = oAuth2Response.getName(), email = oAuth2Response.getEmail(), profileImage = oAuth2Response.getProfileImage(), - role = "ROLE_USER", + role = Role.MEMBER.name, registrationId = registrationId, userId = savedMember.id, ) @@ -50,7 +51,7 @@ class CustomOAuth2UserService( return CustomOAuth2User(userDto) } ?: run { val newMember = Member.create( - name = oAuth2Response.getName(), + nickname = oAuth2Response.getName(), email = oAuth2Response.getEmail(), profileImageUrl = oAuth2Response.getProfileImage(), provider = Provider.getProviderByValue(registrationId) @@ -63,7 +64,7 @@ class CustomOAuth2UserService( name = oAuth2Response.getName(), email = oAuth2Response.getEmail(), profileImage = oAuth2Response.getProfileImage(), - role = "ROLE_USER", + role = Role.MEMBER.name, registrationId = registrationId, userId = savedMember.id, ) diff --git a/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt b/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt index 84f68d87..009c027f 100644 --- a/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt +++ b/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt @@ -2,7 +2,6 @@ package gomushin.backend.core.service import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.infrastructure.exception.BadRequestException -import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService @@ -10,20 +9,14 @@ import org.springframework.stereotype.Service @Service class CustomUserDetailsService(private val memberRepository: MemberRepository) : UserDetailsService { - override fun loadUserByUsername(username: String?): UserDetails { - if (username == null) { - throw BadRequestException("saranggun.member.not-provided-name") - } - - return memberRepository.findByName(username!!)?.let { createUserDetail(it) } + override fun loadUserByUsername(email: String): UserDetails { // 파라미터명 username → email로 변경 + val member = memberRepository.findByEmail(email) ?: throw BadRequestException("sarangggun.member.not-exist-member") + return CustomUserDetails(member) } fun loadUserById(id: Long): UserDetails = - memberRepository.findById(id).map { createUserDetail(it) } + memberRepository.findById(id) + .map { CustomUserDetails(it) } .orElseThrow { BadRequestException("sarangggun.member.not-exist-member") } - - private fun createUserDetail(member: Member): UserDetails { - return CustomUserDetails(member) - } } From 9e5f8c4c53d0db5ccba30da30d673f448eaee8d4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:31:16 +0900 Subject: [PATCH 048/357] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9,=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/common/web/response/ApiResponse.kt | 2 ++ .../kotlin/gomushin/backend/member/presentation/ApiPath.kt | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt diff --git a/src/main/kotlin/gomushin/backend/core/common/web/response/ApiResponse.kt b/src/main/kotlin/gomushin/backend/core/common/web/response/ApiResponse.kt index 282e5172..2ca812d8 100644 --- a/src/main/kotlin/gomushin/backend/core/common/web/response/ApiResponse.kt +++ b/src/main/kotlin/gomushin/backend/core/common/web/response/ApiResponse.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty import gomushin.backend.core.common.web.response.exception.ApiError +import io.swagger.v3.oas.annotations.media.Schema import java.util.concurrent.CompletableFuture import java.util.function.Function import java.util.function.Supplier @@ -20,6 +21,7 @@ data class ApiResponse( val result: T? = null, @get:JsonProperty("error") + @Schema(hidden = true) val error: ApiError? = null, ) { init { diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt new file mode 100644 index 00000000..6f8e66bd --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -0,0 +1,6 @@ +package gomushin.backend.member.presentation + +object ApiPath { + const val ONBOARDING = "/v1/member/onboarding" + const val MY_INFO = "/v1/member/my-info" +} From 387356fa11c7e933fb1feca0c0a80b139d06176b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:31:37 +0900 Subject: [PATCH 049/357] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=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 --- .../member/application/OnboardingFacade.kt | 13 ++++++++ .../domain/service/OnboardingService.kt | 22 ++++++++++++++ .../presentation/OnboardingController.kt | 30 +++++++++++++++++++ .../dto/request/OnboardingRequest.kt | 12 ++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt create mode 100644 src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt create mode 100644 src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt create mode 100644 src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt diff --git a/src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt b/src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt new file mode 100644 index 00000000..b26c8168 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt @@ -0,0 +1,13 @@ +package gomushin.backend.member.application + +import gomushin.backend.member.domain.service.OnboardingService +import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import org.springframework.stereotype.Component + +@Component +class OnboardingFacade( + private val onboardingService: OnboardingService, +) { + + fun onboarding(id: Long, onboardingRequest: OnboardingRequest) = onboardingService.onboarding(id, onboardingRequest) +} diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt new file mode 100644 index 00000000..9beed943 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt @@ -0,0 +1,22 @@ +package gomushin.backend.member.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Role +import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class OnboardingService( + private val memberRepository: MemberRepository +) { + + @Transactional + fun onboarding(id: Long, onboardingRequest: OnboardingRequest) { + val member = memberRepository.findById(id).orElseThrow { BadRequestException("sarangggun.member.not-exist-member") } + member.nickname = onboardingRequest.nickname + member.birthDate = onboardingRequest.birthDate + member.role = Role.MEMBER + } +} diff --git a/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt b/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt new file mode 100644 index 00000000..3c30a314 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt @@ -0,0 +1,30 @@ +package gomushin.backend.member.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.member.application.OnboardingFacade +import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "온보딩", description = "OnboardingController") +class OnboardingController( + private val onboardingFacade: OnboardingFacade, +) { + + + @PostMapping(ApiPath.ONBOARDING) + @Operation(summary = "온보딩", description = "onboarding") + fun onboarding( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody onboardingRequest: OnboardingRequest + ): ApiResponse { + onboardingFacade.onboarding(customUserDetails.getId(), onboardingRequest) + return ApiResponse.success(true) + } +} diff --git a/src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt b/src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt new file mode 100644 index 00000000..1bb0d6c8 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt @@ -0,0 +1,12 @@ +package gomushin.backend.member.presentation.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +data class OnboardingRequest( + @Schema(description = "닉네임", example = "nickname") + val nickname: String, + + @Schema(description = "생일", example = "2000-01-01") + val birthDate: LocalDate, +) From 48e7293b62bd3bf889678de0f557ba1fbcf8b575 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:32:02 +0900 Subject: [PATCH 050/357] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/application/MemberInfoFacade.kt | 22 ++++++++++++++++ .../domain/service/MemberInfoService.kt | 18 +++++++++++++ .../presentation/MemberInfoController.kt | 26 +++++++++++++++++++ .../dto/response/GuestInfoResponse.kt | 11 ++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt create mode 100644 src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt create mode 100644 src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt create mode 100644 src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt diff --git a/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt new file mode 100644 index 00000000..5c915a97 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt @@ -0,0 +1,22 @@ +package gomushin.backend.member.application + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.member.domain.service.MemberInfoService +import gomushin.backend.member.presentation.dto.response.GuestInfoResponse +import org.springframework.stereotype.Component + +@Component +class MemberInfoFacade( + private val memberInfoService: MemberInfoService, +) { + fun getMemberInfo(customUserDetails: CustomUserDetails): Any { + val authorities = customUserDetails.authorities + +// if (authorities.any { it.authority == "ROLE_GUEST" }) { + val member = memberInfoService.getGuestInfo(customUserDetails.getId()) + return GuestInfoResponse.of(member) +// } + + // TODO: Member 구현 시 , 분기를 통해 MEMBER와 GUEST를 구분할 수 있도록 수정 + } +} diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt new file mode 100644 index 00000000..9b3293aa --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt @@ -0,0 +1,18 @@ +package gomushin.backend.member.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.repository.MemberRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class MemberInfoService( + private val memberRepository: MemberRepository, +) { + + @Transactional(readOnly = true) + fun getGuestInfo(id: Long): Member = + memberRepository.findById(id).orElseThrow { BadRequestException("sarangggun.member.not-exist-member") } + +} diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt new file mode 100644 index 00000000..4b4aaa7a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -0,0 +1,26 @@ +package gomushin.backend.member.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.member.application.MemberInfoFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "회원 정보", description = "MemberController") +class MemberInfoController( + private val memberInfoFacade: MemberInfoFacade, +) { + + @GetMapping(ApiPath.MY_INFO) + @Operation(summary = "내 정보 조회", description = "getMyInfo") + fun get( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ): ApiResponse { + val member = memberInfoFacade.getMemberInfo(customUserDetails) + return ApiResponse.success(member) + } +} diff --git a/src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt b/src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt new file mode 100644 index 00000000..fa6bc484 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt @@ -0,0 +1,11 @@ +package gomushin.backend.member.presentation.dto.response + +import gomushin.backend.member.domain.entity.Member + +data class GuestInfoResponse( + val nickname: String, +) { + companion object { + fun of(member: Member) = GuestInfoResponse(member.nickname) + } +} From b75256f82db02c7f56c716d42104b5382c16ca0e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:32:35 +0900 Subject: [PATCH 051/357] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9?= =?UTF-8?q?=EC=9D=80=20ROLE=5FGUEST=20=EB=A7=8C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/SecurityConfiguration.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 0e9e03c4..9b104168 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -1,8 +1,8 @@ package gomushin.backend.core.configuration.security -import gomushin.backend.core.oauth.handler.CustomSuccessHandler import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.oauth.handler.CustomSuccessHandler import gomushin.backend.core.oauth.service.CustomOAuth2UserService import gomushin.backend.core.service.CustomUserDetailsService import gomushin.backend.member.domain.repository.MemberRepository @@ -18,7 +18,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @EnableWebSecurity class SecurityConfiguration( private val jwtTokenProvider: JwtTokenProvider, - private val memberRepository: MemberRepository + private val memberRepository: MemberRepository, ) { @Bean @@ -67,6 +67,8 @@ class SecurityConfiguration( "/favicon.ico", "/error" ).permitAll() + it.requestMatchers("/v1/member/onboarding").hasRole("GUEST") + it.anyRequest().authenticated() } .addFilterBefore( JwtAuthenticationFilter( From dff9a40e17204d7a06d42304cdb0eeb87ccaa626 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:32:47 +0900 Subject: [PATCH 052/357] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=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 --- .../application/MemberInfoFacadeTest.kt | 63 +++++++++++++++++++ .../application/OnboardingFacadeTest.kt | 35 +++++++++++ .../domain/service/MemberInfoServiceTest.kt | 48 ++++++++++++++ .../domain/service/OnboardingServiceTest.kt | 58 +++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt create mode 100644 src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt create mode 100644 src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt create mode 100644 src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt diff --git a/src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt new file mode 100644 index 00000000..1c72b3a3 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt @@ -0,0 +1,63 @@ +package gomushin.backend.member.application + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.service.MemberInfoService +import gomushin.backend.member.domain.value.Provider +import gomushin.backend.member.domain.value.Role +import gomushin.backend.member.presentation.dto.response.GuestInfoResponse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import kotlin.test.assertEquals + +@ExtendWith(MockitoExtension::class) +class MemberInfoFacadeTest { + @Mock + private lateinit var memberInfoService: MemberInfoService + + @InjectMocks + private lateinit var memberInfoFacade: MemberInfoFacade + + private lateinit var customUserDetails: CustomUserDetails + private lateinit var member: Member + + @BeforeEach + fun setUp() { + member = Member( + id = 1L, + nickname = "테스트 닉네임", + email = "test@test.com", + birthDate = null, + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.GUEST, + ) + + val authorities = mutableListOf(SimpleGrantedAuthority("ROLE_GUEST")) + customUserDetails = mock(CustomUserDetails::class.java) + + `when`(customUserDetails.getId()).thenReturn(1L) + `when`(customUserDetails.authorities).thenReturn(authorities) + + } + + @DisplayName("내 정보 조회 - [GUEST]") + @Test + fun getMyInfo() { + // given + `when`(memberInfoService.getGuestInfo(customUserDetails.getId())).thenReturn(member) + // when + val result = memberInfoFacade.getMemberInfo(customUserDetails) + // then + verify(memberInfoService).getGuestInfo(1L) + assertEquals(member.nickname, (result as GuestInfoResponse).nickname) + } +} diff --git a/src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt new file mode 100644 index 00000000..b111c346 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt @@ -0,0 +1,35 @@ +package gomushin.backend.member.application + +import gomushin.backend.member.domain.service.OnboardingService +import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.junit.jupiter.MockitoExtension + +@ExtendWith(MockitoExtension::class) +class OnboardingFacadeTest { + + @Mock + private lateinit var onboardingService: OnboardingService + + @InjectMocks + private lateinit var onboardingFacade: OnboardingFacade + + @DisplayName("온보딩 테스트") + @Test + fun onboarding_success() { + // given + val id = 1L + val onboardingRequest = mock(OnboardingRequest::class.java) + // when + onboardingFacade.onboarding(id, onboardingRequest) + // then + verify(onboardingService).onboarding(id, onboardingRequest) + } + +} diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt new file mode 100644 index 00000000..2c15689d --- /dev/null +++ b/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt @@ -0,0 +1,48 @@ +package gomushin.backend.member.domain.service + +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Provider +import gomushin.backend.member.domain.value.Role +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension +import java.util.* +import kotlin.test.assertEquals + +@ExtendWith(MockitoExtension::class) +class MemberInfoServiceTest { + + @Mock + private lateinit var memberRepository: MemberRepository + + @InjectMocks + private lateinit var memberInfoService: MemberInfoService + + @DisplayName("내 정보 조회 - [GUEST]") + @Test + fun getGuestInfo_success() { + // given + val memberId = 1L + val expectedMember = Member( + id = 1L, + nickname = "테스트 닉네임", + email = "test@test.com", + birthDate = null, + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.GUEST, + ) + + // when + `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember)) + val result = memberInfoService.getGuestInfo(memberId) + + // then + assertEquals(expectedMember, result) + } +} diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt new file mode 100644 index 00000000..c81b9050 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt @@ -0,0 +1,58 @@ +package gomushin.backend.member.domain.service + +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Provider +import gomushin.backend.member.domain.value.Role +import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension +import java.time.LocalDate +import java.util.* +import kotlin.test.Test + +@ExtendWith(MockitoExtension::class) +class OnboardingServiceTest { + @Mock + private lateinit var memberRepository: MemberRepository + + private val onboardingService: OnboardingService by lazy { + OnboardingService(memberRepository) + } + + @Test + fun `onboarding 성공 케이스`() { + // given + val memberId = 1L + val existingMember = Member( + id = memberId, + nickname = "원래 닉네임", + email = "test@example.com", + birthDate = LocalDate.of(1990, 1, 1), + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.GUEST, + ) + + val onboardingRequest = OnboardingRequest( + nickname = "새로운 닉네임", + birthDate = LocalDate.of(2000, 1, 1) + ) + + `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(existingMember)) + + // when + onboardingService.onboarding(memberId, onboardingRequest) + + // then + assertEquals("새로운 닉네임", existingMember.nickname) + assertEquals(LocalDate.of(2000, 1, 1), existingMember.birthDate) + assertEquals(Role.MEMBER, existingMember.role) + + verify(memberRepository).findById(memberId) + } +} From 9ed0c7eee8c4a610b48af79e24b0814406210915 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 15 Apr 2025 00:59:28 +0900 Subject: [PATCH 053/357] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=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 --- .../configuration/security/CustomCorsConfiguration.kt | 2 +- .../infrastructure/filter/JwtAuthenticationFilter.kt | 9 ++++++--- .../core/oauth/service/CustomOAuth2UserService.kt | 2 +- .../backend/member/presentation/OnboardingController.kt | 1 - 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index 65d7b840..53a700eb 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -13,7 +13,7 @@ class CustomCorsConfiguration { fun corsConfigurationSource(): CorsConfigurationSource { val configuration = CorsConfiguration() configuration.allowedOrigins = - listOf("http://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app/") + listOf("http://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app") configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") configuration.allowCredentials = true diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index 72cf3d2e..7c4742e9 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -17,9 +17,12 @@ class JwtAuthenticationFilter( ) : OncePerRequestFilter() { override fun shouldNotFilter(request: HttpServletRequest): Boolean { - return request.requestURI.contains("/v1/auth") || request.requestURI.contains("/v1/oauth") || request.requestURI.contains( - "/swagger" - ) || request.requestURI.contains("/v3/api-docs") || request.requestURI.contains("/api-docs") + val excludedPaths = listOf( + "/v1/auth", "/v1/oauth", "/swagger", "/v3/api-docs", "/api-docs" + ) + return excludedPaths.any { path -> + request.requestURI.startsWith(path) || request.requestURI == path + } } override fun doFilterInternal( diff --git a/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt b/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt index 417f49ae..93c77876 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt @@ -54,7 +54,7 @@ class CustomOAuth2UserService( nickname = oAuth2Response.getName(), email = oAuth2Response.getEmail(), profileImageUrl = oAuth2Response.getProfileImage(), - provider = Provider.getProviderByValue(registrationId) + provider = Provider.getProviderByValue(registrationId), ) val savedMember = memberRepository.save(newMember) diff --git a/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt b/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt index 3c30a314..791173b3 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt @@ -17,7 +17,6 @@ class OnboardingController( private val onboardingFacade: OnboardingFacade, ) { - @PostMapping(ApiPath.ONBOARDING) @Operation(summary = "온보딩", description = "onboarding") fun onboarding( From 6a4c57d998e243bdaff8d74c6182e99577e10bb9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Apr 2025 22:30:39 +0900 Subject: [PATCH 054/357] =?UTF-8?q?refactor:=20=EC=98=A8=EB=B3=B4=EB=94=A9?= =?UTF-8?q?=20=EC=8B=9C=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth/handler/CustomSuccessHandler.kt | 3 ++- .../oauth/service/CustomOAuth2UserService.kt | 3 ++- .../backend/member/domain/entity/Member.kt | 19 ++++++++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index cdcf8436..614c0173 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -40,7 +40,8 @@ class CustomSuccessHandler( } response!!.addCookie(createCookie("access_token", accessToken)) - response.sendRedirect("https://frontend-sarang.vercel.app") +// response.sendRedirect("https://frontend-sarang.vercel.app") + response.sendRedirect("http://localhost:8080") } private fun createCookie(key: String, value: String): Cookie { diff --git a/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt b/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt index 93c77876..cca01bae 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt @@ -33,7 +33,7 @@ class CustomOAuth2UserService( getMemberByEmail(email)?.let { it.email = oAuth2Response.getEmail() - it.nickname = oAuth2Response.getName() + it.name = oAuth2Response.getName() it.profileImageUrl = oAuth2Response.getProfileImage() val savedMember = memberRepository.save(it) @@ -51,6 +51,7 @@ class CustomOAuth2UserService( return CustomOAuth2User(userDto) } ?: run { val newMember = Member.create( + name = oAuth2Response.getName(), nickname = oAuth2Response.getName(), email = oAuth2Response.getEmail(), profileImageUrl = oAuth2Response.getProfileImage(), diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index ae41d7d2..495104ac 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -13,6 +13,9 @@ class Member( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L, + @Column(name = "name", nullable = false) + var name: String, + @Column(name = "nickname", nullable = false) var nickname: String, @@ -32,20 +35,30 @@ class Member( @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) var role: Role = Role.GUEST, -) : BaseEntity() { + + @Column(name = "is_couple", nullable = false) + var isCouple: Long = 0L, + + ) : BaseEntity() { companion object { fun create( - nickname: String, + name: String, + nickname: String?, email: String, profileImageUrl: String?, provider: Provider ): Member { return Member( - nickname = nickname, + name = name, + nickname = nickname ?: name, email = email, profileImageUrl = profileImageUrl ?: "", provider = provider, ) } } + + fun updateCoupleStatus() { + this.isCouple = if (this.isCouple == 0L) 1L else 0L + } } From 3049acea43dc5a2c934755e3b07869d92a781c89 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Apr 2025 22:30:57 +0900 Subject: [PATCH 055/357] =?UTF-8?q?refactor:=20=EB=82=B4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B3=B4=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/application/MemberInfoFacade.kt | 14 ++++---------- .../member/domain/service/MemberInfoService.kt | 10 ++++++++-- .../member/presentation/MemberInfoController.kt | 3 ++- .../{GuestInfoResponse.kt => MyInfoResponse.kt} | 8 ++++++-- 4 files changed, 20 insertions(+), 15 deletions(-) rename src/main/kotlin/gomushin/backend/member/presentation/dto/response/{GuestInfoResponse.kt => MyInfoResponse.kt} (50%) diff --git a/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt index 5c915a97..473c8caa 100644 --- a/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt @@ -2,21 +2,15 @@ package gomushin.backend.member.application import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.service.MemberInfoService -import gomushin.backend.member.presentation.dto.response.GuestInfoResponse +import gomushin.backend.member.presentation.dto.response.MyInfoResponse import org.springframework.stereotype.Component @Component class MemberInfoFacade( private val memberInfoService: MemberInfoService, ) { - fun getMemberInfo(customUserDetails: CustomUserDetails): Any { - val authorities = customUserDetails.authorities - -// if (authorities.any { it.authority == "ROLE_GUEST" }) { - val member = memberInfoService.getGuestInfo(customUserDetails.getId()) - return GuestInfoResponse.of(member) -// } - - // TODO: Member 구현 시 , 분기를 통해 MEMBER와 GUEST를 구분할 수 있도록 수정 + fun getMemberInfo(customUserDetails: CustomUserDetails): MyInfoResponse { + val member = memberInfoService.getById(customUserDetails.getId()) + return MyInfoResponse.of(member) } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt index 9b3293aa..cefa20be 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt @@ -3,6 +3,7 @@ package gomushin.backend.member.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -12,7 +13,12 @@ class MemberInfoService( ) { @Transactional(readOnly = true) - fun getGuestInfo(id: Long): Member = - memberRepository.findById(id).orElseThrow { BadRequestException("sarangggun.member.not-exist-member") } + fun getById(id: Long): Member { + return findById(id) ?: throw BadRequestException("sarangggun.member.not-exist-member") + } + @Transactional(readOnly = true) + fun findById(id: Long): Member? { + return memberRepository.findByIdOrNull(id) + } } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 4b4aaa7a..4ec4fd96 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -3,6 +3,7 @@ package gomushin.backend.member.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.member.application.MemberInfoFacade +import gomushin.backend.member.presentation.dto.response.MyInfoResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -19,7 +20,7 @@ class MemberInfoController( @Operation(summary = "내 정보 조회", description = "getMyInfo") fun get( @AuthenticationPrincipal customUserDetails: CustomUserDetails - ): ApiResponse { + ): ApiResponse { val member = memberInfoFacade.getMemberInfo(customUserDetails) return ApiResponse.success(member) } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt b/src/main/kotlin/gomushin/backend/member/presentation/dto/response/MyInfoResponse.kt similarity index 50% rename from src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt rename to src/main/kotlin/gomushin/backend/member/presentation/dto/response/MyInfoResponse.kt index fa6bc484..5d9b56ab 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/dto/response/MyInfoResponse.kt @@ -2,10 +2,14 @@ package gomushin.backend.member.presentation.dto.response import gomushin.backend.member.domain.entity.Member -data class GuestInfoResponse( +data class MyInfoResponse( val nickname: String, + val isCouple: Long, ) { companion object { - fun of(member: Member) = GuestInfoResponse(member.nickname) + fun of(member: Member) = MyInfoResponse( + member.nickname, + member.isCouple + ) } } From 3b4ec4d844b67758540c2ce3c343f89428f285e5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Apr 2025 22:31:14 +0900 Subject: [PATCH 056/357] =?UTF-8?q?refactor:=20=EC=98=A8=EB=B3=B4=EB=94=A9?= =?UTF-8?q?=20=EC=A0=9C=EC=99=B8=20,=20=EC=A0=84=EB=B6=80=20MEMBER=20?= =?UTF-8?q?=EB=A7=8C=20=EC=A0=91=EA=B7=BC=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/SecurityConfiguration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 9b104168..905c135b 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -68,7 +68,7 @@ class SecurityConfiguration( "/error" ).permitAll() it.requestMatchers("/v1/member/onboarding").hasRole("GUEST") - it.anyRequest().authenticated() + it.anyRequest().hasRole("MEMBER") } .addFilterBefore( JwtAuthenticationFilter( From 297234388a90da2961e37367d439c94bbe8f07d4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Apr 2025 22:31:37 +0900 Subject: [PATCH 057/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=BB=A4=ED=94=8C?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20URI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/couple/presentation/ApiPath.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt new file mode 100644 index 00000000..d4528102 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -0,0 +1,6 @@ +package gomushin.backend.couple.presentation + +object ApiPath { + const val COUPLE_CODE_GENERATE = "/v1/couple/code-generate" + const val COUPLE_CONNECT = "/v1/couple/connect" +} From f6442d25aafbc00a6e1994f60fa27d6d5201d626 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Apr 2025 22:31:50 +0900 Subject: [PATCH 058/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/entity/Couple.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt new file mode 100644 index 00000000..02711f18 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt @@ -0,0 +1,46 @@ +package gomushin.backend.couple.domain.entity + +import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import jakarta.persistence.* +import java.time.LocalDate + +@Entity +@Table(name = "couple") +class Couple( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + @Column(name = "invitor_id", nullable = false) + val invitorId: Long = 0L, + + @Column(name = "invitee_id", nullable = false) + val inviteeId: Long = 0L, + + @Column(name = "relationship_start_date") + var relationshipStartDate: LocalDate? = null, + + @Column(name = "relationship_end_date") + var relationshipEndDate: LocalDate? = null, + + @Column(name = "military_start_date") + var militaryStartDate: LocalDate? = null, + + @Column(name = "military_end_date") + var militaryEndDate: LocalDate? = null, + + @Column(name = "advancement_date") + var advancementDate: LocalDate? = null, +): BaseEntity() { + companion object { + fun of( + invitorId: Long, + inviteeId: Long, + ): Couple { + return Couple( + invitorId = invitorId, + inviteeId = inviteeId, + ) + } + } +} From d6bf4376302bd97b6de0ff20a72ec30e1c5781dc Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Apr 2025 22:32:53 +0900 Subject: [PATCH 059/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/CoupleConnectRequest.kt | 5 +++ .../presentation/CoupleConnectController.kt | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/request/CoupleConnectRequest.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleConnectRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleConnectRequest.kt new file mode 100644 index 00000000..92e1cb44 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleConnectRequest.kt @@ -0,0 +1,5 @@ +package gomushin.backend.couple.dto.request + +data class CoupleConnectRequest( + val coupleCode: String +) diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt new file mode 100644 index 00000000..8a65aab7 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt @@ -0,0 +1,42 @@ +package gomushin.backend.couple.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.couple.dto.request.CoupleConnectRequest +import gomushin.backend.couple.facade.CoupleFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "커플 코드 생성", description = "CoupleConnectController") +class CoupleConnectController( + private val coupleFacade: CoupleFacade +) { + + @PostMapping(ApiPath.COUPLE_CODE_GENERATE) + @Operation( + summary = "커플 코드 생성", + description = "커플 코드를 생성합니다. 커플 코드는 60분 동안 유효합니다." + ) + fun generateCoupleCode( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ): ApiResponse = + ApiResponse.success(coupleFacade.requestCoupleCodeGeneration(customUserDetails)) + + @PostMapping(ApiPath.COUPLE_CONNECT) + @Operation( + summary = "커플 코드 연결", + description = "커플 코드를 통해 남자친구(여자친구)와 연결합니다." + ) + fun connectCouple( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody request: CoupleConnectRequest, + ): ApiResponse { + coupleFacade.requestCoupleConnect(customUserDetails, request) + return ApiResponse.success(true) + } +} From 42885859b251181f170e1c8d8683c4e8abd4cdd5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Apr 2025 22:33:13 +0900 Subject: [PATCH 060/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/CoupleRepository.kt | 6 ++ .../domain/service/CoupleConnectService.kt | 74 +++++++++++++++++++ .../backend/couple/facade/CoupleFacade.kt | 21 ++++++ .../member/util/CoupleCodeGeneratorUtil.kt | 10 +++ 4 files changed, 111 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt create mode 100644 src/main/kotlin/gomushin/backend/member/util/CoupleCodeGeneratorUtil.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt new file mode 100644 index 00000000..e76755d6 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt @@ -0,0 +1,6 @@ +package gomushin.backend.couple.domain.repository + +import gomushin.backend.couple.domain.entity.Couple +import org.springframework.data.jpa.repository.JpaRepository + +interface CoupleRepository : JpaRepository diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt new file mode 100644 index 00000000..1a8d5cae --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -0,0 +1,74 @@ +package gomushin.backend.couple.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.CoupleRepository +import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.util.CoupleCodeGeneratorUtil +import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.Duration + +@Service +class CoupleConnectService( + private val redisTemplate: StringRedisTemplate, + private val coupleRepository: CoupleRepository, + private val memberRepository: MemberRepository, +) { + + companion object { + private val CODE_DURATION = Duration.ofMinutes(60) + private const val COUPLE_CODE_PREFIX = "COUPLE_CODE:" + } + + fun generateCoupleCode(userId: Long): String { + delete(userId.toString()) + val coupleCode = CoupleCodeGeneratorUtil.generateCoupleCode() + val key = getCoupleCodeKey(coupleCode) + redisTemplate.opsForValue().set(key, userId.toString(), CODE_DURATION) + return coupleCode + } + + @Transactional + fun connectCouple(userId: Long, coupleCode: String) { + val key = getCoupleCodeKey(coupleCode) + val invitorId = getCoupleCodeOrNull(key) + ?: throw BadRequestException("sarangggun.couple.invalid-couple-code") + if (invitorId == userId) { + throw BadRequestException("sarangggun.couple.couple-code-same") + } + delete(key) + + val couple = Couple.of( + invitorId, + userId, + ) + + save(couple) + } + + @Transactional + fun save(couple: Couple): Couple { + updateCoupleStatus(couple.invitorId) + updateCoupleStatus(couple.inviteeId) + return coupleRepository.save(couple) + } + + @Transactional + fun updateCoupleStatus(userId: Long) { + memberRepository.findById(userId).get().updateCoupleStatus() + } + + fun getCoupleCodeOrNull(key: String): Long? { + return redisTemplate.opsForValue().get(key)?.toLongOrNull() + } + + fun delete(key: String) { + redisTemplate.delete(key) + } + + fun getCoupleCodeKey(code: String): String { + return "$COUPLE_CODE_PREFIX$code" + } +} diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt new file mode 100644 index 00000000..f0df2292 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -0,0 +1,21 @@ +package gomushin.backend.couple.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.service.CoupleConnectService +import gomushin.backend.couple.dto.request.CoupleConnectRequest +import org.springframework.stereotype.Component + +@Component +class CoupleFacade( + private val coupleConnectService: CoupleConnectService +) { + + fun requestCoupleCodeGeneration(customUserDetails: CustomUserDetails) = + coupleConnectService.generateCoupleCode(customUserDetails.getId()) + + + fun requestCoupleConnect( + customUserDetails: CustomUserDetails, + request: CoupleConnectRequest + ) = coupleConnectService.connectCouple(customUserDetails.getId(), request.coupleCode) +} diff --git a/src/main/kotlin/gomushin/backend/member/util/CoupleCodeGeneratorUtil.kt b/src/main/kotlin/gomushin/backend/member/util/CoupleCodeGeneratorUtil.kt new file mode 100644 index 00000000..87b4190d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/util/CoupleCodeGeneratorUtil.kt @@ -0,0 +1,10 @@ +package gomushin.backend.member.util + +object CoupleCodeGeneratorUtil { + fun generateCoupleCode(): String { + val characters = ('A'..'Z') + ('0'..'9') + return (1..6) + .map { characters.random() } + .joinToString("") + } +} From bfcbc4b0e66ecf0621520b3b894fe95af970fe85 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 16 Apr 2025 22:44:30 +0900 Subject: [PATCH 061/357] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=20?= =?UTF-8?q?=EC=BB=A4=ED=94=8C=20=EC=97=B0=EA=B2=B0=EC=9D=B4=20=EB=90=98?= =?UTF-8?q?=EC=96=B4=EC=9E=88=EB=8A=94=20=EA=B2=BD=EC=9A=B0,=20=ED=82=A4?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C,=20Optional=20=EC=B2=98=EB=A6=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 --- .../domain/service/CoupleConnectService.kt | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt index 1a8d5cae..e4052367 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -23,7 +23,6 @@ class CoupleConnectService( } fun generateCoupleCode(userId: Long): String { - delete(userId.toString()) val coupleCode = CoupleCodeGeneratorUtil.generateCoupleCode() val key = getCoupleCodeKey(coupleCode) redisTemplate.opsForValue().set(key, userId.toString(), CODE_DURATION) @@ -38,14 +37,18 @@ class CoupleConnectService( if (invitorId == userId) { throw BadRequestException("sarangggun.couple.couple-code-same") } - delete(key) val couple = Couple.of( invitorId, userId, ) + if (isAlreadyConnected(userId) || isAlreadyConnected(invitorId)) { + throw BadRequestException("sarangggun.couple.already-connected") + } + save(couple) + delete(key) } @Transactional @@ -57,18 +60,29 @@ class CoupleConnectService( @Transactional fun updateCoupleStatus(userId: Long) { - memberRepository.findById(userId).get().updateCoupleStatus() + val member = memberRepository.findById(userId).orElseThrow { + BadRequestException("sarangggun.member.not-found") + } + member.updateCoupleStatus() } - fun getCoupleCodeOrNull(key: String): Long? { + private fun getCoupleCodeOrNull(key: String): Long? { return redisTemplate.opsForValue().get(key)?.toLongOrNull() } - fun delete(key: String) { + private fun delete(key: String) { redisTemplate.delete(key) } - fun getCoupleCodeKey(code: String): String { + private fun getCoupleCodeKey(code: String): String { return "$COUPLE_CODE_PREFIX$code" } + + private fun isAlreadyConnected(userId: Long): Boolean { + val member = memberRepository.findById(userId).orElseThrow { + BadRequestException("sarangggun.member.not-found") + } + + return member.isCouple > 0 + } } From f72d69c49597af03653f32866d5146fcc29fd195 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Apr 2025 11:22:20 +0900 Subject: [PATCH 062/357] =?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=20Auth=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/{presentation => }/MainController.kt | 2 +- src/main/kotlin/gomushin/backend/auth/application/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/domain/entity/.gitkeep | 0 .../kotlin/gomushin/backend/auth/domain/repository/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/domain/service/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/domain/value/.gitkeep | 0 src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt | 4 ---- .../gomushin/backend/auth/presentation/request/.gitkeep | 0 .../gomushin/backend/auth/presentation/response/.gitkeep | 0 9 files changed, 1 insertion(+), 5 deletions(-) rename src/main/kotlin/gomushin/backend/auth/{presentation => }/MainController.kt (88%) delete mode 100644 src/main/kotlin/gomushin/backend/auth/application/.gitkeep delete mode 100644 src/main/kotlin/gomushin/backend/auth/domain/entity/.gitkeep delete mode 100644 src/main/kotlin/gomushin/backend/auth/domain/repository/.gitkeep delete mode 100644 src/main/kotlin/gomushin/backend/auth/domain/service/.gitkeep delete mode 100644 src/main/kotlin/gomushin/backend/auth/domain/value/.gitkeep delete mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt delete mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/request/.gitkeep delete mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/response/.gitkeep diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt b/src/main/kotlin/gomushin/backend/auth/MainController.kt similarity index 88% rename from src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt rename to src/main/kotlin/gomushin/backend/auth/MainController.kt index 9f22723a..c313e101 100644 --- a/src/main/kotlin/gomushin/backend/auth/presentation/MainController.kt +++ b/src/main/kotlin/gomushin/backend/auth/MainController.kt @@ -1,4 +1,4 @@ -package gomushin.backend.auth.presentation +package gomushin.backend.auth import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping diff --git a/src/main/kotlin/gomushin/backend/auth/application/.gitkeep b/src/main/kotlin/gomushin/backend/auth/application/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/kotlin/gomushin/backend/auth/domain/entity/.gitkeep b/src/main/kotlin/gomushin/backend/auth/domain/entity/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/kotlin/gomushin/backend/auth/domain/repository/.gitkeep b/src/main/kotlin/gomushin/backend/auth/domain/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/kotlin/gomushin/backend/auth/domain/service/.gitkeep b/src/main/kotlin/gomushin/backend/auth/domain/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/kotlin/gomushin/backend/auth/domain/value/.gitkeep b/src/main/kotlin/gomushin/backend/auth/domain/value/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt deleted file mode 100644 index dcc9b8fb..00000000 --- a/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt +++ /dev/null @@ -1,4 +0,0 @@ -package gomushin.backend.auth.presentation - -object ApiPath { -} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/request/.gitkeep b/src/main/kotlin/gomushin/backend/auth/presentation/request/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/response/.gitkeep b/src/main/kotlin/gomushin/backend/auth/presentation/response/.gitkeep deleted file mode 100644 index e69de29b..00000000 From c85a2946a07eaa38d560d41b849cd05e5b7a7cac Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Apr 2025 11:23:46 +0900 Subject: [PATCH 063/357] refactor: application -> facade --- .../member/domain/service/OnboardingService.kt | 2 +- .../dto/request/OnboardingRequest.kt | 2 +- .../{presentation => }/dto/response/MyInfoResponse.kt | 2 +- .../{application => facade}/MemberInfoFacade.kt | 4 ++-- .../{application => facade}/OnboardingFacade.kt | 4 ++-- .../member/presentation/MemberInfoController.kt | 4 ++-- .../member/presentation/OnboardingController.kt | 4 ++-- .../member/domain/service/MemberInfoServiceTest.kt | 3 ++- .../member/domain/service/OnboardingServiceTest.kt | 3 ++- .../{application => facade}/MemberInfoFacadeTest.kt | 11 ++++++----- .../{application => facade}/OnboardingFacadeTest.kt | 4 ++-- 11 files changed, 23 insertions(+), 20 deletions(-) rename src/main/kotlin/gomushin/backend/member/{presentation => }/dto/request/OnboardingRequest.kt (83%) rename src/main/kotlin/gomushin/backend/member/{presentation => }/dto/response/MyInfoResponse.kt (82%) rename src/main/kotlin/gomushin/backend/member/{application => facade}/MemberInfoFacade.kt (79%) rename src/main/kotlin/gomushin/backend/member/{application => facade}/OnboardingFacade.kt (73%) rename src/test/kotlin/gomushin/backend/member/{application => facade}/MemberInfoFacadeTest.kt (84%) rename src/test/kotlin/gomushin/backend/member/{application => facade}/OnboardingFacadeTest.kt (88%) diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt index 9beed943..7b19c232 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt @@ -3,7 +3,7 @@ package gomushin.backend.member.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Role -import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import gomushin.backend.member.dto.request.OnboardingRequest import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional diff --git a/src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/OnboardingRequest.kt similarity index 83% rename from src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt rename to src/main/kotlin/gomushin/backend/member/dto/request/OnboardingRequest.kt index 1bb0d6c8..e17491fa 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/request/OnboardingRequest.kt @@ -1,4 +1,4 @@ -package gomushin.backend.member.presentation.dto.request +package gomushin.backend.member.dto.request import io.swagger.v3.oas.annotations.media.Schema import java.time.LocalDate diff --git a/src/main/kotlin/gomushin/backend/member/presentation/dto/response/MyInfoResponse.kt b/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt similarity index 82% rename from src/main/kotlin/gomushin/backend/member/presentation/dto/response/MyInfoResponse.kt rename to src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt index 5d9b56ab..5bc44bce 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/dto/response/MyInfoResponse.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt @@ -1,4 +1,4 @@ -package gomushin.backend.member.presentation.dto.response +package gomushin.backend.member.dto.response import gomushin.backend.member.domain.entity.Member diff --git a/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt similarity index 79% rename from src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt rename to src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 473c8caa..fa168462 100644 --- a/src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -1,8 +1,8 @@ -package gomushin.backend.member.application +package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.service.MemberInfoService -import gomushin.backend.member.presentation.dto.response.MyInfoResponse +import gomushin.backend.member.dto.response.MyInfoResponse import org.springframework.stereotype.Component @Component diff --git a/src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/OnboardingFacade.kt similarity index 73% rename from src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt rename to src/main/kotlin/gomushin/backend/member/facade/OnboardingFacade.kt index b26c8168..f6a59154 100644 --- a/src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/OnboardingFacade.kt @@ -1,7 +1,7 @@ -package gomushin.backend.member.application +package gomushin.backend.member.facade import gomushin.backend.member.domain.service.OnboardingService -import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import gomushin.backend.member.dto.request.OnboardingRequest import org.springframework.stereotype.Component @Component diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 4ec4fd96..92a5742e 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -2,8 +2,8 @@ package gomushin.backend.member.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.member.application.MemberInfoFacade -import gomushin.backend.member.presentation.dto.response.MyInfoResponse +import gomushin.backend.member.facade.MemberInfoFacade +import gomushin.backend.member.dto.response.MyInfoResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.security.core.annotation.AuthenticationPrincipal diff --git a/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt b/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt index 791173b3..a8200d72 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt @@ -2,8 +2,8 @@ package gomushin.backend.member.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.member.application.OnboardingFacade -import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import gomushin.backend.member.facade.OnboardingFacade +import gomushin.backend.member.dto.request.OnboardingRequest import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.security.core.annotation.AuthenticationPrincipal diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt index 2c15689d..baecd677 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt @@ -30,6 +30,7 @@ class MemberInfoServiceTest { val memberId = 1L val expectedMember = Member( id = 1L, + name = "테스트", nickname = "테스트 닉네임", email = "test@test.com", birthDate = null, @@ -40,7 +41,7 @@ class MemberInfoServiceTest { // when `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember)) - val result = memberInfoService.getGuestInfo(memberId) + val result = memberInfoService.getById(memberId) // then assertEquals(expectedMember, result) diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt index c81b9050..2e7b2088 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt @@ -4,7 +4,7 @@ import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role -import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import gomushin.backend.member.dto.request.OnboardingRequest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock @@ -30,6 +30,7 @@ class OnboardingServiceTest { val memberId = 1L val existingMember = Member( id = memberId, + name = "테스트", nickname = "원래 닉네임", email = "test@example.com", birthDate = LocalDate.of(1990, 1, 1), diff --git a/src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt similarity index 84% rename from src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt rename to src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 1c72b3a3..643b430e 100644 --- a/src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -1,11 +1,11 @@ -package gomushin.backend.member.application +package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.service.MemberInfoService import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role -import gomushin.backend.member.presentation.dto.response.GuestInfoResponse +import gomushin.backend.member.dto.response.MyInfoResponse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -33,6 +33,7 @@ class MemberInfoFacadeTest { fun setUp() { member = Member( id = 1L, + name = "테스트", nickname = "테스트 닉네임", email = "test@test.com", birthDate = null, @@ -53,11 +54,11 @@ class MemberInfoFacadeTest { @Test fun getMyInfo() { // given - `when`(memberInfoService.getGuestInfo(customUserDetails.getId())).thenReturn(member) + `when`(memberInfoService.getById(customUserDetails.getId())).thenReturn(member) // when val result = memberInfoFacade.getMemberInfo(customUserDetails) // then - verify(memberInfoService).getGuestInfo(1L) - assertEquals(member.nickname, (result as GuestInfoResponse).nickname) + verify(memberInfoService).getById(1L) + assertEquals(member.nickname, (result as MyInfoResponse).nickname) } } diff --git a/src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/OnboardingFacadeTest.kt similarity index 88% rename from src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt rename to src/test/kotlin/gomushin/backend/member/facade/OnboardingFacadeTest.kt index b111c346..3a9536c4 100644 --- a/src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/OnboardingFacadeTest.kt @@ -1,7 +1,7 @@ -package gomushin.backend.member.application +package gomushin.backend.member.facade import gomushin.backend.member.domain.service.OnboardingService -import gomushin.backend.member.presentation.dto.request.OnboardingRequest +import gomushin.backend.member.dto.request.OnboardingRequest import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith From 3225e63a7d7f8411122284bcf748b75bb0f62dd4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Apr 2025 11:32:24 +0900 Subject: [PATCH 064/357] =?UTF-8?q?refactor:=20couple=20=EC=97=AC=EB=B6=80?= =?UTF-8?q?=EB=A5=BC=20Boolean=20=EC=9C=BC=EB=A1=9C=20=ED=8C=90=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleConnectService.kt | 14 +++++++------- .../backend/member/domain/entity/Member.kt | 4 ++-- .../backend/member/dto/response/MyInfoResponse.kt | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt index e4052367..59a01edd 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -18,32 +18,32 @@ class CoupleConnectService( ) { companion object { - private val CODE_DURATION = Duration.ofMinutes(60) + private val COUPLE_CODE_DURATION = Duration.ofMinutes(60) private const val COUPLE_CODE_PREFIX = "COUPLE_CODE:" } fun generateCoupleCode(userId: Long): String { val coupleCode = CoupleCodeGeneratorUtil.generateCoupleCode() val key = getCoupleCodeKey(coupleCode) - redisTemplate.opsForValue().set(key, userId.toString(), CODE_DURATION) + redisTemplate.opsForValue().set(key, userId.toString(), COUPLE_CODE_DURATION) return coupleCode } @Transactional - fun connectCouple(userId: Long, coupleCode: String) { + fun connectCouple(inviteeId: Long, coupleCode: String) { val key = getCoupleCodeKey(coupleCode) val invitorId = getCoupleCodeOrNull(key) ?: throw BadRequestException("sarangggun.couple.invalid-couple-code") - if (invitorId == userId) { + if (invitorId == inviteeId) { throw BadRequestException("sarangggun.couple.couple-code-same") } val couple = Couple.of( invitorId, - userId, + inviteeId, ) - if (isAlreadyConnected(userId) || isAlreadyConnected(invitorId)) { + if (isAlreadyConnected(inviteeId) || isAlreadyConnected(invitorId)) { throw BadRequestException("sarangggun.couple.already-connected") } @@ -83,6 +83,6 @@ class CoupleConnectService( BadRequestException("sarangggun.member.not-found") } - return member.isCouple > 0 + return member.isCouple } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index 495104ac..35bfbc4f 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -37,7 +37,7 @@ class Member( var role: Role = Role.GUEST, @Column(name = "is_couple", nullable = false) - var isCouple: Long = 0L, + var isCouple: Boolean = false, ) : BaseEntity() { companion object { @@ -59,6 +59,6 @@ class Member( } fun updateCoupleStatus() { - this.isCouple = if (this.isCouple == 0L) 1L else 0L + this.isCouple = !this.isCouple } } diff --git a/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt b/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt index 5bc44bce..2a2442c7 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt @@ -4,7 +4,7 @@ import gomushin.backend.member.domain.entity.Member data class MyInfoResponse( val nickname: String, - val isCouple: Long, + val isCouple: Boolean, ) { companion object { fun of(member: Member) = MyInfoResponse( From 7aadc36931a0faa94987db3536ba632dd6187887 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Apr 2025 17:14:15 +0900 Subject: [PATCH 065/357] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=84=EB=B6=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/couple/domain/entity/Couple.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt index 02711f18..855820ae 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt @@ -20,9 +20,6 @@ class Couple( @Column(name = "relationship_start_date") var relationshipStartDate: LocalDate? = null, - @Column(name = "relationship_end_date") - var relationshipEndDate: LocalDate? = null, - @Column(name = "military_start_date") var militaryStartDate: LocalDate? = null, From 170f9f5bd7ef8329987f7560ac88381ebc0d84d1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 17 Apr 2025 17:45:34 +0900 Subject: [PATCH 066/357] =?UTF-8?q?refactor:=20Test=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20h2=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- build.gradle.kts | 3 +++ .../gomushin/backend/member/facade/MemberInfoFacadeTest.kt | 6 ++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4f799909..0a7d2e2a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,7 +36,7 @@ jobs: - name: YML 파일 세팅 env: APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }} - # TEST_APPLICATION_PROPERTIES: ${{ secrets.TEST_APPLICATION_PROPERTIES }} + TEST_APPLICATION_PROPERTIES: ${{ secrets.TEST_APPLICATION_PROPERTIES }} run: | cd ./src rm -rf main/resources/application.yml diff --git a/build.gradle.kts b/build.gradle.kts index a6ff3033..31a6f96d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,9 @@ dependencies { // mysql runtimeOnly("mysql:mysql-connector-java:8.0.33") + // h2 + runtimeOnly("com.h2database:h2") + // swagger implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0") diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 643b430e..1c739ae0 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -39,14 +39,12 @@ class MemberInfoFacadeTest { birthDate = null, profileImageUrl = null, provider = Provider.KAKAO, - role = Role.GUEST, + role = Role.MEMBER, ) - val authorities = mutableListOf(SimpleGrantedAuthority("ROLE_GUEST")) customUserDetails = mock(CustomUserDetails::class.java) `when`(customUserDetails.getId()).thenReturn(1L) - `when`(customUserDetails.authorities).thenReturn(authorities) } @@ -59,6 +57,6 @@ class MemberInfoFacadeTest { val result = memberInfoFacade.getMemberInfo(customUserDetails) // then verify(memberInfoService).getById(1L) - assertEquals(member.nickname, (result as MyInfoResponse).nickname) + assertEquals(member.nickname, result.nickname) } } From a945525c0fc70eecfd3f4a497607e8bdb725ce61 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Apr 2025 14:21:00 +0900 Subject: [PATCH 067/357] =?UTF-8?q?refactor:=20CRUD=EB=8A=94=20MemberServi?= =?UTF-8?q?ce=20=EB=A1=9C=20=EB=AC=B6=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/{MemberInfoService.kt => MemberService.kt} | 2 +- .../gomushin/backend/member/facade/MemberInfoFacade.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/main/kotlin/gomushin/backend/member/domain/service/{MemberInfoService.kt => MemberService.kt} (96%) diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt similarity index 96% rename from src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt rename to src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index cefa20be..7f3e0bd8 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service -class MemberInfoService( +class MemberService( private val memberRepository: MemberRepository, ) { diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index fa168462..9fcc9a6d 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -1,16 +1,16 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails -import gomushin.backend.member.domain.service.MemberInfoService +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.dto.response.MyInfoResponse import org.springframework.stereotype.Component @Component class MemberInfoFacade( - private val memberInfoService: MemberInfoService, + private val memberService: MemberService, ) { fun getMemberInfo(customUserDetails: CustomUserDetails): MyInfoResponse { - val member = memberInfoService.getById(customUserDetails.getId()) + val member = memberService.getById(customUserDetails.getId()) return MyInfoResponse.of(member) } } From b72089462d0c8050e07a93c4998cd84dfc345851 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Apr 2025 14:21:12 +0900 Subject: [PATCH 068/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20API=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index d4528102..daf8ebbb 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -3,4 +3,5 @@ package gomushin.backend.couple.presentation object ApiPath { const val COUPLE_CODE_GENERATE = "/v1/couple/code-generate" const val COUPLE_CONNECT = "/v1/couple/connect" + const val COUPLE_ANNIVERSARY = "/v1/couple/anniversary" } From 5cbef1e611991f554bf6e7895c3bff505d28c6b3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Apr 2025 14:21:34 +0900 Subject: [PATCH 069/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=ED=97=A4=EC=96=B4=EC=A7=84=20=EB=82=A0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C,=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/couple/domain/entity/Couple.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt index 855820ae..ae250a78 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt @@ -26,8 +26,6 @@ class Couple( @Column(name = "military_end_date") var militaryEndDate: LocalDate? = null, - @Column(name = "advancement_date") - var advancementDate: LocalDate? = null, ): BaseEntity() { companion object { fun of( @@ -40,4 +38,15 @@ class Couple( ) } } + + fun updateAnniversary( + relationshipStartDate: LocalDate?, + militaryStartDate: LocalDate?, + militaryEndDate: LocalDate?, + ): Couple { + this.relationshipStartDate = relationshipStartDate ?: this.relationshipStartDate + this.militaryStartDate = militaryStartDate ?: this.militaryStartDate + this.militaryEndDate = militaryEndDate ?: this.militaryEndDate + return this + } } From bb07c558adff41d21e4c9c4131f8b07be2934b98 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Apr 2025 14:21:51 +0900 Subject: [PATCH 070/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20API=20=EB=9E=91=20dto=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/CoupleAnniversaryRequest.kt | 18 +++++++++++++ .../presentation/CoupleConnectController.kt | 25 +++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt new file mode 100644 index 00000000..cf678060 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt @@ -0,0 +1,18 @@ +package gomushin.backend.couple.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +class CoupleAnniversaryRequest( + @Schema(description = "커플 ID", example = "1") + val coupleId: Long, + + @Schema(description = "처음 만난 날", example = "2022-10-01") + val relationshipStartDate: LocalDate, + + @Schema(description = "입대일", example = "2023-01-01") + val militaryStartDate: LocalDate, + + @Schema(description = "전역일", example = "2024-10-28") + val militaryEndDate: LocalDate, +) diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt index 8a65aab7..e4a7c078 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt @@ -2,6 +2,8 @@ package gomushin.backend.couple.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.dto.request.CoupleConnectRequest import gomushin.backend.couple.facade.CoupleFacade import io.swagger.v3.oas.annotations.Operation @@ -19,8 +21,8 @@ class CoupleConnectController( @PostMapping(ApiPath.COUPLE_CODE_GENERATE) @Operation( - summary = "커플 코드 생성", - description = "커플 코드를 생성합니다. 커플 코드는 60분 동안 유효합니다." + summary = "커플 코드를 생성합니다. 커플 코드는 60분 동안 유효합니다.", + description = "generateCoupleCode" ) fun generateCoupleCode( @AuthenticationPrincipal customUserDetails: CustomUserDetails @@ -29,14 +31,27 @@ class CoupleConnectController( @PostMapping(ApiPath.COUPLE_CONNECT) @Operation( - summary = "커플 코드 연결", - description = "커플 코드를 통해 남자친구(여자친구)와 연결합니다." + summary = "커플 코드를 통해 남자친구(여자친구)와 연결합니다.", + description = "connectCouple" ) fun connectCouple( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @RequestBody request: CoupleConnectRequest, + ): ApiResponse { + val couple = coupleFacade.requestCoupleConnect(customUserDetails, request) + return ApiResponse.success(couple) + } + + @PostMapping(ApiPath.COUPLE_ANNIVERSARY) + @Operation( + summary = "커플 기념일(처음 만난 날, 입대일, 전역일)을 등록합니다.(이건 수정 아니고, 초기 등록)", + description = "registerAnniversary" + ) + fun registerAnniversary( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody request: CoupleAnniversaryRequest, ): ApiResponse { - coupleFacade.requestCoupleConnect(customUserDetails, request) + coupleFacade.registerAnniversary(customUserDetails, request) return ApiResponse.success(true) } } From ed191ca73b758a7a55384aebb3bf2200d73afb99 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Apr 2025 14:22:17 +0900 Subject: [PATCH 071/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CoupleConnectService.kt | 50 +++++++++++++------ .../backend/couple/facade/CoupleFacade.kt | 13 ++++- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt index 59a01edd..7ce25f62 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -2,19 +2,19 @@ package gomushin.backend.couple.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Couple -import gomushin.backend.couple.domain.repository.CoupleRepository -import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.util.CoupleCodeGeneratorUtil import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.Duration +import java.time.LocalDate @Service class CoupleConnectService( private val redisTemplate: StringRedisTemplate, - private val coupleRepository: CoupleRepository, - private val memberRepository: MemberRepository, + private val coupleService: CoupleService, + private val memberService: MemberService, ) { companion object { @@ -30,7 +30,7 @@ class CoupleConnectService( } @Transactional - fun connectCouple(inviteeId: Long, coupleCode: String) { + fun connectCouple(inviteeId: Long, coupleCode: String): Couple { val key = getCoupleCodeKey(coupleCode) val invitorId = getCoupleCodeOrNull(key) ?: throw BadRequestException("sarangggun.couple.invalid-couple-code") @@ -47,25 +47,50 @@ class CoupleConnectService( throw BadRequestException("sarangggun.couple.already-connected") } - save(couple) + val savedCouple = save(couple) delete(key) + + return savedCouple } @Transactional fun save(couple: Couple): Couple { updateCoupleStatus(couple.invitorId) updateCoupleStatus(couple.inviteeId) - return coupleRepository.save(couple) + return coupleService.save(couple) } @Transactional fun updateCoupleStatus(userId: Long) { - val member = memberRepository.findById(userId).orElseThrow { - BadRequestException("sarangggun.member.not-found") - } + val member = memberService.getById(userId) member.updateCoupleStatus() } + @Transactional + fun registerAnniversary( + userId: Long, + coupleId: Long, + relationshipStartDate: LocalDate, + militaryStartDate: LocalDate, + militaryEndDate: LocalDate, + ) { + val couple = coupleService.getById(coupleId) + if (!checkUserInCouple(userId, couple)) { + throw BadRequestException("sarangggun.couple.not-in-couple") + } + couple.updateAnniversary( + relationshipStartDate = relationshipStartDate, + militaryStartDate = militaryStartDate, + militaryEndDate = militaryEndDate, + ) + } + + private fun checkUserInCouple(userId: Long, couple: Couple): Boolean { + val member = memberService.getById(userId) + return member.id == couple.invitorId || member.id == couple.inviteeId + } + + private fun getCoupleCodeOrNull(key: String): Long? { return redisTemplate.opsForValue().get(key)?.toLongOrNull() } @@ -79,10 +104,7 @@ class CoupleConnectService( } private fun isAlreadyConnected(userId: Long): Boolean { - val member = memberRepository.findById(userId).orElseThrow { - BadRequestException("sarangggun.member.not-found") - } - + val member = memberService.getById(userId) return member.isCouple } } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index f0df2292..a7d0b47f 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -2,6 +2,7 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.service.CoupleConnectService +import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.dto.request.CoupleConnectRequest import org.springframework.stereotype.Component @@ -13,9 +14,19 @@ class CoupleFacade( fun requestCoupleCodeGeneration(customUserDetails: CustomUserDetails) = coupleConnectService.generateCoupleCode(customUserDetails.getId()) - fun requestCoupleConnect( customUserDetails: CustomUserDetails, request: CoupleConnectRequest ) = coupleConnectService.connectCouple(customUserDetails.getId(), request.coupleCode) + + fun registerAnniversary( + customUserDetails: CustomUserDetails, + request: CoupleAnniversaryRequest + ) = coupleConnectService.registerAnniversary( + customUserDetails.getId(), + request.coupleId, + request.relationshipStartDate, + request.militaryStartDate, + request.militaryEndDate + ) } From d84d245ffa8ff6c1d2ca5dcd612253548583a47d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Apr 2025 14:22:29 +0900 Subject: [PATCH 072/357] =?UTF-8?q?test=20:=20Test=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleService.kt | 28 ++++++ .../domain/service/CoupleServiceTest.kt | 85 +++++++++++++++++++ ...nfoServiceTest.kt => MemberServiceTest.kt} | 6 +- .../member/facade/MemberInfoFacadeTest.kt | 11 +-- 4 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt create mode 100644 src/test/kotlin/gomushin/backend/couple/domain/service/CoupleServiceTest.kt rename src/test/kotlin/gomushin/backend/member/domain/service/{MemberInfoServiceTest.kt => MemberServiceTest.kt} (90%) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt new file mode 100644 index 00000000..79d26e11 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt @@ -0,0 +1,28 @@ +package gomushin.backend.couple.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.CoupleRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CoupleService( + private val coupleRepository: CoupleRepository +) { + @Transactional(readOnly = true) + fun getById(id: Long): Couple { + return findById(id) ?: throw BadRequestException("sarangggun.couple.not-found") + } + + @Transactional(readOnly = true) + fun findById(id: Long): Couple? { + return coupleRepository.findByIdOrNull(id) + } + + @Transactional + fun save(couple: Couple): Couple { + return coupleRepository.save(couple) + } +} diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleServiceTest.kt new file mode 100644 index 00000000..9b7cdf43 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleServiceTest.kt @@ -0,0 +1,85 @@ +package gomushin.backend.couple.domain.service + +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.CoupleRepository +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension +import java.util.* + +@ExtendWith(MockitoExtension::class) +class CoupleServiceTest { + + @Mock + private lateinit var coupleRepository: CoupleRepository + + @InjectMocks + private lateinit var coupleService: CoupleService + + @DisplayName("getById - 성공") + @Test + fun getById_success() { + // given + val coupleId = 1L + val couple = Couple( + id = coupleId, + invitorId = 1L, + inviteeId = 2L, + ) + + `when`(coupleRepository.findById(coupleId)).thenReturn(Optional.of(couple)) + + // when + val result = coupleService.getById(coupleId) + + // then + assert(result.id == coupleId) + verify(coupleRepository).findById(coupleId) + } + + @DisplayName("findById - 성공") + @Test + fun findById_success() { + // given + val coupleId = 1L + val couple = Couple( + id = coupleId, + invitorId = 1L, + inviteeId = 2L, + ) + + `when`(coupleRepository.findById(coupleId)).thenReturn(Optional.of(couple)) + + // when + val result = coupleService.findById(coupleId) + + // then + assert(result?.id == coupleId) + verify(coupleRepository).findById(coupleId) + } + + @DisplayName("save - 성공") + @Test + fun save_success() { + // given + val couple = Couple( + id = 1L, + invitorId = 1L, + inviteeId = 2L, + ) + + `when`(coupleRepository.save(couple)).thenReturn(couple) + + // when + val result = coupleService.save(couple) + + // then + assert(result.id == couple.id) + verify(coupleRepository).save(couple) + } +} diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt similarity index 90% rename from src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt rename to src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt index baecd677..2d0f017b 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt @@ -15,13 +15,13 @@ import java.util.* import kotlin.test.assertEquals @ExtendWith(MockitoExtension::class) -class MemberInfoServiceTest { +class MemberServiceTest { @Mock private lateinit var memberRepository: MemberRepository @InjectMocks - private lateinit var memberInfoService: MemberInfoService + private lateinit var memberService: MemberService @DisplayName("내 정보 조회 - [GUEST]") @Test @@ -41,7 +41,7 @@ class MemberInfoServiceTest { // when `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember)) - val result = memberInfoService.getById(memberId) + val result = memberService.getById(memberId) // then assertEquals(expectedMember, result) diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 1c739ae0..d7610c36 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -2,10 +2,9 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.entity.Member -import gomushin.backend.member.domain.service.MemberInfoService +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role -import gomushin.backend.member.dto.response.MyInfoResponse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -14,14 +13,12 @@ import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.core.authority.SimpleGrantedAuthority import kotlin.test.assertEquals @ExtendWith(MockitoExtension::class) class MemberInfoFacadeTest { @Mock - private lateinit var memberInfoService: MemberInfoService + private lateinit var memberService: MemberService @InjectMocks private lateinit var memberInfoFacade: MemberInfoFacade @@ -52,11 +49,11 @@ class MemberInfoFacadeTest { @Test fun getMyInfo() { // given - `when`(memberInfoService.getById(customUserDetails.getId())).thenReturn(member) + `when`(memberService.getById(customUserDetails.getId())).thenReturn(member) // when val result = memberInfoFacade.getMemberInfo(customUserDetails) // then - verify(memberInfoService).getById(1L) + verify(memberService).getById(1L) assertEquals(member.nickname, result.nickname) } } From 6a7b3b6a44aade3ad224b5f09dfd268a177fc4ea Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Apr 2025 15:02:18 +0900 Subject: [PATCH 073/357] =?UTF-8?q?refactor:=20dto=20data=20class=20?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/dto/request/CoupleAnniversaryRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt index cf678060..8d4a499d 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt @@ -3,7 +3,7 @@ package gomushin.backend.couple.dto.request import io.swagger.v3.oas.annotations.media.Schema import java.time.LocalDate -class CoupleAnniversaryRequest( +data class CoupleAnniversaryRequest( @Schema(description = "커플 ID", example = "1") val coupleId: Long, From 57cfdd5dc60834373d762145a72322b00dcef2ff Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 18 Apr 2025 15:02:37 +0900 Subject: [PATCH 074/357] =?UTF-8?q?refactor:=203=EA=B0=9C=20=EC=9D=B4?= =?UTF-8?q?=EC=83=81=EC=9D=98=20=EB=A7=A4=EA=B0=9C=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=8A=94=20dto=EB=A1=9C=20=EB=84=98=EA=B8=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleConnectService.kt | 14 ++++++-------- .../gomushin/backend/couple/facade/CoupleFacade.kt | 5 +---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt index 7ce25f62..1ca6f196 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -2,6 +2,7 @@ package gomushin.backend.couple.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.util.CoupleCodeGeneratorUtil import org.springframework.data.redis.core.StringRedisTemplate @@ -69,19 +70,16 @@ class CoupleConnectService( @Transactional fun registerAnniversary( userId: Long, - coupleId: Long, - relationshipStartDate: LocalDate, - militaryStartDate: LocalDate, - militaryEndDate: LocalDate, + request: CoupleAnniversaryRequest ) { - val couple = coupleService.getById(coupleId) + val couple = coupleService.getById(request.coupleId) if (!checkUserInCouple(userId, couple)) { throw BadRequestException("sarangggun.couple.not-in-couple") } couple.updateAnniversary( - relationshipStartDate = relationshipStartDate, - militaryStartDate = militaryStartDate, - militaryEndDate = militaryEndDate, + relationshipStartDate = request.relationshipStartDate, + militaryStartDate = request.militaryStartDate, + militaryEndDate = request.militaryEndDate, ) } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index a7d0b47f..22105e64 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -24,9 +24,6 @@ class CoupleFacade( request: CoupleAnniversaryRequest ) = coupleConnectService.registerAnniversary( customUserDetails.getId(), - request.coupleId, - request.relationshipStartDate, - request.militaryStartDate, - request.militaryEndDate + request ) } From 388ea101043eaddc120d825a4f845718ad428d9e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Apr 2025 09:20:07 +0900 Subject: [PATCH 075/357] =?UTF-8?q?feat:=20=EA=B2=B0=EA=B3=BC=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EB=8A=94=20=EC=83=81=ED=83=9C=EC=BD=94=EB=93=9C=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 --- .../backend/couple/presentation/CoupleConnectController.kt | 5 +++++ .../backend/member/presentation/MemberInfoController.kt | 3 +++ .../backend/member/presentation/OnboardingController.kt | 3 +++ 3 files changed, 11 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt index e4a7c078..aa6c2bf2 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleConnectController.kt @@ -8,9 +8,11 @@ import gomushin.backend.couple.dto.request.CoupleConnectRequest import gomushin.backend.couple.facade.CoupleFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @@ -19,6 +21,7 @@ class CoupleConnectController( private val coupleFacade: CoupleFacade ) { + @ResponseStatus(HttpStatus.CREATED) @PostMapping(ApiPath.COUPLE_CODE_GENERATE) @Operation( summary = "커플 코드를 생성합니다. 커플 코드는 60분 동안 유효합니다.", @@ -29,6 +32,7 @@ class CoupleConnectController( ): ApiResponse = ApiResponse.success(coupleFacade.requestCoupleCodeGeneration(customUserDetails)) + @ResponseStatus(HttpStatus.CREATED) @PostMapping(ApiPath.COUPLE_CONNECT) @Operation( summary = "커플 코드를 통해 남자친구(여자친구)와 연결합니다.", @@ -42,6 +46,7 @@ class CoupleConnectController( return ApiResponse.success(couple) } + @ResponseStatus(HttpStatus.CREATED) @PostMapping(ApiPath.COUPLE_ANNIVERSARY) @Operation( summary = "커플 기념일(처음 만난 날, 입대일, 전역일)을 등록합니다.(이건 수정 아니고, 초기 등록)", diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 92a5742e..ab02f149 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -6,8 +6,10 @@ import gomushin.backend.member.facade.MemberInfoFacade import gomushin.backend.member.dto.response.MyInfoResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @@ -16,6 +18,7 @@ class MemberInfoController( private val memberInfoFacade: MemberInfoFacade, ) { + @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.MY_INFO) @Operation(summary = "내 정보 조회", description = "getMyInfo") fun get( diff --git a/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt b/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt index a8200d72..5101aa3a 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt @@ -6,9 +6,11 @@ import gomushin.backend.member.facade.OnboardingFacade import gomushin.backend.member.dto.request.OnboardingRequest import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @@ -17,6 +19,7 @@ class OnboardingController( private val onboardingFacade: OnboardingFacade, ) { + @ResponseStatus(HttpStatus.CREATED) @PostMapping(ApiPath.ONBOARDING) @Operation(summary = "온보딩", description = "onboarding") fun onboarding( From be61fed8a7375f63b7b292015a5be58a4a99728d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Apr 2025 09:22:49 +0900 Subject: [PATCH 076/357] =?UTF-8?q?feat:=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=8B=9C=20=EB=B0=98=ED=99=98=20=EA=B0=92=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/gomushin/backend/couple/domain/entity/Couple.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt index ae250a78..c4ae611c 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt @@ -26,7 +26,7 @@ class Couple( @Column(name = "military_end_date") var militaryEndDate: LocalDate? = null, -): BaseEntity() { + ) : BaseEntity() { companion object { fun of( invitorId: Long, @@ -43,10 +43,9 @@ class Couple( relationshipStartDate: LocalDate?, militaryStartDate: LocalDate?, militaryEndDate: LocalDate?, - ): Couple { + ) { this.relationshipStartDate = relationshipStartDate ?: this.relationshipStartDate this.militaryStartDate = militaryStartDate ?: this.militaryStartDate this.militaryEndDate = militaryEndDate ?: this.militaryEndDate - return this } } From e44c023f44d487953ec43eb7bfc6d2734b8000e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 19 Apr 2025 19:37:32 +0900 Subject: [PATCH 077/357] =?UTF-8?q?main=EB=B8=8C=EB=9E=9C=EC=B9=98?= =?UTF-8?q?=EC=99=80=20=EB=A8=B8=EC=A7=80=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/couple/domain/entity/Couple.kt | 2 +- .../gomushin/backend/couple/facade/CoupleFacade.kt | 10 +++++++++- .../gomushin/backend/couple/presentation/ApiPath.kt | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt index c4ae611c..0373106d 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt @@ -26,7 +26,7 @@ class Couple( @Column(name = "military_end_date") var militaryEndDate: LocalDate? = null, - ) : BaseEntity() { +): BaseEntity() { companion object { fun of( invitorId: Long, diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 22105e64..d669fe24 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -3,12 +3,15 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest +import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.dto.request.CoupleConnectRequest +import gomushin.backend.couple.dto.response.CoupleGradeResponse import org.springframework.stereotype.Component @Component class CoupleFacade( - private val coupleConnectService: CoupleConnectService + private val coupleConnectService: CoupleConnectService, + private val coupleInfoService: CoupleInfoService ) { fun requestCoupleCodeGeneration(customUserDetails: CustomUserDetails) = @@ -26,4 +29,9 @@ class CoupleFacade( customUserDetails.getId(), request ) + + fun getGradeInfo(customUserDetails: CustomUserDetails): CoupleGradeResponse { + val grade = coupleInfoService.getGrade(customUserDetails.getId()) + return CoupleGradeResponse.of(grade) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index daf8ebbb..fedd54b2 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -4,4 +4,5 @@ object ApiPath { const val COUPLE_CODE_GENERATE = "/v1/couple/code-generate" const val COUPLE_CONNECT = "/v1/couple/connect" const val COUPLE_ANNIVERSARY = "/v1/couple/anniversary" + const val COUPLE_PROFILE = "/v1/couple/profile" } From 4924f68c8c422f7cfda5cf9b3957f4b17442bd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 19 Apr 2025 23:10:34 +0900 Subject: [PATCH 078/357] =?UTF-8?q?feat=20:=20grade=EA=B5=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20api=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/CoupleRepository.kt | 5 +- .../domain/service/CoupleInfoService.kt | 34 ++++++ .../dto/response/CoupleGradeResponse.kt | 10 ++ .../presentation/CoupleInfoController.kt | 26 +++++ .../domain/service/CoupleInfoServiceTest.kt | 106 ++++++++++++++++++ .../backend/member/facade/CoupleFacadeTest.kt | 85 ++++++++++++++ 6 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/CoupleGradeResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt create mode 100644 src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt create mode 100644 src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt index e76755d6..f9b173b4 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt @@ -3,4 +3,7 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Couple import org.springframework.data.jpa.repository.JpaRepository -interface CoupleRepository : JpaRepository +interface CoupleRepository : JpaRepository { + fun findByInvitorId(invitorId: Long) : Couple? + fun findByInviteeId(inviteeId: Long) : Couple? +} diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt new file mode 100644 index 00000000..f9d84c86 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -0,0 +1,34 @@ +package gomushin.backend.couple.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.repository.CoupleRepository +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.Period + +@Service +class CoupleInfoService( + private val coupleRepository: CoupleRepository +) { + fun getGrade(id: Long): Int { + val couple = coupleRepository.findByInvitorId(id) + ?: coupleRepository.findByInviteeId(id) + ?: throw BadRequestException("saranggun.couple.not-connected") + val militaryStartDate = couple.militaryStartDate ?: throw BadRequestException("saranggun.couple.not-defined-militaryStartDate") + val today = LocalDate.now() + return computeGrade(militaryStartDate, today) + } + + fun computeGrade(militaryStartDate: LocalDate, today: LocalDate) : Int { + val period = Period.between(militaryStartDate, today) + val totalMonths = period.years * 12 + period.months + if (period.days >= 1) 1 else 0 + + return when { + totalMonths < 2 -> 1 + totalMonths < 8 -> 2 + totalMonths < 14 -> 3 + else -> 4 + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleGradeResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleGradeResponse.kt new file mode 100644 index 00000000..a11b34fe --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleGradeResponse.kt @@ -0,0 +1,10 @@ +package gomushin.backend.couple.dto.response +data class CoupleGradeResponse( + val grade : Int +){ + companion object{ + fun of(grade: Int) = CoupleGradeResponse( + grade + ) + } +} diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt new file mode 100644 index 00000000..9a98426c --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -0,0 +1,26 @@ +package gomushin.backend.couple.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.couple.facade.CoupleFacade +import gomushin.backend.couple.dto.response.CoupleGradeResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "커플 정보 불러오기", description = "CoupleInfoController") +class CoupleInfoController ( + private val coupleFacade: CoupleFacade +) { + @GetMapping(ApiPath.COUPLE_PROFILE) + @Operation(summary = "프로필 조회", description = "입대일 날짜 기준으로 grade측정") + fun profile( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ): ApiResponse { + val grade = coupleFacade.getGradeInfo(customUserDetails) + return ApiResponse.success(grade) + } +} \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt new file mode 100644 index 00000000..3128857f --- /dev/null +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -0,0 +1,106 @@ +package gomushin.backend.couple.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.CoupleRepository +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.boot.test.context.SpringBootTest +import java.time.LocalDate + + +@ExtendWith(MockitoExtension::class) +class CoupleInfoServiceTest { + @Mock + private lateinit var coupleRepository: CoupleRepository + + @InjectMocks + private lateinit var coupleInfoService: CoupleInfoService + + @DisplayName("getGrade - 성공(invitorId가 주어졌을 떄)") + @Test + fun getGrade_success_1() { + // given + val coupleId = 1L + val invitorId = 1L + val couple = Couple( + id = coupleId, + invitorId = 1L, + inviteeId = 2L, + militaryStartDate = LocalDate.of(2021,5,24) + ) + + Mockito.`when`(coupleRepository.findByInvitorId(invitorId)).thenReturn(couple) + + // when + coupleInfoService.getGrade(invitorId) + } + + @DisplayName("getGrade - 성공(inviteeId가 주어졌을 떄)") + @Test + fun getGrade_success_2() { + // given + val coupleId = 1L + val inviteeId = 2L + val couple = Couple( + id = coupleId, + invitorId = 1L, + inviteeId = 2L, + militaryStartDate = LocalDate.of(2021,5,24) + ) + + `when`(coupleRepository.findByInvitorId(anyLong())).thenReturn(null) + `when`(coupleRepository.findByInvitorId(inviteeId)).thenReturn(couple) + + + // when + coupleInfoService.getGrade(inviteeId) + } + +// @DisplayName("getGrade - 실패(couple이 아닐 때)") +// @Test +// fun getGrade_fail() { +// // given +// val invitorId = 1L +// +// `when`(coupleRepository.findByInvitorId(anyLong())).thenReturn(null) +// `when`(coupleRepository.findByInviteeId(anyLong())).thenReturn(null) +// +// // when & then +// val exception = assertThrows(BadRequestException::class.java) { +// coupleInfoService.getGrade(invitorId) +// } +// assert(exception.message == "saranggun.couple.not-connected") +// } + + @DisplayName("computeGrade - 성공") + @Test + fun computeGrade_success() { + // given + val gradeMilitaryStartDate = LocalDate.of(2021, 5, 24) + + val gradeOneToday = LocalDate.of(2021, 6, 1) + val gradeTwoToday = LocalDate.of(2021, 7, 1) + val gradeThreeToday = LocalDate.of(2022, 2, 1) + val gradeFourToday = LocalDate.of(2022,8,1) + + // when + val resultGradeOne = coupleInfoService.computeGrade(gradeMilitaryStartDate, gradeOneToday) + val resultGradeTwo = coupleInfoService.computeGrade(gradeMilitaryStartDate, gradeTwoToday) + val resultGradeThree = coupleInfoService.computeGrade(gradeMilitaryStartDate, gradeThreeToday) + val resultGradeFour = coupleInfoService.computeGrade(gradeMilitaryStartDate, gradeFourToday) + + // then + assert(resultGradeOne == 1) + assert(resultGradeTwo == 2) + assert(resultGradeThree == 3) + assert(resultGradeFour == 4) + } +} \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt new file mode 100644 index 00000000..f80e0666 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -0,0 +1,85 @@ +package gomushin.backend.member.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.service.CoupleConnectService +import gomushin.backend.couple.domain.service.CoupleInfoService +import gomushin.backend.couple.facade.CoupleFacade +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.value.Provider +import gomushin.backend.member.domain.value.Role +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension +import java.time.LocalDate +import kotlin.test.assertEquals + +@ExtendWith(MockitoExtension::class) +class CoupleFacadeTest { + @Mock + private lateinit var coupleInfoService: CoupleInfoService + + @Mock + private lateinit var coupleConnectService: CoupleConnectService + + @InjectMocks + private lateinit var coupleFacade: CoupleFacade + + private lateinit var customUserDetails: CustomUserDetails + private lateinit var member1: Member + private lateinit var member2: Member + private lateinit var couple : Couple + + @BeforeEach + fun setUp(){ + member1 = Member( + id = 1L, + name = "곰신", + nickname = "곰신닉네임", + email = "test1@test.com", + birthDate = null, + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.MEMBER, + ) + + member2 = Member( + id = 2L, + name = "꽃신", + nickname = "꽃신닉네임", + email = "test2@test.com", + birthDate = null, + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.MEMBER, + ) + + couple = Couple( + id = 1L, + invitorId = 1L, + inviteeId = 2L, + militaryStartDate = LocalDate.of(2021,5,24) + ) + + customUserDetails = Mockito.mock(CustomUserDetails::class.java) + + `when`(customUserDetails.getId()).thenReturn(1L) + } + + @DisplayName("grade 조회") + @Test + fun getGradeInfo(){ + `when`(coupleInfoService.getGrade(customUserDetails.getId())).thenReturn(1) + val result = coupleFacade.getGradeInfo(customUserDetails) + verify(coupleInfoService).getGrade(1L) + assertEquals(1, result.grade) + } + +} \ No newline at end of file From ec54ec32a8e2537790eaf9d6e5269f56624f91aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 20 Apr 2025 02:47:48 +0900 Subject: [PATCH 079/357] =?UTF-8?q?feat=20:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CoupleInfoService.kt | 11 +++++-- .../backend/couple/facade/CoupleFacade.kt | 4 +++ .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/CoupleInfoController.kt | 8 +++++ .../domain/service/CoupleInfoServiceTest.kt | 30 ++++++++++++++++++- .../backend/member/facade/CoupleFacadeTest.kt | 9 ++++++ 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index f9d84c86..93771aaf 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -1,6 +1,7 @@ package gomushin.backend.couple.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.CoupleRepository import org.springframework.stereotype.Service import java.time.LocalDate @@ -11,8 +12,7 @@ class CoupleInfoService( private val coupleRepository: CoupleRepository ) { fun getGrade(id: Long): Int { - val couple = coupleRepository.findByInvitorId(id) - ?: coupleRepository.findByInviteeId(id) + val couple = getCouple(id) ?: throw BadRequestException("saranggun.couple.not-connected") val militaryStartDate = couple.militaryStartDate ?: throw BadRequestException("saranggun.couple.not-defined-militaryStartDate") val today = LocalDate.now() @@ -31,4 +31,11 @@ class CoupleInfoService( } } + fun getCouple(id : Long): Couple? { + return coupleRepository.findByInvitorId(id) + ?: coupleRepository.findByInviteeId(id) + } + + fun checkCouple(id: Long): Boolean = getCouple(id) != null + } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index d669fe24..f234881d 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -34,4 +34,8 @@ class CoupleFacade( val grade = coupleInfoService.getGrade(customUserDetails.getId()) return CoupleGradeResponse.of(grade) } + + fun checkConnect(customUserDetails: CustomUserDetails): Boolean { + return coupleInfoService.checkCouple(customUserDetails.getId()) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index fedd54b2..041e3798 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -5,4 +5,5 @@ object ApiPath { const val COUPLE_CONNECT = "/v1/couple/connect" const val COUPLE_ANNIVERSARY = "/v1/couple/anniversary" const val COUPLE_PROFILE = "/v1/couple/profile" + const val COUPLE_CHECK_CONNECT = "/v1/couple/check-connect" } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index 9a98426c..8f3edbdc 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -23,4 +23,12 @@ class CoupleInfoController ( val grade = coupleFacade.getGradeInfo(customUserDetails) return ApiResponse.success(grade) } + + @GetMapping(ApiPath.COUPLE_CHECK_CONNECT) + @Operation(summary = "커플 연동 여부", description = "커플 연동 여부 true, false로 불러오기") + fun check( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ): ApiResponse { + return ApiResponse.success(coupleFacade.checkConnect(customUserDetails)) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index 3128857f..b13f2531 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -14,6 +14,7 @@ import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension import org.springframework.boot.test.context.SpringBootTest import java.time.LocalDate +import kotlin.test.assertEquals @ExtendWith(MockitoExtension::class) @@ -57,7 +58,7 @@ class CoupleInfoServiceTest { ) `when`(coupleRepository.findByInvitorId(anyLong())).thenReturn(null) - `when`(coupleRepository.findByInvitorId(inviteeId)).thenReturn(couple) + `when`(coupleRepository.findByInviteeId(inviteeId)).thenReturn(couple) // when @@ -103,4 +104,31 @@ class CoupleInfoServiceTest { assert(resultGradeThree == 3) assert(resultGradeFour == 4) } + + @DisplayName("checkCouple - 성공") + @Test + fun checkCouple_success() { + //given + val coupleId = 1L + val invitorId = 1L + val inviteeId = 2L + val couple = Couple( + id = coupleId, + invitorId = invitorId, + inviteeId = inviteeId, + militaryStartDate = LocalDate.of(2021,5,24) + ) + `when`(coupleRepository.findByInvitorId(invitorId)).thenReturn(couple) + `when`(coupleRepository.findByInviteeId(inviteeId)).thenReturn(couple) + + //when + val resultTrue1 = coupleInfoService.checkCouple(1L) + val resultTrue2 = coupleInfoService.checkCouple(2L) + val resultFalse = coupleInfoService.checkCouple(3L) + + //then + assertEquals(true, resultTrue1) + assertEquals(true, resultTrue2) + assertEquals(false, resultFalse) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index f80e0666..75a8bca0 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -82,4 +82,13 @@ class CoupleFacadeTest { assertEquals(1, result.grade) } + @DisplayName("커플 연동 여부 조회") + @Test + fun checkConnect(){ + `when`(coupleInfoService.checkCouple(customUserDetails.getId())).thenReturn(true) + val result = coupleFacade.checkConnect(customUserDetails) + verify(coupleInfoService).checkCouple(1L) + assertEquals(true, result) + } + } \ No newline at end of file From 231196f7815133a7c74a1fb0b9240d74b54e22d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 20 Apr 2025 17:34:25 +0900 Subject: [PATCH 080/357] =?UTF-8?q?feat=20:=20=EB=94=94=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CoupleInfoService.kt | 21 ++++++++ .../couple/dto/response/DdayResponse.kt | 17 +++++++ .../backend/couple/facade/CoupleFacade.kt | 5 ++ .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/CoupleInfoController.kt | 9 ++++ .../domain/service/CoupleInfoServiceTest.kt | 51 +++++++++++++++++-- .../backend/member/facade/CoupleFacadeTest.kt | 23 +++++++++ 7 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index 93771aaf..b14f07d5 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -3,9 +3,11 @@ package gomushin.backend.couple.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.CoupleRepository +import gomushin.backend.couple.dto.response.DdayResponse import org.springframework.stereotype.Service import java.time.LocalDate import java.time.Period +import java.time.temporal.ChronoUnit @Service class CoupleInfoService( @@ -37,5 +39,24 @@ class CoupleInfoService( } fun checkCouple(id: Long): Boolean = getCouple(id) != null + fun getDday(id: Long): DdayResponse { + val couple = getCouple(id) + ?: throw BadRequestException("saranggun.couple.not-connected") + val today = LocalDate.now() + val sinceLove: Int? = couple.relationshipStartDate?.let { startLove -> + computeDday(startLove, today) + } + val sinceMilitaryStart : Int? = couple.militaryStartDate?.let { startMilitary -> + computeDday(startMilitary, today) + } + val militaryEndLeft : Int? = couple.militaryEndDate?.let { endMilitary -> + computeDday(endMilitary, today) + } + return DdayResponse(sinceLove, sinceMilitaryStart, militaryEndLeft) + } + + fun computeDday(day1: LocalDate, day2: LocalDate) : Int { + return ChronoUnit.DAYS.between(day1, day2).toInt() + } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt new file mode 100644 index 00000000..c14b9fff --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt @@ -0,0 +1,17 @@ +package gomushin.backend.couple.dto.response + +data class DdayResponse ( + val sinceLove : Int?, + val sinceMilitaryStart : Int?, + val militaryEndLeft : Int? +){ + companion object{ + fun of(sinceLove: Int, + sinceMilitaryStart: Int, + militaryEndLeft: Int) = DdayResponse ( + sinceLove, + sinceMilitaryStart, + militaryEndLeft + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index f234881d..d3ded804 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -6,6 +6,7 @@ import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.dto.request.CoupleConnectRequest import gomushin.backend.couple.dto.response.CoupleGradeResponse +import gomushin.backend.couple.dto.response.DdayResponse import org.springframework.stereotype.Component @Component @@ -38,4 +39,8 @@ class CoupleFacade( fun checkConnect(customUserDetails: CustomUserDetails): Boolean { return coupleInfoService.checkCouple(customUserDetails.getId()) } + + fun getDday(customUserDetails: CustomUserDetails): DdayResponse { + return coupleInfoService.getDday(customUserDetails.getId()) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index 041e3798..806be1a7 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -6,4 +6,5 @@ object ApiPath { const val COUPLE_ANNIVERSARY = "/v1/couple/anniversary" const val COUPLE_PROFILE = "/v1/couple/profile" const val COUPLE_CHECK_CONNECT = "/v1/couple/check-connect" + const val COUPLE_DDAY_INFO = "/v1/couple/d-day" } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index 8f3edbdc..8859b399 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.couple.dto.response.CoupleGradeResponse +import gomushin.backend.couple.dto.response.DdayResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -31,4 +32,12 @@ class CoupleInfoController ( ): ApiResponse { return ApiResponse.success(coupleFacade.checkConnect(customUserDetails)) } + + @GetMapping(ApiPath.COUPLE_DDAY_INFO) + @Operation(summary = "디데이 정보", description = "사귄지, 입대한지 얼마되었는지 그리고 전역까지 얼마나 남았는지") + fun dDay( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ):ApiResponse { + return ApiResponse.success(coupleFacade.getDday(customUserDetails)) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index b13f2531..9e563177 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -1,9 +1,8 @@ package gomushin.backend.couple.domain.service -import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.CoupleRepository -import org.junit.jupiter.api.Assertions.assertThrows +import gomushin.backend.couple.dto.response.DdayResponse import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -12,7 +11,6 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension -import org.springframework.boot.test.context.SpringBootTest import java.time.LocalDate import kotlin.test.assertEquals @@ -131,4 +129,51 @@ class CoupleInfoServiceTest { assertEquals(true, resultTrue2) assertEquals(false, resultFalse) } + + @DisplayName("computeDday") + @Test + fun computeDday(){ + //given + val today = LocalDate.of(2025, 4, 20) + val yesterday = LocalDate.of(2025, 4, 19) + val tomorrow = LocalDate.of(2025, 4, 21) + + //when + val plusDday = coupleInfoService.computeDday(yesterday, today) + val minusDday = coupleInfoService.computeDday(tomorrow, today) + + //then + assertEquals(-1, minusDday) + assertEquals(1, plusDday) + } + + @DisplayName("getDday") + @Test + fun getDday(){ + //given + val coupleId = 1L + val invitorId = 1L + val inviteeId = 2L + val today = LocalDate.now() + val couple = Couple( + id = coupleId, + invitorId = invitorId, + inviteeId = inviteeId, + militaryStartDate = LocalDate.of(2024,5,24), + militaryEndDate = LocalDate.of(2025, 11, 23), + relationshipStartDate = LocalDate.of(2024,4,24) + ) + val militaryStartDate = couple.militaryStartDate!! + val militaryEndDate = couple.militaryEndDate!! + val relationshipStartDate = couple.relationshipStartDate!! + `when`(coupleRepository.findByInvitorId(invitorId)).thenReturn(couple) + + //when + val response = coupleInfoService.getDday(invitorId) + + //then + assertEquals(coupleInfoService.computeDday(militaryStartDate, today), response.sinceMilitaryStart) + assertEquals(coupleInfoService.computeDday(militaryEndDate, today), response.militaryEndLeft) + assertEquals(coupleInfoService.computeDday(relationshipStartDate, today), response.sinceLove) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 75a8bca0..5c1cc537 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.domain.service.CoupleInfoService +import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.value.Provider @@ -91,4 +92,26 @@ class CoupleFacadeTest { assertEquals(true, result) } + @DisplayName("디데이 조회 - 정상응답") + @Test + fun getDday(){ + `when`(coupleInfoService.getDday(customUserDetails.getId())).thenReturn(DdayResponse(100, 200, -345)) + val result = coupleFacade.getDday(customUserDetails) + verify(coupleInfoService).getDday(1L) + assertEquals(100, result.sinceLove) + assertEquals(200, result.sinceMilitaryStart) + assertEquals(-345, result.militaryEndLeft) + } + + @DisplayName("디데이 조회 - 날짜 정보 안 들어갔을 때") + @Test + fun getDdayNull(){ + `when`(coupleInfoService.getDday(customUserDetails.getId())).thenReturn(DdayResponse(null, null, null)) + val result = coupleFacade.getDday(customUserDetails) + verify(coupleInfoService).getDday(1L) + assertEquals(null, result.sinceLove) + assertEquals(null, result.sinceMilitaryStart) + assertEquals(null, result.militaryEndLeft) + } + } \ No newline at end of file From 2d3291160396d48fdd5cc59b7fbd4e56ecefc576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 20 Apr 2025 18:43:03 +0900 Subject: [PATCH 081/357] =?UTF-8?q?feat=20:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CoupleInfoService.kt | 22 ++++++- .../couple/dto/response/DdayResponse.kt | 6 +- .../couple/dto/response/NicknameResponse.kt | 16 +++++ .../backend/couple/facade/CoupleFacade.kt | 5 ++ .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/CoupleInfoController.kt | 15 +++++ .../domain/service/CoupleInfoServiceTest.kt | 62 +++++++++++++++++-- .../backend/member/facade/CoupleFacadeTest.kt | 11 ++++ 8 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/NicknameResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index b14f07d5..c61c006d 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -4,6 +4,8 @@ import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.CoupleRepository import gomushin.backend.couple.dto.response.DdayResponse +import gomushin.backend.couple.dto.response.NicknameResponse +import gomushin.backend.member.domain.repository.MemberRepository import org.springframework.stereotype.Service import java.time.LocalDate import java.time.Period @@ -11,7 +13,8 @@ import java.time.temporal.ChronoUnit @Service class CoupleInfoService( - private val coupleRepository: CoupleRepository + private val coupleRepository: CoupleRepository, + private val memberRepository: MemberRepository ) { fun getGrade(id: Long): Int { val couple = getCouple(id) @@ -52,11 +55,26 @@ class CoupleInfoService( val militaryEndLeft : Int? = couple.militaryEndDate?.let { endMilitary -> computeDday(endMilitary, today) } - return DdayResponse(sinceLove, sinceMilitaryStart, militaryEndLeft) + return DdayResponse.of(sinceLove, sinceMilitaryStart, militaryEndLeft) } fun computeDday(day1: LocalDate, day2: LocalDate) : Int { return ChronoUnit.DAYS.between(day1, day2).toInt() } + fun nickName(id: Long): NicknameResponse { + val userMember = memberRepository.findById(id).orElseThrow { + BadRequestException("saranggun.member.not-found") + } + + val couple = getCouple(id) ?: throw BadRequestException("saranggun.couple.not-connected") + val coupleMemberId = if (couple.invitorId == id) couple.inviteeId else couple.invitorId + + val coupleMember = memberRepository.findById(coupleMemberId).orElseThrow { + BadRequestException("sarangggun.couple.not-exist-couple") + } + + return NicknameResponse.of(userMember.nickname, coupleMember.nickname) + } + } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt index c14b9fff..8e08c78b 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt @@ -6,9 +6,9 @@ data class DdayResponse ( val militaryEndLeft : Int? ){ companion object{ - fun of(sinceLove: Int, - sinceMilitaryStart: Int, - militaryEndLeft: Int) = DdayResponse ( + fun of(sinceLove: Int?, + sinceMilitaryStart: Int?, + militaryEndLeft: Int?) = DdayResponse ( sinceLove, sinceMilitaryStart, militaryEndLeft diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/NicknameResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/NicknameResponse.kt new file mode 100644 index 00000000..de33266e --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/NicknameResponse.kt @@ -0,0 +1,16 @@ +package gomushin.backend.couple.dto.response + +data class NicknameResponse ( + val userNickname : String, + val coupleNickname : String +) { + companion object { + fun of( + userNickname: String, + coupleNickname: String + ) = NicknameResponse( + userNickname, + coupleNickname + ) + } +} diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index d3ded804..e88e6496 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -7,6 +7,7 @@ import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.dto.request.CoupleConnectRequest import gomushin.backend.couple.dto.response.CoupleGradeResponse import gomushin.backend.couple.dto.response.DdayResponse +import gomushin.backend.couple.dto.response.NicknameResponse import org.springframework.stereotype.Component @Component @@ -43,4 +44,8 @@ class CoupleFacade( fun getDday(customUserDetails: CustomUserDetails): DdayResponse { return coupleInfoService.getDday(customUserDetails.getId()) } + + fun nickName(customUserDetails: CustomUserDetails) : NicknameResponse { + return coupleInfoService.nickName(customUserDetails.getId()) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index 806be1a7..4087fd39 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -7,4 +7,5 @@ object ApiPath { const val COUPLE_PROFILE = "/v1/couple/profile" const val COUPLE_CHECK_CONNECT = "/v1/couple/check-connect" const val COUPLE_DDAY_INFO = "/v1/couple/d-day" + const val COUPLE_NICKNAME = "/v1/couple/nick-name" } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index 8859b399..b1faf090 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -5,10 +5,13 @@ import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.couple.dto.response.CoupleGradeResponse import gomushin.backend.couple.dto.response.DdayResponse +import gomushin.backend.couple.dto.response.NicknameResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @@ -16,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController class CoupleInfoController ( private val coupleFacade: CoupleFacade ) { + @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_PROFILE) @Operation(summary = "프로필 조회", description = "입대일 날짜 기준으로 grade측정") fun profile( @@ -25,6 +29,7 @@ class CoupleInfoController ( return ApiResponse.success(grade) } + @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_CHECK_CONNECT) @Operation(summary = "커플 연동 여부", description = "커플 연동 여부 true, false로 불러오기") fun check( @@ -33,6 +38,7 @@ class CoupleInfoController ( return ApiResponse.success(coupleFacade.checkConnect(customUserDetails)) } + @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_DDAY_INFO) @Operation(summary = "디데이 정보", description = "사귄지, 입대한지 얼마되었는지 그리고 전역까지 얼마나 남았는지") fun dDay( @@ -40,4 +46,13 @@ class CoupleInfoController ( ):ApiResponse { return ApiResponse.success(coupleFacade.getDday(customUserDetails)) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.COUPLE_NICKNAME) + @Operation(summary = "닉네임 조회", description = "userNickname = 내 닉네임, coupleNickName = 내 여(남)친 닉네임") + fun nickName( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ):ApiResponse{ + return ApiResponse.success(coupleFacade.nickName(customUserDetails)) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index 9e563177..ad89ed3d 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -2,7 +2,10 @@ package gomushin.backend.couple.domain.service import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.CoupleRepository -import gomushin.backend.couple.dto.response.DdayResponse +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Provider +import gomushin.backend.member.domain.value.Role import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -12,6 +15,7 @@ import org.mockito.Mockito import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension import java.time.LocalDate +import java.util.* import kotlin.test.assertEquals @@ -19,6 +23,8 @@ import kotlin.test.assertEquals class CoupleInfoServiceTest { @Mock private lateinit var coupleRepository: CoupleRepository + @Mock + private lateinit var memberRepository: MemberRepository @InjectMocks private lateinit var coupleInfoService: CoupleInfoService @@ -130,9 +136,9 @@ class CoupleInfoServiceTest { assertEquals(false, resultFalse) } - @DisplayName("computeDday") + @DisplayName("computeDday - 성공") @Test - fun computeDday(){ + fun computeDday_success(){ //given val today = LocalDate.of(2025, 4, 20) val yesterday = LocalDate.of(2025, 4, 19) @@ -147,9 +153,9 @@ class CoupleInfoServiceTest { assertEquals(1, plusDday) } - @DisplayName("getDday") + @DisplayName("getDday - 성공") @Test - fun getDday(){ + fun getDday_success(){ //given val coupleId = 1L val invitorId = 1L @@ -176,4 +182,50 @@ class CoupleInfoServiceTest { assertEquals(coupleInfoService.computeDday(militaryEndDate, today), response.militaryEndLeft) assertEquals(coupleInfoService.computeDday(relationshipStartDate, today), response.sinceLove) } + + @DisplayName("nickName-성공") + @Test + fun nickName(){ + //given + val coupleId = 1L + val userId = 1L + val coupleUserId = 2L + val couple = Couple( + id = coupleId, + invitorId = coupleUserId, + inviteeId = userId, + ) + val user = Member( + id = 1L, + name="김영록", + nickname="김영록", + email="test@test.com", + profileImageUrl = "url", + birthDate= LocalDate.of(2001,3,27), + provider=Provider.KAKAO, + role= Role.MEMBER, + isCouple= true + ) + val coupleUser = Member( + id = 2L, + name="김영록 여친", + nickname="김영록 여친", + email="test2@test.com", + profileImageUrl = "url2", + birthDate= LocalDate.of(2001,5,19), + provider=Provider.KAKAO, + role= Role.MEMBER, + isCouple= true + ) + `when`(coupleRepository.findByInvitorId(userId)).thenReturn(couple) + `when`(memberRepository.findById(userId)).thenReturn(Optional.of(user)) + `when`(memberRepository.findById(coupleUserId)).thenReturn(Optional.of(coupleUser)) + + //when + val nicknameResponse = coupleInfoService.nickName(userId) + + //then + assertEquals("김영록 여친", nicknameResponse.coupleNickname) + assertEquals("김영록", nicknameResponse.userNickname) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 5c1cc537..4cde2b22 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.dto.response.DdayResponse +import gomushin.backend.couple.dto.response.NicknameResponse import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.value.Provider @@ -114,4 +115,14 @@ class CoupleFacadeTest { assertEquals(null, result.militaryEndLeft) } + @DisplayName("닉네임 조회 - 정상응답") + @Test + fun nickName(){ + `when`(coupleInfoService.nickName(customUserDetails.getId())).thenReturn(NicknameResponse("김영록", "김영록 여친")) + val result = coupleFacade.nickName(customUserDetails) + verify(coupleInfoService).nickName(1L) + assertEquals("김영록", result.userNickname) + assertEquals("김영록 여친", result.coupleNickname) + } + } \ No newline at end of file From 988044c050093cfcbcc52081800652bf3594ce6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 20 Apr 2025 22:50:40 +0900 Subject: [PATCH 082/357] =?UTF-8?q?feat=20:=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=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 --- .../domain/service/CoupleInfoService.kt | 10 ++++++ .../dto/response/StatusMessageResponse.kt | 13 +++++++ .../backend/couple/facade/CoupleFacade.kt | 6 ++++ .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/CoupleInfoController.kt | 10 ++++++ .../backend/member/domain/entity/Member.kt | 3 ++ .../domain/service/CoupleInfoServiceTest.kt | 34 +++++++++++++++++++ .../backend/member/facade/CoupleFacadeTest.kt | 9 +++++ 8 files changed, 86 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/StatusMessageResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index c61c006d..e5e3ad5e 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -77,4 +77,14 @@ class CoupleInfoService( return NicknameResponse.of(userMember.nickname, coupleMember.nickname) } + fun getStatusMessage(id: Long): String? { + val couple = getCouple(id) ?: throw BadRequestException("saranggun.couple.not-connected") + val coupleMemberId = if (couple.invitorId == id) couple.inviteeId else couple.invitorId + + val coupleMember = memberRepository.findById(coupleMemberId).orElseThrow { + BadRequestException("sarangggun.couple.not-exist-couple") + } + return coupleMember.statusMessage + } + } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/StatusMessageResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/StatusMessageResponse.kt new file mode 100644 index 00000000..c80d4359 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/StatusMessageResponse.kt @@ -0,0 +1,13 @@ +package gomushin.backend.couple.dto.response + +data class StatusMessageResponse ( + val statusMessage : String? +) { + companion object { + fun of( + statusMessage: String? + )=StatusMessageResponse( + statusMessage + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index e88e6496..472292d9 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -8,6 +8,7 @@ import gomushin.backend.couple.dto.request.CoupleConnectRequest import gomushin.backend.couple.dto.response.CoupleGradeResponse import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse +import gomushin.backend.couple.dto.response.StatusMessageResponse import org.springframework.stereotype.Component @Component @@ -48,4 +49,9 @@ class CoupleFacade( fun nickName(customUserDetails: CustomUserDetails) : NicknameResponse { return coupleInfoService.nickName(customUserDetails.getId()) } + + fun statusMessage(customUserDetails: CustomUserDetails): StatusMessageResponse { + val statusMessage = coupleInfoService.getStatusMessage(customUserDetails.getId()) + return StatusMessageResponse.of(statusMessage) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index 4087fd39..ea55f864 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -8,4 +8,5 @@ object ApiPath { const val COUPLE_CHECK_CONNECT = "/v1/couple/check-connect" const val COUPLE_DDAY_INFO = "/v1/couple/d-day" const val COUPLE_NICKNAME = "/v1/couple/nick-name" + const val COUPLE_STATUS_MESSAGE = "/v1/couple/status-message" } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index b1faf090..7ae8b224 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -6,6 +6,7 @@ import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.couple.dto.response.CoupleGradeResponse import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse +import gomushin.backend.couple.dto.response.StatusMessageResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus @@ -55,4 +56,13 @@ class CoupleInfoController ( ):ApiResponse{ return ApiResponse.success(coupleFacade.nickName(customUserDetails)) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.COUPLE_STATUS_MESSAGE) + @Operation(summary = "상태 메시지 조회", description = "상태 메시지 조회") + fun statusMessage( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ):ApiResponse{ + return ApiResponse.success(coupleFacade.statusMessage(customUserDetails)) + } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index 35bfbc4f..7e4f12d7 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -25,6 +25,9 @@ class Member( @Column(name = "birth_date") var birthDate: LocalDate? = null, + @Column(name = "status_message") + var statusMessage : String? = null, + @Column(name = "profile_image_url") var profileImageUrl: String?, diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index ad89ed3d..b1bf21cc 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -228,4 +228,38 @@ class CoupleInfoServiceTest { assertEquals("김영록 여친", nicknameResponse.coupleNickname) assertEquals("김영록", nicknameResponse.userNickname) } + + @DisplayName("statusMessage-성공") + @Test + fun statusMessage(){ + //given + val coupleId = 1L + val userId = 1L + val coupleUserId = 2L + val couple = Couple( + id = coupleId, + invitorId = coupleUserId, + inviteeId = userId, + ) + val coupleUser = Member( + id = 2L, + name="김영록 여친", + nickname="김영록 여친", + email="test2@test.com", + profileImageUrl = "url2", + birthDate= LocalDate.of(2001,5,19), + provider=Provider.KAKAO, + role= Role.MEMBER, + isCouple= true, + statusMessage = "기분이 좋아용" + ) + `when`(coupleRepository.findByInvitorId(userId)).thenReturn(couple) + `when`(memberRepository.findById(coupleUserId)).thenReturn(Optional.of(coupleUser)) + + //when + val statusMessage = coupleInfoService.getStatusMessage(userId) + + //then + assertEquals("기분이 좋아용", statusMessage) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 4cde2b22..594af899 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -125,4 +125,13 @@ class CoupleFacadeTest { assertEquals("김영록 여친", result.coupleNickname) } + @DisplayName("상태 메시지 조회 - 정상응답") + @Test + fun statusMessage(){ + `when`(coupleInfoService.getStatusMessage(customUserDetails.getId())).thenReturn("기분이 좋아용") + val result = coupleFacade.statusMessage(customUserDetails) + verify(coupleInfoService).getStatusMessage(1L) + assertEquals("기분이 좋아용", result.statusMessage) + } + } \ No newline at end of file From 0b170222d20df7fe89f8589653a5263a48af911a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 21 Apr 2025 21:12:21 +0900 Subject: [PATCH 083/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=ED=95=B4=EC=84=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?JPQL=EC=82=AC=EC=9A=A9=ED=95=B4=EC=84=9C=20=EC=BB=A4=ED=94=8C?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EA=B5=AC=ED=95=A0=20=EB=95=8C=20findByInv?= =?UTF-8?q?itorId,=20findByInviteeId=20=EB=91=90=EB=B2=88=20=EC=95=88?= =?UTF-8?q?=EC=93=B0=EA=B2=8C=20=ED=96=88=EC=9D=8C=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=EB=90=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=94=B0=EB=A1=9C=20=EB=B6=84=EB=A6=AC=EC=8B=9C=ED=82=B4(?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=EC=9D=98=20=EC=BB=A4=ED=94=8C=20member?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EA=B5=AC=ED=95=98=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81)=20=EC=BB=A4=ED=94=8C=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=ED=95=A0=20=EB=95=8C=20member=EC=9D=98=20isC?= =?UTF-8?q?ouple=EC=BB=AC=EB=9F=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EA=B2=8C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=8F=84=20=EC=88=98=EC=A0=95=ED=95=9C=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/CoupleRepository.kt | 5 ++ .../domain/service/CoupleInfoService.kt | 38 +++++++----- .../backend/couple/facade/CoupleFacade.kt | 2 +- .../presentation/CoupleInfoController.kt | 10 +-- .../domain/service/CoupleInfoServiceTest.kt | 61 +++++++++++++------ .../backend/member/facade/CoupleFacadeTest.kt | 4 +- 6 files changed, 79 insertions(+), 41 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt index f9b173b4..e508a722 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt @@ -2,8 +2,13 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Couple import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param interface CoupleRepository : JpaRepository { fun findByInvitorId(invitorId: Long) : Couple? fun findByInviteeId(inviteeId: Long) : Couple? + + @Query("SELECT c FROM Couple c WHERE c.invitorId = :memberId OR c.inviteeId = :memberId") + fun findByMemberId(@Param("memberId") memberId: Long): Couple? } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index e5e3ad5e..9ae6e463 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -1,12 +1,13 @@ package gomushin.backend.couple.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException -import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.CoupleRepository import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse +import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import java.time.LocalDate import java.time.Period import java.time.temporal.ChronoUnit @@ -16,8 +17,9 @@ class CoupleInfoService( private val coupleRepository: CoupleRepository, private val memberRepository: MemberRepository ) { + @Transactional(readOnly = true) fun getGrade(id: Long): Int { - val couple = getCouple(id) + val couple = coupleRepository.findByMemberId(id) ?: throw BadRequestException("saranggun.couple.not-connected") val militaryStartDate = couple.militaryStartDate ?: throw BadRequestException("saranggun.couple.not-defined-militaryStartDate") val today = LocalDate.now() @@ -36,14 +38,16 @@ class CoupleInfoService( } } - fun getCouple(id : Long): Couple? { - return coupleRepository.findByInvitorId(id) - ?: coupleRepository.findByInviteeId(id) + @Transactional(readOnly = true) + fun checkCouple(id: Long): Boolean { + return memberRepository.findById(id) + .orElseThrow{ BadRequestException("sarangggun.member.not-exist-member") } + .isCouple } - fun checkCouple(id: Long): Boolean = getCouple(id) != null + @Transactional(readOnly = true) fun getDday(id: Long): DdayResponse { - val couple = getCouple(id) + val couple = coupleRepository.findByMemberId(id) ?: throw BadRequestException("saranggun.couple.not-connected") val today = LocalDate.now() val sinceLove: Int? = couple.relationshipStartDate?.let { startLove -> @@ -62,29 +66,31 @@ class CoupleInfoService( return ChronoUnit.DAYS.between(day1, day2).toInt() } - fun nickName(id: Long): NicknameResponse { + @Transactional(readOnly = true) + fun getNickName(id: Long): NicknameResponse { val userMember = memberRepository.findById(id).orElseThrow { BadRequestException("saranggun.member.not-found") } - val couple = getCouple(id) ?: throw BadRequestException("saranggun.couple.not-connected") - val coupleMemberId = if (couple.invitorId == id) couple.inviteeId else couple.invitorId - - val coupleMember = memberRepository.findById(coupleMemberId).orElseThrow { - BadRequestException("sarangggun.couple.not-exist-couple") - } + val coupleMember = findCoupleMember(id) return NicknameResponse.of(userMember.nickname, coupleMember.nickname) } + @Transactional(readOnly = true) fun getStatusMessage(id: Long): String? { - val couple = getCouple(id) ?: throw BadRequestException("saranggun.couple.not-connected") + val coupleMember = findCoupleMember(id) + return coupleMember.statusMessage + } + + private fun findCoupleMember(id : Long): Member { + val couple = coupleRepository.findByMemberId(id) ?: throw BadRequestException("saranggun.couple.not-connected") val coupleMemberId = if (couple.invitorId == id) couple.inviteeId else couple.invitorId val coupleMember = memberRepository.findById(coupleMemberId).orElseThrow { BadRequestException("sarangggun.couple.not-exist-couple") } - return coupleMember.statusMessage + return coupleMember } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 472292d9..fc41658d 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -47,7 +47,7 @@ class CoupleFacade( } fun nickName(customUserDetails: CustomUserDetails) : NicknameResponse { - return coupleInfoService.nickName(customUserDetails.getId()) + return coupleInfoService.getNickName(customUserDetails.getId()) } fun statusMessage(customUserDetails: CustomUserDetails): StatusMessageResponse { diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index 7ae8b224..ff80525c 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -23,7 +23,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_PROFILE) @Operation(summary = "프로필 조회", description = "입대일 날짜 기준으로 grade측정") - fun profile( + fun getGrade( @AuthenticationPrincipal customUserDetails: CustomUserDetails ): ApiResponse { val grade = coupleFacade.getGradeInfo(customUserDetails) @@ -33,7 +33,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_CHECK_CONNECT) @Operation(summary = "커플 연동 여부", description = "커플 연동 여부 true, false로 불러오기") - fun check( + fun coupleConnectCheck( @AuthenticationPrincipal customUserDetails: CustomUserDetails ): ApiResponse { return ApiResponse.success(coupleFacade.checkConnect(customUserDetails)) @@ -42,7 +42,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_DDAY_INFO) @Operation(summary = "디데이 정보", description = "사귄지, 입대한지 얼마되었는지 그리고 전역까지 얼마나 남았는지") - fun dDay( + fun getDday( @AuthenticationPrincipal customUserDetails: CustomUserDetails ):ApiResponse { return ApiResponse.success(coupleFacade.getDday(customUserDetails)) @@ -51,7 +51,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_NICKNAME) @Operation(summary = "닉네임 조회", description = "userNickname = 내 닉네임, coupleNickName = 내 여(남)친 닉네임") - fun nickName( + fun getNickName( @AuthenticationPrincipal customUserDetails: CustomUserDetails ):ApiResponse{ return ApiResponse.success(coupleFacade.nickName(customUserDetails)) @@ -60,7 +60,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_STATUS_MESSAGE) @Operation(summary = "상태 메시지 조회", description = "상태 메시지 조회") - fun statusMessage( + fun getStatusMessage( @AuthenticationPrincipal customUserDetails: CustomUserDetails ):ApiResponse{ return ApiResponse.success(coupleFacade.statusMessage(customUserDetails)) diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index b1bf21cc..344b7321 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -42,7 +42,7 @@ class CoupleInfoServiceTest { militaryStartDate = LocalDate.of(2021,5,24) ) - Mockito.`when`(coupleRepository.findByInvitorId(invitorId)).thenReturn(couple) + `when`(coupleRepository.findByMemberId(invitorId)).thenReturn(couple) // when coupleInfoService.getGrade(invitorId) @@ -61,8 +61,7 @@ class CoupleInfoServiceTest { militaryStartDate = LocalDate.of(2021,5,24) ) - `when`(coupleRepository.findByInvitorId(anyLong())).thenReturn(null) - `when`(coupleRepository.findByInviteeId(inviteeId)).thenReturn(couple) + `when`(coupleRepository.findByMemberId(inviteeId)).thenReturn(couple) // when @@ -113,17 +112,45 @@ class CoupleInfoServiceTest { @Test fun checkCouple_success() { //given - val coupleId = 1L - val invitorId = 1L - val inviteeId = 2L - val couple = Couple( - id = coupleId, - invitorId = invitorId, - inviteeId = inviteeId, - militaryStartDate = LocalDate.of(2021,5,24) + val userId = 1L + val coupleUserId = 2L + val notCoupleUserId = 3L + val user = Member( + id = userId, + name="김영록", + nickname="김영록", + email="test@test.com", + profileImageUrl = "url", + birthDate= LocalDate.of(2001,3,27), + provider=Provider.KAKAO, + role= Role.MEMBER, + isCouple= true ) - `when`(coupleRepository.findByInvitorId(invitorId)).thenReturn(couple) - `when`(coupleRepository.findByInviteeId(inviteeId)).thenReturn(couple) + val coupleUser = Member( + id = coupleUserId, + name="김영록 여친", + nickname="김영록 여친", + email="test2@test.com", + profileImageUrl = "url2", + birthDate= LocalDate.of(2001,5,19), + provider=Provider.KAKAO, + role= Role.MEMBER, + isCouple= true + ) + val notCoupleUser = Member( + id = coupleUserId, + name="김영록 여친", + nickname="김영록 여친", + email="test2@test.com", + profileImageUrl = "url2", + birthDate= LocalDate.of(2001,5,19), + provider=Provider.KAKAO, + role= Role.MEMBER, + isCouple= false + ) + `when`(memberRepository.findById(userId)).thenReturn(Optional.of(user)) + `when`(memberRepository.findById(coupleUserId)).thenReturn(Optional.of(coupleUser)) + `when`(memberRepository.findById(notCoupleUserId)).thenReturn(Optional.of(notCoupleUser)) //when val resultTrue1 = coupleInfoService.checkCouple(1L) @@ -172,7 +199,7 @@ class CoupleInfoServiceTest { val militaryStartDate = couple.militaryStartDate!! val militaryEndDate = couple.militaryEndDate!! val relationshipStartDate = couple.relationshipStartDate!! - `when`(coupleRepository.findByInvitorId(invitorId)).thenReturn(couple) + `when`(coupleRepository.findByMemberId(invitorId)).thenReturn(couple) //when val response = coupleInfoService.getDday(invitorId) @@ -217,12 +244,12 @@ class CoupleInfoServiceTest { role= Role.MEMBER, isCouple= true ) - `when`(coupleRepository.findByInvitorId(userId)).thenReturn(couple) + `when`(coupleRepository.findByMemberId(userId)).thenReturn(couple) `when`(memberRepository.findById(userId)).thenReturn(Optional.of(user)) `when`(memberRepository.findById(coupleUserId)).thenReturn(Optional.of(coupleUser)) //when - val nicknameResponse = coupleInfoService.nickName(userId) + val nicknameResponse = coupleInfoService.getNickName(userId) //then assertEquals("김영록 여친", nicknameResponse.coupleNickname) @@ -253,7 +280,7 @@ class CoupleInfoServiceTest { isCouple= true, statusMessage = "기분이 좋아용" ) - `when`(coupleRepository.findByInvitorId(userId)).thenReturn(couple) + `when`(coupleRepository.findByMemberId(userId)).thenReturn(couple) `when`(memberRepository.findById(coupleUserId)).thenReturn(Optional.of(coupleUser)) //when diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 594af899..794b6952 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -118,9 +118,9 @@ class CoupleFacadeTest { @DisplayName("닉네임 조회 - 정상응답") @Test fun nickName(){ - `when`(coupleInfoService.nickName(customUserDetails.getId())).thenReturn(NicknameResponse("김영록", "김영록 여친")) + `when`(coupleInfoService.getNickName(customUserDetails.getId())).thenReturn(NicknameResponse("김영록", "김영록 여친")) val result = coupleFacade.nickName(customUserDetails) - verify(coupleInfoService).nickName(1L) + verify(coupleInfoService).getNickName(1L) assertEquals("김영록", result.userNickname) assertEquals("김영록 여친", result.coupleNickname) } From f6d12a7af0dbab0d0443a81526a9cd3c76243238 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Apr 2025 13:03:07 +0900 Subject: [PATCH 084/357] =?UTF-8?q?refactor:=20fcmToken=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/member/domain/entity/Member.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index 7e4f12d7..9da0e767 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -42,6 +42,12 @@ class Member( @Column(name = "is_couple", nullable = false) var isCouple: Boolean = false, + @Column(name = "emotion") + var emotion: String = "", + + @Column(name = "fcm_token", nullable = false) + var fcmToken: String = "", + ) : BaseEntity() { companion object { fun create( @@ -49,7 +55,7 @@ class Member( nickname: String?, email: String, profileImageUrl: String?, - provider: Provider + provider: Provider, ): Member { return Member( name = name, @@ -61,6 +67,10 @@ class Member( } } + fun updateFcmToken(fcmToken: String) { + this.fcmToken = fcmToken + } + fun updateCoupleStatus() { this.isCouple = !this.isCouple } From d02c28ff55d080e50e1df4894e08018b54d23381 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Apr 2025 13:03:30 +0900 Subject: [PATCH 085/357] =?UTF-8?q?feat:=20Notification=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/domain/entity/Notification.kt | 40 +++++++++++++++++++ .../repository/NotificationRepository.kt | 8 ++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt create mode 100644 src/main/kotlin/gomushin/backend/member/domain/repository/NotificationRepository.kt diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt new file mode 100644 index 00000000..834889b4 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt @@ -0,0 +1,40 @@ +package gomushin.backend.member.domain.entity + +import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import jakarta.persistence.* + +@Entity +@Table(name = "notification") +class Notification( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + @Column(name = "member_id", nullable = false) + val memberId: Long, + + @Column(name = "dday", nullable = false) + var dday: Boolean = false, + + @Column(name = "partner_status", nullable = false) + var partnerStatus: Boolean = false, + + ) : BaseEntity() { + companion object { + fun create( + memberId: Long, + ): Notification { + return Notification( + memberId = memberId + ) + } + } + + fun updateDday() { + this.dday = !this.dday + } + + fun updatePartnerStatus() { + this.partnerStatus = !this.partnerStatus + } +} diff --git a/src/main/kotlin/gomushin/backend/member/domain/repository/NotificationRepository.kt b/src/main/kotlin/gomushin/backend/member/domain/repository/NotificationRepository.kt new file mode 100644 index 00000000..7433c705 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/repository/NotificationRepository.kt @@ -0,0 +1,8 @@ +package gomushin.backend.member.domain.repository + +import gomushin.backend.member.domain.entity.Notification +import org.springframework.data.jpa.repository.JpaRepository + +interface NotificationRepository : JpaRepository { + fun findByMemberId(memberId: Long): Notification? +} From 468b0cc3cfb8fdea160159aa07f5a3b3884b6fe8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Apr 2025 13:03:53 +0900 Subject: [PATCH 086/357] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=8B=9C=20notification=20=EC=83=9D=EC=84=B1=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 --- .../domain/service/NotificationService.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt new file mode 100644 index 00000000..0f404ae7 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt @@ -0,0 +1,38 @@ +package gomushin.backend.member.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.member.domain.entity.Notification +import gomushin.backend.member.domain.repository.NotificationRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class NotificationService( + private val notificationRepository: NotificationRepository +) { + + @Transactional + fun initNotification(memberId: Long, isNotification: Boolean) { + val notification = Notification.create(memberId) + if (isNotification) { + notification.updateDday() + notification.updatePartnerStatus() + } + save(notification) + } + + @Transactional(readOnly = true) + fun getByMemberId(memberId: Long): Notification { + return findByMemberId(memberId) ?: throw BadRequestException("sarangggun.member.not-exist-member") + } + + @Transactional(readOnly = true) + fun findByMemberId(memberId: Long): Notification? { + return notificationRepository.findByMemberId(memberId) + } + + @Transactional + fun save(notification: Notification): Notification { + return notificationRepository.save(notification) + } +} From 471b7314e4c32f52101863b55f2d23652b86cfd7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Apr 2025 13:04:17 +0900 Subject: [PATCH 087/357] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EB=B0=9B=EC=9D=84=20=EC=A7=80?= =?UTF-8?q?=20=EC=97=AC=EB=B6=80=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/domain/service/OnboardingService.kt | 18 +++++++++++++++--- .../member/dto/request/OnboardingRequest.kt | 6 ++++++ .../backend/member/facade/OnboardingFacade.kt | 7 ++++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt index 7b19c232..1e5299d8 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt @@ -1,22 +1,34 @@ package gomushin.backend.member.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Role import gomushin.backend.member.dto.request.OnboardingRequest +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class OnboardingService( - private val memberRepository: MemberRepository + private val memberRepository: MemberRepository, ) { - @Transactional fun onboarding(id: Long, onboardingRequest: OnboardingRequest) { - val member = memberRepository.findById(id).orElseThrow { BadRequestException("sarangggun.member.not-exist-member") } + val member = getById(id) member.nickname = onboardingRequest.nickname member.birthDate = onboardingRequest.birthDate member.role = Role.MEMBER + member.fcmToken = onboardingRequest.fcmToken + } + + @Transactional(readOnly = true) + fun getById(id: Long): Member { + return findById(id) ?: throw BadRequestException("sarangggun.member.not-exist-member") + } + + @Transactional(readOnly = true) + fun findById(id: Long): Member? { + return memberRepository.findByIdOrNull(id) } } diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/OnboardingRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/OnboardingRequest.kt index e17491fa..616b2313 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/request/OnboardingRequest.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/request/OnboardingRequest.kt @@ -9,4 +9,10 @@ data class OnboardingRequest( @Schema(description = "생일", example = "2000-01-01") val birthDate: LocalDate, + + @Schema(description = "FCM 토큰") + val fcmToken: String, + + @Schema(description = "알림 설정 여부", example = "false") + val isNotification: Boolean, ) diff --git a/src/main/kotlin/gomushin/backend/member/facade/OnboardingFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/OnboardingFacade.kt index f6a59154..18c71377 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/OnboardingFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/OnboardingFacade.kt @@ -1,5 +1,6 @@ package gomushin.backend.member.facade +import gomushin.backend.member.domain.service.NotificationService import gomushin.backend.member.domain.service.OnboardingService import gomushin.backend.member.dto.request.OnboardingRequest import org.springframework.stereotype.Component @@ -7,7 +8,11 @@ import org.springframework.stereotype.Component @Component class OnboardingFacade( private val onboardingService: OnboardingService, + private val notificationService: NotificationService, ) { - fun onboarding(id: Long, onboardingRequest: OnboardingRequest) = onboardingService.onboarding(id, onboardingRequest) + fun onboarding(id: Long, onboardingRequest: OnboardingRequest) { + onboardingService.onboarding(id, onboardingRequest) + notificationService.initNotification(id, onboardingRequest.isNotification) + } } From 531a945a13f88a58abf0867c6976ba34bfd64f0f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 22 Apr 2025 13:04:24 +0900 Subject: [PATCH 088/357] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 --- .../domain/service/NotificationServiceTest.kt | 44 +++++++++++++++++++ .../domain/service/OnboardingServiceTest.kt | 4 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/gomushin/backend/member/domain/service/NotificationServiceTest.kt diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/NotificationServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/NotificationServiceTest.kt new file mode 100644 index 00000000..b5408eeb --- /dev/null +++ b/src/test/kotlin/gomushin/backend/member/domain/service/NotificationServiceTest.kt @@ -0,0 +1,44 @@ +package gomushin.backend.member.domain.service + +import gomushin.backend.member.domain.entity.Notification +import gomushin.backend.member.domain.repository.NotificationRepository +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.ArgumentMatchers.any +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension +import kotlin.test.assertEquals + +@ExtendWith(MockitoExtension::class) +class NotificationServiceTest { + + @Mock + private lateinit var notificationRepository: NotificationRepository + + @InjectMocks + private lateinit var notificationService: NotificationService + + @DisplayName("알림 초기화 성공 케이스") + @Test + fun initNotification_success() { + // given + val memberId = 1L + val isNotification = true + val notification = Notification.create(memberId).apply { + dday = true + partnerStatus = true + } + + // when + `when`(notificationRepository.save(any())).thenReturn(notification) + notificationService.initNotification(memberId, isNotification) + + // then + assertEquals(notification.dday, true) + assertEquals(notification.partnerStatus, true) + + } +} diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt index 2e7b2088..67c947f5 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt @@ -41,7 +41,9 @@ class OnboardingServiceTest { val onboardingRequest = OnboardingRequest( nickname = "새로운 닉네임", - birthDate = LocalDate.of(2000, 1, 1) + birthDate = LocalDate.of(2000, 1, 1), + fcmToken = "fcmToken", + isNotification = false, ) `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(existingMember)) From cd0bb38eb9787c6aa1348f715c8dbb7f671d464f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 01:04:24 +0900 Subject: [PATCH 089/357] =?UTF-8?q?feat:=20=EB=B6=80=EB=8C=80=20=EC=9D=B4?= =?UTF-8?q?=EB=84=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/value/Military.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/value/Military.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/value/Military.kt b/src/main/kotlin/gomushin/backend/couple/domain/value/Military.kt new file mode 100644 index 00000000..3d75718e --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/value/Military.kt @@ -0,0 +1,17 @@ +package gomushin.backend.couple.domain.value + +import gomushin.backend.core.infrastructure.exception.BadRequestException + +enum class Military { + ARMY, + NAVY, + AIR_FORCE, + MARINE; + + companion object { + fun getByName(name: String): Military { + return entries.firstOrNull { it.name == name } + ?: throw BadRequestException("sarangggun.miliatry.not-exist-military") + } + } +} From 71c472a6920fa6fcb08e0f580e0c7b598b95e8c5 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 01:06:02 +0900 Subject: [PATCH 090/357] =?UTF-8?q?refactor:=20Couple=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=EB=B6=80=EB=8C=80=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20=EC=BB=A4=ED=94=8C=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20DTO=20=EC=97=90=20=EB=B6=80=EB=8C=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EB=8F=84=EB=A1=9D=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 --- .../backend/couple/domain/entity/Couple.kt | 16 +++++++++++++++- .../dto/request/CoupleAnniversaryRequest.kt | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt index 0373106d..d5d8703e 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt @@ -1,6 +1,7 @@ package gomushin.backend.couple.domain.entity import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import gomushin.backend.couple.domain.value.Military import jakarta.persistence.* import java.time.LocalDate @@ -26,7 +27,11 @@ class Couple( @Column(name = "military_end_date") var militaryEndDate: LocalDate? = null, -): BaseEntity() { + @Column(name = "army") + @Enumerated(EnumType.STRING) + var military: Military? = null, + + ) : BaseEntity() { companion object { fun of( invitorId: Long, @@ -39,6 +44,12 @@ class Couple( } } + fun updateMilitary( + military: String + ) { + this.military = Military.getByName(military) + } + fun updateAnniversary( relationshipStartDate: LocalDate?, militaryStartDate: LocalDate?, @@ -48,4 +59,7 @@ class Couple( this.militaryStartDate = militaryStartDate ?: this.militaryStartDate this.militaryEndDate = militaryEndDate ?: this.militaryEndDate } + + fun containsUser(userId: Long): Boolean = + userId == invitorId || userId == inviteeId } diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt index 8d4a499d..f0fd9242 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt @@ -15,4 +15,7 @@ data class CoupleAnniversaryRequest( @Schema(description = "전역일", example = "2024-10-28") val militaryEndDate: LocalDate, + + @Schema(description = "부대", example = "ARMY | NAVY | AIR_FORCE | MARINE 중 택 1 ") + val military: String, ) From b2d8072244b74ea62d532b2bd28b27c393515a77 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 01:06:26 +0900 Subject: [PATCH 091/357] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/entity/Anniversary.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt new file mode 100644 index 00000000..dc1b8bb4 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt @@ -0,0 +1,36 @@ +package gomushin.backend.couple.domain.entity + +import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import jakarta.persistence.* +import java.time.LocalDate + +@Entity +@Table(name = "anniversary") +class Anniversary( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + @Column(name = "couple_id", nullable = false) + val coupleId: Long = 0L, + + @Column(name = "title", nullable = false) + var title: String, + + @Column(name = "anniversary_date", nullable = false) + var anniversaryDate: LocalDate, + + @Column(name = "anniversary_property", nullable = false) + var anniversaryProperty: Int +) : BaseEntity() { + companion object { + fun create(coupleId: Long, title: String, anniversaryDate: LocalDate): Anniversary { + return Anniversary( + coupleId = coupleId, + title = title, + anniversaryDate = anniversaryDate, + anniversaryProperty = 0 + ) + } + } +} From f6e1157bec258d528786208473b5ad0034f3792f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 01:07:24 +0900 Subject: [PATCH 092/357] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=9C=20=EC=B2=98=EC=9D=8C=20=EB=A7=8C=EB=82=9C?= =?UTF-8?q?=20=EB=82=A0,=20=EC=9E=85=EB=8C=80=EC=9D=BC,=20=EC=A0=84?= =?UTF-8?q?=EC=97=AD=EC=9D=BC=20=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EA=B5=B0?= =?UTF-8?q?=EB=8C=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EA=B8=B0=EA=B0=84?= =?UTF-8?q?=EB=8F=99=EC=95=88=20=EC=9E=88=EB=8A=94=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=85=90=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 --- .../domain/service/AnniversaryCalculator.kt | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt new file mode 100644 index 00000000..79798603 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt @@ -0,0 +1,86 @@ +package gomushin.backend.couple.domain.service + +import gomushin.backend.couple.domain.entity.Anniversary +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class AnniversaryCalculator { + fun calculateInitAnniversaries( + coupleId: Long, + relationShipStartDate: LocalDate, + militaryStartDate: LocalDate, + militaryEndDate: LocalDate, + anniversaryList: MutableList, + ): List { + calculateAnniversariesBetweenMilitaryStartDateAndMilitaryEndDate( + coupleId, + relationShipStartDate, + militaryStartDate, + militaryEndDate, + anniversaryList + ) + calculateHundredAnniversaryBetweenMilitaryStartDateAndMilitaryEndDate( + coupleId, + relationShipStartDate, + militaryStartDate, + militaryEndDate, + anniversaryList + ) + return anniversaryList + } + + private fun calculateAnniversariesBetweenMilitaryStartDateAndMilitaryEndDate( + coupleId: Long, + relationShipStartDate: LocalDate, + militaryStartDate: LocalDate, + militaryEndDate: LocalDate, + anniversaryList: MutableList, + ) { + var anniversaryYear = 1L + + while (true) { + val anniversaryDate = relationShipStartDate.plusYears(anniversaryYear) + if (anniversaryDate.isAfter(militaryStartDate)) { + break + } else { + anniversaryYear++ + } + } + + while (true) { + val anniversaryDate = relationShipStartDate.plusYears(anniversaryYear) + if (anniversaryDate.isAfter(militaryEndDate)) { + break + } else { + val title = "$anniversaryYear 주년" + val anniversary = Anniversary.create(coupleId, title, anniversaryDate) + anniversaryList.add(anniversary) + anniversaryYear++ + } + } + } + + private fun calculateHundredAnniversaryBetweenMilitaryStartDateAndMilitaryEndDate( + coupleId: Long, + relationShipStartDate: LocalDate, + militaryStartDate: LocalDate, + militaryEndDate: LocalDate, + anniversaryList: MutableList, + ) { + val plusDay = 100L + var anniversaryDay = 0L + var anniversaryDate = relationShipStartDate + while (true) { + anniversaryDate = anniversaryDate.plusDays(plusDay) + anniversaryDay += plusDay + if (anniversaryDate.isAfter(militaryEndDate)) { + break + } else if (anniversaryDate.isAfter(militaryStartDate) && anniversaryDate.isBefore(militaryEndDate)) { + val title = "${anniversaryDay}일" + val anniversary = Anniversary.create(coupleId, title, anniversaryDate) + anniversaryList.add(anniversary) + } + } + } +} From 278e1684ca57a603ae1c18daa1a242d044de9b9c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 01:07:44 +0900 Subject: [PATCH 093/357] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AnniversaryRepository.kt | 6 ++ .../domain/service/AnniversaryService.kt | 57 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt new file mode 100644 index 00000000..1384e8f8 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -0,0 +1,6 @@ +package gomushin.backend.couple.domain.repository + +import gomushin.backend.couple.domain.entity.Anniversary +import org.springframework.data.jpa.repository.JpaRepository + +interface AnniversaryRepository : JpaRepository diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt new file mode 100644 index 00000000..88101ed3 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -0,0 +1,57 @@ +package gomushin.backend.couple.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Anniversary +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.AnniversaryRepository +import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AnniversaryService( + private val anniversaryRepository: AnniversaryRepository, + private val coupleService: CoupleService, + private val anniversaryCalculator: AnniversaryCalculator +) { + + @Transactional + fun registerAnniversary( + userId: Long, + request: CoupleAnniversaryRequest + ) { + val couple = coupleService.getById(request.coupleId) + checkUserInCouple(userId, couple) + + couple.updateMilitary(request.military) + + couple.updateAnniversary( + relationshipStartDate = request.relationshipStartDate, + militaryStartDate = request.militaryStartDate, + militaryEndDate = request.militaryEndDate, + ) + + val anniversaries: MutableList = mutableListOf() + + anniversaryCalculator.calculateInitAnniversaries( + couple.id, + request.relationshipStartDate, + request.militaryStartDate, + request.militaryEndDate, + anniversaries + ) + + saveAll(anniversaries) + } + + @Transactional + fun saveAll(anniversaries: List): List { + return anniversaryRepository.saveAll(anniversaries) + } + + private fun checkUserInCouple(userId: Long, couple: Couple) { + if (!couple.containsUser(userId)) { + throw BadRequestException("sarangggun.couple.not-in-couple") + } + } +} From d705fad5e2c264fd5df26f3ec452551e0bcc41e7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 01:08:09 +0900 Subject: [PATCH 094/357] =?UTF-8?q?refactor:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EB=A1=9C=EC=A7=81=EA=B3=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=85=90=EC=9D=BC=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CoupleConnectService.kt | 23 ------------------- .../backend/couple/facade/CoupleFacade.kt | 4 +++- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt index 1ca6f196..5079b6a3 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -2,14 +2,12 @@ package gomushin.backend.couple.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Couple -import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.util.CoupleCodeGeneratorUtil import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.Duration -import java.time.LocalDate @Service class CoupleConnectService( @@ -67,27 +65,6 @@ class CoupleConnectService( member.updateCoupleStatus() } - @Transactional - fun registerAnniversary( - userId: Long, - request: CoupleAnniversaryRequest - ) { - val couple = coupleService.getById(request.coupleId) - if (!checkUserInCouple(userId, couple)) { - throw BadRequestException("sarangggun.couple.not-in-couple") - } - couple.updateAnniversary( - relationshipStartDate = request.relationshipStartDate, - militaryStartDate = request.militaryStartDate, - militaryEndDate = request.militaryEndDate, - ) - } - - private fun checkUserInCouple(userId: Long, couple: Couple): Boolean { - val member = memberService.getById(userId) - return member.id == couple.invitorId || member.id == couple.inviteeId - } - private fun getCoupleCodeOrNull(key: String): Long? { return redisTemplate.opsForValue().get(key)?.toLongOrNull() diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index fc41658d..e0f26c06 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -1,6 +1,7 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.domain.service.CoupleInfoService @@ -14,6 +15,7 @@ import org.springframework.stereotype.Component @Component class CoupleFacade( private val coupleConnectService: CoupleConnectService, + private val anniversaryService: AnniversaryService private val coupleInfoService: CoupleInfoService ) { @@ -28,7 +30,7 @@ class CoupleFacade( fun registerAnniversary( customUserDetails: CustomUserDetails, request: CoupleAnniversaryRequest - ) = coupleConnectService.registerAnniversary( + ) = anniversaryService.registerAnniversary( customUserDetails.getId(), request ) From 128e365064da2f2373b4bb7d3cb2480856cee881 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 01:19:48 +0900 Subject: [PATCH 095/357] =?UTF-8?q?refactor:=20=EC=9E=85=EB=8C=80,?= =?UTF-8?q?=EC=A0=84=EC=97=AD=EC=9D=BC=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/service/AnniversaryCalculator.kt | 4 ++++ .../kotlin/gomushin/backend/couple/domain/value/Military.kt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt index 79798603..f8d373d5 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt @@ -1,5 +1,6 @@ package gomushin.backend.couple.domain.service +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Anniversary import org.springframework.stereotype.Service import java.time.LocalDate @@ -13,6 +14,9 @@ class AnniversaryCalculator { militaryEndDate: LocalDate, anniversaryList: MutableList, ): List { + + if (militaryStartDate.isAfter(militaryEndDate)) throw BadRequestException("sarangggun.military.invalid-date") + calculateAnniversariesBetweenMilitaryStartDateAndMilitaryEndDate( coupleId, relationShipStartDate, diff --git a/src/main/kotlin/gomushin/backend/couple/domain/value/Military.kt b/src/main/kotlin/gomushin/backend/couple/domain/value/Military.kt index 3d75718e..56312b77 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/value/Military.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/value/Military.kt @@ -11,7 +11,7 @@ enum class Military { companion object { fun getByName(name: String): Military { return entries.firstOrNull { it.name == name } - ?: throw BadRequestException("sarangggun.miliatry.not-exist-military") + ?: throw BadRequestException("sarangggun.military.not-exist-military") } } } From d254c314c73f94ea58f54df092f911bd4289180a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 01:30:25 +0900 Subject: [PATCH 096/357] =?UTF-8?q?refactor:=20OnboardingFacadeTest=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=AA=A8=ED=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/member/facade/OnboardingFacadeTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/member/facade/OnboardingFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/OnboardingFacadeTest.kt index 3a9536c4..538fd3af 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/OnboardingFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/OnboardingFacadeTest.kt @@ -1,5 +1,6 @@ package gomushin.backend.member.facade +import gomushin.backend.member.domain.service.NotificationService import gomushin.backend.member.domain.service.OnboardingService import gomushin.backend.member.dto.request.OnboardingRequest import org.junit.jupiter.api.DisplayName @@ -17,6 +18,9 @@ class OnboardingFacadeTest { @Mock private lateinit var onboardingService: OnboardingService + @Mock + private lateinit var notificationService: NotificationService + @InjectMocks private lateinit var onboardingFacade: OnboardingFacade From b399906d4ae6b88f5b0a101861ab9ac0dad2c941 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 14:58:33 +0900 Subject: [PATCH 097/357] =?UTF-8?q?refactor:=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20=EB=B0=8F=20=ED=8A=B8=EB=A0=88=EC=9D=BC?= =?UTF-8?q?=EB=A7=81=20=EC=BD=A4=EB=A7=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/couple/facade/CoupleFacade.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index e0f26c06..71c9ad25 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -15,8 +15,8 @@ import org.springframework.stereotype.Component @Component class CoupleFacade( private val coupleConnectService: CoupleConnectService, - private val anniversaryService: AnniversaryService - private val coupleInfoService: CoupleInfoService + private val anniversaryService: AnniversaryService, + private val coupleInfoService: CoupleInfoService, ) { fun requestCoupleCodeGeneration(customUserDetails: CustomUserDetails) = From 640f3f43f5600d0c0e8b165e030a37dfc05725f7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 16:42:04 +0900 Subject: [PATCH 098/357] =?UTF-8?q?refactor:=20=EB=84=A4=EC=9D=B4=EB=B0=8D?= =?UTF-8?q?=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/service/AnniversaryCalculator.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt index f8d373d5..527af92b 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt @@ -24,7 +24,7 @@ class AnniversaryCalculator { militaryEndDate, anniversaryList ) - calculateHundredAnniversaryBetweenMilitaryStartDateAndMilitaryEndDate( + calculateHundredsAnniversariesBetweenMilitaryStartDateAndMilitaryEndDate( coupleId, relationShipStartDate, militaryStartDate, @@ -57,7 +57,7 @@ class AnniversaryCalculator { if (anniversaryDate.isAfter(militaryEndDate)) { break } else { - val title = "$anniversaryYear 주년" + val title = "${anniversaryYear}주년" val anniversary = Anniversary.create(coupleId, title, anniversaryDate) anniversaryList.add(anniversary) anniversaryYear++ @@ -65,7 +65,7 @@ class AnniversaryCalculator { } } - private fun calculateHundredAnniversaryBetweenMilitaryStartDateAndMilitaryEndDate( + private fun calculateHundredsAnniversariesBetweenMilitaryStartDateAndMilitaryEndDate( coupleId: Long, relationShipStartDate: LocalDate, militaryStartDate: LocalDate, From 9946dd4a9441a81dc73d8b4ab7ea0c07bbc70953 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 23 Apr 2025 16:49:11 +0900 Subject: [PATCH 099/357] =?UTF-8?q?test:=20CoupleFacadeTest=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 --- .../gomushin/backend/member/facade/CoupleFacadeTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 794b6952..b083dc5c 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -2,6 +2,7 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.dto.response.DdayResponse @@ -31,6 +32,9 @@ class CoupleFacadeTest { @Mock private lateinit var coupleConnectService: CoupleConnectService + @Mock + private lateinit var anniversaryService: AnniversaryService + @InjectMocks private lateinit var coupleFacade: CoupleFacade @@ -134,4 +138,4 @@ class CoupleFacadeTest { assertEquals("기분이 좋아용", result.statusMessage) } -} \ No newline at end of file +} From 81656cbc704258207160a22373176177cef5e499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 23 Apr 2025 12:26:33 +0900 Subject: [PATCH 100/357] =?UTF-8?q?feat=20:=20=EB=82=98=EC=9D=98=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?#22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/dto/response/MyStatusMessageResponse.kt | 13 +++++++++++++ .../backend/member/facade/MemberInfoFacade.kt | 6 ++++++ .../gomushin/backend/member/presentation/ApiPath.kt | 1 + .../member/presentation/MemberInfoController.kt | 11 +++++++++++ .../backend/member/facade/MemberInfoFacadeTest.kt | 13 +++++++++++++ 5 files changed, 44 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/dto/response/MyStatusMessageResponse.kt diff --git a/src/main/kotlin/gomushin/backend/member/dto/response/MyStatusMessageResponse.kt b/src/main/kotlin/gomushin/backend/member/dto/response/MyStatusMessageResponse.kt new file mode 100644 index 00000000..f210fdb5 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/dto/response/MyStatusMessageResponse.kt @@ -0,0 +1,13 @@ +package gomushin.backend.member.dto.response + +import gomushin.backend.member.domain.entity.Member + +data class MyStatusMessageResponse ( + val statusMessage : String? +) { + companion object { + fun of(member: Member) = MyStatusMessageResponse( + member.statusMessage + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 9fcc9a6d..8b87d4ff 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -3,6 +3,7 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.dto.response.MyInfoResponse +import gomushin.backend.member.dto.response.MyStatusMessageResponse import org.springframework.stereotype.Component @Component @@ -13,4 +14,9 @@ class MemberInfoFacade( val member = memberService.getById(customUserDetails.getId()) return MyInfoResponse.of(member) } + + fun getMyStatusMessage(customUserDetails: CustomUserDetails): MyStatusMessageResponse { + val member = memberService.getById(customUserDetails.getId()) + return MyStatusMessageResponse.of(member) + } } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index 6f8e66bd..874cfc86 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -3,4 +3,5 @@ package gomushin.backend.member.presentation object ApiPath { const val ONBOARDING = "/v1/member/onboarding" const val MY_INFO = "/v1/member/my-info" + const val MY_STATUS_MESSAGE = "/v1/member/my-status-message" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index ab02f149..8c608867 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.member.facade.MemberInfoFacade import gomushin.backend.member.dto.response.MyInfoResponse +import gomushin.backend.member.dto.response.MyStatusMessageResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus @@ -27,4 +28,14 @@ class MemberInfoController( val member = memberInfoFacade.getMemberInfo(customUserDetails) return ApiResponse.success(member) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.MY_STATUS_MESSAGE) + @Operation(summary = "내 상태 메시지 조회", description = "getMyStatusMessage") + fun getMyStatusMessage( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ): ApiResponse { + val statusMessage = memberInfoFacade.getMyStatusMessage(customUserDetails) + return ApiResponse.success(statusMessage) + } } diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index d7610c36..9665fba1 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -37,6 +37,7 @@ class MemberInfoFacadeTest { profileImageUrl = null, provider = Provider.KAKAO, role = Role.MEMBER, + statusMessage = "상태 메시지" ) customUserDetails = mock(CustomUserDetails::class.java) @@ -56,4 +57,16 @@ class MemberInfoFacadeTest { verify(memberService).getById(1L) assertEquals(member.nickname, result.nickname) } + + @DisplayName("내 상태 메시지 조회") + @Test + fun getMyStatusMessage() { + //given + `when`(memberService.getById(customUserDetails.getId())).thenReturn(member) + //when + val result = memberInfoFacade.getMyStatusMessage(customUserDetails) + //then + verify(memberService).getById(1L) + assertEquals(member.statusMessage, result.statusMessage) + } } From 9fcdbc4d573ab0be581b2308a577ee0058d6ab58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 24 Apr 2025 00:31:30 +0900 Subject: [PATCH 101/357] =?UTF-8?q?fix=20:=20main=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=EC=99=80=EC=9D=98=20rebase=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20#22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/entity/Member.kt | 10 ++++++++- .../member/domain/service/MemberService.kt | 8 +++++++ .../UpdateMyEmotionAndStatusMessageRequest.kt | 22 +++++++++++++++++++ .../backend/member/facade/MemberInfoFacade.kt | 4 ++++ .../backend/member/presentation/ApiPath.kt | 1 + .../presentation/MemberInfoController.kt | 19 ++++++++++++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index 9da0e767..adf38bd1 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -43,7 +43,7 @@ class Member( var isCouple: Boolean = false, @Column(name = "emotion") - var emotion: String = "", + var emotion : Int? = null, @Column(name = "fcm_token", nullable = false) var fcmToken: String = "", @@ -74,4 +74,12 @@ class Member( fun updateCoupleStatus() { this.isCouple = !this.isCouple } + + fun updateEmotion(emotion: Int) { + this.emotion = emotion + } + + fun updateStatusMessage(statusMessage: String) { + this.statusMessage = statusMessage + } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 7f3e0bd8..7ffbcc25 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -3,6 +3,7 @@ package gomushin.backend.member.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -21,4 +22,11 @@ class MemberService( fun findById(id: Long): Member? { return memberRepository.findByIdOrNull(id) } + + @Transactional + fun updateMyEmotionAndStatusMessage(id: Long, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) { + val member = getById(id) + member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion) + member.updateStatusMessage(updateMyEmotionAndStatusMessageRequest.statusMessage) + } } diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt new file mode 100644 index 00000000..7d19779a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt @@ -0,0 +1,22 @@ +package gomushin.backend.member.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size + + +data class UpdateMyEmotionAndStatusMessageRequest( + @Schema(description = "이모지(1 : 보고싶어요, 2: 기분 좋아요, 3 : 아무느낌 없어요, " + + "4 : 피곤해요, 5: 서운해요, 6 : 걱정돼요, 7 : 짜증나요)", example = "1") + @field:NotNull(message = "sarangggun.member.invalid-emotion") + @field:Min(1, message = "sarangggun.member.invalid-emotion") + @field:Max(7, message = "sarangggun.member.invalid-emotion") + val emotion : Int, + + @Schema(description = "상태 메시지", example = "보고 싶어요") + @field:NotNull(message = "sarangggun.member.empty-status-message") + @field:Size(max = 25, message = "sarangggun.member.status-message-too-long") + val statusMessage : String +) diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 8b87d4ff..98cae42e 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -2,6 +2,7 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.service.MemberService +import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.response.MyInfoResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse import org.springframework.stereotype.Component @@ -19,4 +20,7 @@ class MemberInfoFacade( val member = memberService.getById(customUserDetails.getId()) return MyStatusMessageResponse.of(member) } + + fun updateMyEmotionAndStatusMessage(customUserDetails: CustomUserDetails, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) + = memberService.updateMyEmotionAndStatusMessage(customUserDetails.getId(), updateMyEmotionAndStatusMessageRequest) } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index 874cfc86..e6dd0801 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -4,4 +4,5 @@ object ApiPath { const val ONBOARDING = "/v1/member/onboarding" const val MY_INFO = "/v1/member/my-info" const val MY_STATUS_MESSAGE = "/v1/member/my-status-message" + const val UPDATE_MY_EMOTION_AND_STATUS_MESSAGE = "/v1/member/my-emotion-and-status-message" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 8c608867..72b4ab23 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -2,18 +2,26 @@ package gomushin.backend.member.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.facade.MemberInfoFacade import gomushin.backend.member.dto.response.MyInfoResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.BindingResult +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController +@Validated @Tag(name = "회원 정보", description = "MemberController") class MemberInfoController( private val memberInfoFacade: MemberInfoFacade, @@ -38,4 +46,15 @@ class MemberInfoController( val statusMessage = memberInfoFacade.getMyStatusMessage(customUserDetails) return ApiResponse.success(statusMessage) } + + @ResponseStatus(HttpStatus.OK) + @PostMapping(ApiPath.UPDATE_MY_EMOTION_AND_STATUS_MESSAGE) + @Operation(summary = "내 상태 이모지 및 상태 메시지 저장", description = "updateMyEmotionAndStatusMessage") + fun updateMyEmotionAndStatusMessage( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @Valid @RequestBody updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest + ): ApiResponse { + memberInfoFacade.updateMyEmotionAndStatusMessage(customUserDetails, updateMyEmotionAndStatusMessageRequest) + return ApiResponse.success(true) + } } From 6a9a2fbec1c545c994e1168adbb523528f8700bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 23 Apr 2025 22:19:51 +0900 Subject: [PATCH 102/357] =?UTF-8?q?feat=20:=20=EC=9D=B4=EB=AA=A8=EC=A7=80,?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=A6=AC=ED=80=98=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GlobalExceptionHandler.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/infrastructure/exception/handler/GlobalExceptionHandler.kt diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/exception/handler/GlobalExceptionHandler.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/exception/handler/GlobalExceptionHandler.kt new file mode 100644 index 00000000..0ece31e2 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/exception/handler/GlobalExceptionHandler.kt @@ -0,0 +1,41 @@ +package gomushin.backend.core.infrastructure.exception.handler + +import gomushin.backend.core.common.support.SpringContextHolder +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.core.common.web.response.ExtendedHttpStatus +import gomushin.backend.core.common.web.response.exception.ApiError +import gomushin.backend.core.common.web.response.exception.ApiErrorCode +import gomushin.backend.core.common.web.response.exception.ApiErrorElement +import gomushin.backend.core.common.web.response.exception.ApiErrorMessage +import gomushin.backend.core.configuration.env.AppEnv +import gomushin.backend.core.infrastructure.exception.BadRequestException +import jakarta.validation.ConstraintViolationException +import org.springframework.http.HttpStatus +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException::class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + fun handleMethodArgumentNotValidException(ex: MethodArgumentNotValidException): ApiResponse { + + val code = ex.bindingResult + .fieldErrors + .firstOrNull() + ?.defaultMessage + ?: "bad-request" + return ApiResponse.error( + ApiError.of( + ApiErrorElement( + SpringContextHolder.getBean(AppEnv::class.java).getId(), + ExtendedHttpStatus.BAD_REQUEST, + ApiErrorCode.of(code), + ApiErrorMessage.of(code) + ) + ) + ) + } +} \ No newline at end of file From fc821a33a1e0cb8969d6807e7250780a396d8bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 23 Apr 2025 22:20:12 +0900 Subject: [PATCH 103/357] =?UTF-8?q?test=20:=20=EC=9D=B4=EB=AA=A8=EC=A7=80/?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20api=20=EA=B5=AC=ED=98=84=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=20#22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/MemberServiceTest.kt | 27 +++++++++++++++++++ .../member/facade/MemberInfoFacadeTest.kt | 12 +++++++++ 2 files changed, 39 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt index 2d0f017b..cc2e1c79 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt @@ -4,6 +4,7 @@ import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role +import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -46,4 +47,30 @@ class MemberServiceTest { // then assertEquals(expectedMember, result) } + + @DisplayName("이모지 및 상태 메시지 업데이트 - 성공") + @Test + fun updateMyEmotionAndStatusMessage() { + // given + val memberId = 1L + val expectedMember = Member( + id = 1L, + name = "테스트", + nickname = "테스트 닉네임", + email = "test@test.com", + birthDate = null, + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.GUEST, + emotion = 1, + statusMessage = "상태 변경전" + ) + val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(2, "상태 변경후") + //when + `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember)) + val result = memberService.updateMyEmotionAndStatusMessage(memberId, updateMyEmotionAndStatusMessageRequest) + //then + assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion) + assertEquals(expectedMember.statusMessage, updateMyEmotionAndStatusMessageRequest.statusMessage) + } } diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 9665fba1..3cd03cc3 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -5,6 +5,7 @@ import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role +import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -69,4 +70,15 @@ class MemberInfoFacadeTest { verify(memberService).getById(1L) assertEquals(member.statusMessage, result.statusMessage) } + + @DisplayName("이모지 및 상태 메시지 업데이트") + @Test + fun updateMyEmotionAndStatusMessage() { + //given + val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(1, "좋은 날씨야") + //when + val result = memberInfoFacade.updateMyEmotionAndStatusMessage(customUserDetails, updateMyEmotionAndStatusMessageRequest) + //then + verify(memberService).updateMyEmotionAndStatusMessage(1L, updateMyEmotionAndStatusMessageRequest) + } } From f073b77cfa262394cea55b4d080b8e8b918fc794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 24 Apr 2025 00:20:03 +0900 Subject: [PATCH 104/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=20(?= =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=EC=97=90=EC=84=9C=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=95=A0=20=EA=B2=BD=EC=9A=B0=20=EC=9A=94=EC=B2=AD=EC=9D=B4=20?= =?UTF-8?q?=EB=A7=8E=EC=95=84=EC=A7=80=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=ED=86=B0=EC=BA=A3=20=EC=8A=A4=EB=A0=88=EB=93=9C=EA=B0=80=20?= =?UTF-8?q?=EB=B0=80=EB=A6=B4=20=EC=88=98=20=EC=9E=88=EC=9D=8C=20->=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=98=EA=B8=B0=EB=A1=9C=20=EA=B2=B0=EC=A0=95)=20#2?= =?UTF-8?q?2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GlobalExceptionHandler.kt | 41 ------------------- .../UpdateMyEmotionAndStatusMessageRequest.kt | 9 ---- .../presentation/MemberInfoController.kt | 3 +- 3 files changed, 1 insertion(+), 52 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/core/infrastructure/exception/handler/GlobalExceptionHandler.kt diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/exception/handler/GlobalExceptionHandler.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/exception/handler/GlobalExceptionHandler.kt deleted file mode 100644 index 0ece31e2..00000000 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/exception/handler/GlobalExceptionHandler.kt +++ /dev/null @@ -1,41 +0,0 @@ -package gomushin.backend.core.infrastructure.exception.handler - -import gomushin.backend.core.common.support.SpringContextHolder -import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.core.common.web.response.ExtendedHttpStatus -import gomushin.backend.core.common.web.response.exception.ApiError -import gomushin.backend.core.common.web.response.exception.ApiErrorCode -import gomushin.backend.core.common.web.response.exception.ApiErrorElement -import gomushin.backend.core.common.web.response.exception.ApiErrorMessage -import gomushin.backend.core.configuration.env.AppEnv -import gomushin.backend.core.infrastructure.exception.BadRequestException -import jakarta.validation.ConstraintViolationException -import org.springframework.http.HttpStatus -import org.springframework.web.bind.MethodArgumentNotValidException -import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestControllerAdvice - -@RestControllerAdvice -class GlobalExceptionHandler { - @ExceptionHandler(MethodArgumentNotValidException::class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - fun handleMethodArgumentNotValidException(ex: MethodArgumentNotValidException): ApiResponse { - - val code = ex.bindingResult - .fieldErrors - .firstOrNull() - ?.defaultMessage - ?: "bad-request" - return ApiResponse.error( - ApiError.of( - ApiErrorElement( - SpringContextHolder.getBean(AppEnv::class.java).getId(), - ExtendedHttpStatus.BAD_REQUEST, - ApiErrorCode.of(code), - ApiErrorMessage.of(code) - ) - ) - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt index 7d19779a..65d25044 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt @@ -1,22 +1,13 @@ package gomushin.backend.member.dto.request import io.swagger.v3.oas.annotations.media.Schema -import jakarta.validation.constraints.Max -import jakarta.validation.constraints.Min -import jakarta.validation.constraints.NotNull -import jakarta.validation.constraints.Size data class UpdateMyEmotionAndStatusMessageRequest( @Schema(description = "이모지(1 : 보고싶어요, 2: 기분 좋아요, 3 : 아무느낌 없어요, " + "4 : 피곤해요, 5: 서운해요, 6 : 걱정돼요, 7 : 짜증나요)", example = "1") - @field:NotNull(message = "sarangggun.member.invalid-emotion") - @field:Min(1, message = "sarangggun.member.invalid-emotion") - @field:Max(7, message = "sarangggun.member.invalid-emotion") val emotion : Int, @Schema(description = "상태 메시지", example = "보고 싶어요") - @field:NotNull(message = "sarangggun.member.empty-status-message") - @field:Size(max = 25, message = "sarangggun.member.status-message-too-long") val statusMessage : String ) diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 72b4ab23..0801b24e 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -21,7 +21,6 @@ import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController -@Validated @Tag(name = "회원 정보", description = "MemberController") class MemberInfoController( private val memberInfoFacade: MemberInfoFacade, @@ -52,7 +51,7 @@ class MemberInfoController( @Operation(summary = "내 상태 이모지 및 상태 메시지 저장", description = "updateMyEmotionAndStatusMessage") fun updateMyEmotionAndStatusMessage( @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @Valid @RequestBody updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest + @RequestBody updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest ): ApiResponse { memberInfoFacade.updateMyEmotionAndStatusMessage(customUserDetails, updateMyEmotionAndStatusMessageRequest) return ApiResponse.success(true) From db094c08797fafc9bde65673e9e87eacbc26cffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 24 Apr 2025 00:24:56 +0900 Subject: [PATCH 105/357] =?UTF-8?q?fix=20:=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=EA=B0=80=20=EB=A1=9C=EC=BB=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B0=9C=EB=B0=9C=20=ED=8E=B8=ED=9E=88=20=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20cookie.secure=EC=84=A4=EC=A0=95=20false?= =?UTF-8?q?=EB=A1=9C=20=EC=9E=A0=EC=8B=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 614c0173..f82d3286 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -48,7 +48,7 @@ class CustomSuccessHandler( val cookie = Cookie(key, value) cookie.path = "/" cookie.isHttpOnly = true - cookie.secure = true + cookie.secure = false //Todo : 밋업 할 때는 true로 변경하기 cookie.maxAge = 1800 return cookie } From 4b7b94d6f08ba00980bab7fa7314004ca175c7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 24 Apr 2025 00:25:32 +0900 Subject: [PATCH 106/357] =?UTF-8?q?fix=20:=20sendRedirect=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index f82d3286..b8e9a4dd 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -41,7 +41,7 @@ class CustomSuccessHandler( response!!.addCookie(createCookie("access_token", accessToken)) // response.sendRedirect("https://frontend-sarang.vercel.app") - response.sendRedirect("http://localhost:8080") + response.sendRedirect("http://localhost:5173") } private fun createCookie(key: String, value: String): Cookie { From 36108620616406a91d8bceadefd1b164b3411b4e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 11:49:59 +0900 Subject: [PATCH 107/357] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=8B=9C=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=ED=97=A4=EB=8D=94=EB=A1=9C?= =?UTF-8?q?=EB=8F=84=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SecurityConfiguration.kt | 6 +++-- .../swagger/SwaggerConfiguration.kt | 26 +++++++++++++++++++ .../filter/JwtAuthenticationFilter.kt | 21 ++++++++++++++- .../oauth/handler/CustomSuccessHandler.kt | 5 ++-- 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 905c135b..3f4f6321 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -6,6 +6,7 @@ import gomushin.backend.core.oauth.handler.CustomSuccessHandler import gomushin.backend.core.oauth.service.CustomOAuth2UserService import gomushin.backend.core.service.CustomUserDetailsService import gomushin.backend.member.domain.repository.MemberRepository +import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity @@ -19,6 +20,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic class SecurityConfiguration( private val jwtTokenProvider: JwtTokenProvider, private val memberRepository: MemberRepository, + @Value("\${redirect-url}") private val redirectUrl: String ) { @Bean @@ -50,7 +52,7 @@ class SecurityConfiguration( userInfoEndpointConfigurer .userService(customOAuth2UserService) } - .successHandler(CustomSuccessHandler(jwtTokenProvider, memberRepository)) + .successHandler(CustomSuccessHandler(jwtTokenProvider, memberRepository, redirectUrl)) } .authorizeHttpRequests { it.requestMatchers( @@ -74,7 +76,7 @@ class SecurityConfiguration( JwtAuthenticationFilter( jwtTokenProvider, CustomUserDetailsService( - memberRepository + memberRepository, ) ), UsernamePasswordAuthenticationFilter:: diff --git a/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt new file mode 100644 index 00000000..582af037 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt @@ -0,0 +1,26 @@ +package gomushin.backend.core.configuration.swagger + +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class SwaggerConfiguration { + + @Bean + fun openAPI(): OpenAPI { + val securityScheme = SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + + val securityRequirement = SecurityRequirement().addList("bearerAuth") + + return OpenAPI() + .components(Components().addSecuritySchemes("bearerAuth", securityScheme)) + .addSecurityItem(securityRequirement) + } +} diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index 7c4742e9..fd7e45b6 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -16,6 +16,12 @@ class JwtAuthenticationFilter( private val customUserDetailsService: CustomUserDetailsService ) : OncePerRequestFilter() { + companion object { + private val AT_IN_COOKIE = "access_token" + private val AUTHORIZATION_HEADER = "Authorization" + private val BEARER_PREFIX = "Bearer " + } + override fun shouldNotFilter(request: HttpServletRequest): Boolean { val excludedPaths = listOf( "/v1/auth", "/v1/oauth", "/swagger", "/v3/api-docs", "/api-docs" @@ -31,12 +37,16 @@ class JwtAuthenticationFilter( filterChain: FilterChain ) { - val accessToken = getCookieValue(request, "access_token") + val accessToken = getCookieValue(request, AT_IN_COOKIE) ?: getAccessTokenFromHeader(request) when { accessToken != null && tokenProvider.validateToken(accessToken) -> { applyAuthentication(accessToken) } + + else -> { + SecurityContextHolder.clearContext() + } } filterChain.doFilter(request, response) @@ -57,4 +67,13 @@ class JwtAuthenticationFilter( private fun getCookieValue(request: HttpServletRequest, name: String): String? { return request.cookies?.firstOrNull { it.name == name }?.value } + + private fun getAccessTokenFromHeader(request: HttpServletRequest): String? { + val authorizationHeader = request.getHeader(AUTHORIZATION_HEADER) + return if (authorizationHeader != null && authorizationHeader.startsWith(BEARER_PREFIX)) { + authorizationHeader.substring(7) + } else { + null + } + } } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index b8e9a4dd..2af7a74b 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -10,6 +10,7 @@ import jakarta.servlet.ServletException import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import org.springframework.beans.factory.annotation.Value import org.springframework.security.core.Authentication import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler import org.springframework.stereotype.Component @@ -18,6 +19,7 @@ import org.springframework.stereotype.Component class CustomSuccessHandler( private val jwtTokenProvider: JwtTokenProvider, private val memberRepository: MemberRepository, + @Value("\${redirect-url}") private val redirectUrl: String ) : SimpleUrlAuthenticationSuccessHandler() { @Throws(IOException::class, ServletException::class) @@ -40,8 +42,7 @@ class CustomSuccessHandler( } response!!.addCookie(createCookie("access_token", accessToken)) -// response.sendRedirect("https://frontend-sarang.vercel.app") - response.sendRedirect("http://localhost:5173") + response.sendRedirect(redirectUrl) } private fun createCookie(key: String, value: String): Cookie { From 335e6dbdb941a26a4fc92e4c368ee218b9b95a01 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 02:17:20 +0900 Subject: [PATCH 108/357] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/presentation/ApiPath.kt | 6 +++ .../UpsertAndDeleteScheduleController.kt | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt new file mode 100644 index 00000000..926d5be6 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt @@ -0,0 +1,6 @@ +package gomushin.backend.schedule.presentation + +object ApiPath { + const val SCHEDULES = "/v1/schedules" + const val SCHEDULE = "/v1/schedules/{scheduleId}" +} diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt new file mode 100644 index 00000000..ca3d67e1 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt @@ -0,0 +1,40 @@ +package gomushin.backend.schedule.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.schedule.dto.UpsertScheduleRequest +import gomushin.backend.schedule.facade.UpsertAndDeleteScheduleFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.* + +@RestController +@Tag(name = "일정", description = "UpsertAndDeleteScheduleController") +class UpsertAndDeleteScheduleController( + private val upsertAndDeleteScheduleFacade: UpsertAndDeleteScheduleFacade, +) { + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping(ApiPath.SCHEDULES) + @Operation(summary = "일정 수정하거나 추가하기", description = "upsertSchedule") + fun upsertSchedule( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody upsertScheduleRequest: UpsertScheduleRequest + ): ApiResponse { + upsertAndDeleteScheduleFacade.upsert(customUserDetails, upsertScheduleRequest) + return ApiResponse.success(true) + } + + @ResponseStatus(HttpStatus.OK) + @DeleteMapping(ApiPath.SCHEDULE) + @Operation(summary = "일정 삭제하기", description = "deleteSchedule") + fun deleteSchedule( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable scheduleId: Long + ): ApiResponse { + upsertAndDeleteScheduleFacade.delete(customUserDetails, scheduleId) + return ApiResponse.success(true) + } +} From eae90bd2ee69e7837a8db1bcf3d8fdaf7ef10508 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 02:17:54 +0900 Subject: [PATCH 109/357] =?UTF-8?q?feat:=20CustomUserDetails=20=EC=97=90?= =?UTF-8?q?=20getCouple=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/core/CustomUserDetails.kt | 11 ++++++++++- .../configuration/security/SecurityConfiguration.kt | 4 +++- .../backend/core/service/CustomUserDetailsService.kt | 12 ++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt b/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt index 5cd70851..4b40a0b8 100644 --- a/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt +++ b/src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt @@ -1,5 +1,8 @@ package gomushin.backend.core +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.CoupleRepository import gomushin.backend.member.domain.entity.Member import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority @@ -7,6 +10,7 @@ import org.springframework.security.core.userdetails.UserDetails class CustomUserDetails( private val member: Member, + private val coupleRepository: CoupleRepository, ) : UserDetails { override fun getAuthorities(): MutableCollection { return mutableListOf(SimpleGrantedAuthority("ROLE_${member.role.name}")) @@ -37,6 +41,11 @@ class CustomUserDetails( } fun getId(): Long { - return member.id ?: throw IllegalStateException("존재하지 않는 멤버입니다.") + return member.id + } + + fun getCouple(): Couple { + return coupleRepository.findByMemberId(getId()) + ?: throw BadRequestException("saranggun.couple.not-connected") } } diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 3f4f6321..b6fdaaa3 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -5,6 +5,7 @@ import gomushin.backend.core.jwt.JwtTokenProvider import gomushin.backend.core.oauth.handler.CustomSuccessHandler import gomushin.backend.core.oauth.service.CustomOAuth2UserService import gomushin.backend.core.service.CustomUserDetailsService +import gomushin.backend.couple.domain.repository.CoupleRepository import gomushin.backend.member.domain.repository.MemberRepository import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean @@ -26,7 +27,7 @@ class SecurityConfiguration( @Bean fun filterChain( http: HttpSecurity, corsConfiguration: CustomCorsConfiguration, - customOAuth2UserService: CustomOAuth2UserService + customOAuth2UserService: CustomOAuth2UserService, coupleRepository: CoupleRepository ): SecurityFilterChain { http .csrf { @@ -77,6 +78,7 @@ class SecurityConfiguration( jwtTokenProvider, CustomUserDetailsService( memberRepository, + coupleRepository, ) ), UsernamePasswordAuthenticationFilter:: diff --git a/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt b/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt index 009c027f..fbb97fa7 100644 --- a/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt +++ b/src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt @@ -2,21 +2,25 @@ package gomushin.backend.core.service import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.repository.CoupleRepository import gomushin.backend.member.domain.repository.MemberRepository import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.stereotype.Service @Service -class CustomUserDetailsService(private val memberRepository: MemberRepository) : UserDetailsService { - override fun loadUserByUsername(email: String): UserDetails { // 파라미터명 username → email로 변경 +class CustomUserDetailsService( + private val memberRepository: MemberRepository, + private val coupleRepository: CoupleRepository, +) : UserDetailsService { + override fun loadUserByUsername(email: String): UserDetails { val member = memberRepository.findByEmail(email) ?: throw BadRequestException("sarangggun.member.not-exist-member") - return CustomUserDetails(member) + return CustomUserDetails(member, coupleRepository) } fun loadUserById(id: Long): UserDetails = memberRepository.findById(id) - .map { CustomUserDetails(it) } + .map { CustomUserDetails(it, coupleRepository) } .orElseThrow { BadRequestException("sarangggun.member.not-exist-member") } } From a6424ba3ada10c2b68cc21deaa9b1f88a0d49535 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 02:18:22 +0900 Subject: [PATCH 110/357] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C=20request=20dto=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 --- .../schedule/dto/UpsertScheduleRequest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/UpsertScheduleRequest.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertScheduleRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/UpsertScheduleRequest.kt new file mode 100644 index 00000000..0a6d9e35 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/UpsertScheduleRequest.kt @@ -0,0 +1,30 @@ +package gomushin.backend.schedule.dto + +import gomushin.backend.schedule.domain.entity.Schedule +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class UpsertScheduleRequest( + @Schema(description = "일정 ID(새로 생성 시 null, 업데이트 시 id)", example = "1") + val id: Long?, + @Schema(description = "일정 내용", example = "훈련") + val content: String, + @Schema(description = "일정 시작 시간", example = "2023-10-01T10:00:00") + val startDate: LocalDateTime, + @Schema(description = "일정 종료 시간", example = "2023-10-01T12:00:00") + val endDate: LocalDateTime, + @Schema(description = "피로도 (VERY_TIRED, TIRED, GOOD) ", example = "VERT_TIRED") + val fatigue: String, + @Schema(description = "하루 종일 여부", example = "false") + val isAllDay: Boolean = false, +) { + fun toEntity(coupleId: Long, userId: Long) = Schedule( + coupleId = coupleId, + userId = userId, + content = content, + startDate = startDate, + endDate = endDate, + fatigue = fatigue, + isAllDay = isAllDay + ) +} From 576b276236d30588fa456a38a3fce6ee2e2eb4fb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 02:18:44 +0900 Subject: [PATCH 111/357] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20,=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleService.kt | 10 ++++ .../schedule/domain/entity/Schedule.kt | 56 +++++++++++++++++++ .../domain/repository/ScheduleRepository.kt | 6 ++ .../domain/service/ScheduleService.kt | 45 +++++++++++++++ .../facade/UpsertAndDeleteScheduleFacade.kt | 25 +++++++++ 5 files changed, 142 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt index 79d26e11..b66fd2ff 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt @@ -25,4 +25,14 @@ class CoupleService( fun save(couple: Couple): Couple { return coupleRepository.save(couple) } + + @Transactional + fun getByMemberId(memberId: Long): Couple { + return findByMemberId(memberId) ?: throw BadRequestException("sarangggun.couple.not-found") + } + + @Transactional + fun findByMemberId(memberId: Long): Couple? { + return coupleRepository.findByMemberId(memberId) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt new file mode 100644 index 00000000..a547d131 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt @@ -0,0 +1,56 @@ +package gomushin.backend.schedule.domain.entity + +import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "schedule") +class Schedule( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + @Column(name = "couple_id", nullable = false) + val coupleId: Long = 0L, + + @Column(name = "user_id", nullable = false) + val userId: Long = 0L, + + @Column(name = "content", nullable = false) + var content: String, + + @Column(name = "start_date", nullable = false) + var startDate: LocalDateTime, + + @Column(name = "end_date", nullable = false) + var endDate: LocalDateTime, + + @Column(name = "is_all_day", nullable = false) + var isAllDay: Boolean = false, + + @Column(name = "fatigue", nullable = false) + var fatigue: String, +) : BaseEntity() { + companion object { + fun of( + coupleId: Long, + userId: Long, + content: String, + startDate: LocalDateTime, + endDate: LocalDateTime, + fatigue: String, + isAllDay: Boolean?, + ): Schedule { + return Schedule( + coupleId = coupleId, + userId = userId, + content = content, + startDate = startDate, + endDate = endDate, + fatigue = fatigue, + isAllDay = isAllDay ?: false, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt new file mode 100644 index 00000000..3b47444f --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt @@ -0,0 +1,6 @@ +package gomushin.backend.schedule.domain.repository + +import gomushin.backend.schedule.domain.entity.Schedule +import org.springframework.data.jpa.repository.JpaRepository + +interface ScheduleRepository : JpaRepository diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt new file mode 100644 index 00000000..21117ce3 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt @@ -0,0 +1,45 @@ +package gomushin.backend.schedule.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.domain.repository.ScheduleRepository +import gomushin.backend.schedule.dto.UpsertScheduleRequest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class ScheduleService( + private val scheduleRepository: ScheduleRepository, +) { + + @Transactional + fun upsert(id: Long?, coupleId: Long, userId: Long, upsertScheduleRequest: UpsertScheduleRequest) { + id?.let { + getById(id).let { + it.startDate = upsertScheduleRequest.startDate + it.endDate = upsertScheduleRequest.endDate + it.content = upsertScheduleRequest.content + it.content = upsertScheduleRequest.content + } + } ?: save(upsertScheduleRequest.toEntity(coupleId, userId)) + } + + @Transactional(readOnly = true) + fun getById(id: Long) = findById(id) ?: throw BadRequestException("sarangggun.schedule.not-exist-schedule") + + @Transactional(readOnly = true) + fun findById(id: Long) = scheduleRepository.findByIdOrNull(id) + + @Transactional + fun save(schedule: Schedule) = scheduleRepository.save(schedule) + + @Transactional + fun delete(coupleId: Long, userId: Long, scheduleId: Long) { + val schedule = getById(scheduleId) + if (schedule.coupleId != coupleId || schedule.userId != userId) { + throw BadRequestException("sarangggun.schedule.unauthorized") + } + scheduleRepository.deleteById(scheduleId) + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt new file mode 100644 index 00000000..b012790d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt @@ -0,0 +1,25 @@ +package gomushin.backend.schedule.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.dto.UpsertScheduleRequest +import org.springframework.stereotype.Component + +@Component +class UpsertAndDeleteScheduleFacade( + private val scheduleService: ScheduleService, +) { + + fun upsert(customUserDetails: CustomUserDetails, upsertScheduleRequest: UpsertScheduleRequest) { + scheduleService.upsert( + upsertScheduleRequest.id, + customUserDetails.getCouple().id, + customUserDetails.getId(), + upsertScheduleRequest + ) + } + + fun delete(customUserDetails: CustomUserDetails, scheduleId: Long) { + scheduleService.delete(customUserDetails.getCouple().id, customUserDetails.getId(), scheduleId) + } +} From 08d01dda2c0c508db188b4ba5003e867ff647a09 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 02:19:10 +0900 Subject: [PATCH 112/357] =?UTF-8?q?test:=20=EC=84=B1=EA=B3=B5=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/ScheduleServiceTest.kt | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt new file mode 100644 index 00000000..4c476ab1 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt @@ -0,0 +1,86 @@ +package gomushin.backend.schedule.domain.service + +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.domain.repository.ScheduleRepository +import gomushin.backend.schedule.dto.UpsertScheduleRequest +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import java.time.LocalDateTime +import java.util.* + +@ExtendWith(MockitoExtension::class) +class ScheduleServiceTest { + + @Mock + private lateinit var scheduleRepository: ScheduleRepository + + @InjectMocks + private lateinit var scheduleService: ScheduleService + + @DisplayName("일정 저장 테스트") + @Test + fun insert_success() { + // given + val coupleId = 1L + val userId = 1L + val upsertScheduleRequest = mock(UpsertScheduleRequest::class.java) + `when`(upsertScheduleRequest.toEntity(coupleId, userId)).thenReturn(mock(Schedule::class.java)) + `when`(scheduleRepository.save(any(Schedule::class.java))).thenReturn(mock(Schedule::class.java)) + + // when + scheduleService.upsert(null, coupleId, userId, upsertScheduleRequest) + + // then + verify(scheduleRepository).save(any(Schedule::class.java)) + } + + @DisplayName("일정 수정 테스트") + @Test + fun update_success() { + // given + val id = 1L + val coupleId = 1L + val userId = 1L + val upsertScheduleRequest = mock(UpsertScheduleRequest::class.java) + val mockSchedule = mock(Schedule::class.java) + `when`(scheduleRepository.findById(id)).thenReturn(Optional.of(mockSchedule)) + + // when + scheduleService.upsert(id, coupleId, userId, upsertScheduleRequest) + + // then + verify(scheduleRepository).findById(id) + verify(scheduleRepository, never()).save(any(Schedule::class.java)) + } + + @DisplayName("일정 삭제 테스트") + @Test + fun delete_success() { + // given + val coupleId = 1L + val userId = 1L + val scheduleId = 1L + val mockSchedule = Schedule( + id = scheduleId, + coupleId = coupleId, + userId = userId, + startDate = LocalDateTime.of(2023, 1, 1, 0, 0), + endDate = LocalDateTime.of(2023, 1, 2, 0, 0), + content = "Test Schedule", + isAllDay = false, + fatigue = "VERY_TIRED", + ) + `when`(scheduleRepository.findById(scheduleId)).thenReturn(Optional.of(mockSchedule)) + + // when + scheduleService.delete(coupleId, userId, scheduleId) + + // then + verify(scheduleRepository).deleteById(scheduleId) + } +} From 53af343840892850b12dff5ac7a77f20564c9f96 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 02:31:56 +0900 Subject: [PATCH 113/357] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=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 --- .../backend/schedule/domain/entity/Schedule.kt | 10 ++++++++++ .../backend/schedule/domain/service/ScheduleService.kt | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt index a547d131..d4906928 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt @@ -1,5 +1,6 @@ package gomushin.backend.schedule.domain.entity +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity import jakarta.persistence.* import java.time.LocalDateTime @@ -32,6 +33,15 @@ class Schedule( @Column(name = "fatigue", nullable = false) var fatigue: String, ) : BaseEntity() { + + @PrePersist + @PreUpdate + fun validate() { + if (startDate.isAfter(endDate)) { + throw BadRequestException("sarangggun.schedule.invalid-date") + } + } + companion object { fun of( coupleId: Long, diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt index 21117ce3..15b51969 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt @@ -20,7 +20,8 @@ class ScheduleService( it.startDate = upsertScheduleRequest.startDate it.endDate = upsertScheduleRequest.endDate it.content = upsertScheduleRequest.content - it.content = upsertScheduleRequest.content + it.fatigue = upsertScheduleRequest.fatigue + it.isAllDay = upsertScheduleRequest.isAllDay } } ?: save(upsertScheduleRequest.toEntity(coupleId, userId)) } From bfbf0e9e63847e1ba2f268d49ba36b964ff7897f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 12:25:24 +0900 Subject: [PATCH 114/357] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=EC=97=90=20readonly=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/infrastructure/filter/JwtAuthenticationFilter.kt | 6 +++--- .../gomushin/backend/couple/domain/service/CoupleService.kt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index fd7e45b6..c0e20e87 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -17,9 +17,9 @@ class JwtAuthenticationFilter( ) : OncePerRequestFilter() { companion object { - private val AT_IN_COOKIE = "access_token" - private val AUTHORIZATION_HEADER = "Authorization" - private val BEARER_PREFIX = "Bearer " + private const val AT_IN_COOKIE = "access_token" + private const val AUTHORIZATION_HEADER = "Authorization" + private const val BEARER_PREFIX = "Bearer " } override fun shouldNotFilter(request: HttpServletRequest): Boolean { diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt index b66fd2ff..eea8b82d 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt @@ -26,12 +26,12 @@ class CoupleService( return coupleRepository.save(couple) } - @Transactional + @Transactional(readOnly = true) fun getByMemberId(memberId: Long): Couple { return findByMemberId(memberId) ?: throw BadRequestException("sarangggun.couple.not-found") } - @Transactional + @Transactional(readOnly = true) fun findByMemberId(memberId: Long): Couple? { return coupleRepository.findByMemberId(memberId) } From b76c48671be244dd499ddf6057e9beb67fcf7792 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 13:56:57 +0900 Subject: [PATCH 115/357] =?UTF-8?q?fix:=20swagger=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/swagger/SwaggerConfiguration.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt index 582af037..ac54b174 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt @@ -4,11 +4,16 @@ import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.security.SecurityRequirement import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server +import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration -class SwaggerConfiguration { +class SwaggerConfiguration( + @Value("\${springdoc.api-docs.request-url}") + private val requestUrl: String, +) { @Bean fun openAPI(): OpenAPI { @@ -19,8 +24,13 @@ class SwaggerConfiguration { val securityRequirement = SecurityRequirement().addList("bearerAuth") + val server = Server() + .url(requestUrl) + .description("Production Server") + return OpenAPI() .components(Components().addSecuritySchemes("bearerAuth", securityScheme)) + .servers(listOf(server)) .addSecurityItem(securityRequirement) } } From 52d0e1fc019c42a5a75ae79af6c6db483e668b4e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 14:53:10 +0900 Subject: [PATCH 116/357] =?UTF-8?q?chore:=20ci=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 9 +++++++++ .../core/configuration/swagger/SwaggerConfiguration.kt | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0a7d2e2a..b1aa8f75 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,12 +37,14 @@ jobs: env: APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }} TEST_APPLICATION_PROPERTIES: ${{ secrets.TEST_APPLICATION_PROPERTIES }} + ERROR_MESSAGES_PROPERTIES: ${{ secrets.ERROR_MESSAGES_PROPERTIES }} run: | cd ./src rm -rf main/resources/application.yml mkdir -p test/resources mkdir -p main/resources echo "$APPLICATION_PROPERTIES" > main/resources/application.yml + echo "$ERROR_MESSAGES_PROPERTIES" > main/resources/api-error-messages.properties # echo "$TEST_APPLICATION_PROPERTIES" > test/resources/application.yml - name: gradlew 권한 부여 @@ -51,6 +53,13 @@ jobs: - name: 테스트 수행 run: ./gradlew test + - name: 테스트 리포트 아티팩트 업로드 + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-report + path: build/reports/tests/test + - name: 스프링부트 빌드 run: ./gradlew build diff --git a/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt index ac54b174..40a311aa 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt @@ -11,7 +11,7 @@ import org.springframework.context.annotation.Configuration @Configuration class SwaggerConfiguration( - @Value("\${springdoc.api-docs.request-url}") + @Value("\${swagger.request-url}") private val requestUrl: String, ) { From ec01266df2ce91bd6847ad1512d9e6b58fb374f7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 24 Apr 2025 22:58:41 +0900 Subject: [PATCH 117/357] =?UTF-8?q?fix:=20swagger=20cors=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 --- .../core/configuration/security/CustomCorsConfiguration.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index 53a700eb..89875198 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -13,7 +13,12 @@ class CustomCorsConfiguration { fun corsConfigurationSource(): CorsConfigurationSource { val configuration = CorsConfiguration() configuration.allowedOrigins = - listOf("http://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app") + listOf( + "http://localhost:5173", + "http://localhost:8080", + "https://frontend-sarang.vercel.app", + "https://sarang-backend.o-r.kr" + ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") configuration.allowCredentials = true From c7145a1ebcc80d252cf8cbefad058f41b107c801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 25 Apr 2025 00:09:37 +0900 Subject: [PATCH 118/357] =?UTF-8?q?fix=20:=20cors=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0(https=EB=8A=94=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EA=B7=BC=EC=9B=90=EC=A7=80=EC=9D=B4=EA=B8=B0=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20=ED=95=84=EC=9A=94=EC=97=86=EC=9D=8C=20htt?= =?UTF-8?q?p=EB=A1=9C=20=EC=88=98=EC=A0=95=ED=95=B4=EC=95=BC=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/CustomCorsConfiguration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index 89875198..43ac86d5 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -17,7 +17,7 @@ class CustomCorsConfiguration { "http://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app", - "https://sarang-backend.o-r.kr" + "http://sarang-backend.o-r.kr" ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") From 26eb43588f6ef4f95780dbb7fe5fdea13e68a969 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:59:15 +0900 Subject: [PATCH 119/357] =?UTF-8?q?fix:=20preflight=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/SecurityConfiguration.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index b6fdaaa3..c956a193 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -15,6 +15,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.web.cors.CorsUtils @Configuration @EnableWebSecurity @@ -60,6 +61,8 @@ class SecurityConfiguration( "/", "/v1/auth/**", "/v1/oauth/**", + "/oauth2/**", + "/oauth2/authorization/**", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", @@ -70,6 +73,7 @@ class SecurityConfiguration( "/favicon.ico", "/error" ).permitAll() + it.requestMatchers(CorsUtils::isPreFlightRequest).permitAll() it.requestMatchers("/v1/member/onboarding").hasRole("GUEST") it.anyRequest().hasRole("MEMBER") } From 8a1baa02947c4435c1f9fa36a75f8d1468bb7e38 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:42:58 +0900 Subject: [PATCH 120/357] =?UTF-8?q?chore:=20aws=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20minio=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 4 ++++ docker-compose-minio.yml | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 docker-compose-minio.yml diff --git a/build.gradle.kts b/build.gradle.kts index 31a6f96d..19020423 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,8 +67,12 @@ dependencies { // configuration processor annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + // aws + implementation("software.amazon.awssdk:s3:2.30.38") + implementation("org.springframework.boot:spring-boot-starter-validation") testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/docker-compose-minio.yml b/docker-compose-minio.yml new file mode 100644 index 00000000..116dd84a --- /dev/null +++ b/docker-compose-minio.yml @@ -0,0 +1,16 @@ +version: '3.8' +services: + minio: + image: quay.io/minio/minio + command: server /data --console-address ":9001" + environment: + - MINIO_ROOT_USER=admin + - MINIO_ROOT_PASSWORD=12345678 + volumes: + - minio_data:/data + ports: + - "9000:9000" + - "9001:9001" + restart: always +volumes: + minio_data: From 008c9c4cd1e5679a4ba03ec0ec59c491296a39f7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:43:33 +0900 Subject: [PATCH 121/357] =?UTF-8?q?feat:=20S3=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/s3/S3Configuration.kt | 45 +++++++++++++++++++ .../backend/core/service/S3Service.kt | 43 ++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/s3/S3Configuration.kt create mode 100644 src/main/kotlin/gomushin/backend/core/service/S3Service.kt diff --git a/src/main/kotlin/gomushin/backend/core/configuration/s3/S3Configuration.kt b/src/main/kotlin/gomushin/backend/core/configuration/s3/S3Configuration.kt new file mode 100644 index 00000000..bb7c53be --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/s3/S3Configuration.kt @@ -0,0 +1,45 @@ +package gomushin.backend.core.configuration.s3 + +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.core.client.config.ClientOverrideConfiguration +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.S3Configuration +import java.net.URI +import java.time.Duration + +@Configuration +class S3Configuration( + @Value("\${aws.s3.endpoint}") val endpoint: String, + @Value("\${aws.s3.accessKey}") val accessKey: String, + @Value("\${aws.s3.secretKey}") val secretKey: String, + @Value("\${aws.s3.region}") val region: String, +) { + @Bean + fun s3Client(): S3Client { + return S3Client.builder() + .endpointOverride(URI.create(endpoint)) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey) + ) + ) + .region(Region.of(region)) + .serviceConfiguration( + S3Configuration.builder() + .pathStyleAccessEnabled(true) + .build() + ) + .overrideConfiguration( + ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofSeconds(30)) + .apiCallAttemptTimeout(Duration.ofSeconds(10)) + .build() + ) + .build() + } +} diff --git a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt new file mode 100644 index 00000000..9ed68e90 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt @@ -0,0 +1,43 @@ +package gomushin.backend.core.service + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +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.* + +@Service +class S3Service( + private val s3Client: S3Client, + @Value("\${aws.s3.endpoint}") val endpoint: String, + @Value("\${aws.s3.bucket}") private val bucket: String, +) { + + @Transactional + fun uploadFile(multipartFile: MultipartFile): String { + + val fileName = generateFileName(multipartFile) + + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .contentType(multipartFile.contentType) + .build(), + RequestBody.fromInputStream(multipartFile.inputStream, multipartFile.size) + ) + + return getFileUrl(fileName) + } + + private fun getFileUrl(fileName: String): String { + val normalizedEndpoint = endpoint.removeSuffix("/") + return "$normalizedEndpoint/$bucket/$fileName" + } + + private fun generateFileName(file: MultipartFile) = + "${UUID.randomUUID()}-${file.originalFilename?.replace(" ", "_")}" +} From 2943933ec03f6b9ee3921f67395bea5f1492241b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:43:46 +0900 Subject: [PATCH 122/357] =?UTF-8?q?feat:=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/schedule/presentation/ApiPath.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt index 926d5be6..2be1922c 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt @@ -3,4 +3,10 @@ package gomushin.backend.schedule.presentation object ApiPath { const val SCHEDULES = "/v1/schedules" const val SCHEDULE = "/v1/schedules/{scheduleId}" + + const val LETTERS = "/v1/schedules/letters" + const val LETTER = "/v1/schedules/{scheduleId}/letters/{letterId}" + + const val COMMENTS = "/v1/schedules/letters/{letterId}/comments" + const val COMMENT = "/v1/schedules/letters/{letterId}/comments/{commentId}" } From 440dda0be2fcd88d319460cbee85625961c2fe32 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:44:28 +0900 Subject: [PATCH 123/357] =?UTF-8?q?feat:=20Picture=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/entity/Picture.kt | 29 ++++++++++++++++ .../domain/repository/PictureRepository.kt | 9 +++++ .../schedule/domain/service/PictureService.kt | 33 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/entity/Picture.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Picture.kt b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Picture.kt new file mode 100644 index 00000000..e166e910 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Picture.kt @@ -0,0 +1,29 @@ +package gomushin.backend.schedule.domain.entity + +import jakarta.persistence.* + +@Entity +@Table(name = "picture") +class Picture( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + @Column(name = "letter_id", nullable = false) + val letterId: Long = 0L, + + @Column(name = "picture_url", nullable = false) + val pictureUrl: String = "", +) { + companion object { + fun of( + letterId: Long, + pictureUrl: String, + ): Picture { + return Picture( + letterId = letterId, + pictureUrl = pictureUrl, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt new file mode 100644 index 00000000..c1c1a779 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt @@ -0,0 +1,9 @@ +package gomushin.backend.schedule.domain.repository + +import gomushin.backend.schedule.domain.entity.Picture +import org.springframework.data.jpa.repository.JpaRepository + +interface PictureRepository: JpaRepository { + fun findAllByPictureUrlIn(urls: List): List + fun deleteAllByLetterId(letterId: Long) +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt new file mode 100644 index 00000000..2cb0c08a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt @@ -0,0 +1,33 @@ +package gomushin.backend.schedule.domain.service + +import gomushin.backend.schedule.domain.entity.Picture +import gomushin.backend.schedule.domain.repository.PictureRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class PictureService( + private val pictureRepository: PictureRepository, +) { + @Transactional + fun upsert(letterId: Long, pictureUrls: List) { + deleteAllByLetterId(letterId) + val newPictures = pictureUrls.map { Picture(letterId = letterId, pictureUrl = it) } + saveAll(newPictures) + } + + @Transactional(readOnly = true) + fun findAll(urls: List): List { + return pictureRepository.findAllByPictureUrlIn(urls) + } + + @Transactional + fun saveAll(pictures: List): List { + return pictureRepository.saveAll(pictures) + } + + @Transactional + fun deleteAllByLetterId(letterId: Long) { + pictureRepository.deleteAllByLetterId(letterId) + } +} From 565fcfff6033e276193f7e4a1fbd1064063bb0c9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:44:47 +0900 Subject: [PATCH 124/357] =?UTF-8?q?feat:=20=ED=8E=B8=EC=A7=80=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=B0=8F=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/entity/Letter.kt | 41 ++++++++++++++++ .../domain/repository/LetterRepository.kt | 6 +++ .../schedule/domain/service/LetterService.kt | 48 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/entity/Letter.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Letter.kt b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Letter.kt new file mode 100644 index 00000000..d3e42a11 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Letter.kt @@ -0,0 +1,41 @@ +package gomushin.backend.schedule.domain.entity + +import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import jakarta.persistence.* + +@Entity +@Table(name = "letter") +class Letter( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + @Column(name = "schedule_id", nullable = false) + val scheduleId: Long = 0L, + + @Column(name = "author_id", nullable = false) + val authorId: Long = 0L, + + @Column(name = "title", nullable = false) + var title: String = "", + + @Column(name = "content", nullable = false) + var content: String = "", + + ) : BaseEntity() { + companion object { + fun of( + scheduleId: Long, + authorId: Long, + title: String, + content: String, + ): Letter { + return Letter( + scheduleId = scheduleId, + authorId = authorId, + title = title, + content = content, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt new file mode 100644 index 00000000..b1ff0af0 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -0,0 +1,6 @@ +package gomushin.backend.schedule.domain.repository + +import gomushin.backend.schedule.domain.entity.Letter +import org.springframework.data.jpa.repository.JpaRepository + +interface LetterRepository : JpaRepository diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt new file mode 100644 index 00000000..8652a223 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -0,0 +1,48 @@ +package gomushin.backend.schedule.domain.service + +import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.repository.LetterRepository +import gomushin.backend.schedule.dto.UpsertLetterRequest +import org.apache.coyote.BadRequestException +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class LetterService( + private val letterRepository: LetterRepository, +) { + @Transactional + fun upsert(authorId: Long, upsertLetterRequest: UpsertLetterRequest): Letter { + return upsertLetterRequest.letterId?.let { letterId -> + getById(letterId).apply { + title = upsertLetterRequest.title + content = upsertLetterRequest.content + }.let { savedLetter -> + save(savedLetter) + } + } ?: save( + Letter.of( + scheduleId = upsertLetterRequest.scheduleId, + authorId = authorId, + title = upsertLetterRequest.title, + content = upsertLetterRequest.content, + ) + ) + + } + + @Transactional(readOnly = true) + fun getById(id: Long) = findById(id) ?: throw BadRequestException("sarangggun.letter.not-exist") + + @Transactional(readOnly = true) + fun findById(id: Long) = letterRepository.findByIdOrNull(id) + + @Transactional + fun save(letter: Letter) = letterRepository.save(letter) + + @Transactional + fun delete(letterId: Long) { + letterRepository.deleteById(letterId) + } +} From 08027411893181c63a22f94074bd237187db98c4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:45:06 +0900 Subject: [PATCH 125/357] =?UTF-8?q?feat:=20=ED=8E=B8=EC=A7=80=20CUD=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 --- .../schedule/dto/UpsertLetterRequest.kt | 14 +++++ .../facade/UpsertAndDeleteLetterFacade.kt | 61 +++++++++++++++++++ .../UpsertAndDeleteLetterController.kt | 46 ++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/UpsertLetterRequest.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertLetterRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/UpsertLetterRequest.kt new file mode 100644 index 00000000..1988b7d6 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/UpsertLetterRequest.kt @@ -0,0 +1,14 @@ +package gomushin.backend.schedule.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class UpsertLetterRequest( + @Schema(description = "편지 ID(새로 생성 시 null, 업데이트 시 id)", example = "1") + val letterId: Long? = null, + @Schema(description = "일정 ID", example = "1") + val scheduleId: Long, + @Schema(description = "편지 제목", example = "훈련 힘내") + val title: String, + @Schema(description = "편지 내용", example = "화이팅") + val content: String, +) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt new file mode 100644 index 00000000..fafc225d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt @@ -0,0 +1,61 @@ +package gomushin.backend.schedule.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.core.service.S3Service +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.dto.UpsertLetterRequest +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +@Component +class UpsertAndDeleteLetterFacade( + private val letterService: LetterService, + private val s3Service: S3Service, + private val pictureService: PictureService +) { + + @Transactional + fun upsert( + customUserDetails: CustomUserDetails, + upsertLetterRequest: UpsertLetterRequest, + pictures: List? + ) { + val letter = letterService.upsert( + customUserDetails.getId(), + upsertLetterRequest + ) + + val pictureList = mutableListOf() + + pictures?.let { + it.forEach { picture -> + val fileUrl = s3Service.uploadFile(picture) + pictureList.add(fileUrl) + } + pictureService.upsert(letter.id, pictureList) + } + } + + @Transactional + fun delete( + customUserDetails: CustomUserDetails, + scheduleId: Long, + letterId: Long + ) { + val letter = letterService.getById(letterId) + + if (letter.authorId != customUserDetails.getId()) { + throw BadRequestException("sarangggun.letter.unauthorized") + } + + if (letter.scheduleId != scheduleId) { + throw BadRequestException("sarangggun.letter.invalid-schedule") + } + + letterService.delete(letterId) + pictureService.deleteAllByLetterId(letterId) + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt new file mode 100644 index 00000000..a2dfd1ee --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt @@ -0,0 +1,46 @@ +package gomushin.backend.schedule.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.schedule.dto.UpsertLetterRequest +import gomushin.backend.schedule.facade.UpsertAndDeleteLetterFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile + +@RestController +@Tag(name = "편지 생성 , 수정 , 삭제", description = "UpsertAndDeleteLetterController") +class UpsertAndDeleteLetterController( + private val upsertAndDeleteLetterFacade: UpsertAndDeleteLetterFacade, +) { + @ResponseStatus(HttpStatus.CREATED) + @PostMapping( + ApiPath.LETTERS, + consumes = [MediaType.MULTIPART_FORM_DATA_VALUE] + ) + @Operation(summary = "편지 수정하거나 추가하기", description = "upsertLetter") + fun upsertLetter( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestPart("data") upsertLetterRequest: UpsertLetterRequest, + @RequestPart("pictures", required = false) pictures: List?, + ): ApiResponse { + upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) + return ApiResponse.success(true) + } + + @ResponseStatus(HttpStatus.OK) + @DeleteMapping(ApiPath.LETTER) + @Operation(summary = "편지 삭제하기", description = "deleteLetter") + fun deleteLetter( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable scheduleId: Long, + @PathVariable letterId: Long, + ): ApiResponse { + upsertAndDeleteLetterFacade.delete(customUserDetails, scheduleId, letterId) + return ApiResponse.success(true) + } +} From bc45cb9e954c62c3d9f5905347ab92940a633947 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:45:30 +0900 Subject: [PATCH 126/357] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20CUD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/entity/Comment.kt | 39 ++++++++++++ .../domain/repository/CommentRepository.kt | 6 ++ .../schedule/domain/service/CommentService.kt | 59 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/entity/Comment.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Comment.kt b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Comment.kt new file mode 100644 index 00000000..5007356d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Comment.kt @@ -0,0 +1,39 @@ +package gomushin.backend.schedule.domain.entity + +import jakarta.persistence.* + +@Entity +@Table(name = "comment") +class Comment( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L, + + @Column(name = "letter_id", nullable = false) + val letterId: Long = 0L, + + @Column(name = "author_id", nullable = false) + val authorId: Long = 0L, + + @Column(name = "nickname", nullable = false) + var nickname: String = "", + + @Column(name = "content", nullable = false) + var content: String = "", +) { + companion object { + fun of( + letterId: Long, + authorId: Long, + nickname: String, + content: String, + ): Comment { + return Comment( + letterId = letterId, + authorId = authorId, + nickname = nickname, + content = content, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt new file mode 100644 index 00000000..32b5c098 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt @@ -0,0 +1,6 @@ +package gomushin.backend.schedule.domain.repository + +import gomushin.backend.schedule.domain.entity.Comment +import org.springframework.data.jpa.repository.JpaRepository + +interface CommentRepository: JpaRepository diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt new file mode 100644 index 00000000..6dd1d3c6 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt @@ -0,0 +1,59 @@ +package gomushin.backend.schedule.domain.service + +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.schedule.domain.entity.Comment +import gomushin.backend.schedule.domain.repository.CommentRepository +import gomushin.backend.schedule.dto.UpsertCommentRequest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CommentService( + private val commentRepository: CommentRepository, +) { + @Transactional + fun upsert( + id: Long?, + letterId: Long, + authorId: Long, + nickname: String, + upsertCommentRequest: UpsertCommentRequest + ) { + id?.let { commentId -> + getById(commentId).let { + if (it.authorId != authorId) { + throw BadRequestException("sarangggun.comment.unauthorized") + } + it.content = upsertCommentRequest.content + } + } ?: save( + Comment.of( + letterId = letterId, + authorId = authorId, + nickname = nickname, + content = upsertCommentRequest.content, + ) + ) + } + + @Transactional(readOnly = true) + fun getById(id: Long): Comment { + return findById(id) ?: throw BadRequestException("sarangggun.comment.not-found") + } + + @Transactional(readOnly = true) + fun findById(id: Long): Comment? { + return commentRepository.findByIdOrNull(id) + } + + @Transactional + fun save(comment: Comment): Comment { + return commentRepository.save(comment) + } + + @Transactional + fun delete(id: Long) { + commentRepository.deleteById(id) + } +} From 05777978840fdf5575a0b12171e91a9c3f71bbee Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:45:55 +0900 Subject: [PATCH 127/357] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20CUD=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/dto/UpsertCommentRequest.kt | 10 ++++ .../facade/UpsertAndDeleteCommentFacade.kt | 43 +++++++++++++++++ .../UpsertAndDeleteCommentController.kt | 47 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/UpsertCommentRequest.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteCommentFacade.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteCommentController.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertCommentRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/UpsertCommentRequest.kt new file mode 100644 index 00000000..ca6d291b --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/UpsertCommentRequest.kt @@ -0,0 +1,10 @@ +package gomushin.backend.schedule.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class UpsertCommentRequest( + @Schema(description = "댓글 ID(새로 생성 시 null, 업데이트 시 id)", example = "1") + val commentId: Long?, + @Schema(description = "내용", example = "훈련 힘내") + val content: String, +) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteCommentFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteCommentFacade.kt new file mode 100644 index 00000000..b286449b --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteCommentFacade.kt @@ -0,0 +1,43 @@ +package gomushin.backend.schedule.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.member.domain.service.MemberService +import gomushin.backend.schedule.domain.service.CommentService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.dto.UpsertCommentRequest +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class UpsertAndDeleteCommentFacade( + private val commentService: CommentService, + private val letterService: LetterService, + private val memberService: MemberService, +) { + @Transactional + fun upsert(customUserDetails: CustomUserDetails, letterId: Long, upsertCommentRequest: UpsertCommentRequest) { + val member = memberService.getById(customUserDetails.getId()) + val letter = letterService.getById(letterId) + commentService.upsert( + id = upsertCommentRequest.commentId, + letterId = letter.id, + authorId = member.id, + nickname = member.nickname, + upsertCommentRequest = upsertCommentRequest + ) + } + + @Transactional + fun delete(customUserDetails: CustomUserDetails, letterId: Long, commentId: Long) { + val member = memberService.getById(customUserDetails.getId()) + val comment = commentService.getById(commentId) + if (comment.authorId != member.id) { + throw BadRequestException("sarangggun.comment.unauthorized") + } + if (comment.letterId != letterId) { + throw BadRequestException("sarangggun.comment.invalid-letter") + } + commentService.delete(commentId) + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteCommentController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteCommentController.kt new file mode 100644 index 00000000..90e4cc9a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteCommentController.kt @@ -0,0 +1,47 @@ +package gomushin.backend.schedule.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.schedule.dto.UpsertCommentRequest +import gomushin.backend.schedule.facade.UpsertAndDeleteCommentFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "댓글 생성 , 수정 , 삭제", description = "UpsertAndDeleteCommentController") +class UpsertAndDeleteCommentController( + private val upsertAndDeleteCommentFacade: UpsertAndDeleteCommentFacade, +) { + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping(ApiPath.COMMENTS) + @Operation(summary = "댓글 수정하거나 추가하기", description = "upsertComment") + fun upsertComment( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable letterId: Long, + @RequestBody upsertCommentRequest: UpsertCommentRequest, + ): ApiResponse { + upsertAndDeleteCommentFacade.upsert(customUserDetails, letterId, upsertCommentRequest) + return ApiResponse.success(true) + } + + @ResponseStatus(HttpStatus.OK) + @DeleteMapping(ApiPath.COMMENT) + @Operation(summary = "댓글 삭제하기", description = "deleteComment") + fun deleteComment( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable letterId: Long, + @PathVariable commentId: Long, + ): ApiResponse { + upsertAndDeleteCommentFacade.delete(customUserDetails, letterId, commentId) + return ApiResponse.success(true) + } +} From b85e999e3a0e1bf499f91a8ca3cfb13944b1660e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:46:10 +0900 Subject: [PATCH 128/357] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20=ED=83=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/presentation/UpsertAndDeleteScheduleController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt index ca3d67e1..d6e4dc6a 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt @@ -11,7 +11,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.* @RestController -@Tag(name = "일정", description = "UpsertAndDeleteScheduleController") +@Tag(name = "일정 생성 , 수정 , 삭제", description = "UpsertAndDeleteScheduleController") class UpsertAndDeleteScheduleController( private val upsertAndDeleteScheduleFacade: UpsertAndDeleteScheduleFacade, ) { From ab0abc17de8d93db2f35506b9b643abd307115aa Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 13:46:25 +0900 Subject: [PATCH 129/357] =?UTF-8?q?test:=20=EC=84=B1=EA=B3=B5=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=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 --- .../UpsertAndDeleteCommentFacadeTest.kt | 98 +++++++++++++++ .../facade/UpsertAndDeleteLetterFacadeTest.kt | 119 ++++++++++++++++++ .../domain/service/CommentServiceTest.kt | 74 +++++++++++ .../domain/service/PictureServiceTest.kt | 37 ++++++ 4 files changed, 328 insertions(+) create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/service/PictureServiceTest.kt diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt new file mode 100644 index 00000000..2690ef3f --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt @@ -0,0 +1,98 @@ +package gomushin.backend.schedule.domain.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.service.MemberService +import gomushin.backend.schedule.domain.entity.Comment +import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.service.CommentService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.dto.UpsertCommentRequest +import gomushin.backend.schedule.facade.UpsertAndDeleteCommentFacade +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import kotlin.test.Test + +@ExtendWith(MockitoExtension::class) +class UpsertAndDeleteCommentFacadeTest { + + @Mock + private lateinit var commentService: CommentService + + @Mock + private lateinit var letterService: LetterService + + @Mock + private lateinit var memberService: MemberService + + @InjectMocks + private lateinit var upsertAndDeleteCommentFacade: UpsertAndDeleteCommentFacade + + @DisplayName("댓글 생성 또는 수정 성공") + @Test + fun upsert_success() { + // given + val customUserDetails = mock(CustomUserDetails::class.java) + val letterId = 1L + val memberId = 1L + val upsertCommentRequest = UpsertCommentRequest( + commentId = null, + content = "댓글 내용" + ) + + val mockMember = mock(Member::class.java).apply { + `when`(id).thenReturn(memberId) + `when`(nickname).thenReturn("테스트유저") + } + val mockLetter = mock(Letter::class.java).apply { + `when`(id).thenReturn(letterId) + } + + `when`(customUserDetails.getId()).thenReturn(memberId) + `when`(memberService.getById(memberId)).thenReturn(mockMember) + `when`(letterService.getById(letterId)).thenReturn(mockLetter) + + // when + upsertAndDeleteCommentFacade.upsert(customUserDetails, letterId, upsertCommentRequest) + + // then + verify(commentService, times(1)).upsert( + id = upsertCommentRequest.commentId, + letterId = mockLetter.id, + authorId = mockMember.id, + nickname = "테스트유저", + upsertCommentRequest = upsertCommentRequest + ) + } + + @DisplayName("댓글 삭제 성공") + @Test + fun delete_success() { + // given + val customUserDetails = mock(CustomUserDetails::class.java) + val letterId = 1L + val commentId = 1L + val mockMember = mock(Member::class.java) + val mockComment = mock(Comment::class.java) + + // when + `when`(customUserDetails.getId()).thenReturn(1L) + `when`(memberService.getById(customUserDetails.getId())).thenReturn(mockMember) + `when`(commentService.getById(commentId)).thenReturn(mockComment) + `when`(mockMember.id).thenReturn(1L) + `when`(mockComment.authorId).thenReturn(1L) + `when`(mockComment.letterId).thenReturn(letterId) + `when`(mockComment.letterId).thenReturn(letterId) + + upsertAndDeleteCommentFacade.delete(customUserDetails, letterId, commentId) + + // then + verify(memberService, times(1)).getById(customUserDetails.getId()) + verify(commentService, times(1)).getById(commentId) + verify(commentService, times(1)).delete(commentId) + } +} diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt new file mode 100644 index 00000000..b13ee525 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt @@ -0,0 +1,119 @@ +package gomushin.backend.schedule.domain.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.service.S3Service +import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.dto.UpsertLetterRequest +import gomushin.backend.schedule.facade.UpsertAndDeleteLetterFacade +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.web.multipart.MultipartFile + +@ExtendWith(MockitoExtension::class) +class UpsertAndDeleteLetterFacadeTest { + + @Mock + private lateinit var letterService: LetterService + + @Mock + private lateinit var s3Service: S3Service + + @Mock + private lateinit var pictureService: PictureService + + @InjectMocks + private lateinit var upsertAndDeleteLetterFacade: UpsertAndDeleteLetterFacade + + + @Nested + inner class Upsert { + + @DisplayName("업로드 성공 - 업로드된 사진이 있을 때") + @Test + fun upsertWithPictures_success() { + // given + val customUserDetails = mock(CustomUserDetails::class.java) + val upsertLetterRequest = UpsertLetterRequest( + title = "제목", + content = "내용", + scheduleId = 1L + ) + val pictures = listOf(mock(MultipartFile::class.java)) + + val letter = mock(Letter::class.java) + + // when + `when`(letter.id).thenReturn(1L) + `when`(customUserDetails.getId()).thenReturn(1L) + `when`(letterService.upsert(1L, upsertLetterRequest)).thenReturn(letter) + `when`(s3Service.uploadFile(pictures[0])).thenReturn("http://example.com/test.jpg") + doNothing().`when`(pictureService).upsert(1L, listOf("http://example.com/test.jpg")) + upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) + + // then + verify( + letterService, + times(1) + ).upsert(1L, upsertLetterRequest) + } + + @DisplayName("업로드 성공 - 업로드된 사진이 없을 때") + @Test + fun upsertWithoutPictures_success() { + // given + val customUserDetails = mock(CustomUserDetails::class.java) + val upsertLetterRequest = UpsertLetterRequest( + title = "제목", + content = "내용", + scheduleId = 1L + ) + val pictures: List? = null + val letter = mock(Letter::class.java) + + // when + `when`(customUserDetails.getId()).thenReturn(1L) + `when`(letterService.upsert(1L, upsertLetterRequest)).thenReturn(letter) + upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) + + // then + verify(letterService, times(1)).upsert(1L, upsertLetterRequest) + verify(s3Service, never()).uploadFile(org.mockito.kotlin.any()) + verify(pictureService, never()).upsert(org.mockito.kotlin.any(), org.mockito.kotlin.any()) + } + } + + @DisplayName("삭제 성공") + @Test + fun delete_success() { + // given + val customUserDetails = mock(CustomUserDetails::class.java) + val scheduleId = 1L + val letterId = 1L + val mockLetter = mock(Letter::class.java) + + // when + `when`(letterService.getById(letterId)).thenReturn(mockLetter) + `when`(customUserDetails.getId()).thenReturn(1L) + `when`(mockLetter.authorId).thenReturn(1L) + `when`(mockLetter.scheduleId).thenReturn(scheduleId) + upsertAndDeleteLetterFacade.delete( + customUserDetails, + scheduleId, + letterId + ) + + // then + verify(letterService, times(1)).getById(letterId) + verify(letterService, times(1)).delete(letterId) + verify(pictureService, times(1)).deleteAllByLetterId(letterId) + + } +} diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt new file mode 100644 index 00000000..6f90b6dd --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt @@ -0,0 +1,74 @@ +package gomushin.backend.schedule.domain.service + +import gomushin.backend.schedule.domain.entity.Comment +import gomushin.backend.schedule.domain.repository.CommentRepository +import gomushin.backend.schedule.dto.UpsertCommentRequest +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import java.util.* +import kotlin.test.Test + +@ExtendWith(MockitoExtension::class) +class CommentServiceTest { + + @Mock + lateinit var commentRepository: CommentRepository + + @InjectMocks + lateinit var commentService: CommentService + + @Nested + inner class Upsert { + + @DisplayName("업데이트 성공 - 댓글이 존재할 때") + @Test + fun upload_success() { + // given + val id = 1L + val letterId = 1L + val authorId = 1L + val nickname = "닉네임" + val upsertCommentRequest = UpsertCommentRequest( + commentId = 1L, + content = "내용" + ) + val mockComment = mock(Comment::class.java).apply { + `when`(this.authorId).thenReturn(authorId) + } + + // when + `when`(commentRepository.findById(id)).thenReturn(Optional.of(mockComment)) + commentService.upsert(id, letterId, authorId, nickname, upsertCommentRequest) + + // then + verify(mockComment).content = upsertCommentRequest.content + } + + @DisplayName("댓글 생성 성공") + @Test + fun insert_success() { + // given + val letterId = 1L + val authorId = 1L + val nickname = "닉네임" + val upsertCommentRequest = UpsertCommentRequest( + commentId = null, + content = "내용" + ) + val mockComment = mock(Comment::class.java) + + // when + `when`(commentRepository.save(any())).thenReturn(mockComment) + commentService.upsert(null, letterId, authorId, nickname, upsertCommentRequest) + + // then + verify(commentRepository, times(1)).save(org.mockito.kotlin.any()) + } + + } +} diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/PictureServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/PictureServiceTest.kt new file mode 100644 index 00000000..c8bc0939 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/PictureServiceTest.kt @@ -0,0 +1,37 @@ +package gomushin.backend.schedule.domain.service + +import gomushin.backend.schedule.domain.repository.PictureRepository +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.anyList +import org.mockito.Mockito.verify +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.times +import kotlin.test.Test + +@ExtendWith(MockitoExtension::class) +class PictureServiceTest { + + @Mock + private lateinit var pictureRepository: PictureRepository + + @InjectMocks + private lateinit var pictureService: PictureService + + @DisplayName("upsert 성공") + @Test + fun upsert_success() { + // given + val letterId = 1L + val pictureUrls = listOf("http://example.com/test.jpg") + + // when + pictureService.upsert(letterId, pictureUrls) + + // then + verify(pictureRepository).deleteAllByLetterId(letterId) + verify(pictureRepository, times(1)).saveAll(anyList()) + } +} From 7abd3c7b221170a8d4d9bc7a468ea55f35b81725 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 15:58:41 +0900 Subject: [PATCH 130/357] =?UTF-8?q?refactor:=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=97=90=20=EB=8C=80=ED=95=9C=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=ED=99=95=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/service/S3Service.kt | 6 ++++-- .../facade/UpsertAndDeleteLetterFacade.kt | 11 ++++++++++- .../facade/UpsertAndDeleteLetterFacadeTest.kt | 19 ++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt index 9ed68e90..48ad5370 100644 --- a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt +++ b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt @@ -38,6 +38,8 @@ class S3Service( return "$normalizedEndpoint/$bucket/$fileName" } - private fun generateFileName(file: MultipartFile) = - "${UUID.randomUUID()}-${file.originalFilename?.replace(" ", "_")}" + private fun generateFileName(file: MultipartFile): String { + val safeName = file.originalFilename?.replace(" ", "_") ?: "unknown-file" + return "${UUID.randomUUID()}-$safeName" + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt index fafc225d..a3309a15 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt @@ -5,6 +5,7 @@ import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.core.service.S3Service import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.UpsertLetterRequest import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -14,7 +15,8 @@ import org.springframework.web.multipart.MultipartFile class UpsertAndDeleteLetterFacade( private val letterService: LetterService, private val s3Service: S3Service, - private val pictureService: PictureService + private val pictureService: PictureService, + private val scheduleService: ScheduleService, ) { @Transactional @@ -23,6 +25,13 @@ class UpsertAndDeleteLetterFacade( upsertLetterRequest: UpsertLetterRequest, pictures: List? ) { + + val schedule = scheduleService.getById(upsertLetterRequest.scheduleId) + + if (schedule.coupleId != customUserDetails.getCouple().id) { + throw BadRequestException("sarangggun.letter.not-in-couple") + } + val letter = letterService.upsert( customUserDetails.getId(), upsertLetterRequest diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt index b13ee525..532a341d 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt @@ -2,9 +2,12 @@ package gomushin.backend.schedule.domain.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.service.S3Service +import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.UpsertLetterRequest import gomushin.backend.schedule.facade.UpsertAndDeleteLetterFacade import org.junit.jupiter.api.DisplayName @@ -29,6 +32,9 @@ class UpsertAndDeleteLetterFacadeTest { @Mock private lateinit var pictureService: PictureService + @Mock + private lateinit var scheduleService: ScheduleService + @InjectMocks private lateinit var upsertAndDeleteLetterFacade: UpsertAndDeleteLetterFacade @@ -47,12 +53,17 @@ class UpsertAndDeleteLetterFacadeTest { scheduleId = 1L ) val pictures = listOf(mock(MultipartFile::class.java)) - + val schedule = mock(Schedule::class.java) val letter = mock(Letter::class.java) + val couple = mock(Couple::class.java) // when `when`(letter.id).thenReturn(1L) `when`(customUserDetails.getId()).thenReturn(1L) + `when`(scheduleService.getById(upsertLetterRequest.scheduleId)).thenReturn(schedule) + `when`(customUserDetails.getCouple()).thenReturn(couple) + `when`(customUserDetails.getCouple().id).thenReturn(1L) + `when`(schedule.coupleId).thenReturn(1L) `when`(letterService.upsert(1L, upsertLetterRequest)).thenReturn(letter) `when`(s3Service.uploadFile(pictures[0])).thenReturn("http://example.com/test.jpg") doNothing().`when`(pictureService).upsert(1L, listOf("http://example.com/test.jpg")) @@ -77,9 +88,15 @@ class UpsertAndDeleteLetterFacadeTest { ) val pictures: List? = null val letter = mock(Letter::class.java) + val couple = mock(Couple::class.java) + val schedule = mock(Schedule::class.java) // when `when`(customUserDetails.getId()).thenReturn(1L) + `when`(scheduleService.getById(upsertLetterRequest.scheduleId)).thenReturn(schedule) + `when`(customUserDetails.getCouple()).thenReturn(couple) + `when`(customUserDetails.getCouple().id).thenReturn(1L) + `when`(schedule.coupleId).thenReturn(1L) `when`(letterService.upsert(1L, upsertLetterRequest)).thenReturn(letter) upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) From 11af4c756e472bf6d4affd033f1fd40b75f74b1c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 17:26:22 +0900 Subject: [PATCH 131/357] =?UTF-8?q?fix:=20BadRequestException=20import=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 --- .../gomushin/backend/schedule/domain/service/LetterService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index 8652a223..f7d59d36 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -1,9 +1,9 @@ package gomushin.backend.schedule.domain.service +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.repository.LetterRepository import gomushin.backend.schedule.dto.UpsertLetterRequest -import org.apache.coyote.BadRequestException import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional From f7eae8227608197de62ef506779b09540b8f913d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 17:26:36 +0900 Subject: [PATCH 132/357] =?UTF-8?q?refactor:=20s3=20=EB=AC=BC=EB=A6=AC?= =?UTF-8?q?=EC=A0=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/core/service/S3Service.kt | 9 +++++++++ .../schedule/domain/repository/PictureRepository.kt | 1 + .../backend/schedule/domain/service/PictureService.kt | 5 +++++ .../schedule/facade/UpsertAndDeleteLetterFacade.kt | 6 ++++++ 4 files changed, 21 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt index 48ad5370..78946c5b 100644 --- a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt +++ b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt @@ -33,6 +33,15 @@ class S3Service( return getFileUrl(fileName) } + @Transactional + fun deleteFile(fileName: String) { + s3Client.deleteObject { it.bucket(bucket).key(fileName) } + } + + private fun getFileName(fileUrl: String): String { + return fileUrl.substringAfterLast("/") + } + private fun getFileUrl(fileName: String): String { val normalizedEndpoint = endpoint.removeSuffix("/") return "$normalizedEndpoint/$bucket/$fileName" diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt index c1c1a779..27199b76 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt @@ -6,4 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository interface PictureRepository: JpaRepository { fun findAllByPictureUrlIn(urls: List): List fun deleteAllByLetterId(letterId: Long) + fun findAllByLetterId(letterId: Long): List } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt index 2cb0c08a..9a4caf71 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt @@ -21,6 +21,11 @@ class PictureService( return pictureRepository.findAllByPictureUrlIn(urls) } + @Transactional(readOnly = true) + fun findAllByLetterId(letterId: Long): List { + return pictureRepository.findAllByLetterId(letterId) + } + @Transactional fun saveAll(pictures: List): List { return pictureRepository.saveAll(pictures) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt index a3309a15..b692efc1 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt @@ -64,6 +64,12 @@ class UpsertAndDeleteLetterFacade( throw BadRequestException("sarangggun.letter.invalid-schedule") } + pictureService.findAllByLetterId(letter.id) + .takeIf { it.isNotEmpty() } + ?.forEach { picture -> + s3Service.deleteFile(picture.pictureUrl) + } + letterService.delete(letterId) pictureService.deleteAllByLetterId(letterId) } From a873f02abf814ec6aa39365c0b9d579872d09dee Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 17:37:44 +0900 Subject: [PATCH 133/357] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/gomushin/backend/core/service/S3Service.kt | 5 ++--- .../backend/schedule/facade/UpsertAndDeleteLetterFacade.kt | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt index 78946c5b..13f61ee4 100644 --- a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt +++ b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt @@ -2,6 +2,7 @@ package gomushin.backend.core.service import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile import software.amazon.awssdk.core.sync.RequestBody @@ -16,7 +17,6 @@ class S3Service( @Value("\${aws.s3.bucket}") private val bucket: String, ) { - @Transactional fun uploadFile(multipartFile: MultipartFile): String { val fileName = generateFileName(multipartFile) @@ -33,9 +33,8 @@ class S3Service( return getFileUrl(fileName) } - @Transactional fun deleteFile(fileName: String) { - s3Client.deleteObject { it.bucket(bucket).key(fileName) } + s3Client.deleteObject { it.bucket(bucket).key(getFileName(fileName)) } } private fun getFileName(fileUrl: String): String { diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt index b692efc1..2e72a90b 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt @@ -18,7 +18,7 @@ class UpsertAndDeleteLetterFacade( private val pictureService: PictureService, private val scheduleService: ScheduleService, ) { - + // TODO: 이벤트 드리븐하게 수정 @Transactional fun upsert( customUserDetails: CustomUserDetails, From 40e826083259ce66da295e4d137c31e2c5549e68 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 22:02:22 +0900 Subject: [PATCH 134/357] =?UTF-8?q?fix:=20=EC=BF=A0=ED=82=A4=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B3=B4=EC=95=88=20=EC=84=A4=EC=A0=95=20=EC=99=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/CustomCorsConfiguration.kt | 2 +- .../gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index 43ac86d5..fd2ea051 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -20,7 +20,7 @@ class CustomCorsConfiguration { "http://sarang-backend.o-r.kr" ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") - configuration.allowedHeaders = listOf("*") + configuration.allowedHeaders = listOf("Set-Cookie", "Authorization", "Content-Type") configuration.allowCredentials = true configuration.maxAge = 3600 val source = UrlBasedCorsConfigurationSource() diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 2af7a74b..0828e9d7 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -51,6 +51,7 @@ class CustomSuccessHandler( cookie.isHttpOnly = true cookie.secure = false //Todo : 밋업 할 때는 true로 변경하기 cookie.maxAge = 1800 + cookie.setAttribute("SameSite", "Lax") return cookie } From f4d9da81ba99b73a77f0cab0adb7353aeda35a69 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 25 Apr 2025 23:40:06 +0900 Subject: [PATCH 135/357] =?UTF-8?q?fix:=20https=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/CustomCorsConfiguration.kt | 5 +++-- .../backend/core/oauth/handler/CustomSuccessHandler.kt | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index fd2ea051..6c43443d 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -15,13 +15,14 @@ class CustomCorsConfiguration { configuration.allowedOrigins = listOf( "http://localhost:5173", + "https://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app", - "http://sarang-backend.o-r.kr" ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") - configuration.allowedHeaders = listOf("Set-Cookie", "Authorization", "Content-Type") + configuration.allowedHeaders = listOf("*") configuration.allowCredentials = true + configuration.exposedHeaders = listOf("Authorization", "Set-Cookie") configuration.maxAge = 3600 val source = UrlBasedCorsConfigurationSource() source.registerCorsConfiguration("/**", configuration) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 0828e9d7..750cbce3 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -49,9 +49,8 @@ class CustomSuccessHandler( val cookie = Cookie(key, value) cookie.path = "/" cookie.isHttpOnly = true - cookie.secure = false //Todo : 밋업 할 때는 true로 변경하기 + cookie.secure = true cookie.maxAge = 1800 - cookie.setAttribute("SameSite", "Lax") return cookie } From 28d9d3b0d343b2d345ca3ae27e724921072f77d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 11:01:55 +0900 Subject: [PATCH 136/357] =?UTF-8?q?fix=20:=20cookie=20sameSite=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/oauth/handler/CustomSuccessHandler.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 750cbce3..a2ec6773 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -41,17 +41,12 @@ class CustomSuccessHandler( accessToken = jwtTokenProvider.provideAccessToken(principal.getUserId(), principal.getRole()) } - response!!.addCookie(createCookie("access_token", accessToken)) + response!!.addHeader("Set-Cookie", createSetCookieHeader("access_token", accessToken)) response.sendRedirect(redirectUrl) } - private fun createCookie(key: String, value: String): Cookie { - val cookie = Cookie(key, value) - cookie.path = "/" - cookie.isHttpOnly = true - cookie.secure = true - cookie.maxAge = 1800 - return cookie + private fun createSetCookieHeader(key: String, value: String): String { + return "$key=$value; Path=/; Max-Age=1800; HttpOnly; Secure; SameSite=None" } private fun getMemberByEmail(email: String): Member? { From f0d0717f507427feb9d64b131aef834fdc9757eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 25 Apr 2025 21:52:21 +0900 Subject: [PATCH 137/357] =?UTF-8?q?feat=20:=20=EB=82=98=EC=9D=98=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/dto/response/MyEmotionResponse.kt | 13 +++++++++++++ .../backend/member/facade/MemberInfoFacade.kt | 6 ++++++ .../gomushin/backend/member/presentation/ApiPath.kt | 1 + .../member/presentation/MemberInfoController.kt | 11 +++++++++++ 4 files changed, 31 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/dto/response/MyEmotionResponse.kt diff --git a/src/main/kotlin/gomushin/backend/member/dto/response/MyEmotionResponse.kt b/src/main/kotlin/gomushin/backend/member/dto/response/MyEmotionResponse.kt new file mode 100644 index 00000000..3913fd3f --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/dto/response/MyEmotionResponse.kt @@ -0,0 +1,13 @@ +package gomushin.backend.member.dto.response + +import gomushin.backend.member.domain.entity.Member + +data class MyEmotionResponse ( + val emotion : Int? +) { + companion object { + fun of(member: Member) = MyEmotionResponse ( + member.emotion + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 98cae42e..1ce73e67 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -3,6 +3,7 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest +import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.dto.response.MyInfoResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse import org.springframework.stereotype.Component @@ -23,4 +24,9 @@ class MemberInfoFacade( fun updateMyEmotionAndStatusMessage(customUserDetails: CustomUserDetails, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) = memberService.updateMyEmotionAndStatusMessage(customUserDetails.getId(), updateMyEmotionAndStatusMessageRequest) + + fun getMemberEmotion(customUserDetails: CustomUserDetails): MyEmotionResponse { + val member = memberService.getById(customUserDetails.getId()) + return MyEmotionResponse.of(member) + } } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index e6dd0801..403de96f 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -5,4 +5,5 @@ object ApiPath { const val MY_INFO = "/v1/member/my-info" const val MY_STATUS_MESSAGE = "/v1/member/my-status-message" const val UPDATE_MY_EMOTION_AND_STATUS_MESSAGE = "/v1/member/my-emotion-and-status-message" + const val MY_EMOTION = "/v1/member/my-emotion" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 0801b24e..adcf9cab 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest +import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.facade.MemberInfoFacade import gomushin.backend.member.dto.response.MyInfoResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse @@ -56,4 +57,14 @@ class MemberInfoController( memberInfoFacade.updateMyEmotionAndStatusMessage(customUserDetails, updateMyEmotionAndStatusMessageRequest) return ApiResponse.success(true) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.MY_EMOTION) + @Operation(summary = "내 상태 이모지 조회", description = "getMyEmotion") + fun getMyEmotion( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ):ApiResponse { + val emotion = memberInfoFacade.getMemberEmotion(customUserDetails) + return ApiResponse.success(emotion) + } } From 04d3525258ed86a2c9ee933f6384d34ea56fe2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 25 Apr 2025 21:59:01 +0900 Subject: [PATCH 138/357] =?UTF-8?q?test=20:=20=EB=82=98=EC=9D=98=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=EC=A1=B0=ED=9A=8C=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/facade/MemberInfoFacadeTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 3cd03cc3..6bea9bce 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -81,4 +81,16 @@ class MemberInfoFacadeTest { //then verify(memberService).updateMyEmotionAndStatusMessage(1L, updateMyEmotionAndStatusMessageRequest) } + + @DisplayName("이모지 조회 테스트") + @Test + fun getMyEmotion() { + //given + `when`(memberService.getById(customUserDetails.getId())).thenReturn(member) + //when + val result = memberInfoFacade.getMemberEmotion(customUserDetails) + //then + verify(memberService).getById(1L) + assertEquals(member.emotion, result.emotion) + } } From cf28a37332f293c77d3b2763ed8699666291ba41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 25 Apr 2025 22:18:38 +0900 Subject: [PATCH 139/357] =?UTF-8?q?feat=20:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/entity/Member.kt | 4 ++++ .../member/domain/service/MemberService.kt | 7 +++++++ .../dto/request/UpdateMyNickNameRequest.kt | 8 ++++++++ .../backend/member/facade/MemberInfoFacade.kt | 4 ++++ .../backend/member/presentation/ApiPath.kt | 1 + .../member/presentation/MemberInfoController.kt | 16 ++++++++++++---- 6 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyNickNameRequest.kt diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index adf38bd1..edbcc4d8 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -82,4 +82,8 @@ class Member( fun updateStatusMessage(statusMessage: String) { this.statusMessage = statusMessage } + + fun updateNickname(nickname: String) { + this.nickname = nickname + } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 7ffbcc25..bb0d6c53 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest +import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -29,4 +30,10 @@ class MemberService( member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion) member.updateStatusMessage(updateMyEmotionAndStatusMessageRequest.statusMessage) } + + @Transactional + fun updateMyNickname(id: Long, updateMyNickNameRequest: UpdateMyNickNameRequest) { + val member = getById(id) + member.updateNickname(updateMyNickNameRequest.nickname) + } } diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyNickNameRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyNickNameRequest.kt new file mode 100644 index 00000000..1ee03919 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyNickNameRequest.kt @@ -0,0 +1,8 @@ +package gomushin.backend.member.dto.request + +import io.swagger.v3.oas.annotations.media.Schema + +data class UpdateMyNickNameRequest ( + @Schema(description = "닉네임", example = "김꽃신") + val nickname : String +) \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 1ce73e67..f35242e2 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -3,6 +3,7 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest +import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.dto.response.MyInfoResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse @@ -29,4 +30,7 @@ class MemberInfoFacade( val member = memberService.getById(customUserDetails.getId()) return MyEmotionResponse.of(member) } + + fun updateMyNickname(customUserDetails: CustomUserDetails, updateMyNickNameRequest: UpdateMyNickNameRequest) + = memberService.updateMyNickname(customUserDetails.getId(), updateMyNickNameRequest) } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index 403de96f..33dcc604 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -6,4 +6,5 @@ object ApiPath { const val MY_STATUS_MESSAGE = "/v1/member/my-status-message" const val UPDATE_MY_EMOTION_AND_STATUS_MESSAGE = "/v1/member/my-emotion-and-status-message" const val MY_EMOTION = "/v1/member/my-emotion" + const val UPDATE_MY_NICKNAME = "/v1/member/my-nickname" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index adcf9cab..97bd5937 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -2,19 +2,16 @@ package gomushin.backend.member.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest +import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.facade.MemberInfoFacade import gomushin.backend.member.dto.response.MyInfoResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag -import jakarta.validation.Valid import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.validation.BindingResult -import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -67,4 +64,15 @@ class MemberInfoController( val emotion = memberInfoFacade.getMemberEmotion(customUserDetails) return ApiResponse.success(emotion) } + + @ResponseStatus(HttpStatus.OK) + @PostMapping(ApiPath.UPDATE_MY_NICKNAME) + @Operation(summary = "내 닉네임 수정", description = "updateMyNickname") + fun updateMyNickname( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody updateMyNickNameRequest: UpdateMyNickNameRequest + ):ApiResponse { + memberInfoFacade.updateMyNickname(customUserDetails, updateMyNickNameRequest) + return ApiResponse.success(true) + } } From 109964956fd946dda8bc26c5d398e212ed903124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 25 Apr 2025 22:24:21 +0900 Subject: [PATCH 140/357] =?UTF-8?q?test=20:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/MemberServiceTest.kt | 26 +++++++++++++++++++ .../member/facade/MemberInfoFacadeTest.kt | 12 +++++++++ 2 files changed, 38 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt index cc2e1c79..bf32f3c5 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt @@ -5,6 +5,7 @@ import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest +import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -73,4 +74,29 @@ class MemberServiceTest { assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion) assertEquals(expectedMember.statusMessage, updateMyEmotionAndStatusMessageRequest.statusMessage) } + + @DisplayName("닉네임 수정 - 성공") + @Test + fun updateMyNickname() { + // given + val memberId = 1L + val expectedMember = Member( + id = 1L, + name = "테스트", + nickname = "테스트 닉네임", + email = "test@test.com", + birthDate = null, + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.GUEST, + emotion = 1, + statusMessage = "상태 변경전" + ) + val updateMyNickNameRequest = UpdateMyNickNameRequest("테스트 닉네임 수정") + //when + `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember)) + val result = memberService.updateMyNickname(memberId, updateMyNickNameRequest) + //then + assertEquals(expectedMember.nickname, updateMyNickNameRequest.nickname) + } } diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 6bea9bce..103e4ddc 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -6,6 +6,7 @@ import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest +import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -93,4 +94,15 @@ class MemberInfoFacadeTest { verify(memberService).getById(1L) assertEquals(member.emotion, result.emotion) } + + @DisplayName("닉네임 수정") + @Test + fun updateMyNickname() { + //given + val updateMyNickNameRequest = UpdateMyNickNameRequest("테스트 닉네임 수정완료") + //when + val result = memberInfoFacade.updateMyNickname(customUserDetails, updateMyNickNameRequest) + //then + verify(memberService).updateMyNickname(1L, updateMyNickNameRequest) + } } From e052b0dd779fd3536dc25de56880a00b5cfbd63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 25 Apr 2025 22:34:57 +0900 Subject: [PATCH 141/357] =?UTF-8?q?feat=20:=20=EC=83=9D=EB=85=84=EC=9B=94?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/member/domain/entity/Member.kt | 4 ++++ .../backend/member/domain/service/MemberService.kt | 7 +++++++ .../member/dto/request/UpdateMyBirthdayRequest.kt | 9 +++++++++ .../backend/member/facade/MemberInfoFacade.kt | 4 ++++ .../gomushin/backend/member/presentation/ApiPath.kt | 1 + .../member/presentation/MemberInfoController.kt | 12 ++++++++++++ 6 files changed, 37 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyBirthdayRequest.kt diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index edbcc4d8..c1a6ff53 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -86,4 +86,8 @@ class Member( fun updateNickname(nickname: String) { this.nickname = nickname } + + fun updateBirthday(birthDate : LocalDate) { + this.birthDate = birthDate + } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index bb0d6c53..33a53ee4 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -3,6 +3,7 @@ package gomushin.backend.member.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import org.springframework.data.repository.findByIdOrNull @@ -36,4 +37,10 @@ class MemberService( val member = getById(id) member.updateNickname(updateMyNickNameRequest.nickname) } + + @Transactional + fun updateMyBirthDate(id: Long, updateMyBirthdayRequest: UpdateMyBirthdayRequest) { + val member = getById(id) + member.updateBirthday(updateMyBirthdayRequest.birthDate) + } } diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyBirthdayRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyBirthdayRequest.kt new file mode 100644 index 00000000..3be0c1b7 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyBirthdayRequest.kt @@ -0,0 +1,9 @@ +package gomushin.backend.member.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +data class UpdateMyBirthdayRequest ( + @Schema(description = "생일", example = "2001-03-27") + val birthDate : LocalDate +) \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index f35242e2..35c75cd4 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -2,6 +2,7 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.service.MemberService +import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import gomushin.backend.member.dto.response.MyEmotionResponse @@ -33,4 +34,7 @@ class MemberInfoFacade( fun updateMyNickname(customUserDetails: CustomUserDetails, updateMyNickNameRequest: UpdateMyNickNameRequest) = memberService.updateMyNickname(customUserDetails.getId(), updateMyNickNameRequest) + + fun updateMyBirthday(customUserDetails: CustomUserDetails, updateMyBirthdayRequest: UpdateMyBirthdayRequest) + = memberService.updateMyBirthDate(customUserDetails.getId(), updateMyBirthdayRequest) } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index 33dcc604..f1685ac0 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -7,4 +7,5 @@ object ApiPath { const val UPDATE_MY_EMOTION_AND_STATUS_MESSAGE = "/v1/member/my-emotion-and-status-message" const val MY_EMOTION = "/v1/member/my-emotion" const val UPDATE_MY_NICKNAME = "/v1/member/my-nickname" + const val UPDATE_MY_BIRTHDAY = "/v1/member/my-birthday" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 97bd5937..ef42d8f2 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -2,6 +2,7 @@ package gomushin.backend.member.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import gomushin.backend.member.dto.response.MyEmotionResponse @@ -75,4 +76,15 @@ class MemberInfoController( memberInfoFacade.updateMyNickname(customUserDetails, updateMyNickNameRequest) return ApiResponse.success(true) } + + @ResponseStatus(HttpStatus.OK) + @PostMapping(ApiPath.UPDATE_MY_BIRTHDAY) + @Operation(summary = "내 생일 수정", description = "updateMyBirthDate") + fun updateMyBirthDate( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody updateMyBirthdayRequest : UpdateMyBirthdayRequest + ):ApiResponse { + memberInfoFacade.updateMyBirthday(customUserDetails, updateMyBirthdayRequest) + return ApiResponse.success(true) + } } From 42090e93c7d2ba14b28cb0b80059e42c212da4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 25 Apr 2025 22:40:41 +0900 Subject: [PATCH 142/357] =?UTF-8?q?test=20:=20=EC=83=9D=EB=85=84=EC=9B=94?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95=20API=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=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/facade/MemberInfoFacade.kt | 2 +- .../presentation/MemberInfoController.kt | 2 +- .../domain/service/MemberServiceTest.kt | 27 +++++++++++++++++++ .../member/facade/MemberInfoFacadeTest.kt | 13 +++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 35c75cd4..152d6c32 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -35,6 +35,6 @@ class MemberInfoFacade( fun updateMyNickname(customUserDetails: CustomUserDetails, updateMyNickNameRequest: UpdateMyNickNameRequest) = memberService.updateMyNickname(customUserDetails.getId(), updateMyNickNameRequest) - fun updateMyBirthday(customUserDetails: CustomUserDetails, updateMyBirthdayRequest: UpdateMyBirthdayRequest) + fun updateMyBirthDate(customUserDetails: CustomUserDetails, updateMyBirthdayRequest: UpdateMyBirthdayRequest) = memberService.updateMyBirthDate(customUserDetails.getId(), updateMyBirthdayRequest) } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index ef42d8f2..fbcaec55 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -84,7 +84,7 @@ class MemberInfoController( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @RequestBody updateMyBirthdayRequest : UpdateMyBirthdayRequest ):ApiResponse { - memberInfoFacade.updateMyBirthday(customUserDetails, updateMyBirthdayRequest) + memberInfoFacade.updateMyBirthDate(customUserDetails, updateMyBirthdayRequest) return ApiResponse.success(true) } } diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt index bf32f3c5..0fc6a0aa 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt @@ -4,6 +4,7 @@ import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role +import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import org.junit.jupiter.api.DisplayName @@ -13,6 +14,7 @@ import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.junit.jupiter.MockitoExtension +import java.time.LocalDate import java.util.* import kotlin.test.assertEquals @@ -99,4 +101,29 @@ class MemberServiceTest { //then assertEquals(expectedMember.nickname, updateMyNickNameRequest.nickname) } + + @DisplayName("생년월일 수정 - 성공") + @Test + fun updateMyBirthDate() { + // given + val memberId = 1L + val expectedMember = Member( + id = 1L, + name = "테스트", + nickname = "테스트 닉네임", + email = "test@test.com", + birthDate = LocalDate.of(2001, 3, 27), + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.GUEST, + emotion = 1, + statusMessage = "상태 변경전", + ) + val updateMyBirthdayRequest = UpdateMyBirthdayRequest(LocalDate.of(2001, 3, 30)) + //when + `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember)) + val result = memberService.updateMyBirthDate(memberId, updateMyBirthdayRequest) + //then + assertEquals(expectedMember.birthDate, updateMyBirthdayRequest.birthDate) + } } diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 103e4ddc..f682b6a2 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -5,6 +5,7 @@ import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role +import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import org.junit.jupiter.api.BeforeEach @@ -15,6 +16,7 @@ import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension +import java.time.LocalDate import kotlin.test.assertEquals @ExtendWith(MockitoExtension::class) @@ -105,4 +107,15 @@ class MemberInfoFacadeTest { //then verify(memberService).updateMyNickname(1L, updateMyNickNameRequest) } + + @DisplayName("생년월일 수정") + @Test + fun updateBirthDate() { + //given + val updateMyBirthdayRequest = UpdateMyBirthdayRequest(LocalDate.of(2001, 3, 30)) + //when + val result = memberInfoFacade.updateMyBirthDate(customUserDetails, updateMyBirthdayRequest) + //then + verify(memberService).updateMyBirthDate(1L, updateMyBirthdayRequest) + } } From e44412902de8978167edc3543c84053d275a6016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 12:55:37 +0900 Subject: [PATCH 143/357] =?UTF-8?q?feat=20:=20=EC=9E=85=EB=8C=80,=20?= =?UTF-8?q?=EC=A0=84=EC=97=AD=EC=9D=BC=20=EC=88=98=EC=A0=95=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AnniversaryRepository.kt | 14 +++++++- .../domain/service/CoupleInfoService.kt | 33 ++++++++++++++++++- .../dto/request/UpdateMilitaryDateRequest.kt | 11 +++++++ .../backend/couple/facade/CoupleFacade.kt | 4 +++ .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/CoupleInfoController.kt | 14 ++++++++ 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/request/UpdateMilitaryDateRequest.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index 1384e8f8..9725d2cc 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -2,5 +2,17 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Anniversary import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param -interface AnniversaryRepository : JpaRepository +interface AnniversaryRepository : JpaRepository { + @Modifying + @Query(""" + DELETE FROM Anniversary a + WHERE (a.title LIKE '%일' OR a.title LIKE '%주년') + AND a.anniversaryProperty = 0 + And a.coupleId = :coupleId +""") + fun deleteAnniversariesWithTitleEndingAndPropertyZero(@Param("coupleId")coupleId : Long) +} diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index 9ae6e463..e9861cec 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -1,7 +1,12 @@ package gomushin.backend.couple.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Anniversary +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.domain.repository.CoupleRepository +import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest +import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse import gomushin.backend.member.domain.entity.Member @@ -15,7 +20,9 @@ import java.time.temporal.ChronoUnit @Service class CoupleInfoService( private val coupleRepository: CoupleRepository, - private val memberRepository: MemberRepository + private val memberRepository: MemberRepository, + private val anniversaryRepository: AnniversaryRepository, + private val anniversaryCalculator: AnniversaryCalculator ) { @Transactional(readOnly = true) fun getGrade(id: Long): Int { @@ -93,4 +100,28 @@ class CoupleInfoService( return coupleMember } + @Transactional + fun updateMilitaryDate(id: Long, updateMilitaryDateRequest: UpdateMilitaryDateRequest) { + val couple = coupleRepository.findByMemberId(id) ?: throw BadRequestException("saranggun.couple.not-connected") + updateAnniversary(couple, couple.relationshipStartDate!!, updateMilitaryDateRequest.militaryStartDate, updateMilitaryDateRequest.militaryEndDate) + couple.updateAnniversary(couple.relationshipStartDate!!, + updateMilitaryDateRequest.militaryStartDate, + updateMilitaryDateRequest.militaryEndDate) + } + + private fun updateAnniversary(couple: Couple, + relationshipStartDate: LocalDate, + militaryStartDate: LocalDate, + militaryEndDate : LocalDate) { + anniversaryRepository.deleteAnniversariesWithTitleEndingAndPropertyZero(couple.id) + val anniversaries: MutableList = mutableListOf() + anniversaryCalculator.calculateInitAnniversaries( + couple.id, + relationshipStartDate, + militaryStartDate, + militaryEndDate, + anniversaries + ) + anniversaryRepository.saveAll(anniversaries) + } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateMilitaryDateRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateMilitaryDateRequest.kt new file mode 100644 index 00000000..1e6faf29 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateMilitaryDateRequest.kt @@ -0,0 +1,11 @@ +package gomushin.backend.couple.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +data class UpdateMilitaryDateRequest ( + @Schema(description = "입대일", example = "2023-05-24") + val militaryStartDate : LocalDate, + @Schema(description = "전역일", example = "2024-11-23") + val militaryEndDate : LocalDate +) \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 71c9ad25..4c9add68 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -6,6 +6,7 @@ import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.dto.request.CoupleConnectRequest +import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.response.CoupleGradeResponse import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse @@ -56,4 +57,7 @@ class CoupleFacade( val statusMessage = coupleInfoService.getStatusMessage(customUserDetails.getId()) return StatusMessageResponse.of(statusMessage) } + + fun updateMilitaryDate(customUserDetails: CustomUserDetails, updateMilitaryDateRequest: UpdateMilitaryDateRequest) + = coupleInfoService.updateMilitaryDate(customUserDetails.getId(), updateMilitaryDateRequest) } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index ea55f864..7765c07b 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -9,4 +9,5 @@ object ApiPath { const val COUPLE_DDAY_INFO = "/v1/couple/d-day" const val COUPLE_NICKNAME = "/v1/couple/nick-name" const val COUPLE_STATUS_MESSAGE = "/v1/couple/status-message" + const val COUPLE_UPDATE_MILITARY_DATE = "/v1/couple/military-date" } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index ff80525c..a6abf010 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -2,6 +2,7 @@ package gomushin.backend.couple.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.couple.dto.response.CoupleGradeResponse import gomushin.backend.couple.dto.response.DdayResponse @@ -12,6 +13,8 @@ import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @@ -65,4 +68,15 @@ class CoupleInfoController ( ):ApiResponse{ return ApiResponse.success(coupleFacade.statusMessage(customUserDetails)) } + + @ResponseStatus(HttpStatus.OK) + @PostMapping(ApiPath.COUPLE_UPDATE_MILITARY_DATE) + @Operation(summary = "입대, 전역일 수정 api", description = "입대일과 전역일을 수정함") + fun updateMilitaryDate( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody updateMilitaryDateRequest: UpdateMilitaryDateRequest + ) : ApiResponse { + coupleFacade.updateMilitaryDate(customUserDetails, updateMilitaryDateRequest) + return ApiResponse.success(true) + } } \ No newline at end of file From 13ba638885cae9782a5e9d66da2bba476c5a1124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 12:55:50 +0900 Subject: [PATCH 144/357] =?UTF-8?q?test=20:=20=EC=9E=85=EB=8C=80,=20?= =?UTF-8?q?=EC=A0=84=EC=97=AD=EC=9D=BC=20=EC=88=98=EC=A0=95=20API=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CoupleInfoServiceTest.kt | 58 ++++++++++++++++++- .../backend/member/facade/CoupleFacadeTest.kt | 12 ++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index 344b7321..410a38d1 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -1,7 +1,10 @@ package gomushin.backend.couple.domain.service +import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.domain.repository.CoupleRepository +import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Provider @@ -11,13 +14,12 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.InjectMocks import org.mockito.Mock -import org.mockito.Mockito import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension import java.time.LocalDate import java.util.* import kotlin.test.assertEquals - +import org.mockito.kotlin.any @ExtendWith(MockitoExtension::class) class CoupleInfoServiceTest { @@ -25,6 +27,11 @@ class CoupleInfoServiceTest { private lateinit var coupleRepository: CoupleRepository @Mock private lateinit var memberRepository: MemberRepository + @Mock + private lateinit var anniversaryRepository: AnniversaryRepository + @Mock + private lateinit var anniversaryCalculator: AnniversaryCalculator + @InjectMocks private lateinit var coupleInfoService: CoupleInfoService @@ -289,4 +296,51 @@ class CoupleInfoServiceTest { //then assertEquals("기분이 좋아용", statusMessage) } + + @DisplayName("updateMilitaryDate - 성공") + @Test + fun updateMilitaryDate() { + //given + val coupleId = 1L + val userId = 1L + val coupleUserId = 2L + val couple = Couple( + id = coupleId, + invitorId = coupleUserId, + inviteeId = userId, + relationshipStartDate = LocalDate.of(2020,8,1), + militaryStartDate = LocalDate.of(2021, 5, 24), + militaryEndDate = LocalDate.of(2022,11,23) + ) + `when`(coupleRepository.findByMemberId(userId)).thenReturn(couple) + doNothing().`when`(anniversaryRepository).deleteAnniversariesWithTitleEndingAndPropertyZero(coupleId) + `when`(anniversaryCalculator.calculateInitAnniversaries( + any(), + any(), + any(), + any(), + any>() + )).thenReturn(emptyList()) + `when`(anniversaryRepository.saveAll(anyList())).thenReturn(emptyList()) + val updateMilitaryDateRequest = UpdateMilitaryDateRequest( + LocalDate.of(2022, 5, 24), + LocalDate.of(2023,11,23) + ) + //when + val result = coupleInfoService.updateMilitaryDate(userId, updateMilitaryDateRequest) + + //then + verify(coupleRepository).findByMemberId(userId) + verify(anniversaryRepository).deleteAnniversariesWithTitleEndingAndPropertyZero(coupleId) + verify(anniversaryCalculator).calculateInitAnniversaries( + any(), + any(), + any(), + any(), + any>() + ) + verify(anniversaryRepository).saveAll(anyList()) + assertEquals(couple.militaryStartDate, updateMilitaryDateRequest.militaryStartDate) + assertEquals(couple.militaryEndDate, updateMilitaryDateRequest.militaryEndDate) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index b083dc5c..40dd5009 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.domain.service.CoupleInfoService +import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse import gomushin.backend.couple.facade.CoupleFacade @@ -138,4 +139,15 @@ class CoupleFacadeTest { assertEquals("기분이 좋아용", result.statusMessage) } + @DisplayName("입대일, 전역일 수정 - 정상응답") + @Test + fun updateMilitaryDate() { + val updateMilitaryDateRequest = UpdateMilitaryDateRequest( + LocalDate.of(2022, 5, 24), + LocalDate.of(2023,11,23) + ) + val result = coupleFacade.updateMilitaryDate(customUserDetails, updateMilitaryDateRequest) + verify(coupleInfoService).updateMilitaryDate(customUserDetails.getId(), updateMilitaryDateRequest) + } + } From fdd25378768da36c68e8ba6394a9b4ba421fa273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 13:08:38 +0900 Subject: [PATCH 145/357] =?UTF-8?q?feat=20:=20=EB=A7=8C=EB=82=9C=EB=82=A0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleInfoService.kt | 9 +++++++++ .../request/UpdateRelationshipStartDateRequest.kt | 9 +++++++++ .../gomushin/backend/couple/facade/CoupleFacade.kt | 4 ++++ .../gomushin/backend/couple/presentation/ApiPath.kt | 1 + .../couple/presentation/CoupleInfoController.kt | 12 ++++++++++++ 5 files changed, 35 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/request/UpdateRelationshipStartDateRequest.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index e9861cec..40383793 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -7,6 +7,7 @@ import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.domain.repository.CoupleRepository import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest +import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse import gomushin.backend.member.domain.entity.Member @@ -108,6 +109,14 @@ class CoupleInfoService( updateMilitaryDateRequest.militaryStartDate, updateMilitaryDateRequest.militaryEndDate) } + @Transactional + fun updateRelationshipStartDate(id: Long, updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest) { + val couple = coupleRepository.findByMemberId(id) ?: throw BadRequestException("saranggun.couple.not-connected") + updateAnniversary(couple, updateRelationshipStartDateRequest.relationshipStartDate, couple.militaryStartDate!!, couple.militaryEndDate!!) + couple.updateAnniversary(updateRelationshipStartDateRequest.relationshipStartDate, + couple.militaryStartDate, + couple.militaryEndDate) + } private fun updateAnniversary(couple: Couple, relationshipStartDate: LocalDate, diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateRelationshipStartDateRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateRelationshipStartDateRequest.kt new file mode 100644 index 00000000..4cfb48cd --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateRelationshipStartDateRequest.kt @@ -0,0 +1,9 @@ +package gomushin.backend.couple.dto.request + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +data class UpdateRelationshipStartDateRequest ( + @Schema(description = "사귄날", example = "2023-05-24") + val relationshipStartDate : LocalDate, +) \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 4c9add68..e1e8d5c3 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -7,6 +7,7 @@ import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.dto.request.CoupleConnectRequest import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest +import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.couple.dto.response.CoupleGradeResponse import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse @@ -60,4 +61,7 @@ class CoupleFacade( fun updateMilitaryDate(customUserDetails: CustomUserDetails, updateMilitaryDateRequest: UpdateMilitaryDateRequest) = coupleInfoService.updateMilitaryDate(customUserDetails.getId(), updateMilitaryDateRequest) + + fun updateRelationshipStartDate(customUserDetails: CustomUserDetails, updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest) + = coupleInfoService.updateRelationshipStartDate(customUserDetails.getId(), updateRelationshipStartDateRequest) } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index 7765c07b..5e027860 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -10,4 +10,5 @@ object ApiPath { const val COUPLE_NICKNAME = "/v1/couple/nick-name" const val COUPLE_STATUS_MESSAGE = "/v1/couple/status-message" const val COUPLE_UPDATE_MILITARY_DATE = "/v1/couple/military-date" + const val COUPLE_UPDATE_RELATIONSHIP_DATE = "/v1/couple/relationship-start-date" } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index a6abf010..578e9bb1 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -3,6 +3,7 @@ package gomushin.backend.couple.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest +import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.couple.dto.response.CoupleGradeResponse import gomushin.backend.couple.dto.response.DdayResponse @@ -79,4 +80,15 @@ class CoupleInfoController ( coupleFacade.updateMilitaryDate(customUserDetails, updateMilitaryDateRequest) return ApiResponse.success(true) } + + @ResponseStatus(HttpStatus.OK) + @PostMapping(ApiPath.COUPLE_UPDATE_RELATIONSHIP_DATE) + @Operation(summary = "만난날 수정 api", description = "만난날을 수정함") + fun updateRelationshipStartDate( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest + ) : ApiResponse { + coupleFacade.updateRelationshipStartDate(customUserDetails, updateRelationshipStartDateRequest) + return ApiResponse.success(true) + } } \ No newline at end of file From 304a56f1358587185189547a136fe8d88238b9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 13:22:31 +0900 Subject: [PATCH 146/357] =?UTF-8?q?test=20:=20=EB=A7=8C=EB=82=9C=EB=82=A0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CoupleInfoServiceTest.kt | 47 +++++++++++++++++++ .../backend/member/facade/CoupleFacadeTest.kt | 11 +++++ 2 files changed, 58 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index 410a38d1..4d1813e5 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.domain.repository.CoupleRepository import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest +import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Provider @@ -343,4 +344,50 @@ class CoupleInfoServiceTest { assertEquals(couple.militaryStartDate, updateMilitaryDateRequest.militaryStartDate) assertEquals(couple.militaryEndDate, updateMilitaryDateRequest.militaryEndDate) } + + @DisplayName("updateRelationshipStartDate - 성공") + @Test + fun updateRelationshipStartDate() { + //given + val coupleId = 1L + val userId = 1L + val coupleUserId = 2L + val couple = Couple( + id = coupleId, + invitorId = coupleUserId, + inviteeId = userId, + relationshipStartDate = LocalDate.of(2020,8,1), + militaryStartDate = LocalDate.of(2021, 5, 24), + militaryEndDate = LocalDate.of(2022,11,23) + ) + `when`(coupleRepository.findByMemberId(userId)).thenReturn(couple) + doNothing().`when`(anniversaryRepository).deleteAnniversariesWithTitleEndingAndPropertyZero(coupleId) + `when`(anniversaryCalculator.calculateInitAnniversaries( + any(), + any(), + any(), + any(), + any>() + )).thenReturn(emptyList()) + `when`(anniversaryRepository.saveAll(anyList())).thenReturn(emptyList()) + val updateRelationshipStartDateRequest = UpdateRelationshipStartDateRequest( + LocalDate.of(2020, 7, 24), + ) + + //when + val result = coupleInfoService.updateRelationshipStartDate(userId, updateRelationshipStartDateRequest) + + //then + verify(coupleRepository).findByMemberId(userId) + verify(anniversaryRepository).deleteAnniversariesWithTitleEndingAndPropertyZero(coupleId) + verify(anniversaryCalculator).calculateInitAnniversaries( + any(), + any(), + any(), + any(), + any>() + ) + verify(anniversaryRepository).saveAll(anyList()) + assertEquals(couple.relationshipStartDate, updateRelationshipStartDateRequest.relationshipStartDate) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 40dd5009..ba3ad14c 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -6,6 +6,7 @@ import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest +import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse import gomushin.backend.couple.facade.CoupleFacade @@ -150,4 +151,14 @@ class CoupleFacadeTest { verify(coupleInfoService).updateMilitaryDate(customUserDetails.getId(), updateMilitaryDateRequest) } + @DisplayName("만난날 수정 - 정상응답") + @Test + fun updateRelationshipStartDate() { + val updateRelationshipStartDateRequest = UpdateRelationshipStartDateRequest( + LocalDate.of(2022, 5, 24), + ) + val result = coupleFacade.updateRelationshipStartDate(customUserDetails, updateRelationshipStartDateRequest) + verify(coupleInfoService).updateRelationshipStartDate(customUserDetails.getId(), updateRelationshipStartDateRequest) + } + } From bb59fd6f3eef0a5bbd5875dba934ed1f3ee2ac01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 15:52:32 +0900 Subject: [PATCH 147/357] =?UTF-8?q?chore=20:=20=EC=9E=85=EB=8C=80,=20?= =?UTF-8?q?=EC=A0=84=EC=97=AD=EC=9D=BC=20=EC=88=98=EC=A0=95api,=20?= =?UTF-8?q?=EB=A7=8C=EB=82=9C=EB=82=A0=20=EC=88=98=EC=A0=95=20api=20contro?= =?UTF-8?q?ller=EB=94=B0=EB=A1=9C=20=ED=8C=8C=EC=84=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC(=EA=B8=B0=EC=A1=B4=EC=9D=98=20CoupleInfoController?= =?UTF-8?q?=EB=8A=94=20=EC=BB=A4=ED=94=8C=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=9D=98=20=EC=84=B1=EA=B2=A9=EC=9D=B4=20=EA=B0=95?= =?UTF-8?q?=ED=95=A8)=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/CoupleInfoController.kt | 22 ---------- .../presentation/CoupleUpdateController.kt | 43 +++++++++++++++++++ 2 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/presentation/CoupleUpdateController.kt diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index 578e9bb1..c5b4384d 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -69,26 +69,4 @@ class CoupleInfoController ( ):ApiResponse{ return ApiResponse.success(coupleFacade.statusMessage(customUserDetails)) } - - @ResponseStatus(HttpStatus.OK) - @PostMapping(ApiPath.COUPLE_UPDATE_MILITARY_DATE) - @Operation(summary = "입대, 전역일 수정 api", description = "입대일과 전역일을 수정함") - fun updateMilitaryDate( - @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @RequestBody updateMilitaryDateRequest: UpdateMilitaryDateRequest - ) : ApiResponse { - coupleFacade.updateMilitaryDate(customUserDetails, updateMilitaryDateRequest) - return ApiResponse.success(true) - } - - @ResponseStatus(HttpStatus.OK) - @PostMapping(ApiPath.COUPLE_UPDATE_RELATIONSHIP_DATE) - @Operation(summary = "만난날 수정 api", description = "만난날을 수정함") - fun updateRelationshipStartDate( - @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @RequestBody updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest - ) : ApiResponse { - coupleFacade.updateRelationshipStartDate(customUserDetails, updateRelationshipStartDateRequest) - return ApiResponse.success(true) - } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleUpdateController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleUpdateController.kt new file mode 100644 index 00000000..cce4394d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleUpdateController.kt @@ -0,0 +1,43 @@ +package gomushin.backend.couple.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest +import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest +import gomushin.backend.couple.facade.CoupleFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "커플 정보 수정", description = "CoupleUpdateController") +class CoupleUpdateController( + private val coupleFacade: CoupleFacade +) { + @ResponseStatus(HttpStatus.OK) + @PostMapping(ApiPath.COUPLE_UPDATE_MILITARY_DATE) + @Operation(summary = "입대, 전역일 수정 api", description = "입대일과 전역일을 수정함") + fun updateMilitaryDate( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody updateMilitaryDateRequest: UpdateMilitaryDateRequest + ) : ApiResponse { + coupleFacade.updateMilitaryDate(customUserDetails, updateMilitaryDateRequest) + return ApiResponse.success(true) + } + + @ResponseStatus(HttpStatus.OK) + @PostMapping(ApiPath.COUPLE_UPDATE_RELATIONSHIP_DATE) + @Operation(summary = "만난날 수정 api", description = "만난날을 수정함") + fun updateRelationshipStartDate( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest + ) : ApiResponse { + coupleFacade.updateRelationshipStartDate(customUserDetails, updateRelationshipStartDateRequest) + return ApiResponse.success(true) + } +} \ No newline at end of file From 258c5c54cdc2fb31f8e937e01d76c3b6dd9efb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 16:48:31 +0900 Subject: [PATCH 148/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(description=EC=97=90=EB=8A=94=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EA=B0=80=20=EB=B0=9C=EC=83=9D=ED=96=88=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20=EC=96=B4=EB=96=A4=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EC=99=80=20=EC=97=B0=EA=B4=80=EB=90=98=EC=96=B4=EC=9E=88?= =?UTF-8?q?=EB=8A=94=EC=A7=80=20=EB=B0=94=EB=A1=9C=20=ED=8C=8C=EC=95=85=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=AA=85=EC=9D=B4=20=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EC=98=A4=EA=B2=8C=20=EC=88=98=EC=A0=95)=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/presentation/CoupleInfoController.kt | 10 +++++----- .../couple/presentation/CoupleUpdateController.kt | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index c5b4384d..a1d2a16e 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -26,7 +26,7 @@ class CoupleInfoController ( ) { @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_PROFILE) - @Operation(summary = "프로필 조회", description = "입대일 날짜 기준으로 grade측정") + @Operation(summary = "grade 조회 api", description = "getGrade") fun getGrade( @AuthenticationPrincipal customUserDetails: CustomUserDetails ): ApiResponse { @@ -36,7 +36,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_CHECK_CONNECT) - @Operation(summary = "커플 연동 여부", description = "커플 연동 여부 true, false로 불러오기") + @Operation(summary = "커플 연동 여부 체크 api", description = "coupleConnectCheck") fun coupleConnectCheck( @AuthenticationPrincipal customUserDetails: CustomUserDetails ): ApiResponse { @@ -45,7 +45,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_DDAY_INFO) - @Operation(summary = "디데이 정보", description = "사귄지, 입대한지 얼마되었는지 그리고 전역까지 얼마나 남았는지") + @Operation(summary = "디데이 정보 조회 api", description = "getDday") fun getDday( @AuthenticationPrincipal customUserDetails: CustomUserDetails ):ApiResponse { @@ -54,7 +54,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_NICKNAME) - @Operation(summary = "닉네임 조회", description = "userNickname = 내 닉네임, coupleNickName = 내 여(남)친 닉네임") + @Operation(summary = "닉네임 조회 api", description = "getNickName") fun getNickName( @AuthenticationPrincipal customUserDetails: CustomUserDetails ):ApiResponse{ @@ -63,7 +63,7 @@ class CoupleInfoController ( @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.COUPLE_STATUS_MESSAGE) - @Operation(summary = "상태 메시지 조회", description = "상태 메시지 조회") + @Operation(summary = "상태 메시지 조회 api", description = "getStatusMessage") fun getStatusMessage( @AuthenticationPrincipal customUserDetails: CustomUserDetails ):ApiResponse{ diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleUpdateController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleUpdateController.kt index cce4394d..0628d5af 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleUpdateController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleUpdateController.kt @@ -21,7 +21,7 @@ class CoupleUpdateController( ) { @ResponseStatus(HttpStatus.OK) @PostMapping(ApiPath.COUPLE_UPDATE_MILITARY_DATE) - @Operation(summary = "입대, 전역일 수정 api", description = "입대일과 전역일을 수정함") + @Operation(summary = "입대, 전역일 수정 api", description = "updateMilitaryDate") fun updateMilitaryDate( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @RequestBody updateMilitaryDateRequest: UpdateMilitaryDateRequest @@ -32,7 +32,7 @@ class CoupleUpdateController( @ResponseStatus(HttpStatus.OK) @PostMapping(ApiPath.COUPLE_UPDATE_RELATIONSHIP_DATE) - @Operation(summary = "만난날 수정 api", description = "만난날을 수정함") + @Operation(summary = "만난날 수정 api", description = "updateRelationshipStartDate") fun updateRelationshipStartDate( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @RequestBody updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest From bf866bf3ad733f1172a62e27fcd2780ab3d41163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 17:19:27 +0900 Subject: [PATCH 149/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20(couple=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=EC=9D=80=20facade=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95(service?= =?UTF-8?q?=EC=97=90=EB=8A=94=20=EB=8B=A4=EB=A5=B8=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=95=88=20=EC=84=9E?= =?UTF-8?q?=EC=9D=B4=EA=B2=8C=20=ED=95=98=EA=B8=B0=EC=9C=84=ED=95=A8))=20#?= =?UTF-8?q?36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleInfoService.kt | 6 ++---- .../gomushin/backend/couple/facade/CoupleFacade.kt | 14 ++++++++++---- .../couple/domain/service/CoupleInfoServiceTest.kt | 8 ++------ .../backend/member/facade/CoupleFacadeTest.kt | 8 ++++++-- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index 40383793..a5cd0df5 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -102,16 +102,14 @@ class CoupleInfoService( } @Transactional - fun updateMilitaryDate(id: Long, updateMilitaryDateRequest: UpdateMilitaryDateRequest) { - val couple = coupleRepository.findByMemberId(id) ?: throw BadRequestException("saranggun.couple.not-connected") + fun updateMilitaryDate(couple: Couple, updateMilitaryDateRequest: UpdateMilitaryDateRequest) { updateAnniversary(couple, couple.relationshipStartDate!!, updateMilitaryDateRequest.militaryStartDate, updateMilitaryDateRequest.militaryEndDate) couple.updateAnniversary(couple.relationshipStartDate!!, updateMilitaryDateRequest.militaryStartDate, updateMilitaryDateRequest.militaryEndDate) } @Transactional - fun updateRelationshipStartDate(id: Long, updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest) { - val couple = coupleRepository.findByMemberId(id) ?: throw BadRequestException("saranggun.couple.not-connected") + fun updateRelationshipStartDate(couple: Couple, updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest) { updateAnniversary(couple, updateRelationshipStartDateRequest.relationshipStartDate, couple.militaryStartDate!!, couple.militaryEndDate!!) couple.updateAnniversary(updateRelationshipStartDateRequest.relationshipStartDate, couple.militaryStartDate, diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index e1e8d5c3..d9d20f17 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.domain.service.CoupleInfoService +import gomushin.backend.couple.domain.service.CoupleService import gomushin.backend.couple.dto.request.CoupleConnectRequest import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest @@ -19,6 +20,7 @@ class CoupleFacade( private val coupleConnectService: CoupleConnectService, private val anniversaryService: AnniversaryService, private val coupleInfoService: CoupleInfoService, + private val coupleService: CoupleService ) { fun requestCoupleCodeGeneration(customUserDetails: CustomUserDetails) = @@ -59,9 +61,13 @@ class CoupleFacade( return StatusMessageResponse.of(statusMessage) } - fun updateMilitaryDate(customUserDetails: CustomUserDetails, updateMilitaryDateRequest: UpdateMilitaryDateRequest) - = coupleInfoService.updateMilitaryDate(customUserDetails.getId(), updateMilitaryDateRequest) + fun updateMilitaryDate(customUserDetails: CustomUserDetails, updateMilitaryDateRequest: UpdateMilitaryDateRequest) { + val couple = coupleService.getByMemberId(customUserDetails.getId()) + coupleInfoService.updateMilitaryDate(couple, updateMilitaryDateRequest) + } - fun updateRelationshipStartDate(customUserDetails: CustomUserDetails, updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest) - = coupleInfoService.updateRelationshipStartDate(customUserDetails.getId(), updateRelationshipStartDateRequest) + fun updateRelationshipStartDate(customUserDetails: CustomUserDetails, updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest) { + val couple = coupleService.getByMemberId(customUserDetails.getId()) + coupleInfoService.updateRelationshipStartDate(couple, updateRelationshipStartDateRequest) + } } diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index 4d1813e5..a9be8ac4 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -313,7 +313,6 @@ class CoupleInfoServiceTest { militaryStartDate = LocalDate.of(2021, 5, 24), militaryEndDate = LocalDate.of(2022,11,23) ) - `when`(coupleRepository.findByMemberId(userId)).thenReturn(couple) doNothing().`when`(anniversaryRepository).deleteAnniversariesWithTitleEndingAndPropertyZero(coupleId) `when`(anniversaryCalculator.calculateInitAnniversaries( any(), @@ -328,10 +327,9 @@ class CoupleInfoServiceTest { LocalDate.of(2023,11,23) ) //when - val result = coupleInfoService.updateMilitaryDate(userId, updateMilitaryDateRequest) + val result = coupleInfoService.updateMilitaryDate(couple, updateMilitaryDateRequest) //then - verify(coupleRepository).findByMemberId(userId) verify(anniversaryRepository).deleteAnniversariesWithTitleEndingAndPropertyZero(coupleId) verify(anniversaryCalculator).calculateInitAnniversaries( any(), @@ -360,7 +358,6 @@ class CoupleInfoServiceTest { militaryStartDate = LocalDate.of(2021, 5, 24), militaryEndDate = LocalDate.of(2022,11,23) ) - `when`(coupleRepository.findByMemberId(userId)).thenReturn(couple) doNothing().`when`(anniversaryRepository).deleteAnniversariesWithTitleEndingAndPropertyZero(coupleId) `when`(anniversaryCalculator.calculateInitAnniversaries( any(), @@ -375,10 +372,9 @@ class CoupleInfoServiceTest { ) //when - val result = coupleInfoService.updateRelationshipStartDate(userId, updateRelationshipStartDateRequest) + val result = coupleInfoService.updateRelationshipStartDate(couple, updateRelationshipStartDateRequest) //then - verify(coupleRepository).findByMemberId(userId) verify(anniversaryRepository).deleteAnniversariesWithTitleEndingAndPropertyZero(coupleId) verify(anniversaryCalculator).calculateInitAnniversaries( any(), diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index ba3ad14c..18f9e89b 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.domain.service.CoupleInfoService +import gomushin.backend.couple.domain.service.CoupleService import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.couple.dto.response.DdayResponse @@ -37,6 +38,9 @@ class CoupleFacadeTest { @Mock private lateinit var anniversaryService: AnniversaryService + @Mock + private lateinit var coupleService: CoupleService + @InjectMocks private lateinit var coupleFacade: CoupleFacade @@ -148,7 +152,7 @@ class CoupleFacadeTest { LocalDate.of(2023,11,23) ) val result = coupleFacade.updateMilitaryDate(customUserDetails, updateMilitaryDateRequest) - verify(coupleInfoService).updateMilitaryDate(customUserDetails.getId(), updateMilitaryDateRequest) + verify(coupleInfoService).updateMilitaryDate(customUserDetails.getCouple(), updateMilitaryDateRequest) } @DisplayName("만난날 수정 - 정상응답") @@ -158,7 +162,7 @@ class CoupleFacadeTest { LocalDate.of(2022, 5, 24), ) val result = coupleFacade.updateRelationshipStartDate(customUserDetails, updateRelationshipStartDateRequest) - verify(coupleInfoService).updateRelationshipStartDate(customUserDetails.getId(), updateRelationshipStartDateRequest) + verify(coupleInfoService).updateRelationshipStartDate(customUserDetails.getCouple(), updateRelationshipStartDateRequest) } } From 94cc7b3e8c3c2db91359d75dfcd391c69d1a3948 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 26 Apr 2025 16:33:14 +0900 Subject: [PATCH 150/357] =?UTF-8?q?fix:=20=EC=BF=A0=ED=82=A4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- .../core/oauth/handler/CustomSuccessHandler.kt | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b1aa8f75..22d38571 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Sarang Backend CI/CD on: push: - branches: [main] + branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index a2ec6773..8f3c0ee9 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -11,6 +11,7 @@ import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.beans.factory.annotation.Value +import org.springframework.http.ResponseCookie import org.springframework.security.core.Authentication import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler import org.springframework.stereotype.Component @@ -41,12 +42,20 @@ class CustomSuccessHandler( accessToken = jwtTokenProvider.provideAccessToken(principal.getUserId(), principal.getRole()) } - response!!.addHeader("Set-Cookie", createSetCookieHeader("access_token", accessToken)) + val cookie = createCookie("access_token", accessToken) + response!!.addHeader("Set-Cookie", cookie.toString()) response.sendRedirect(redirectUrl) } - private fun createSetCookieHeader(key: String, value: String): String { - return "$key=$value; Path=/; Max-Age=1800; HttpOnly; Secure; SameSite=None" + private fun createCookie(key: String, value: String): ResponseCookie { + return ResponseCookie.from(key, value) + .path("/") + .httpOnly(true) + .secure(true) + .sameSite("None") + .domain(".sarang-backend.o-r.kr") + .maxAge(432000) + .build() } private fun getMemberByEmail(email: String): Member? { From b384dfa64075c5dd93258ec19cf512bb0e7d23ec Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 14:33:30 +0900 Subject: [PATCH 151/357] =?UTF-8?q?fix:=20cors=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/CustomCorsConfiguration.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index 6c43443d..b6abb6fe 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -18,6 +18,7 @@ class CustomCorsConfiguration { "https://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app", + "https://vite.sarang-backend.o-r.kr:5173" ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") From 05ef33bb3b030ac16abb3eb801956c4bdf7ed44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 18:44:37 +0900 Subject: [PATCH 152/357] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20API=20=EA=B5=AC=ED=98=84=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/UpdateMyNotificationRequest.kt | 10 ++++++++++ .../backend/member/facade/MemberInfoFacade.kt | 12 ++++++++++++ .../gomushin/backend/member/presentation/ApiPath.kt | 1 + .../member/presentation/MemberInfoController.kt | 12 ++++++++++++ 4 files changed, 35 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyNotificationRequest.kt diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyNotificationRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyNotificationRequest.kt new file mode 100644 index 00000000..ec236f06 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyNotificationRequest.kt @@ -0,0 +1,10 @@ +package gomushin.backend.member.dto.request + +import io.swagger.v3.oas.annotations.media.Schema + +data class UpdateMyNotificationRequest ( + @Schema(description = "디데이 알림 설정", example = "true") + val dday : Boolean, + @Schema(description = "연인상태 알림 설정", example = "false") + val partnerStatus : Boolean +) \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 152d6c32..1029e5a8 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -2,17 +2,21 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.service.MemberService +import gomushin.backend.member.domain.service.NotificationService import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest +import gomushin.backend.member.dto.request.UpdateMyNotificationRequest import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.dto.response.MyInfoResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional @Component class MemberInfoFacade( private val memberService: MemberService, + private val notificationService: NotificationService ) { fun getMemberInfo(customUserDetails: CustomUserDetails): MyInfoResponse { val member = memberService.getById(customUserDetails.getId()) @@ -37,4 +41,12 @@ class MemberInfoFacade( fun updateMyBirthDate(customUserDetails: CustomUserDetails, updateMyBirthdayRequest: UpdateMyBirthdayRequest) = memberService.updateMyBirthDate(customUserDetails.getId(), updateMyBirthdayRequest) + + @Transactional + fun updateMyNotification(customUserDetails: CustomUserDetails, updateMyNotificationRequest: UpdateMyNotificationRequest) { + notificationService.getByMemberId(customUserDetails.getId()).apply { + if (updateMyNotificationRequest.dday) updateDday() + if (updateMyNotificationRequest.partnerStatus) updatePartnerStatus() + } + } } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index f1685ac0..ba554580 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -8,4 +8,5 @@ object ApiPath { const val MY_EMOTION = "/v1/member/my-emotion" const val UPDATE_MY_NICKNAME = "/v1/member/my-nickname" const val UPDATE_MY_BIRTHDAY = "/v1/member/my-birthday" + const val UPDATE_MY_NOTIFICATION_POLICY = "/v1/member/my-notification" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index fbcaec55..11d29b74 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -5,6 +5,7 @@ import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest +import gomushin.backend.member.dto.request.UpdateMyNotificationRequest import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.facade.MemberInfoFacade import gomushin.backend.member.dto.response.MyInfoResponse @@ -87,4 +88,15 @@ class MemberInfoController( memberInfoFacade.updateMyBirthDate(customUserDetails, updateMyBirthdayRequest) return ApiResponse.success(true) } + + @ResponseStatus(HttpStatus.OK) + @PostMapping(ApiPath.UPDATE_MY_NOTIFICATION_POLICY) + @Operation(summary = "내 알림 정책 수정", description = "updateMyNotification") + fun updateMyNotification( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody updateMyNotificationRequest: UpdateMyNotificationRequest + ):ApiResponse { + memberInfoFacade.updateMyNotification(customUserDetails, updateMyNotificationRequest) + return ApiResponse.success(true) + } } From 28af0bb3efafd5c3cb80841f928508584f969ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 26 Apr 2025 18:52:09 +0900 Subject: [PATCH 153/357] =?UTF-8?q?test=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20API=20=EA=B5=AC=ED=98=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/facade/MemberInfoFacadeTest.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index f682b6a2..57547f52 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -2,12 +2,15 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.entity.Notification import gomushin.backend.member.domain.service.MemberService +import gomushin.backend.member.domain.service.NotificationService import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest +import gomushin.backend.member.dto.request.UpdateMyNotificationRequest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -23,12 +26,15 @@ import kotlin.test.assertEquals class MemberInfoFacadeTest { @Mock private lateinit var memberService: MemberService + @Mock + private lateinit var notificationService: NotificationService @InjectMocks private lateinit var memberInfoFacade: MemberInfoFacade private lateinit var customUserDetails: CustomUserDetails private lateinit var member: Member + private lateinit var notification: Notification @BeforeEach fun setUp() { @@ -44,6 +50,13 @@ class MemberInfoFacadeTest { statusMessage = "상태 메시지" ) + notification = Notification( + id = 1L, + memberId = 1L, + dday = false, + partnerStatus = false + ) + customUserDetails = mock(CustomUserDetails::class.java) `when`(customUserDetails.getId()).thenReturn(1L) @@ -118,4 +131,19 @@ class MemberInfoFacadeTest { //then verify(memberService).updateMyBirthDate(1L, updateMyBirthdayRequest) } + + @DisplayName("알림설정 수정") + @Test + fun updateNotification() { + //given + val updateMyNotificationRequest = UpdateMyNotificationRequest(true, true) + `when`(notificationService.getByMemberId(customUserDetails.getId())).thenReturn(notification) + val previousDay = notification.dday + val previousSuperstate = notification.partnerStatus + //when + val result = memberInfoFacade.updateMyNotification(customUserDetails, updateMyNotificationRequest) + //then + assertEquals(notification.dday, !previousDay) + assertEquals(notification.partnerStatus, !previousSuperstate) + } } From d90036121905ef2289b3514a5917eeec84ecb2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 27 Apr 2025 02:04:33 +0900 Subject: [PATCH 154/357] =?UTF-8?q?feat=20:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=ED=95=B4=EC=A0=9C=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AnniversaryRepository.kt | 4 ++ .../domain/repository/CoupleRepository.kt | 5 +++ .../domain/service/AnniversaryService.kt | 5 +++ .../couple/domain/service/CoupleService.kt | 5 +++ .../repository/NotificationRepository.kt | 2 + .../member/domain/service/LeaveService.kt | 42 +++++++++++++++++++ .../member/domain/service/MemberService.kt | 5 +++ .../domain/service/NotificationService.kt | 5 +++ .../backend/member/facade/LeaveFacade.kt | 14 +++++++ .../backend/member/presentation/ApiPath.kt | 1 + .../presentation/MemberInfoController.kt | 13 ++++++ .../domain/repository/CommentRepository.kt | 9 +++- .../domain/repository/LetterRepository.kt | 12 +++++- .../domain/repository/PictureRepository.kt | 10 +++++ .../domain/repository/ScheduleRepository.kt | 9 +++- .../schedule/domain/service/CommentService.kt | 5 +++ .../schedule/domain/service/LetterService.kt | 9 ++++ .../schedule/domain/service/PictureService.kt | 10 +++++ .../domain/service/ScheduleService.kt | 5 +++ 19 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/member/domain/service/LeaveService.kt create mode 100644 src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index 9725d2cc..296d100f 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -15,4 +15,8 @@ interface AnniversaryRepository : JpaRepository { And a.coupleId = :coupleId """) fun deleteAnniversariesWithTitleEndingAndPropertyZero(@Param("coupleId")coupleId : Long) + + @Modifying + @Query("DELETE FROM Anniversary a WHERE a.coupleId = :coupleId") + fun deleteAllByCoupleId(@Param("coupleId") coupleId: Long) } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt index e508a722..33cc7add 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt @@ -2,6 +2,7 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Couple import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param @@ -11,4 +12,8 @@ interface CoupleRepository : JpaRepository { @Query("SELECT c FROM Couple c WHERE c.invitorId = :memberId OR c.inviteeId = :memberId") fun findByMemberId(@Param("memberId") memberId: Long): Couple? + + @Modifying + @Query("DELETE FROM Couple c WHERE c.invitorId = :memberId OR c.inviteeId = :memberId") + fun deleteByMemberId(@Param("memberId") memberId: Long) } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 88101ed3..a6c066c4 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -49,6 +49,11 @@ class AnniversaryService( return anniversaryRepository.saveAll(anniversaries) } + @Transactional + fun deleteAllByCoupleId(coupleId : Long) { + return anniversaryRepository.deleteAllByCoupleId(coupleId) + } + private fun checkUserInCouple(userId: Long, couple: Couple) { if (!couple.containsUser(userId)) { throw BadRequestException("sarangggun.couple.not-in-couple") diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt index eea8b82d..ebbc1642 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt @@ -35,4 +35,9 @@ class CoupleService( fun findByMemberId(memberId: Long): Couple? { return coupleRepository.findByMemberId(memberId) } + + @Transactional + fun deleteByMemberId(memberId: Long) { + return coupleRepository.deleteByMemberId(memberId) + } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/repository/NotificationRepository.kt b/src/main/kotlin/gomushin/backend/member/domain/repository/NotificationRepository.kt index 7433c705..9ba781ef 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/repository/NotificationRepository.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/repository/NotificationRepository.kt @@ -5,4 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository interface NotificationRepository : JpaRepository { fun findByMemberId(memberId: Long): Notification? + + fun deleteAllByMemberId(memberId: Long) } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/LeaveService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/LeaveService.kt new file mode 100644 index 00000000..f7ff3fda --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/service/LeaveService.kt @@ -0,0 +1,42 @@ +package gomushin.backend.member.domain.service + +import gomushin.backend.core.service.S3Service +import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.domain.service.CoupleService +import gomushin.backend.schedule.domain.service.CommentService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.domain.service.ScheduleService +import org.springframework.stereotype.Service + +@Service +class LeaveService( + private val anniversaryService: AnniversaryService, + private val commentService: CommentService, + private val coupleService: CoupleService, + private val letterService: LetterService, + private val memberService: MemberService, + private val notificationService: NotificationService, + private val pictureService: PictureService, + private val scheduleService: ScheduleService, + private val s3Service: S3Service, +) { + fun leave(memberId: Long, coupleId : Long) { + anniversaryService.deleteAllByCoupleId(coupleId) + commentService.deleteAllByMemberId(memberId) + coupleService.deleteByMemberId(memberId) + notificationService.deleteAllByMember(memberId) + scheduleService.deleteAllByMember(memberId) + + val letters = letterService.findAllByAuthorId(memberId) + pictureService.findAllByLetterIds(letters) + .takeIf { it.isNotEmpty() } + ?.forEach { picture -> + s3Service.deleteFile(picture.pictureUrl) + } + + pictureService.deleteAllByLetterIds(letters) + letterService.deleteAllByMemberId(memberId) + memberService.deleteMember(memberId) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 33a53ee4..0f291649 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -43,4 +43,9 @@ class MemberService( val member = getById(id) member.updateBirthday(updateMyBirthdayRequest.birthDate) } + + @Transactional + fun deleteMember(id : Long) { + return memberRepository.deleteById(id) + } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt index 0f404ae7..d5154535 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt @@ -35,4 +35,9 @@ class NotificationService( fun save(notification: Notification): Notification { return notificationRepository.save(notification) } + + @Transactional + fun deleteAllByMember(memberId: Long) { + return notificationRepository.deleteAllByMemberId(memberId) + } } diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt new file mode 100644 index 00000000..b407640f --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -0,0 +1,14 @@ +package gomushin.backend.member.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.member.domain.service.LeaveService +import org.springframework.stereotype.Component + +@Component +class LeaveFacade( + private val leaveService: LeaveService +) { + fun leave(customUserDetails: CustomUserDetails) { + leaveService.leave(customUserDetails.getId(), customUserDetails.getCouple().id) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index ba554580..ed127c9b 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -9,4 +9,5 @@ object ApiPath { const val UPDATE_MY_NICKNAME = "/v1/member/my-nickname" const val UPDATE_MY_BIRTHDAY = "/v1/member/my-birthday" const val UPDATE_MY_NOTIFICATION_POLICY = "/v1/member/my-notification" + const val DELETE_ALL_MY_DATA ="/v1/member/all-my-data" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 11d29b74..833fb46f 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -10,10 +10,12 @@ import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.facade.MemberInfoFacade import gomushin.backend.member.dto.response.MyInfoResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse +import gomushin.backend.member.facade.LeaveFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -24,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController @Tag(name = "회원 정보", description = "MemberController") class MemberInfoController( private val memberInfoFacade: MemberInfoFacade, + private val leaveFacade: LeaveFacade ) { @ResponseStatus(HttpStatus.OK) @@ -99,4 +102,14 @@ class MemberInfoController( memberInfoFacade.updateMyNotification(customUserDetails, updateMyNotificationRequest) return ApiResponse.success(true) } + + @ResponseStatus(HttpStatus.OK) + @DeleteMapping(ApiPath.DELETE_ALL_MY_DATA) + @Operation(summary = "커플 연동 해제(유저 관련 데이터 모두 삭제)", description = "deleteMyData") + fun deleteMyData( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ) : ApiResponse { + leaveFacade.leave(customUserDetails) + return ApiResponse.success(true) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt index 32b5c098..a519ddd2 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt @@ -2,5 +2,12 @@ package gomushin.backend.schedule.domain.repository import gomushin.backend.schedule.domain.entity.Comment import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param -interface CommentRepository: JpaRepository +interface CommentRepository: JpaRepository { + @Modifying + @Query("DELETE FROM Comment c WHERE c.authorId = :authorId") + fun deleteAllByAuthorId(@Param("authorId") authorId: Long) +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index b1ff0af0..1c6d4429 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -2,5 +2,15 @@ package gomushin.backend.schedule.domain.repository import gomushin.backend.schedule.domain.entity.Letter import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param -interface LetterRepository : JpaRepository +interface LetterRepository : JpaRepository { + @Query("SELECT l.id FROM Letter l WHERE l.authorId = :authorId") + fun findAllByAuthorId(@Param("authorId") authorId: Long): List + + @Modifying + @Query("DELETE FROM Letter l WHERE l.authorId = :authorId") + fun deleteAllByAuthorId(@Param("authorId") authorId: Long) +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt index 27199b76..a0cdfe74 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt @@ -2,9 +2,19 @@ package gomushin.backend.schedule.domain.repository import gomushin.backend.schedule.domain.entity.Picture import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param interface PictureRepository: JpaRepository { fun findAllByPictureUrlIn(urls: List): List fun deleteAllByLetterId(letterId: Long) fun findAllByLetterId(letterId: Long): List + + @Query("SELECT p FROM Picture p WHERE p.letterId IN :letterIds") + fun findAllByLetterIdIn(@Param("letterIds") letterIds: List): List + + @Modifying + @Query("DELETE FROM Picture p WHERE p.letterId IN :letterIds") + fun deleteAllByLetterIdIn(@Param("letterIds") letterIds: List) } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt index 3b47444f..9569af93 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt @@ -2,5 +2,12 @@ package gomushin.backend.schedule.domain.repository import gomushin.backend.schedule.domain.entity.Schedule import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param -interface ScheduleRepository : JpaRepository +interface ScheduleRepository : JpaRepository { + @Modifying + @Query("DELETE FROM Schedule s WHERE s.userId = :userId") + fun deleteAllByUserId(@Param("userId") userId: Long) +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt index 6dd1d3c6..a1413d3b 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt @@ -56,4 +56,9 @@ class CommentService( fun delete(id: Long) { commentRepository.deleteById(id) } + + @Transactional + fun deleteAllByMemberId(memberId: Long) { + commentRepository.deleteAllByAuthorId(memberId) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index f7d59d36..2df639fd 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -45,4 +45,13 @@ class LetterService( fun delete(letterId: Long) { letterRepository.deleteById(letterId) } + @Transactional + fun deleteAllByMemberId(memberId : Long) { + letterRepository.deleteAllByAuthorId(memberId) + } + + @Transactional(readOnly = true) + fun findAllByAuthorId(memberId: Long) : List{ + return letterRepository.findAllByAuthorId(memberId) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt index 9a4caf71..fb9f6aaf 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt @@ -35,4 +35,14 @@ class PictureService( fun deleteAllByLetterId(letterId: Long) { pictureRepository.deleteAllByLetterId(letterId) } + + @Transactional(readOnly = true) + fun findAllByLetterIds(letterIds : List) : List { + return pictureRepository.findAllByLetterIdIn(letterIds) + } + + @Transactional + fun deleteAllByLetterIds(letterIds: List) { + return pictureRepository.deleteAllByLetterIdIn(letterIds) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt index 15b51969..18f9ff69 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt @@ -43,4 +43,9 @@ class ScheduleService( } scheduleRepository.deleteById(scheduleId) } + + @Transactional + fun deleteAllByMember(memberId : Long) { + scheduleRepository.deleteAllByUserId(memberId) + } } From 199fc0d146d84c2a2914caccde3d7becc3f2e801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 27 Apr 2025 14:47:00 +0900 Subject: [PATCH 155/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20retur?= =?UTF-8?q?n=EB=AC=B8=20=EC=A0=9C=EA=B1=B0,=20leaveService=EB=A5=BC=20leav?= =?UTF-8?q?eFacade=EB=A1=9C=20=EC=98=AE=EA=B8=B0=EA=B8=B0,=20Transactional?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EB=B9=BC?= =?UTF-8?q?=EB=A8=B9=EC=9D=80=EA=B1=B0=20=EC=B6=94=EA=B0=80)=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleService.kt | 2 +- .../member/domain/service/LeaveService.kt | 42 ------------------- .../member/domain/service/MemberService.kt | 2 +- .../backend/member/facade/LeaveFacade.kt | 41 ++++++++++++++++-- 4 files changed, 40 insertions(+), 47 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/member/domain/service/LeaveService.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt index ebbc1642..a377adf1 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt @@ -38,6 +38,6 @@ class CoupleService( @Transactional fun deleteByMemberId(memberId: Long) { - return coupleRepository.deleteByMemberId(memberId) + coupleRepository.deleteByMemberId(memberId) } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/LeaveService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/LeaveService.kt deleted file mode 100644 index f7ff3fda..00000000 --- a/src/main/kotlin/gomushin/backend/member/domain/service/LeaveService.kt +++ /dev/null @@ -1,42 +0,0 @@ -package gomushin.backend.member.domain.service - -import gomushin.backend.core.service.S3Service -import gomushin.backend.couple.domain.service.AnniversaryService -import gomushin.backend.couple.domain.service.CoupleService -import gomushin.backend.schedule.domain.service.CommentService -import gomushin.backend.schedule.domain.service.LetterService -import gomushin.backend.schedule.domain.service.PictureService -import gomushin.backend.schedule.domain.service.ScheduleService -import org.springframework.stereotype.Service - -@Service -class LeaveService( - private val anniversaryService: AnniversaryService, - private val commentService: CommentService, - private val coupleService: CoupleService, - private val letterService: LetterService, - private val memberService: MemberService, - private val notificationService: NotificationService, - private val pictureService: PictureService, - private val scheduleService: ScheduleService, - private val s3Service: S3Service, -) { - fun leave(memberId: Long, coupleId : Long) { - anniversaryService.deleteAllByCoupleId(coupleId) - commentService.deleteAllByMemberId(memberId) - coupleService.deleteByMemberId(memberId) - notificationService.deleteAllByMember(memberId) - scheduleService.deleteAllByMember(memberId) - - val letters = letterService.findAllByAuthorId(memberId) - pictureService.findAllByLetterIds(letters) - .takeIf { it.isNotEmpty() } - ?.forEach { picture -> - s3Service.deleteFile(picture.pictureUrl) - } - - pictureService.deleteAllByLetterIds(letters) - letterService.deleteAllByMemberId(memberId) - memberService.deleteMember(memberId) - } -} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 0f291649..603534b2 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -46,6 +46,6 @@ class MemberService( @Transactional fun deleteMember(id : Long) { - return memberRepository.deleteById(id) + memberRepository.deleteById(id) } } diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt index b407640f..49f788f4 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -1,14 +1,49 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails -import gomushin.backend.member.domain.service.LeaveService +import gomushin.backend.core.service.S3Service +import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.domain.service.CoupleService +import gomushin.backend.member.domain.service.MemberService +import gomushin.backend.member.domain.service.NotificationService +import gomushin.backend.schedule.domain.service.CommentService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.domain.service.ScheduleService import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional @Component class LeaveFacade( - private val leaveService: LeaveService + private val anniversaryService: AnniversaryService, + private val commentService: CommentService, + private val coupleService: CoupleService, + private val letterService: LetterService, + private val memberService: MemberService, + private val notificationService: NotificationService, + private val pictureService: PictureService, + private val scheduleService: ScheduleService, + private val s3Service: S3Service, ) { + @Transactional fun leave(customUserDetails: CustomUserDetails) { - leaveService.leave(customUserDetails.getId(), customUserDetails.getCouple().id) + val memberId = customUserDetails.getId() + val coupleId = customUserDetails.getCouple().id + anniversaryService.deleteAllByCoupleId(coupleId) + commentService.deleteAllByMemberId(memberId) + coupleService.deleteByMemberId(memberId) + notificationService.deleteAllByMember(memberId) + scheduleService.deleteAllByMember(memberId) + + val letters = letterService.findAllByAuthorId(memberId) + pictureService.findAllByLetterIds(letters) + .takeIf { it.isNotEmpty() } + ?.forEach { picture -> + s3Service.deleteFile(picture.pictureUrl) + } + + pictureService.deleteAllByLetterIds(letters) + letterService.deleteAllByMemberId(memberId) + memberService.deleteMember(memberId) } } \ No newline at end of file From 6290543e31a03f2aa03e0fc93cf436114b4c7692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 27 Apr 2025 15:00:56 +0900 Subject: [PATCH 156/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20retur?= =?UTF-8?q?n=EB=AC=B8=20=EC=A0=9C=EA=B1=B0)=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/service/NotificationService.kt | 2 +- .../gomushin/backend/schedule/domain/service/PictureService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt index d5154535..b228e19d 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt @@ -38,6 +38,6 @@ class NotificationService( @Transactional fun deleteAllByMember(memberId: Long) { - return notificationRepository.deleteAllByMemberId(memberId) + notificationRepository.deleteAllByMemberId(memberId) } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt index fb9f6aaf..b48e6eb1 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt @@ -43,6 +43,6 @@ class PictureService( @Transactional fun deleteAllByLetterIds(letterIds: List) { - return pictureRepository.deleteAllByLetterIdIn(letterIds) + pictureRepository.deleteAllByLetterIdIn(letterIds) } } From a7a1a1ffc90ec74c1af636d7e5752964b9269d0e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 15:42:48 +0900 Subject: [PATCH 157/357] =?UTF-8?q?fix:=20cors=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ .../core/configuration/security/CustomCorsConfiguration.kt | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 22d38571..5705fe0e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] + pull_request: + branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index b6abb6fe..9d68aa98 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -18,7 +18,8 @@ class CustomCorsConfiguration { "https://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app", - "https://vite.sarang-backend.o-r.kr:5173" + "https://vite.sarang-backend.o-r.kr:5173", + "https://sarang-backend.o-r.kr", ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") From e31a42efd56bb624cace897181e8ac7c3b29bef6 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 15:51:57 +0900 Subject: [PATCH 158/357] =?UTF-8?q?feat:=20docker-compose=20=EB=A0=88?= =?UTF-8?q?=EB=94=94=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 +-- docker-compose.yml | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5705fe0e..44fbe54c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -97,8 +97,7 @@ jobs: password: ${{ secrets.NCP_PASSWORD }} port: ${{ secrets.NCP_PORT }} script: | - docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/sarang-backend:${{ github.sha }} - sudo chmod +x ./deploy.sh export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} export GITHUB_SHA=${{ github.sha }} + sudo chmod +x ./deploy.sh ./deploy.sh diff --git a/docker-compose.yml b/docker-compose.yml index 5fdc1a5b..cdc77773 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +version: "3.8" + services: blue: image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" @@ -6,6 +8,9 @@ services: TZ: Asia/Seoul ports: - '8080:8080' + depends_on: + - redis + - minio networks: - sarang-backend-network @@ -16,8 +21,54 @@ services: TZ: Asia/Seoul ports: - '8081:8080' + depends_on: + - redis + - minio + networks: + - sarang-backend-network + + redis: + image: redis:6.0.9 + container_name: redis + ports: + - '6379:6379' + volumes: + - redis-data:/data + networks: + - sarang-backend-network + + minio: + image: quay.io/minio/minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + volumes: + - minio_data:/data + ports: + - "9000:9000" + - "9001:9001" networks: - sarang-backend-network + restart: unless-stopped + + mc: + image: minio/mc + depends_on: + - minio + entrypoint: > + /bin/sh -c " + until mc alias set myminio http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}; do echo 'MinIO 아직 준비 안됨...'; sleep 5; done; + mc mb -p myminio/gomushin; + mc anonymous set download myminio/gomushin; + exit 0; + " + networks: + - sarang-backend-network + +volumes: + redis-data: + minio_data: networks: sarang-backend-network: From 28905e44675e4e82b83f7d791c1bde01cca3e2ad Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 17:22:06 +0900 Subject: [PATCH 159/357] =?UTF-8?q?feat:=20docker-compose=20=EB=A0=88?= =?UTF-8?q?=EB=94=94=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 -- docker-compose.yml | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 44fbe54c..b786a8d0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,6 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: ci: diff --git a/docker-compose.yml b/docker-compose.yml index cdc77773..0f9b2532 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,7 @@ services: minio: image: quay.io/minio/minio + container_name: minio command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} From ce7e913dbe07c6b2643c9da325b2e6e7a7c05249 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 00:58:58 +0900 Subject: [PATCH 160/357] =?UTF-8?q?refactor:=20request=20dto=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/schedule/domain/service/CommentService.kt | 2 +- .../gomushin/backend/schedule/domain/service/LetterService.kt | 2 +- .../backend/schedule/dto/{ => request}/UpsertCommentRequest.kt | 2 +- .../backend/schedule/dto/{ => request}/UpsertLetterRequest.kt | 2 +- .../backend/schedule/dto/{ => request}/UpsertScheduleRequest.kt | 2 +- .../backend/schedule/facade/UpsertAndDeleteCommentFacade.kt | 2 +- .../backend/schedule/facade/UpsertAndDeleteLetterFacade.kt | 2 +- .../backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt | 2 +- .../schedule/presentation/UpsertAndDeleteCommentController.kt | 2 +- .../schedule/presentation/UpsertAndDeleteLetterController.kt | 2 +- .../schedule/presentation/UpsertAndDeleteScheduleController.kt | 2 +- .../schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt | 2 +- .../schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt | 2 +- .../backend/schedule/domain/service/CommentServiceTest.kt | 2 +- .../backend/schedule/domain/service/ScheduleServiceTest.kt | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) rename src/main/kotlin/gomushin/backend/schedule/dto/{ => request}/UpsertCommentRequest.kt (86%) rename src/main/kotlin/gomushin/backend/schedule/dto/{ => request}/UpsertLetterRequest.kt (91%) rename src/main/kotlin/gomushin/backend/schedule/dto/{ => request}/UpsertScheduleRequest.kt (95%) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt index a1413d3b..b9e1efb9 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt @@ -3,7 +3,7 @@ package gomushin.backend.schedule.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.schedule.domain.entity.Comment import gomushin.backend.schedule.domain.repository.CommentRepository -import gomushin.backend.schedule.dto.UpsertCommentRequest +import gomushin.backend.schedule.dto.request.UpsertCommentRequest import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index 2df639fd..755f4e4c 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -3,7 +3,7 @@ package gomushin.backend.schedule.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.repository.LetterRepository -import gomushin.backend.schedule.dto.UpsertLetterRequest +import gomushin.backend.schedule.dto.request.UpsertLetterRequest import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertCommentRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertCommentRequest.kt similarity index 86% rename from src/main/kotlin/gomushin/backend/schedule/dto/UpsertCommentRequest.kt rename to src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertCommentRequest.kt index ca6d291b..2e8e7c52 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertCommentRequest.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertCommentRequest.kt @@ -1,4 +1,4 @@ -package gomushin.backend.schedule.dto +package gomushin.backend.schedule.dto.request import io.swagger.v3.oas.annotations.media.Schema diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertLetterRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertLetterRequest.kt similarity index 91% rename from src/main/kotlin/gomushin/backend/schedule/dto/UpsertLetterRequest.kt rename to src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertLetterRequest.kt index 1988b7d6..de50355a 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertLetterRequest.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertLetterRequest.kt @@ -1,4 +1,4 @@ -package gomushin.backend.schedule.dto +package gomushin.backend.schedule.dto.request import io.swagger.v3.oas.annotations.media.Schema diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertScheduleRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertScheduleRequest.kt similarity index 95% rename from src/main/kotlin/gomushin/backend/schedule/dto/UpsertScheduleRequest.kt rename to src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertScheduleRequest.kt index 0a6d9e35..e174b247 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/UpsertScheduleRequest.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertScheduleRequest.kt @@ -1,4 +1,4 @@ -package gomushin.backend.schedule.dto +package gomushin.backend.schedule.dto.request import gomushin.backend.schedule.domain.entity.Schedule import io.swagger.v3.oas.annotations.media.Schema diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteCommentFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteCommentFacade.kt index b286449b..a1224c88 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteCommentFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteCommentFacade.kt @@ -5,7 +5,7 @@ import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.service.MemberService import gomushin.backend.schedule.domain.service.CommentService import gomushin.backend.schedule.domain.service.LetterService -import gomushin.backend.schedule.dto.UpsertCommentRequest +import gomushin.backend.schedule.dto.request.UpsertCommentRequest import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt index 2e72a90b..69f7fc64 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt @@ -6,7 +6,7 @@ import gomushin.backend.core.service.S3Service import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService -import gomushin.backend.schedule.dto.UpsertLetterRequest +import gomushin.backend.schedule.dto.request.UpsertLetterRequest import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt index b012790d..ef562461 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt @@ -2,7 +2,7 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.schedule.domain.service.ScheduleService -import gomushin.backend.schedule.dto.UpsertScheduleRequest +import gomushin.backend.schedule.dto.request.UpsertScheduleRequest import org.springframework.stereotype.Component @Component diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteCommentController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteCommentController.kt index 90e4cc9a..d249f496 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteCommentController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteCommentController.kt @@ -2,7 +2,7 @@ package gomushin.backend.schedule.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.schedule.dto.UpsertCommentRequest +import gomushin.backend.schedule.dto.request.UpsertCommentRequest import gomushin.backend.schedule.facade.UpsertAndDeleteCommentFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt index a2dfd1ee..38f7ea11 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt @@ -2,7 +2,7 @@ package gomushin.backend.schedule.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.schedule.dto.UpsertLetterRequest +import gomushin.backend.schedule.dto.request.UpsertLetterRequest import gomushin.backend.schedule.facade.UpsertAndDeleteLetterFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt index d6e4dc6a..73848766 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteScheduleController.kt @@ -2,7 +2,7 @@ package gomushin.backend.schedule.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.schedule.dto.UpsertScheduleRequest +import gomushin.backend.schedule.dto.request.UpsertScheduleRequest import gomushin.backend.schedule.facade.UpsertAndDeleteScheduleFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt index 2690ef3f..14334823 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt @@ -7,7 +7,7 @@ import gomushin.backend.schedule.domain.entity.Comment import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.service.CommentService import gomushin.backend.schedule.domain.service.LetterService -import gomushin.backend.schedule.dto.UpsertCommentRequest +import gomushin.backend.schedule.dto.request.UpsertCommentRequest import gomushin.backend.schedule.facade.UpsertAndDeleteCommentFacade import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.extension.ExtendWith diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt index 532a341d..3ea98206 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt @@ -8,7 +8,7 @@ import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService -import gomushin.backend.schedule.dto.UpsertLetterRequest +import gomushin.backend.schedule.dto.request.UpsertLetterRequest import gomushin.backend.schedule.facade.UpsertAndDeleteLetterFacade import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt index 6f90b6dd..b10d7b86 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt @@ -2,7 +2,7 @@ package gomushin.backend.schedule.domain.service import gomushin.backend.schedule.domain.entity.Comment import gomushin.backend.schedule.domain.repository.CommentRepository -import gomushin.backend.schedule.dto.UpsertCommentRequest +import gomushin.backend.schedule.dto.request.UpsertCommentRequest import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.extension.ExtendWith diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt index 4c476ab1..c1bcf592 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt @@ -2,7 +2,7 @@ package gomushin.backend.schedule.domain.service import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.ScheduleRepository -import gomushin.backend.schedule.dto.UpsertScheduleRequest +import gomushin.backend.schedule.dto.request.UpsertScheduleRequest import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith From 0e8aa50bbcd82091684ee33aa94c825eceff7e37 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 00:59:40 +0900 Subject: [PATCH 161/357] =?UTF-8?q?chore:=20=EC=BF=A0=ED=82=A4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=EB=A1=9C=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/security/SecurityConfiguration.kt | 12 ++++++++++-- .../core/oauth/handler/CustomSuccessHandler.kt | 5 +++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index c956a193..1123dc10 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -22,7 +22,8 @@ import org.springframework.web.cors.CorsUtils class SecurityConfiguration( private val jwtTokenProvider: JwtTokenProvider, private val memberRepository: MemberRepository, - @Value("\${redirect-url}") private val redirectUrl: String + @Value("\${redirect-url}") private val redirectUrl: String, + @Value("\${cookie.domain}") private val cookieDomain: String ) { @Bean @@ -54,7 +55,14 @@ class SecurityConfiguration( userInfoEndpointConfigurer .userService(customOAuth2UserService) } - .successHandler(CustomSuccessHandler(jwtTokenProvider, memberRepository, redirectUrl)) + .successHandler( + CustomSuccessHandler( + jwtTokenProvider, + memberRepository, + redirectUrl, + cookieDomain + ) + ) } .authorizeHttpRequests { it.requestMatchers( diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 8f3c0ee9..b281e938 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -20,7 +20,8 @@ import org.springframework.stereotype.Component class CustomSuccessHandler( private val jwtTokenProvider: JwtTokenProvider, private val memberRepository: MemberRepository, - @Value("\${redirect-url}") private val redirectUrl: String + @Value("\${redirect-url}") private val redirectUrl: String, + @Value("\${cookie.domain}") private val cookieDomain: String, ) : SimpleUrlAuthenticationSuccessHandler() { @Throws(IOException::class, ServletException::class) @@ -53,7 +54,7 @@ class CustomSuccessHandler( .httpOnly(true) .secure(true) .sameSite("None") - .domain(".sarang-backend.o-r.kr") + .domain(cookieDomain) .maxAge(432000) .build() } From 11652c0c54d07f201a4593542d4f4d8a1f5a4a48 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 01:00:08 +0900 Subject: [PATCH 162/357] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/presentation/ApiPath.kt | 1 + .../presentation/ReadScheduleController.kt | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt index 2be1922c..a0773306 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt @@ -3,6 +3,7 @@ package gomushin.backend.schedule.presentation object ApiPath { const val SCHEDULES = "/v1/schedules" const val SCHEDULE = "/v1/schedules/{scheduleId}" + const val SCHEDULES_BY_DATE = "/v1/schedules/date" const val LETTERS = "/v1/schedules/letters" const val LETTER = "/v1/schedules/{scheduleId}/letters/{letterId}" diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt new file mode 100644 index 00000000..fe546c8c --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt @@ -0,0 +1,42 @@ +package gomushin.backend.schedule.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse +import gomushin.backend.schedule.facade.ReadScheduleFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@Tag(name = "일정 조회", description = "ReadScheduleController") +class ReadScheduleController( + private val readScheduleFacade: ReadScheduleFacade +) { + + @GetMapping(ApiPath.SCHEDULES) + @Operation(summary = "특정 달의 일정 리스트 가져오기", description = "getScheduleList") + fun getScheduleList( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestParam year: Int, + @RequestParam month: Int, + ): ApiResponse { + val schedules = readScheduleFacade.getList(customUserDetails, year, month) + return ApiResponse.success(schedules) + } + + @GetMapping(ApiPath.SCHEDULES_BY_DATE) + @Operation(summary = "특정 날짜의 일정 가져오기", description = "getSchedule") + fun getSchedule( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestParam date: LocalDate, + ): ApiResponse> { + val schedules = readScheduleFacade.get(customUserDetails, date) + return ApiResponse.success(schedules) + } +} From 72478752ef8d2aa88e1b397afce8695436b7e8dd Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 01:00:33 +0900 Subject: [PATCH 163/357] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?Response=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/MonthlyAnniversariesResponse.kt | 8 ++++++++ .../MonthlySchedulesAndAnniversariesResponse.kt | 17 +++++++++++++++++ .../dto/response/MonthlySchedulesResponse.kt | 10 ++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/MonthlyAnniversariesResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/MonthlyAnniversariesResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/MonthlyAnniversariesResponse.kt new file mode 100644 index 00000000..d2d59f55 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/MonthlyAnniversariesResponse.kt @@ -0,0 +1,8 @@ +package gomushin.backend.couple.dto.response + +import java.time.LocalDate + +data class MonthlyAnniversariesResponse( + val title: String, + val anniversaryDate: LocalDate +) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt new file mode 100644 index 00000000..c21e2f42 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt @@ -0,0 +1,17 @@ +package gomushin.backend.schedule.dto.response + +import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse + +data class MonthlySchedulesAndAnniversariesResponse( + val schedules: List, + val anniversaries: List, +) { + companion object { + fun of( + schedules: List, + anniversaries: List, + ): MonthlySchedulesAndAnniversariesResponse { + return MonthlySchedulesAndAnniversariesResponse(schedules, anniversaries) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt new file mode 100644 index 00000000..4adeeb1b --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt @@ -0,0 +1,10 @@ +package gomushin.backend.schedule.dto.response + +import java.time.LocalDateTime + +data class MonthlySchedulesResponse( + val content: String, + val startDate: LocalDateTime, + val endDate: LocalDateTime, + val fatigue: String, +) From 2bc00db4c44c48642bee113cadd9cd6b98e31005 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 01:01:01 +0900 Subject: [PATCH 164/357] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=9D=BC=EB=B3=84=20=EC=A1=B0=ED=9A=8C,=20=EC=97=B0=EB=8F=84?= =?UTF-8?q?=EC=99=80=20=EB=8B=AC=20=EA=B8=B0=EC=A4=80=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AnniversaryRepository.kt | 15 +++++++++ .../domain/service/AnniversaryService.kt | 6 ++++ .../domain/repository/ScheduleRepository.kt | 28 +++++++++++++++++ .../domain/service/ScheduleService.kt | 14 ++++++++- .../schedule/facade/ReadScheduleFacade.kt | 31 +++++++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index 296d100f..e8377c20 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -1,6 +1,7 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Anniversary +import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query @@ -19,4 +20,18 @@ interface AnniversaryRepository : JpaRepository { @Modifying @Query("DELETE FROM Anniversary a WHERE a.coupleId = :coupleId") fun deleteAllByCoupleId(@Param("coupleId") coupleId: Long) + + @Query( + """ + SELECT new gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse( + a.title, + a.anniversaryDate + ) + FROM Anniversary a + WHERE a.coupleId = :coupleId + AND function('YEAR', a.anniversaryDate) = :year + AND function('MONTH', a.anniversaryDate) = :month + """ + ) + fun findByCoupleIdAndYearAndMonth(coupleId: Long, year: Int, month: Int): List } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index a6c066c4..d67e0138 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest +import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -15,6 +16,11 @@ class AnniversaryService( private val anniversaryCalculator: AnniversaryCalculator ) { + @Transactional(readOnly = true) + fun findByCoupleIdAndYearAndMonth(couple: Couple, year: Int, month: Int): List { + return anniversaryRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) + } + @Transactional fun registerAnniversary( userId: Long, diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt index 9569af93..0d2512dc 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt @@ -1,13 +1,41 @@ package gomushin.backend.schedule.domain.repository import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param +import java.time.LocalDate interface ScheduleRepository : JpaRepository { @Modifying @Query("DELETE FROM Schedule s WHERE s.userId = :userId") fun deleteAllByUserId(@Param("userId") userId: Long) + + @Query( + """ + SELECT new gomushin.backend.schedule.dto.response.MonthlySchedulesResponse( + s.content, + s.startDate, + s.endDate, + s.fatigue + ) + FROM Schedule s + WHERE s.coupleId = :coupleId + AND function('YEAR', s.startDate) = :year + AND function('MONTH', s.startDate) = :month + """ + ) + fun findByCoupleIdAndYearAndMonth(coupleId: Long, year: Int, month: Int): List + + @Query( + """ + SELECT s + FROM Schedule s + WHERE s.coupleId = :coupleId + AND function('DATE', s.startDate) = :startDate + """ + ) + fun findByCoupleIdAndStartDate(coupleId: Long, startDate: LocalDate): List } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt index 18f9ff69..c0264a52 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt @@ -1,17 +1,29 @@ package gomushin.backend.schedule.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.ScheduleRepository -import gomushin.backend.schedule.dto.UpsertScheduleRequest +import gomushin.backend.schedule.dto.request.UpsertScheduleRequest +import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate @Service class ScheduleService( private val scheduleRepository: ScheduleRepository, ) { + @Transactional(readOnly = true) + fun findByCoupleIdAndYearAndMonth(couple: Couple, year: Int, month: Int): List { + return scheduleRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) + } + + @Transactional(readOnly = true) + fun findByDate(couple: Couple, date: LocalDate): List { + return scheduleRepository.findByCoupleIdAndStartDate(couple.id, date) + } @Transactional fun upsert(id: Long?, coupleId: Long, userId: Long, upsertScheduleRequest: UpsertScheduleRequest) { diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt new file mode 100644 index 00000000..6570a70c --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -0,0 +1,31 @@ +package gomushin.backend.schedule.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse +import org.springframework.stereotype.Component +import java.time.LocalDate + +@Component +class ReadScheduleFacade( + private val scheduleService: ScheduleService, + private val anniversaryService: AnniversaryService, +) { + + fun getList( + customUserDetails: CustomUserDetails, + year: Int, + month: Int + ): MonthlySchedulesAndAnniversariesResponse { + val monthlySchedules = scheduleService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) + val monthlyAnniversaries = + anniversaryService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) + return MonthlySchedulesAndAnniversariesResponse.of(monthlySchedules, monthlyAnniversaries) + } + + fun get(customUserDetails: CustomUserDetails, date: LocalDate): List { + return scheduleService.findByDate(customUserDetails.getCouple(), date) + } +} From 956e1e9cf0591f02a63438a16fd1e846972985bc Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 01:01:18 +0900 Subject: [PATCH 165/357] =?UTF-8?q?test:=20=EC=9D=BC=EC=A0=95=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=84=B1=EA=B3=B5=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 --- .../domain/facade/ReadScheduleFacadeTest.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt new file mode 100644 index 00000000..98484673 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt @@ -0,0 +1,76 @@ +package gomushin.backend.schedule.domain.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse +import gomushin.backend.schedule.facade.ReadScheduleFacade +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import java.time.LocalDate + +@ExtendWith(MockitoExtension::class) +class ReadScheduleFacadeTest { + + @Mock + private lateinit var scheduleService: ScheduleService + + @Mock + private lateinit var anniversaryService: AnniversaryService + + @InjectMocks + private lateinit var readScheduleFacade: ReadScheduleFacade + + private val customUserDetails = mock(CustomUserDetails::class.java) + + @BeforeEach + fun setUp() { + `when`(customUserDetails.getCouple()).thenReturn(mock(Couple::class.java)) + } + + @DisplayName("getList - 성공") + @Test + fun getList_success() { + // given + val year = 2025 + val month = 4 + val monthlySchedulesResponse = mock(MonthlySchedulesResponse::class.java) + val monthlyAnniversariesResponse = mock(MonthlyAnniversariesResponse::class.java) + + // when + `when`(scheduleService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month)) + .thenReturn(listOf(monthlySchedulesResponse)) + `when`(anniversaryService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month)) + .thenReturn(listOf(monthlyAnniversariesResponse)) + readScheduleFacade.getList(customUserDetails, year, month) + + // then + verify(scheduleService, times(1)).findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) + verify(anniversaryService, times(1)).findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) + } + + @DisplayName("get - 성공") + @Test + fun get_success() { + // given + val date = LocalDate.of(2025, 4, 1) + val mockSchedules = listOf(mock(Schedule::class.java)) + + // when + `when`(scheduleService.findByDate(customUserDetails.getCouple(), date)).thenReturn(mockSchedules) + readScheduleFacade.get(customUserDetails, date) + + // then + verify(scheduleService, times(1)).findByDate(customUserDetails.getCouple(), date) + } + +} From da25dffc3d7fc63e5a369fd55055b9e76fc1e5ac Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 01:58:27 +0900 Subject: [PATCH 166/357] =?UTF-8?q?feat:=20GUEST=20,=20MEMBER=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=A0=91=EA=B7=BC=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SecurityConfiguration.kt | 7 +++- .../infrastructure/JwtTokenProviderImpl.kt | 9 +---- .../handler/CustomAccessDeniedHandler.kt | 40 +++++++++++++++++++ .../oauth/handler/CustomSuccessHandler.kt | 1 - 4 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 1123dc10..1cf4c044 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -2,6 +2,7 @@ package gomushin.backend.core.configuration.security import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.oauth.handler.CustomAccessDeniedHandler import gomushin.backend.core.oauth.handler.CustomSuccessHandler import gomushin.backend.core.oauth.service.CustomOAuth2UserService import gomushin.backend.core.service.CustomUserDetailsService @@ -29,7 +30,8 @@ class SecurityConfiguration( @Bean fun filterChain( http: HttpSecurity, corsConfiguration: CustomCorsConfiguration, - customOAuth2UserService: CustomOAuth2UserService, coupleRepository: CoupleRepository + customOAuth2UserService: CustomOAuth2UserService, coupleRepository: CoupleRepository, + customAccessDeniedHandler: CustomAccessDeniedHandler ): SecurityFilterChain { http .csrf { @@ -64,6 +66,9 @@ class SecurityConfiguration( ) ) } + .exceptionHandling { + it.accessDeniedHandler(customAccessDeniedHandler) + } .authorizeHttpRequests { it.requestMatchers( "/", diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt index 19f11bde..2ee97798 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt @@ -36,14 +36,7 @@ class JwtTokenProviderImpl( Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token) return true } catch (e: Exception) { - when (e) { - is ExpiredJwtException, - is UnsupportedJwtException, - is MalformedJwtException, - is IllegalArgumentException -> return false - - else -> throw e - } + return false } } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt new file mode 100644 index 00000000..851e517b --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt @@ -0,0 +1,40 @@ +package gomushin.backend.core.oauth.handler + +import com.fasterxml.jackson.databind.ObjectMapper +import gomushin.backend.core.common.web.response.ExtendedHttpStatus +import gomushin.backend.core.common.web.response.exception.ErrorCodeResolvingApiErrorException +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.MediaType +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.stereotype.Component + + +@Component +class CustomAccessDeniedHandler : AccessDeniedHandler { + override fun handle( + request: HttpServletRequest, + response: HttpServletResponse, + accessDeniedException: AccessDeniedException + ) { + if (response.isCommitted) return + + response.contentType = MediaType.APPLICATION_JSON_VALUE + response.status = HttpServletResponse.SC_FORBIDDEN + response.characterEncoding = "UTF-8" + + + if (request.requestURI.contains("/v1/member/onboarding")) { + val exception = ErrorCodeResolvingApiErrorException( + ExtendedHttpStatus.FORBIDDEN, + "sarangggun.auth.guest-only" + ) + response.writer.write( + ObjectMapper().writeValueAsString(exception.error) + ) + } + + } +} + diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index b281e938..34ef76a2 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -7,7 +7,6 @@ import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import io.jsonwebtoken.io.IOException import jakarta.servlet.ServletException -import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.beans.factory.annotation.Value From 9001ada7d8f2bb0030a7f95a8074250290d11b8a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 01:01:01 +0900 Subject: [PATCH 167/357] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=9D=BC=EB=B3=84=20=EC=A1=B0=ED=9A=8C,=20=EC=97=B0=EB=8F=84?= =?UTF-8?q?=EC=99=80=20=EB=8B=AC=20=EA=B8=B0=EC=A4=80=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/repository/AnniversaryRepository.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index e8377c20..25961f18 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -9,7 +9,8 @@ import org.springframework.data.repository.query.Param interface AnniversaryRepository : JpaRepository { @Modifying - @Query(""" + @Query( + """ DELETE FROM Anniversary a WHERE (a.title LIKE '%일' OR a.title LIKE '%주년') AND a.anniversaryProperty = 0 From 4db7f6aab8e16ea2b9a83c9f85cc755fe2c310dc Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 18:20:06 +0900 Subject: [PATCH 168/357] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=ED=8E=B8=EC=A7=80=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 --- .../kotlin/gomushin/backend/schedule/presentation/ApiPath.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt index a0773306..7588d5ae 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt @@ -7,6 +7,7 @@ object ApiPath { const val LETTERS = "/v1/schedules/letters" const val LETTER = "/v1/schedules/{scheduleId}/letters/{letterId}" + const val LETTERS_BY_SCHEDULE = "/v1/schedules/{scheduleId}" const val COMMENTS = "/v1/schedules/letters/{letterId}/comments" const val COMMENT = "/v1/schedules/letters/{letterId}/comments/{commentId}" From 155b60959738e400cfaee2f657b0931e69d4f35b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 18:20:28 +0900 Subject: [PATCH 169/357] =?UTF-8?q?feat:=20=EC=97=94=ED=8B=B0=ED=8B=B0=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 --- .../gomushin/backend/schedule/domain/entity/Comment.kt | 3 ++- .../gomushin/backend/schedule/domain/entity/Letter.kt | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Comment.kt b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Comment.kt index 5007356d..d6d7ca9b 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Comment.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Comment.kt @@ -1,5 +1,6 @@ package gomushin.backend.schedule.domain.entity +import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity import jakarta.persistence.* @Entity @@ -20,7 +21,7 @@ class Comment( @Column(name = "content", nullable = false) var content: String = "", -) { +) : BaseEntity() { companion object { fun of( letterId: Long, diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Letter.kt b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Letter.kt index d3e42a11..f112d144 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Letter.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Letter.kt @@ -10,12 +10,18 @@ class Letter( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L, + @Column(name = "couple_id", nullable = false) + val coupleId: Long = 0L, + @Column(name = "schedule_id", nullable = false) val scheduleId: Long = 0L, @Column(name = "author_id", nullable = false) val authorId: Long = 0L, + @Column(name = "author", nullable = false) + val author: String = "", + @Column(name = "title", nullable = false) var title: String = "", @@ -25,14 +31,18 @@ class Letter( ) : BaseEntity() { companion object { fun of( + coupleId: Long, scheduleId: Long, authorId: Long, + author: String, title: String, content: String, ): Letter { return Letter( + coupleId = coupleId, scheduleId = scheduleId, authorId = authorId, + author = author, title = title, content = content, ) From e5d49868aecfb6db7209ee343f1fb798318e3563 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 18:21:17 +0900 Subject: [PATCH 170/357] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/domain/service/LetterService.kt | 24 ++++++- .../schedule/facade/ReadLetterFacade.kt | 63 +++++++++++++++++++ .../facade/UpsertAndDeleteLetterFacade.kt | 4 +- .../presentation/ReadLetterController.kt | 41 ++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index 755f4e4c..b2371433 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -1,7 +1,9 @@ package gomushin.backend.schedule.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.LetterRepository import gomushin.backend.schedule.dto.request.UpsertLetterRequest import org.springframework.data.repository.findByIdOrNull @@ -13,7 +15,7 @@ class LetterService( private val letterRepository: LetterRepository, ) { @Transactional - fun upsert(authorId: Long, upsertLetterRequest: UpsertLetterRequest): Letter { + fun upsert(authorId: Long, authorName: String, couple: Couple, upsertLetterRequest: UpsertLetterRequest): Letter { return upsertLetterRequest.letterId?.let { letterId -> getById(letterId).apply { title = upsertLetterRequest.title @@ -23,13 +25,14 @@ class LetterService( } } ?: save( Letter.of( + coupleId = couple.id, scheduleId = upsertLetterRequest.scheduleId, authorId = authorId, + author = authorName, title = upsertLetterRequest.title, content = upsertLetterRequest.content, ) ) - } @Transactional(readOnly = true) @@ -38,6 +41,23 @@ class LetterService( @Transactional(readOnly = true) fun findById(id: Long) = letterRepository.findByIdOrNull(id) + + @Transactional(readOnly = true) + fun getByCoupleAndScheduleAndId( + couple: Couple, + schedule: Schedule, + letterId: Long, + ) = findByCoupleIdAndScheduleIdAndId(couple, schedule.id, letterId) + ?: throw BadRequestException("sarangggun.letter.not-exist") + + @Transactional(readOnly = true) + fun findByCoupleIdAndScheduleIdAndId(couple: Couple, scheduleId: Long, letterId: Long) = + letterRepository.findByCoupleIdAndScheduleIdAndId(couple.id, letterId, scheduleId) + + @Transactional(readOnly = true) + fun findByCoupleAndSchedule(couple: Couple, schedule: Schedule) = + letterRepository.findByCoupleIdAndScheduleId(couple.id, schedule.id) + @Transactional fun save(letter: Letter) = letterRepository.save(letter) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt new file mode 100644 index 00000000..26bac8c1 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -0,0 +1,63 @@ +package gomushin.backend.schedule.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.schedule.domain.service.CommentService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.dto.response.* +import org.springframework.stereotype.Component + +@Component +class ReadLetterFacade( + private val letterService: LetterService, + private val scheduleService: ScheduleService, + private val pictureService: PictureService, + private val commentService: CommentService, +) { + + fun getList( + customUserDetails: CustomUserDetails, + scheduleId: Long, + ): List { + val schedule = scheduleService.getById(scheduleId) + val letters = letterService.findByCoupleAndSchedule( + customUserDetails.getCouple(), + schedule, + ) + return letters.map { letter -> + val picture = pictureService.findFirstByLetterId(letter.id) + LetterPreviewResponse.of(letter, picture) + } + } + + fun get( + customUserDetails: CustomUserDetails, + scheduleId: Long, + letterId: Long, + ): LetterDetailResponse { + val schedule = scheduleService.getById(scheduleId) + val letter = letterService.getByCoupleAndScheduleAndId( + customUserDetails.getCouple(), + schedule, + letterId, + ) + val letterResponse = LetterResponse.of(letter) + + val pictures = pictureService.findAllByLetter(letter) + val pictureResponses = pictures.map { picture -> + PictureResponse.of(picture) + } + + val comments = commentService.findAllByLetter(letter) + val commentResponses = comments.map { comment -> + CommentResponse.of(comment) + } + + return LetterDetailResponse.of( + letter = letterResponse, + pictures = pictureResponses, + comments = commentResponses, + ) + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt index 69f7fc64..4260f461 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt @@ -34,6 +34,8 @@ class UpsertAndDeleteLetterFacade( val letter = letterService.upsert( customUserDetails.getId(), + customUserDetails.username, + customUserDetails.getCouple(), upsertLetterRequest ) @@ -64,7 +66,7 @@ class UpsertAndDeleteLetterFacade( throw BadRequestException("sarangggun.letter.invalid-schedule") } - pictureService.findAllByLetterId(letter.id) + pictureService.findAllByLetter(letter) .takeIf { it.isNotEmpty() } ?.forEach { picture -> s3Service.deleteFile(picture.pictureUrl) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt new file mode 100644 index 00000000..21c9284e --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -0,0 +1,41 @@ +package gomushin.backend.schedule.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.schedule.dto.response.LetterDetailResponse +import gomushin.backend.schedule.dto.response.LetterPreviewResponse +import gomushin.backend.schedule.facade.ReadLetterFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "편지 조회", description = "ReadLetterController") +class ReadLetterController( + private val readLetterFacade: ReadLetterFacade +) { + + @GetMapping(ApiPath.LETTERS_BY_SCHEDULE) + @Operation(summary = "특정 일정의 편지 리스트 가져오기", description = "getLetterList") + fun getLetterList( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable scheduleId: Long, + ): ApiResponse> { + val letters = readLetterFacade.getList(customUserDetails, scheduleId) + return ApiResponse.success(letters) + } + + @GetMapping(ApiPath.LETTER) + @Operation(summary = "특정 편지 가져오기", description = "getLetter") + fun getLetter( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable scheduleId: Long, + @PathVariable letterId: Long, + ): ApiResponse { + val letter = readLetterFacade.get(customUserDetails, scheduleId, letterId) + return ApiResponse.success(letter) + } +} From cb8bfb2a63f02301cf532b8711f1d82379680c96 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 18:21:49 +0900 Subject: [PATCH 171/357] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20repository=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/repository/CommentRepository.kt | 4 +++- .../backend/schedule/domain/repository/LetterRepository.kt | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt index a519ddd2..381dfa7e 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt @@ -6,7 +6,9 @@ import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param -interface CommentRepository: JpaRepository { +interface CommentRepository : JpaRepository { + fun findAllByLetterId(letterId: Long): List + @Modifying @Query("DELETE FROM Comment c WHERE c.authorId = :authorId") fun deleteAllByAuthorId(@Param("authorId") authorId: Long) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index 1c6d4429..0efe2f44 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -7,6 +7,10 @@ import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param interface LetterRepository : JpaRepository { + fun findByCoupleIdAndScheduleId(coupleId: Long, scheduleId: Long): List + + fun findByCoupleIdAndScheduleIdAndId(coupleId: Long, scheduleId: Long, letterId: Long): Letter? + @Query("SELECT l.id FROM Letter l WHERE l.authorId = :authorId") fun findAllByAuthorId(@Param("authorId") authorId: Long): List From 64866b2cecd7c1875a95c4da988b67fb0495aa5a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 18:22:29 +0900 Subject: [PATCH 172/357] =?UTF-8?q?feat:=20Picture=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/service/CommentService.kt | 6 ++++++ .../backend/schedule/domain/service/PictureService.kt | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt index b9e1efb9..4ec56a5b 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt @@ -2,6 +2,7 @@ package gomushin.backend.schedule.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.schedule.domain.entity.Comment +import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.repository.CommentRepository import gomushin.backend.schedule.dto.request.UpsertCommentRequest import org.springframework.data.repository.findByIdOrNull @@ -37,6 +38,11 @@ class CommentService( ) } + @Transactional(readOnly = true) + fun findAllByLetter(letter: Letter): List { + return commentRepository.findAllByLetterId(letter.id) + } + @Transactional(readOnly = true) fun getById(id: Long): Comment { return findById(id) ?: throw BadRequestException("sarangggun.comment.not-found") diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt index b48e6eb1..16da3566 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt @@ -1,5 +1,6 @@ package gomushin.backend.schedule.domain.service +import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.entity.Picture import gomushin.backend.schedule.domain.repository.PictureRepository import org.springframework.stereotype.Service @@ -22,8 +23,13 @@ class PictureService( } @Transactional(readOnly = true) - fun findAllByLetterId(letterId: Long): List { - return pictureRepository.findAllByLetterId(letterId) + fun findFirstByLetterId(letterId: Long): Picture? { + return pictureRepository.findFirstByLetterIdOrderByIdAsc(letterId) + } + + @Transactional(readOnly = true) + fun findAllByLetter(letter: Letter): List { + return pictureRepository.findAllByLetterId(letter.id) } @Transactional From 0a88e6e012f0a5ee3cdf344cc68a1d80c2119ed9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 18:22:51 +0900 Subject: [PATCH 173/357] =?UTF-8?q?feat:=20Picture=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=82=AC=EC=A7=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/repository/PictureRepository.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt index a0cdfe74..09e18afb 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt @@ -6,10 +6,11 @@ import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param -interface PictureRepository: JpaRepository { +interface PictureRepository : JpaRepository { fun findAllByPictureUrlIn(urls: List): List fun deleteAllByLetterId(letterId: Long) fun findAllByLetterId(letterId: Long): List + fun findFirstByLetterIdOrderByIdAsc(letterId: Long): Picture? @Query("SELECT p FROM Picture p WHERE p.letterId IN :letterIds") fun findAllByLetterIdIn(@Param("letterIds") letterIds: List): List From 3233d25bf1618f76b742d9a66b6556b1d1a1ec0e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 18:23:04 +0900 Subject: [PATCH 174/357] =?UTF-8?q?feat:=20=EC=9D=91=EB=8B=B5=20dto=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 --- .../schedule/dto/response/CommentResponse.kt | 22 +++++++++++++ .../dto/response/LetterDetailResponse.kt | 21 ++++++++++++ .../dto/response/LetterPreviewResponse.kt | 33 +++++++++++++++++++ .../schedule/dto/response/LetterResponse.kt | 24 ++++++++++++++ .../schedule/dto/response/PictureResponse.kt | 20 +++++++++++ 5 files changed, 120 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/CommentResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/LetterResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/PictureResponse.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/CommentResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/CommentResponse.kt new file mode 100644 index 00000000..aadecea8 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/CommentResponse.kt @@ -0,0 +1,22 @@ +package gomushin.backend.schedule.dto.response + +import gomushin.backend.schedule.domain.entity.Comment +import java.time.LocalDateTime + +data class CommentResponse( + val id: Long, + val content: String, + val author: String, + val createdAt: LocalDateTime, +) { + companion object { + fun of(comment: Comment): CommentResponse { + return CommentResponse( + id = comment.id, + content = comment.content, + author = comment.nickname, + createdAt = comment.createdAt, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt new file mode 100644 index 00000000..e21a77ed --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt @@ -0,0 +1,21 @@ +package gomushin.backend.schedule.dto.response + +data class LetterDetailResponse( + val letter: LetterResponse, + val pictures: List, + val comments: List, +) { + companion object { + fun of( + letter: LetterResponse, + pictures: List, + comments: List, + ): LetterDetailResponse { + return LetterDetailResponse( + letter = letter, + pictures = pictures, + comments = comments, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt new file mode 100644 index 00000000..b5ebaef5 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt @@ -0,0 +1,33 @@ +package gomushin.backend.schedule.dto.response + +import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.entity.Picture +import java.time.LocalDateTime + +data class LetterPreviewResponse( + val letterId: Long?, + val title: String?, + val content: String?, + val pictureUrl: String?, + val createdAt: LocalDateTime?, +) { + companion object { + fun of( + letter: Letter?, + picture: Picture? + ): LetterPreviewResponse { + val previewContent = if (letter?.content != null && letter.content.length > 30) { + letter.content.take(27) + "..." + } else { + letter?.content + } + return LetterPreviewResponse( + letterId = letter?.id, + title = letter?.title, + content = previewContent, + pictureUrl = picture?.pictureUrl, + createdAt = letter?.createdAt + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterResponse.kt new file mode 100644 index 00000000..01e03d7d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterResponse.kt @@ -0,0 +1,24 @@ +package gomushin.backend.schedule.dto.response + +import gomushin.backend.schedule.domain.entity.Letter +import java.time.LocalDateTime + +data class LetterResponse( + val id: Long, + val title: String, + val content: String, + val author: String, + val createdAt: LocalDateTime, +) { + companion object { + fun of(letter: Letter): LetterResponse { + return LetterResponse( + id = letter.id, + title = letter.title, + content = letter.content, + author = letter.author, + createdAt = letter.createdAt, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/PictureResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/PictureResponse.kt new file mode 100644 index 00000000..81002414 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/PictureResponse.kt @@ -0,0 +1,20 @@ +package gomushin.backend.schedule.dto.response + +import gomushin.backend.schedule.domain.entity.Picture + +data class PictureResponse( + val id: Long, + val pictureUrl: String, + val letterId: Long, +) { + companion object { + fun of(picture: Picture): PictureResponse { + return PictureResponse( + id = picture.id, + pictureUrl = picture.pictureUrl, + letterId = picture.letterId, + ) + } + } +} + From edad9efe61ff3298d7f510dac8060512482e34c7 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 18:23:19 +0900 Subject: [PATCH 175/357] =?UTF-8?q?refactor:=20=EB=A7=A4=EC=A7=81=EB=84=98?= =?UTF-8?q?=EB=B2=84=20=EC=83=81=EC=88=98=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/LetterPreviewResponse.kt | 7 +- .../domain/facade/ReadLetterFacadeTest.kt | 79 +++++++++++++++++++ .../facade/UpsertAndDeleteLetterFacadeTest.kt | 10 ++- 3 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt index b5ebaef5..3735f424 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt @@ -12,12 +12,15 @@ data class LetterPreviewResponse( val createdAt: LocalDateTime?, ) { companion object { + private const val MAX_CONTENT_LENGTH = 30 + private const val PREVIEW_CONTENT_LENGTH = 27 + fun of( letter: Letter?, picture: Picture? ): LetterPreviewResponse { - val previewContent = if (letter?.content != null && letter.content.length > 30) { - letter.content.take(27) + "..." + val previewContent = if (letter?.content != null && letter.content.length > MAX_CONTENT_LENGTH) { + letter.content.take(PREVIEW_CONTENT_LENGTH) + "..." } else { letter?.content } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt new file mode 100644 index 00000000..a1657cab --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt @@ -0,0 +1,79 @@ +package gomushin.backend.schedule.domain.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.entity.Picture +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.domain.service.CommentService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.facade.ReadLetterFacade +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import kotlin.test.Test + +@ExtendWith(MockitoExtension::class) +class ReadLetterFacadeTest { + + @Mock + lateinit var letterService: LetterService + + @Mock + lateinit var scheduleService: ScheduleService + + @Mock + lateinit var commentService: CommentService + + @Mock + lateinit var pictureService: PictureService + + @InjectMocks + lateinit var readLetterFacade: ReadLetterFacade + + + @DisplayName("getList - 성공") + @Test + fun getList_success() { + // given + val scheduleId = 1L + val customUserDetails = mock(CustomUserDetails::class.java) + val schedule = mock(Schedule::class.java) + val letter = mock(Letter::class.java) + + // when + `when`(customUserDetails.getCouple()).thenReturn(mock(Couple::class.java)) + `when`(scheduleService.getById(scheduleId)).thenReturn(schedule) + `when`( + letterService.findByCoupleAndSchedule( + customUserDetails.getCouple(), + schedule + ) + ).thenReturn( + listOf( + letter + ) + ) + `when`(pictureService.findFirstByLetterId(letter.id)).thenReturn(mock(Picture::class.java)) + + readLetterFacade.getList( + customUserDetails, + scheduleId + ) + + // then + verify(scheduleService, times(1)).getById(scheduleId) + verify(letterService, times(1)).findByCoupleAndSchedule( + customUserDetails.getCouple(), + schedule + ) + verify(pictureService, times(1)).findFirstByLetterId(letter.id) + } + + // TODO: get 테스트 작성(성공) +} diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt index 3ea98206..23606431 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt @@ -63,8 +63,9 @@ class UpsertAndDeleteLetterFacadeTest { `when`(scheduleService.getById(upsertLetterRequest.scheduleId)).thenReturn(schedule) `when`(customUserDetails.getCouple()).thenReturn(couple) `when`(customUserDetails.getCouple().id).thenReturn(1L) + `when`(customUserDetails.username).thenReturn("테스트 이름") `when`(schedule.coupleId).thenReturn(1L) - `when`(letterService.upsert(1L, upsertLetterRequest)).thenReturn(letter) + `when`(letterService.upsert(1L, customUserDetails.username, couple, upsertLetterRequest)).thenReturn(letter) `when`(s3Service.uploadFile(pictures[0])).thenReturn("http://example.com/test.jpg") doNothing().`when`(pictureService).upsert(1L, listOf("http://example.com/test.jpg")) upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) @@ -73,7 +74,7 @@ class UpsertAndDeleteLetterFacadeTest { verify( letterService, times(1) - ).upsert(1L, upsertLetterRequest) + ).upsert(1L, "테스트 이름", couple, upsertLetterRequest) } @DisplayName("업로드 성공 - 업로드된 사진이 없을 때") @@ -96,12 +97,13 @@ class UpsertAndDeleteLetterFacadeTest { `when`(scheduleService.getById(upsertLetterRequest.scheduleId)).thenReturn(schedule) `when`(customUserDetails.getCouple()).thenReturn(couple) `when`(customUserDetails.getCouple().id).thenReturn(1L) + `when`(customUserDetails.username).thenReturn("테스트 이름") `when`(schedule.coupleId).thenReturn(1L) - `when`(letterService.upsert(1L, upsertLetterRequest)).thenReturn(letter) + `when`(letterService.upsert(1L, customUserDetails.username, couple, upsertLetterRequest)).thenReturn(letter) upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) // then - verify(letterService, times(1)).upsert(1L, upsertLetterRequest) + verify(letterService, times(1)).upsert(1L, "테스트 이름", couple, upsertLetterRequest) verify(s3Service, never()).uploadFile(org.mockito.kotlin.any()) verify(pictureService, never()).upsert(org.mockito.kotlin.any(), org.mockito.kotlin.any()) } From af75dc24ea376a1115f0af8ff231ca09ac843f62 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 23:51:07 +0900 Subject: [PATCH 176/357] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=82=B4=EC=9A=A9=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(co?= =?UTF-8?q?ntetn=20->=20title)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/entity/Schedule.kt | 8 ++++---- .../domain/repository/ScheduleRepository.kt | 14 +++++++++++--- .../schedule/domain/service/ScheduleService.kt | 5 +++-- .../schedule/dto/request/UpsertScheduleRequest.kt | 6 +++--- .../dto/response/MonthlySchedulesResponse.kt | 2 +- .../schedule/domain/service/ScheduleServiceTest.kt | 2 +- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt index d4906928..24bde287 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/entity/Schedule.kt @@ -18,8 +18,8 @@ class Schedule( @Column(name = "user_id", nullable = false) val userId: Long = 0L, - @Column(name = "content", nullable = false) - var content: String, + @Column(name = "title", nullable = false) + var title: String, @Column(name = "start_date", nullable = false) var startDate: LocalDateTime, @@ -46,7 +46,7 @@ class Schedule( fun of( coupleId: Long, userId: Long, - content: String, + title: String, startDate: LocalDateTime, endDate: LocalDateTime, fatigue: String, @@ -55,7 +55,7 @@ class Schedule( return Schedule( coupleId = coupleId, userId = userId, - content = content, + title = title, startDate = startDate, endDate = endDate, fatigue = fatigue, diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt index 0d2512dc..5c95bdda 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt @@ -1,6 +1,7 @@ package gomushin.backend.schedule.domain.repository import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.dto.response.DailyScheduleResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying @@ -16,7 +17,7 @@ interface ScheduleRepository : JpaRepository { @Query( """ SELECT new gomushin.backend.schedule.dto.response.MonthlySchedulesResponse( - s.content, + s.title, s.startDate, s.endDate, s.fatigue @@ -31,11 +32,18 @@ interface ScheduleRepository : JpaRepository { @Query( """ - SELECT s + SELECT new gomushin.backend.schedule.dto.response.DailyScheduleResponse( + s.id, + s.title, + s.fatigue, + s.startDate, + s.endDate, + s.isAllDay + ) FROM Schedule s WHERE s.coupleId = :coupleId AND function('DATE', s.startDate) = :startDate """ ) - fun findByCoupleIdAndStartDate(coupleId: Long, startDate: LocalDate): List + fun findByCoupleIdAndStartDate(coupleId: Long, startDate: LocalDate): List } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt index c0264a52..ba784400 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.ScheduleRepository import gomushin.backend.schedule.dto.request.UpsertScheduleRequest +import gomushin.backend.schedule.dto.response.DailyScheduleResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -21,7 +22,7 @@ class ScheduleService( } @Transactional(readOnly = true) - fun findByDate(couple: Couple, date: LocalDate): List { + fun findByDate(couple: Couple, date: LocalDate): List { return scheduleRepository.findByCoupleIdAndStartDate(couple.id, date) } @@ -31,7 +32,7 @@ class ScheduleService( getById(id).let { it.startDate = upsertScheduleRequest.startDate it.endDate = upsertScheduleRequest.endDate - it.content = upsertScheduleRequest.content + it.title = upsertScheduleRequest.title it.fatigue = upsertScheduleRequest.fatigue it.isAllDay = upsertScheduleRequest.isAllDay } diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertScheduleRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertScheduleRequest.kt index e174b247..fbad5c1b 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertScheduleRequest.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertScheduleRequest.kt @@ -7,8 +7,8 @@ import java.time.LocalDateTime data class UpsertScheduleRequest( @Schema(description = "일정 ID(새로 생성 시 null, 업데이트 시 id)", example = "1") val id: Long?, - @Schema(description = "일정 내용", example = "훈련") - val content: String, + @Schema(description = "일정 제목", example = "훈련") + val title: String, @Schema(description = "일정 시작 시간", example = "2023-10-01T10:00:00") val startDate: LocalDateTime, @Schema(description = "일정 종료 시간", example = "2023-10-01T12:00:00") @@ -21,7 +21,7 @@ data class UpsertScheduleRequest( fun toEntity(coupleId: Long, userId: Long) = Schedule( coupleId = coupleId, userId = userId, - content = content, + title = title, startDate = startDate, endDate = endDate, fatigue = fatigue, diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt index 4adeeb1b..dcb98f5f 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt @@ -3,7 +3,7 @@ package gomushin.backend.schedule.dto.response import java.time.LocalDateTime data class MonthlySchedulesResponse( - val content: String, + val title: String, val startDate: LocalDateTime, val endDate: LocalDateTime, val fatigue: String, diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt index c1bcf592..ed0569ac 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt @@ -71,7 +71,7 @@ class ScheduleServiceTest { userId = userId, startDate = LocalDateTime.of(2023, 1, 1, 0, 0), endDate = LocalDateTime.of(2023, 1, 2, 0, 0), - content = "Test Schedule", + title = "Test Schedule", isAllDay = false, fatigue = "VERY_TIRED", ) From eecfa017df685c8ef76e896ced2c24fb6defef50 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 27 Apr 2025 23:52:16 +0900 Subject: [PATCH 177/357] =?UTF-8?q?refactor:=20=EC=9B=94=20=EB=B3=84=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=9D=91=EB=8B=B5=20=EC=8B=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=85=90=EC=9D=BC=20=EC=A0=9C=EC=99=B8,=20=EC=9D=BC=20?= =?UTF-8?q?=EB=B3=84=20=EC=9D=BC=EC=A0=95=20=EC=9D=91=EB=8B=B5=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B8=B0=EB=85=90=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AnniversaryRepository.kt | 22 +++++++++++++++++-- .../domain/service/AnniversaryService.kt | 7 ++++++ .../dto/response/DailyAnniversaryResponse.kt | 9 ++++++++ .../dto/response/DailyScheduleResponse.kt | 12 ++++++++++ .../DailySchedulesAndAnniversariesResponse.kt | 15 +++++++++++++ ...onthlySchedulesAndAnniversariesResponse.kt | 4 ++-- .../schedule/facade/ReadScheduleFacade.kt | 8 ++++--- .../presentation/ReadScheduleController.kt | 4 ++-- .../domain/facade/ReadScheduleFacadeTest.kt | 4 ++-- 9 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/DailyAnniversaryResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/DailyScheduleResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/DailySchedulesAndAnniversariesResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index 25961f18..52df4583 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -2,10 +2,12 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse +import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param +import java.time.LocalDate interface AnniversaryRepository : JpaRepository { @Modifying @@ -15,8 +17,9 @@ interface AnniversaryRepository : JpaRepository { WHERE (a.title LIKE '%일' OR a.title LIKE '%주년') AND a.anniversaryProperty = 0 And a.coupleId = :coupleId -""") - fun deleteAnniversariesWithTitleEndingAndPropertyZero(@Param("coupleId")coupleId : Long) +""" + ) + fun deleteAnniversariesWithTitleEndingAndPropertyZero(@Param("coupleId") coupleId: Long) @Modifying @Query("DELETE FROM Anniversary a WHERE a.coupleId = :coupleId") @@ -35,4 +38,19 @@ interface AnniversaryRepository : JpaRepository { """ ) fun findByCoupleIdAndYearAndMonth(coupleId: Long, year: Int, month: Int): List + + @Query( + """ + SELECT new gomushin.backend.schedule.dto.response.DailyAnniversaryResponse( + a.id, + a.title, + a.anniversaryDate + ) + FROM Anniversary a + WHERE a.coupleId = :coupleId + AND function('DATE', a.anniversaryDate) = :startDate + """ + ) + fun findByCoupleIdAndDate(coupleId: Long, date: LocalDate): List + } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index d67e0138..b1445569 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -6,8 +6,10 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse +import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate @Service class AnniversaryService( @@ -21,6 +23,11 @@ class AnniversaryService( return anniversaryRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) } + @Transactional(readOnly = true) + fun findByDate(couple: Couple, date: LocalDate): List { + return anniversaryRepository.findByCoupleIdAndDate(couple.id, date) + } + @Transactional fun registerAnniversary( userId: Long, diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyAnniversaryResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyAnniversaryResponse.kt new file mode 100644 index 00000000..ac381f03 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyAnniversaryResponse.kt @@ -0,0 +1,9 @@ +package gomushin.backend.schedule.dto.response + +import java.time.LocalDate + +data class DailyAnniversaryResponse( + val id: Long, + val title: String, + val anniversaryDate: LocalDate, +) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyScheduleResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyScheduleResponse.kt new file mode 100644 index 00000000..547ee9d2 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyScheduleResponse.kt @@ -0,0 +1,12 @@ +package gomushin.backend.schedule.dto.response + +import java.time.LocalDateTime + +data class DailyScheduleResponse( + val id: Long, + val title: String, + val fatigue: String, + val startDate: LocalDateTime, + val endDate: LocalDateTime, + val isAllDay: Boolean +) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/DailySchedulesAndAnniversariesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/DailySchedulesAndAnniversariesResponse.kt new file mode 100644 index 00000000..f1d1cdb3 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/DailySchedulesAndAnniversariesResponse.kt @@ -0,0 +1,15 @@ +package gomushin.backend.schedule.dto.response + +data class DailySchedulesAndAnniversariesResponse( + val schedules: List, + val anniversaries: List, +) { + companion object { + fun of( + schedules: List, + anniversaries: List, + ): DailySchedulesAndAnniversariesResponse { + return DailySchedulesAndAnniversariesResponse(schedules, anniversaries) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt index c21e2f42..647563e9 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt @@ -4,14 +4,14 @@ import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse data class MonthlySchedulesAndAnniversariesResponse( val schedules: List, - val anniversaries: List, +// val anniversaries: List, ) { companion object { fun of( schedules: List, anniversaries: List, ): MonthlySchedulesAndAnniversariesResponse { - return MonthlySchedulesAndAnniversariesResponse(schedules, anniversaries) + return MonthlySchedulesAndAnniversariesResponse(schedules) } } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index 6570a70c..c7ea8a72 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -2,8 +2,8 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.service.AnniversaryService -import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.dto.response.DailySchedulesAndAnniversariesResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse import org.springframework.stereotype.Component import java.time.LocalDate @@ -25,7 +25,9 @@ class ReadScheduleFacade( return MonthlySchedulesAndAnniversariesResponse.of(monthlySchedules, monthlyAnniversaries) } - fun get(customUserDetails: CustomUserDetails, date: LocalDate): List { - return scheduleService.findByDate(customUserDetails.getCouple(), date) + fun get(customUserDetails: CustomUserDetails, date: LocalDate): DailySchedulesAndAnniversariesResponse { + val dailySchedules = scheduleService.findByDate(customUserDetails.getCouple(), date) + val dailyAnniversaries = anniversaryService.findByDate(customUserDetails.getCouple(), date) + return DailySchedulesAndAnniversariesResponse.of(dailySchedules, dailyAnniversaries) } } diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt index fe546c8c..bff0b060 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt @@ -2,7 +2,7 @@ package gomushin.backend.schedule.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.dto.response.DailySchedulesAndAnniversariesResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse import gomushin.backend.schedule.facade.ReadScheduleFacade import io.swagger.v3.oas.annotations.Operation @@ -35,7 +35,7 @@ class ReadScheduleController( fun getSchedule( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @RequestParam date: LocalDate, - ): ApiResponse> { + ): ApiResponse { val schedules = readScheduleFacade.get(customUserDetails, date) return ApiResponse.success(schedules) } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt index 98484673..d942ff41 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt @@ -4,8 +4,8 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse -import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.dto.response.DailyScheduleResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse import gomushin.backend.schedule.facade.ReadScheduleFacade import org.junit.jupiter.api.BeforeEach @@ -63,7 +63,7 @@ class ReadScheduleFacadeTest { fun get_success() { // given val date = LocalDate.of(2025, 4, 1) - val mockSchedules = listOf(mock(Schedule::class.java)) + val mockSchedules = listOf(mock(DailyScheduleResponse::class.java)) // when `when`(scheduleService.findByDate(customUserDetails.getCouple(), date)).thenReturn(mockSchedules) From d81288111161aa71e810faaeaeca26406ac93b84 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Apr 2025 13:48:31 +0900 Subject: [PATCH 178/357] =?UTF-8?q?feat:=20=EB=82=98=EC=97=90=EA=B2=8C=20?= =?UTF-8?q?=EC=98=A8=20=ED=8E=B8=EC=A7=80=20API=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/schedule/presentation/ApiPath.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt index 7588d5ae..66e28b1e 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt @@ -7,6 +7,7 @@ object ApiPath { const val LETTERS = "/v1/schedules/letters" const val LETTER = "/v1/schedules/{scheduleId}/letters/{letterId}" + const val LETTERS_TO_ME = "/v1/schedules/letters/to-me" const val LETTERS_BY_SCHEDULE = "/v1/schedules/{scheduleId}" const val COMMENTS = "/v1/schedules/letters/{letterId}/comments" From 2aabe672c422cae42e51da2727d8488c4766a558 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Apr 2025 13:49:25 +0900 Subject: [PATCH 179/357] =?UTF-8?q?feat:=20key=20offset=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20dto=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 --- .../backend/core/common/web/PageResponse.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt diff --git a/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt b/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt new file mode 100644 index 00000000..dcc0d96b --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt @@ -0,0 +1,34 @@ +package gomushin.backend.core.common.web + +import io.swagger.v3.oas.annotations.media.Schema + +data class PageResponse( + @Schema(description = "페이지 응답") + val data: List?, + @Schema(description = "다음 페이지를 위한 커서 키") + val after: Long?, + @Schema(description = "데이터 수") + val count: Int, + @Schema(description = "다음 페이지 URL") + val next: String?, + @Schema(description = "마지막 페이지 여부") + val isLastPage: Boolean, +) { + companion object { + fun of( + data: List, + after: Long?, + count: Int, + next: String?, + isLastPage: Boolean, + ): PageResponse { + return PageResponse( + data = data, + after = after, + count = count, + next = next, + isLastPage = isLastPage + ) + } + } +} From b2f3058c6efab95c6264f9beebba5734082fd537 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Apr 2025 13:49:47 +0900 Subject: [PATCH 180/357] =?UTF-8?q?feat:=20=EB=82=98=EC=97=90=EA=B2=8C=20?= =?UTF-8?q?=EC=98=A8=20=ED=8E=B8=EC=A7=80=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ReadLettersToMePaginationRequest.kt | 7 +++++++ .../schedule/presentation/ReadLetterController.kt | 13 +++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/request/ReadLettersToMePaginationRequest.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/request/ReadLettersToMePaginationRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/request/ReadLettersToMePaginationRequest.kt new file mode 100644 index 00000000..5a2260e6 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/request/ReadLettersToMePaginationRequest.kt @@ -0,0 +1,7 @@ +package gomushin.backend.schedule.dto.request + +data class ReadLettersToMePaginationRequest( + val key: Long = Long.MAX_VALUE, + val orderCreatedAt: String = "DESC", + val take: Long = 10L, +) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index 21c9284e..be958b32 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -1,12 +1,15 @@ package gomushin.backend.schedule.presentation import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.PageResponse import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.schedule.dto.request.ReadLettersToMePaginationRequest import gomushin.backend.schedule.dto.response.LetterDetailResponse import gomushin.backend.schedule.dto.response.LetterPreviewResponse import gomushin.backend.schedule.facade.ReadLetterFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.springdoc.core.annotations.ParameterObject import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -38,4 +41,14 @@ class ReadLetterController( val letter = readLetterFacade.get(customUserDetails, scheduleId, letterId) return ApiResponse.success(letter) } + + @GetMapping(ApiPath.LETTERS_TO_ME) + @Operation(summary = "내가 받은 편지 리스트 가져오기", description = "getLetterListToMe") + fun getLetterListToMe( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @ParameterObject readLettersToMePaginationRequest: ReadLettersToMePaginationRequest + ): ApiResponse> { + val letters = readLetterFacade.getLetterListToMe(customUserDetails,readLettersToMePaginationRequest) + return ApiResponse.success(letters) + } } From 86feb0849bea6b6cfab6bf8b28548aebfd6dbd13 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Apr 2025 13:50:02 +0900 Subject: [PATCH 181/357] =?UTF-8?q?feat:=20=ED=82=A4=20=EC=98=A4=ED=94=84?= =?UTF-8?q?=EC=85=8B=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/LetterRepository.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index 0efe2f44..5e4ed116 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -17,4 +17,24 @@ interface LetterRepository : JpaRepository { @Modifying @Query("DELETE FROM Letter l WHERE l.authorId = :authorId") fun deleteAllByAuthorId(@Param("authorId") authorId: Long) + + @Query( + """ + SELECT * + FROM Letter l + WHERE l.couple_id = :coupleId + AND l.author_id = :partnerPk + AND l.id <:key + ORDER BY l.created_at DESC + LIMIT :take + """, + nativeQuery = true + ) + fun findByLettersToMe( + @Param("coupleId") coupleId: Long, + @Param("partnerPk") partnerPk: Long, + @Param("key") key: Long, + @Param("take") take: Long, + @Param("orderCreatedAt") orderCreatedAt: String, + ): List } From c49dbed8a285b1d5cf18ea3f3c4a760cc8c89d63 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Apr 2025 13:50:09 +0900 Subject: [PATCH 182/357] =?UTF-8?q?feat:=20=ED=82=A4=20=EC=98=A4=ED=94=84?= =?UTF-8?q?=EC=85=8B=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/domain/service/LetterService.kt | 21 ++++++++- .../schedule/facade/ReadLetterFacade.kt | 43 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index b2371433..34bd3f57 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.LetterRepository +import gomushin.backend.schedule.dto.request.ReadLettersToMePaginationRequest import gomushin.backend.schedule.dto.request.UpsertLetterRequest import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -65,13 +66,29 @@ class LetterService( fun delete(letterId: Long) { letterRepository.deleteById(letterId) } + @Transactional - fun deleteAllByMemberId(memberId : Long) { + fun deleteAllByMemberId(memberId: Long) { letterRepository.deleteAllByAuthorId(memberId) } @Transactional(readOnly = true) - fun findAllByAuthorId(memberId: Long) : List{ + fun findAllByAuthorId(memberId: Long): List { return letterRepository.findAllByAuthorId(memberId) } + + @Transactional(readOnly = true) + fun findArrivedToMe( + couple: Couple, + partnerPk: Long, + readLettersToMePaginationRequest: ReadLettersToMePaginationRequest + ): List { + return letterRepository.findByLettersToMe( + couple.id, + partnerPk, + readLettersToMePaginationRequest.key, + readLettersToMePaginationRequest.take, + readLettersToMePaginationRequest.orderCreatedAt + ) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 26bac8c1..2cf28340 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -1,11 +1,14 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.PageResponse import gomushin.backend.schedule.domain.service.CommentService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.dto.request.ReadLettersToMePaginationRequest import gomushin.backend.schedule.dto.response.* +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component @Component @@ -14,8 +17,11 @@ class ReadLetterFacade( private val scheduleService: ScheduleService, private val pictureService: PictureService, private val commentService: CommentService, + @Value("\${server.url}") + private val baseUrl: String, ) { + fun getList( customUserDetails: CustomUserDetails, scheduleId: Long, @@ -60,4 +66,41 @@ class ReadLetterFacade( comments = commentResponses, ) } + + fun getLetterListToMe( + customUserDetails: CustomUserDetails, + readLettersToMePaginationRequest: ReadLettersToMePaginationRequest, + ): PageResponse { + + val partnerPk = if (customUserDetails.getCouple().invitorId == customUserDetails.getId()) { + customUserDetails.getCouple().inviteeId + } else { + customUserDetails.getCouple().invitorId + } + + val letters = letterService.findArrivedToMe(customUserDetails.getCouple(), partnerPk, readLettersToMePaginationRequest) + + val letterPreviewResponses = letters.map { letter -> + val picture = letter?.let { pictureService.findFirstByLetterId(it.id) } + LetterPreviewResponse.of(letter, picture) + } + + val isLastPage = letterPreviewResponses.size < readLettersToMePaginationRequest.take + + val hasData = letterPreviewResponses.isNotEmpty() + + val nextUrl = if (!isLastPage && hasData) { + "${baseUrl}/v1/schedules/letters/to-me?key=${letterPreviewResponses.last().letterId}&orderCreatedAt=DESC&take=${readLettersToMePaginationRequest.take}" + } else { + null + } + + return PageResponse.of( + data = letterPreviewResponses, + after = if (hasData) letterPreviewResponses.last().letterId else null, + count = letterPreviewResponses.size, + next = nextUrl, + isLastPage = isLastPage + ) + } } From c2ac49533d95aedab8f352629d0f0b5351b31680 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Apr 2025 17:35:29 +0900 Subject: [PATCH 183/357] =?UTF-8?q?test:=20ReadFacadeTest=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9E=90=20=ED=98=B8=EC=B6=9C=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=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 --- .../domain/facade/ReadLetterFacadeTest.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt index a1657cab..becd7d1a 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt @@ -10,9 +10,9 @@ import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.facade.ReadLetterFacade +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension @@ -33,10 +33,21 @@ class ReadLetterFacadeTest { @Mock lateinit var pictureService: PictureService - @InjectMocks lateinit var readLetterFacade: ReadLetterFacade + @BeforeEach + fun setUp() { + val baseUrl = "http://localhost:8080" + readLetterFacade = ReadLetterFacade( + letterService, + scheduleService, + pictureService, + commentService, + baseUrl + ) + } + @DisplayName("getList - 성공") @Test fun getList_success() { From 96e0c1887466b4a7e7c9c85e65876a23649b7e2c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 28 Apr 2025 17:52:55 +0900 Subject: [PATCH 184/357] =?UTF-8?q?refactor:=20=EB=84=A4=EC=9D=B4=ED=8B=B0?= =?UTF-8?q?=EB=B8=8C=20=EC=BF=BC=EB=A6=AC=EC=97=90=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/repository/LetterRepository.kt | 1 - .../gomushin/backend/schedule/domain/service/LetterService.kt | 1 - .../backend/schedule/domain/facade/ReadLetterFacadeTest.kt | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index 5e4ed116..3959688b 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -35,6 +35,5 @@ interface LetterRepository : JpaRepository { @Param("partnerPk") partnerPk: Long, @Param("key") key: Long, @Param("take") take: Long, - @Param("orderCreatedAt") orderCreatedAt: String, ): List } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index 34bd3f57..66ae82c1 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -88,7 +88,6 @@ class LetterService( partnerPk, readLettersToMePaginationRequest.key, readLettersToMePaginationRequest.take, - readLettersToMePaginationRequest.orderCreatedAt ) } } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt index becd7d1a..651a76b3 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt @@ -33,7 +33,7 @@ class ReadLetterFacadeTest { @Mock lateinit var pictureService: PictureService - lateinit var readLetterFacade: ReadLetterFacade + private lateinit var readLetterFacade: ReadLetterFacade @BeforeEach From 1cbd82d4c83b887eb9903b3b8f57178cb02793c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 28 Apr 2025 21:59:08 +0900 Subject: [PATCH 185/357] =?UTF-8?q?feat=20:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleInfoService.kt | 6 ++++++ .../dto/response/CoupleEmotionResponse.kt | 11 +++++++++++ .../backend/couple/facade/CoupleFacade.kt | 10 ++++++---- .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/CoupleInfoController.kt | 18 ++++++++++-------- 5 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/CoupleEmotionResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index a5cd0df5..c54aca3d 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -131,4 +131,10 @@ class CoupleInfoService( ) anniversaryRepository.saveAll(anniversaries) } + + @Transactional(readOnly = true) + fun getEmotion(id: Long): Int { + val coupleMember = findCoupleMember(id) + return coupleMember.emotion!! + } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleEmotionResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleEmotionResponse.kt new file mode 100644 index 00000000..31d66360 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleEmotionResponse.kt @@ -0,0 +1,11 @@ +package gomushin.backend.couple.dto.response + +data class CoupleEmotionResponse ( + val emotion : Int +) { + companion object{ + fun of(emotion: Int) = CoupleEmotionResponse( + emotion + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index d9d20f17..e3e61a0b 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -9,10 +9,7 @@ import gomushin.backend.couple.domain.service.CoupleService import gomushin.backend.couple.dto.request.CoupleConnectRequest import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest -import gomushin.backend.couple.dto.response.CoupleGradeResponse -import gomushin.backend.couple.dto.response.DdayResponse -import gomushin.backend.couple.dto.response.NicknameResponse -import gomushin.backend.couple.dto.response.StatusMessageResponse +import gomushin.backend.couple.dto.response.* import org.springframework.stereotype.Component @Component @@ -70,4 +67,9 @@ class CoupleFacade( val couple = coupleService.getByMemberId(customUserDetails.getId()) coupleInfoService.updateRelationshipStartDate(couple, updateRelationshipStartDateRequest) } + + fun getCoupleEmotion(customUserDetails: CustomUserDetails): CoupleEmotionResponse { + val emotion = coupleInfoService.getEmotion(customUserDetails.getId()) + return CoupleEmotionResponse.of(emotion) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index 5e027860..a4f913d5 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -11,4 +11,5 @@ object ApiPath { const val COUPLE_STATUS_MESSAGE = "/v1/couple/status-message" const val COUPLE_UPDATE_MILITARY_DATE = "/v1/couple/military-date" const val COUPLE_UPDATE_RELATIONSHIP_DATE = "/v1/couple/relationship-start-date" + const val COUPLE_EMOJI = "/v1/couple/emotion" } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index a1d2a16e..d0acaa44 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -2,20 +2,13 @@ package gomushin.backend.couple.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest -import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest +import gomushin.backend.couple.dto.response.* import gomushin.backend.couple.facade.CoupleFacade -import gomushin.backend.couple.dto.response.CoupleGradeResponse -import gomushin.backend.couple.dto.response.DdayResponse -import gomushin.backend.couple.dto.response.NicknameResponse -import gomushin.backend.couple.dto.response.StatusMessageResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @@ -69,4 +62,13 @@ class CoupleInfoController ( ):ApiResponse{ return ApiResponse.success(coupleFacade.statusMessage(customUserDetails)) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.COUPLE_EMOJI) + @Operation(summary = "커플 이모지 조회 api", description = "getCoupleEmotion") + fun getCoupleEmotion( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ):ApiResponse{ + return ApiResponse.success(coupleFacade.getCoupleEmotion(customUserDetails)) + } } \ No newline at end of file From c47ddaff366e4589800d41feb630c136771d0eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 28 Apr 2025 22:06:06 +0900 Subject: [PATCH 186/357] =?UTF-8?q?chore=20:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=97=86=EC=9D=84=20=EB=95=8C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/couple/domain/service/CoupleInfoService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index c54aca3d..ab2f93e6 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -135,6 +135,6 @@ class CoupleInfoService( @Transactional(readOnly = true) fun getEmotion(id: Long): Int { val coupleMember = findCoupleMember(id) - return coupleMember.emotion!! + return coupleMember.emotion ?: throw BadRequestException("sarangggun.member.not-exist-emoji") } } \ No newline at end of file From 6a0f56501d13b416ad207342ff47c1fa138c0cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 28 Apr 2025 22:11:40 +0900 Subject: [PATCH 187/357] =?UTF-8?q?test=20:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EC=9D=B4=EB=AA=A8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/CoupleInfoService.kt | 3 +- .../backend/couple/facade/CoupleFacade.kt | 2 +- .../domain/service/CoupleInfoServiceTest.kt | 35 +++++++++++++++++++ .../backend/member/facade/CoupleFacadeTest.kt | 9 +++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index ab2f93e6..a9e137c5 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -5,7 +5,6 @@ import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.domain.repository.CoupleRepository -import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.couple.dto.response.DdayResponse @@ -133,7 +132,7 @@ class CoupleInfoService( } @Transactional(readOnly = true) - fun getEmotion(id: Long): Int { + fun getCoupleEmotion(id: Long): Int { val coupleMember = findCoupleMember(id) return coupleMember.emotion ?: throw BadRequestException("sarangggun.member.not-exist-emoji") } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index e3e61a0b..40a6d80a 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -69,7 +69,7 @@ class CoupleFacade( } fun getCoupleEmotion(customUserDetails: CustomUserDetails): CoupleEmotionResponse { - val emotion = coupleInfoService.getEmotion(customUserDetails.getId()) + val emotion = coupleInfoService.getCoupleEmotion(customUserDetails.getId()) return CoupleEmotionResponse.of(emotion) } } diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index a9be8ac4..c5598663 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -386,4 +386,39 @@ class CoupleInfoServiceTest { verify(anniversaryRepository).saveAll(anyList()) assertEquals(couple.relationshipStartDate, updateRelationshipStartDateRequest.relationshipStartDate) } + + @DisplayName("getCoupleEmotion - 성공") + @Test + fun getCoupleEmotion(){ + //given + val coupleId = 1L + val userId = 1L + val coupleUserId = 2L + val couple = Couple( + id = coupleId, + invitorId = coupleUserId, + inviteeId = userId, + ) + val coupleUser = Member( + id = 2L, + name="김영록 여친", + nickname="김영록 여친", + email="test2@test.com", + profileImageUrl = "url2", + birthDate= LocalDate.of(2001,5,19), + provider=Provider.KAKAO, + role= Role.MEMBER, + isCouple= true, + statusMessage = "기분이 좋아용", + emotion = 2 + ) + `when`(coupleRepository.findByMemberId(userId)).thenReturn(couple) + `when`(memberRepository.findById(coupleUserId)).thenReturn(Optional.of(coupleUser)) + + //when + val emotion = coupleInfoService.getCoupleEmotion(userId) + + //then + assertEquals(coupleUser.emotion, emotion) + } } \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 18f9e89b..03c0cd3b 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -165,4 +165,13 @@ class CoupleFacadeTest { verify(coupleInfoService).updateRelationshipStartDate(customUserDetails.getCouple(), updateRelationshipStartDateRequest) } + @DisplayName("이모지 조회 - 정상응답") + @Test + fun getEmotion() { + `when`(coupleInfoService.getCoupleEmotion(customUserDetails.getId())).thenReturn(1) + val result = coupleFacade.getCoupleEmotion(customUserDetails) + verify(coupleInfoService).getCoupleEmotion(1L) + assertEquals(1, result.emotion) + } + } From 4717f74abaec2fc3409525528d3553d32a60b7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 01:20:46 +0900 Subject: [PATCH 188/357] =?UTF-8?q?feat=20:=20=EC=9D=B4=EB=AA=A8=EC=A7=80?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EC=A0=95=EA=B8=B0=EC=A0=81=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EA=B5=AC=ED=98=84=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 6 ++ .../gomushin/backend/BackendApplication.kt | 2 + .../gomushin/backend/alarm/dto/FCMMessage.kt | 17 ++++ .../backend/alarm/service/FCMService.kt | 81 +++++++++++++++++++ .../alarm/service/StatusAlarmService.kt | 64 +++++++++++++++ .../domain/service/CoupleInfoService.kt | 3 +- .../member/domain/service/MemberService.kt | 2 +- .../backend/member/facade/MemberInfoFacade.kt | 17 +++- 8 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt create mode 100644 src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt create mode 100644 src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt diff --git a/build.gradle.kts b/build.gradle.kts index 19020423..5466ff02 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -70,6 +70,12 @@ dependencies { // aws implementation("software.amazon.awssdk:s3:2.30.38") + //okhttp3 + implementation("com.squareup.okhttp3:okhttp:4.9.3") + + //google auth + implementation("com.google.auth:google-auth-library-oauth2-http:1.18.0") + implementation("org.springframework.boot:spring-boot-starter-validation") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") diff --git a/src/main/kotlin/gomushin/backend/BackendApplication.kt b/src/main/kotlin/gomushin/backend/BackendApplication.kt index ab29ecb3..3afdf3a8 100644 --- a/src/main/kotlin/gomushin/backend/BackendApplication.kt +++ b/src/main/kotlin/gomushin/backend/BackendApplication.kt @@ -3,9 +3,11 @@ package gomushin.backend import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableAsync @ConfigurationPropertiesScan @SpringBootApplication +@EnableAsync class BackendApplication fun main(args: Array) { diff --git a/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt b/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt new file mode 100644 index 00000000..9d359f9b --- /dev/null +++ b/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt @@ -0,0 +1,17 @@ +package gomushin.backend.alarm.dto + +data class FCMMessage( + val validateOnly: Boolean, + val message: Message +) { + data class Message( + val notification: Notification, + val token: String + ) + + data class Notification( + val title: String, + val body: String, + val image: String? + ) +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt new file mode 100644 index 00000000..d6184a89 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt @@ -0,0 +1,81 @@ +package gomushin.backend.alarm.service + + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.auth.oauth2.GoogleCredentials +import gomushin.backend.alarm.dto.FCMMessage +import gomushin.backend.core.infrastructure.exception.BadRequestException +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.io.ClassPathResource +import org.springframework.http.HttpHeaders +import org.springframework.stereotype.Service +import java.io.IOException + +@Service +class FCMService( + private val objectMapper: ObjectMapper, + @Value("\${fcm.api-url}") + private val API_URL : String, + @Value("\${fcm.firebase-config-path}") + private val firebaseConfigPath : String, + @Value("\${fcm.google.scope}") + private val googleScope : String +) { + private val log: Logger = LoggerFactory.getLogger(FCMService::class.java) + @Throws(IOException::class) + fun sendMessageTo(targetToken: String, title: String, body: String) { + val message = makeMessage(targetToken, title, body) + + val client = OkHttpClient() + val requestBody = message + .toRequestBody("application/json; charset=utf-8".toMediaType()) + + val request = Request.Builder() + .url(API_URL) + .post(requestBody) + .addHeader(HttpHeaders.AUTHORIZATION, "Bearer ${getAccessToken()}") + .addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8") + .build() + + client.newCall(request).execute().use { response -> + log.info("fcm 결과 : " + response.body?.string()) + } + } + + @Throws(JsonProcessingException::class) + private fun makeMessage(targetToken: String, title: String, body: String): String { + val fcmMessage = FCMMessage( + validateOnly = false, + message = FCMMessage.Message( + token = targetToken, + notification = FCMMessage.Notification( + title = title, + body = body, + image = null + ) + ) + ) + return objectMapper.writeValueAsString(fcmMessage) + } + + @Throws(IOException::class) + private fun getAccessToken(): String { + try { + val googleCredentials = GoogleCredentials + .fromStream(ClassPathResource(firebaseConfigPath).inputStream) + .createScoped(listOf(googleScope)) + + googleCredentials.refreshIfExpired() + return googleCredentials.accessToken.tokenValue + } catch (e: IOException) { + throw BadRequestException(e.message.toString()) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt new file mode 100644 index 00000000..bbf14b78 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt @@ -0,0 +1,64 @@ +package gomushin.backend.alarm.service + +import gomushin.backend.member.domain.entity.Member +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Service + +@Service +class StatusAlarmService ( + private val fcmService: FCMService +) { + private val dummyList = listOf() + private val emojiMiss = listOf( + "보고싶다는 마음이 도착했어요.+오늘은 짧은 안부라도 건네볼까요?", + "보고 싶다고 표현하는 건 용기예요.+작은 마음이 큰 위로가 될 거예요") + private val emojiTired = listOf( + "오늘 많이 힘들었나봐요.+따뜻한 말 한마디가 큰 힘이 돼요", + "오늘 피곤한 날이래요.+연인에게 따뜻한 응원 어때요?" + ) + private val emojiSad = listOf( + "서운한 마음이 들었대요.+연인과 진심 어린 대화 어때요?", + "연인의 마음이 무거운가봐요.+따뜻한 공감이 필요해요" + ) + private val emojiGood = listOf( + "기분 좋은 하루를 함께 나눠보세요.+당신의 행복이 전해질 거예요.", + "좋은 일이 있었대요!+축하해주고 같이 기뻐해주세요\uD83D\uDC9B" + ) + private val emojiAnnoy = listOf( + "연인이 짜증나는 일이 있었대요.+들어주는 것도 큰 위로가 될거예요", + "OO님 오늘 좀 힘들었나 봐요.+오늘 전화통화 어때요?" + ) + private val emojiWorry = listOf( + "당신을 걱정하는 마음이 담겼어요.+잠깐 안부를 전해주면 어떨까요?", + "연인이 당신을 걱정해요.+연인의 마음 한쪽이 조금 무거웠대요." + ) + private val emojiNothing = listOf( + "연인이 평범한 일상을 보냈대요.+안부를 전하는 일상이 관계를 지켜줄 거예요.", + "감정의 큰 파도는 없지만,+OO님의 하루를 들어줄 누군가가 있다면 좋겠대요." + ) + private val statusMessage = listOf( + dummyList, + emojiMiss, + emojiTired, + emojiSad, + emojiGood, + emojiAnnoy, + emojiWorry, + emojiNothing + ) + + @Async + fun sendStatusAlarm(sender : Member, receiver : Member, emoji : Int) { + val notificationContent = + statusMessage.get(emoji).random() + val parts = notificationContent.split("+") + val title = parts[0] + val content = if (parts[1].contains("OO")) { + parts[1].replace("OO", sender.nickname) + } else { + parts[1] + } + val token = receiver.fcmToken + fcmService.sendMessageTo(token, title, content) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index a9e137c5..d24f9559 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -90,7 +90,8 @@ class CoupleInfoService( return coupleMember.statusMessage } - private fun findCoupleMember(id : Long): Member { + @Transactional(readOnly = true) + fun findCoupleMember(id : Long): Member { val couple = coupleRepository.findByMemberId(id) ?: throw BadRequestException("saranggun.couple.not-connected") val coupleMemberId = if (couple.invitorId == id) couple.inviteeId else couple.invitorId diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 603534b2..9e029a6c 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -26,7 +26,7 @@ class MemberService( } @Transactional - fun updateMyEmotionAndStatusMessage(id: Long, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) { + fun updateMyEmotionAndStatusMessage(id: Long, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest){ val member = getById(id) member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion) member.updateStatusMessage(updateMyEmotionAndStatusMessageRequest.statusMessage) diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 1029e5a8..15de191b 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -1,6 +1,8 @@ package gomushin.backend.member.facade +import gomushin.backend.alarm.service.StatusAlarmService import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.service.NotificationService import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest @@ -16,7 +18,9 @@ import org.springframework.transaction.annotation.Transactional @Component class MemberInfoFacade( private val memberService: MemberService, - private val notificationService: NotificationService + private val notificationService: NotificationService, + private val statusAlarmService: StatusAlarmService, + private val coupleInfoService: CoupleInfoService ) { fun getMemberInfo(customUserDetails: CustomUserDetails): MyInfoResponse { val member = memberService.getById(customUserDetails.getId()) @@ -28,8 +32,15 @@ class MemberInfoFacade( return MyStatusMessageResponse.of(member) } - fun updateMyEmotionAndStatusMessage(customUserDetails: CustomUserDetails, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) - = memberService.updateMyEmotionAndStatusMessage(customUserDetails.getId(), updateMyEmotionAndStatusMessageRequest) + fun updateMyEmotionAndStatusMessage(customUserDetails: CustomUserDetails, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) { + memberService.updateMyEmotionAndStatusMessage(customUserDetails.getId(), updateMyEmotionAndStatusMessageRequest) + coupleInfoService.findCoupleMember(customUserDetails.getId()).also { receiver -> + if (notificationService.getByMemberId(receiver.id).partnerStatus) { + val sender = memberService.getById(customUserDetails.getId()) + statusAlarmService.sendStatusAlarm(sender, receiver, updateMyEmotionAndStatusMessageRequest.emotion) + } + } + } fun getMemberEmotion(customUserDetails: CustomUserDetails): MyEmotionResponse { val member = memberService.getById(customUserDetails.getId()) From b8cd4cefe4518b485d92765a09fd27b0db25e746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 01:48:51 +0900 Subject: [PATCH 189/357] =?UTF-8?q?test=20:=20memberInfoFacade.updateMyEmo?= =?UTF-8?q?tionAndStatusMessage=20=EA=B5=AC=ED=98=84=EC=97=90=EC=84=9C=20f?= =?UTF-8?q?cm=EC=95=8C=EB=A6=BC=20=EB=B3=B4=EB=82=B4=EB=8A=94=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=EC=9D=B4=20=EC=B6=94=EA=B0=80=EB=90=A8=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B3=80=EA=B2=BD(=EB=AA=A8=ED=82=B9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80)=20=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/facade/MemberInfoFacadeTest.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 57547f52..a18b1a31 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -1,6 +1,8 @@ package gomushin.backend.member.facade +import gomushin.backend.alarm.service.StatusAlarmService import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.entity.Notification import gomushin.backend.member.domain.service.MemberService @@ -28,13 +30,19 @@ class MemberInfoFacadeTest { private lateinit var memberService: MemberService @Mock private lateinit var notificationService: NotificationService + @Mock + private lateinit var statusAlarmService: StatusAlarmService + @Mock + private lateinit var coupleInfoService: CoupleInfoService @InjectMocks private lateinit var memberInfoFacade: MemberInfoFacade private lateinit var customUserDetails: CustomUserDetails private lateinit var member: Member + private lateinit var memberCouple : Member private lateinit var notification: Notification + private lateinit var memberCoupleNotification : Notification @BeforeEach fun setUp() { @@ -50,6 +58,18 @@ class MemberInfoFacadeTest { statusMessage = "상태 메시지" ) + memberCouple = Member( + id = 2L, + name = "멤버의 커플", + nickname = "멤버의 커플 닉네임", + email = "test@test.com", + birthDate = null, + profileImageUrl = null, + provider = Provider.KAKAO, + role = Role.MEMBER, + statusMessage = "상태 메시지" + ) + notification = Notification( id = 1L, memberId = 1L, @@ -57,6 +77,13 @@ class MemberInfoFacadeTest { partnerStatus = false ) + memberCoupleNotification = Notification ( + id = 2L, + memberId = 2L, + dday = false, + partnerStatus = true + ) + customUserDetails = mock(CustomUserDetails::class.java) `when`(customUserDetails.getId()).thenReturn(1L) @@ -92,10 +119,18 @@ class MemberInfoFacadeTest { fun updateMyEmotionAndStatusMessage() { //given val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(1, "좋은 날씨야") + `when`(coupleInfoService.findCoupleMember(customUserDetails.getId())).thenReturn(memberCouple) + `when`(notificationService.getByMemberId(memberCouple.id)).thenReturn(memberCoupleNotification) + `when`(memberService.getById(customUserDetails.getId())).thenReturn(member) + doNothing().`when`(statusAlarmService).sendStatusAlarm(member, memberCouple, updateMyEmotionAndStatusMessageRequest.emotion) //when val result = memberInfoFacade.updateMyEmotionAndStatusMessage(customUserDetails, updateMyEmotionAndStatusMessageRequest) //then verify(memberService).updateMyEmotionAndStatusMessage(1L, updateMyEmotionAndStatusMessageRequest) + verify(coupleInfoService).findCoupleMember(customUserDetails.getId()) + verify(notificationService).getByMemberId(memberCouple.id) + verify(memberService).getById(customUserDetails.getId()) + verify(statusAlarmService).sendStatusAlarm(member, memberCouple, updateMyEmotionAndStatusMessageRequest.emotion) } @DisplayName("이모지 조회 테스트") From 09e843218bffebd53e893a8a3aaed6d2b2ae1ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 01:53:01 +0900 Subject: [PATCH 190/357] =?UTF-8?q?feat=20:=20fcm.json=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=A3=BC=EC=9E=85=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b786a8d0..60e5b4cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,19 +33,22 @@ jobs: java-version: '17' distribution: 'temurin' - - name: YML 파일 세팅 + - name: YML 파일 세팅 및 fcm.json 주입 env: APPLICATION_PROPERTIES: ${{ secrets.APPLICATION_PROPERTIES }} TEST_APPLICATION_PROPERTIES: ${{ secrets.TEST_APPLICATION_PROPERTIES }} ERROR_MESSAGES_PROPERTIES: ${{ secrets.ERROR_MESSAGES_PROPERTIES }} + FCM_JSON: ${{secrets.FCM_JSON}} run: | cd ./src rm -rf main/resources/application.yml mkdir -p test/resources mkdir -p main/resources + mkdir -p main/resources/firebase echo "$APPLICATION_PROPERTIES" > main/resources/application.yml echo "$ERROR_MESSAGES_PROPERTIES" > main/resources/api-error-messages.properties - # echo "$TEST_APPLICATION_PROPERTIES" > test/resources/application.yml + echo "$FCM_JSON" > main/resources/firebase/fcm.json + echo "$TEST_APPLICATION_PROPERTIES" > test/resources/application.yml - name: gradlew 권한 부여 run: chmod +x gradlew From 25bad144ba9c4a2bce4ffd2cc0f4c5013596a179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 12:52:01 +0900 Subject: [PATCH 191/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(OkHttpClient=EB=A5=BC=20sendMessageTo?= =?UTF-8?q?=EB=A5=BC=20=ED=98=B8=EC=B6=9C=ED=95=A0=20=EB=95=8C=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20=EC=83=88=EB=A1=9C=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EB=8A=94=EA=B1=B4=20=EB=B9=84=ED=9A=A8=EC=9C=A8=EC=A0=81?= =?UTF-8?q?=EC=9D=B4=EB=9D=BC=20=EC=8B=B1=EA=B8=80=ED=86=A4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=83=9D=EC=84=B1=ED=95=B4=EC=84=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC)=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt index d6184a89..4ec41db0 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt @@ -29,11 +29,13 @@ class FCMService( private val googleScope : String ) { private val log: Logger = LoggerFactory.getLogger(FCMService::class.java) + companion object { + private val client: OkHttpClient = OkHttpClient() + } @Throws(IOException::class) fun sendMessageTo(targetToken: String, title: String, body: String) { val message = makeMessage(targetToken, title, body) - val client = OkHttpClient() val requestBody = message .toRequestBody("application/json; charset=utf-8".toMediaType()) From 8f8a30a66adfc684111e24295e48203b6a99449c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 12:56:14 +0900 Subject: [PATCH 192/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(=EC=97=90=EB=9F=AC=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20exception=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=BB=A4=EC=8A=A4=ED=85=80)=20#4?= =?UTF-8?q?5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt index 4ec41db0..30d295b1 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt @@ -77,7 +77,8 @@ class FCMService( googleCredentials.refreshIfExpired() return googleCredentials.accessToken.tokenValue } catch (e: IOException) { - throw BadRequestException(e.message.toString()) + log.error("FCM AccessToken 발급 중 오류 발생", e) + throw BadRequestException("sarangggun.alarm.fail-issue-accesstoken") } } } \ No newline at end of file From 21e201f33b1134b77d32fa62945c76a6df30e14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 13:27:02 +0900 Subject: [PATCH 193/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(emoji=20->=20emotion=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD(=EC=A7=80=EA=B8=88=EA=B9=8C=EC=A7=80=20?= =?UTF-8?q?=ED=95=B4=20=EC=98=A8=EA=B1=B0=EB=9E=91=20=EC=94=BD=ED=81=AC=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=94=EA=B8=B0=20=EC=9C=84=ED=95=B4=EC=84=9C)=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B3=A0=20emotion=20int=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=88=EB=9D=BC=20enum=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EA=B4=80=EB=A6=AC(=EC=9C=A0=EC=A7=80=EB=B3=B4?= =?UTF-8?q?=EC=88=98=EC=9D=98=20=ED=8E=B8=EC=9D=98=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=EC=84=9C))=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alarm/service/StatusAlarmService.kt | 73 +++++++++---------- .../member/domain/service/MemberService.kt | 2 +- .../UpdateMyEmotionAndStatusMessageRequest.kt | 3 +- .../gomushin/backend/member/value/Emotion.kt | 26 +++++++ .../domain/service/MemberServiceTest.kt | 5 +- .../member/facade/MemberInfoFacadeTest.kt | 3 +- 6 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/member/value/Emotion.kt diff --git a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt index bbf14b78..4432b629 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt @@ -1,6 +1,8 @@ package gomushin.backend.alarm.service +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.value.Emotion import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @@ -8,49 +10,42 @@ import org.springframework.stereotype.Service class StatusAlarmService ( private val fcmService: FCMService ) { - private val dummyList = listOf() - private val emojiMiss = listOf( - "보고싶다는 마음이 도착했어요.+오늘은 짧은 안부라도 건네볼까요?", - "보고 싶다고 표현하는 건 용기예요.+작은 마음이 큰 위로가 될 거예요") - private val emojiTired = listOf( - "오늘 많이 힘들었나봐요.+따뜻한 말 한마디가 큰 힘이 돼요", - "오늘 피곤한 날이래요.+연인에게 따뜻한 응원 어때요?" - ) - private val emojiSad = listOf( - "서운한 마음이 들었대요.+연인과 진심 어린 대화 어때요?", - "연인의 마음이 무거운가봐요.+따뜻한 공감이 필요해요" - ) - private val emojiGood = listOf( - "기분 좋은 하루를 함께 나눠보세요.+당신의 행복이 전해질 거예요.", - "좋은 일이 있었대요!+축하해주고 같이 기뻐해주세요\uD83D\uDC9B" - ) - private val emojiAnnoy = listOf( - "연인이 짜증나는 일이 있었대요.+들어주는 것도 큰 위로가 될거예요", - "OO님 오늘 좀 힘들었나 봐요.+오늘 전화통화 어때요?" - ) - private val emojiWorry = listOf( - "당신을 걱정하는 마음이 담겼어요.+잠깐 안부를 전해주면 어떨까요?", - "연인이 당신을 걱정해요.+연인의 마음 한쪽이 조금 무거웠대요." - ) - private val emojiNothing = listOf( - "연인이 평범한 일상을 보냈대요.+안부를 전하는 일상이 관계를 지켜줄 거예요.", - "감정의 큰 파도는 없지만,+OO님의 하루를 들어줄 누군가가 있다면 좋겠대요." - ) - private val statusMessage = listOf( - dummyList, - emojiMiss, - emojiTired, - emojiSad, - emojiGood, - emojiAnnoy, - emojiWorry, - emojiNothing + private val statusMessage: Map> = mapOf( + Emotion.MISS to listOf( + "보고싶다는 마음이 도착했어요.+오늘은 짧은 안부라도 건네볼까요?", + "보고 싶다고 표현하는 건 용기예요.+작은 마음이 큰 위로가 될 거예요" + ), + Emotion.TIRED to listOf( + "오늘 많이 힘들었나봐요.+따뜻한 말 한마디가 큰 힘이 돼요", + "오늘 피곤한 날이래요.+연인에게 따뜻한 응원 어때요?" + ), + Emotion.SAD to listOf( + "서운한 마음이 들었대요.+연인과 진심 어린 대화 어때요?", + "연인의 마음이 무거운가봐요.+따뜻한 공감이 필요해요" + ), + Emotion.GOOD to listOf( + "기분 좋은 하루를 함께 나눠보세요.+당신의 행복이 전해질 거예요.", + "좋은 일이 있었대요!+축하해주고 같이 기뻐해주세요💛" + ), + Emotion.ANNOY to listOf( + "연인이 짜증나는 일이 있었대요.+들어주는 것도 큰 위로가 될거예요", + "OO님 오늘 좀 힘들었나 봐요.+오늘 전화통화 어때요?" + ), + Emotion.WORRY to listOf( + "당신을 걱정하는 마음이 담겼어요.+잠깐 안부를 전해주면 어떨까요?", + "연인이 당신을 걱정해요.+연인의 마음 한쪽이 조금 무거웠대요." + ), + Emotion.NOTHING to listOf( + "연인이 평범한 일상을 보냈대요.+안부를 전하는 일상이 관계를 지켜줄 거예요.", + "감정의 큰 파도는 없지만,+OO님의 하루를 들어줄 누군가가 있다면 좋겠대요." + ) ) @Async - fun sendStatusAlarm(sender : Member, receiver : Member, emoji : Int) { + fun sendStatusAlarm(sender : Member, receiver : Member, emotion : Emotion) { val notificationContent = - statusMessage.get(emoji).random() + statusMessage[emotion]?.random() + ?: throw BadRequestException("sarangggun.member.not-exist-emoji") val parts = notificationContent.split("+") val title = parts[0] val content = if (parts[1].contains("OO")) { diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 9e029a6c..f4ae8aa8 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -28,7 +28,7 @@ class MemberService( @Transactional fun updateMyEmotionAndStatusMessage(id: Long, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest){ val member = getById(id) - member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion) + member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion.code) member.updateStatusMessage(updateMyEmotionAndStatusMessageRequest.statusMessage) } diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt index 65d25044..12ff7506 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt @@ -1,12 +1,13 @@ package gomushin.backend.member.dto.request +import gomushin.backend.member.value.Emotion import io.swagger.v3.oas.annotations.media.Schema data class UpdateMyEmotionAndStatusMessageRequest( @Schema(description = "이모지(1 : 보고싶어요, 2: 기분 좋아요, 3 : 아무느낌 없어요, " + "4 : 피곤해요, 5: 서운해요, 6 : 걱정돼요, 7 : 짜증나요)", example = "1") - val emotion : Int, + val emotion : Emotion, @Schema(description = "상태 메시지", example = "보고 싶어요") val statusMessage : String diff --git a/src/main/kotlin/gomushin/backend/member/value/Emotion.kt b/src/main/kotlin/gomushin/backend/member/value/Emotion.kt new file mode 100644 index 00000000..635f03c7 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/value/Emotion.kt @@ -0,0 +1,26 @@ +package gomushin.backend.member.value + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonValue +import gomushin.backend.core.infrastructure.exception.BadRequestException + +enum class Emotion (val code: Int) { + MISS(1), + GOOD(2), + NOTHING(3), + TIRED(4), + SAD(5), + WORRY(6), + ANNOY(7); + + companion object { + @JvmStatic + @JsonCreator + fun from(code: Int): Emotion = + entries.find { it.code == code } + ?: throw BadRequestException("sarangggun.member.not-exist-emoji") + } + + @JsonValue + fun toValue(): Int = code +} \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt index 0fc6a0aa..26290413 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt @@ -7,6 +7,7 @@ import gomushin.backend.member.domain.value.Role import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest +import gomushin.backend.member.value.Emotion import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -68,12 +69,12 @@ class MemberServiceTest { emotion = 1, statusMessage = "상태 변경전" ) - val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(2, "상태 변경후") + val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(Emotion.SAD, "상태 변경후") //when `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember)) val result = memberService.updateMyEmotionAndStatusMessage(memberId, updateMyEmotionAndStatusMessageRequest) //then - assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion) + assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion.code) assertEquals(expectedMember.statusMessage, updateMyEmotionAndStatusMessageRequest.statusMessage) } diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index a18b1a31..a440e1a8 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -13,6 +13,7 @@ import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import gomushin.backend.member.dto.request.UpdateMyNotificationRequest +import gomushin.backend.member.value.Emotion import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -118,7 +119,7 @@ class MemberInfoFacadeTest { @Test fun updateMyEmotionAndStatusMessage() { //given - val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(1, "좋은 날씨야") + val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(Emotion.GOOD, "좋은 날씨야") `when`(coupleInfoService.findCoupleMember(customUserDetails.getId())).thenReturn(memberCouple) `when`(notificationService.getByMemberId(memberCouple.id)).thenReturn(memberCoupleNotification) `when`(memberService.getById(customUserDetails.getId())).thenReturn(member) From 778a50fcb161ebc0281b2f5f4e057b8892d92ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 13:28:30 +0900 Subject: [PATCH 194/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(=EC=BD=94=EB=93=9C=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=20=EB=A7=9E=EC=B6=94=EA=B8=B0)=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/member/domain/service/MemberService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index f4ae8aa8..40abdfd2 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -26,7 +26,7 @@ class MemberService( } @Transactional - fun updateMyEmotionAndStatusMessage(id: Long, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest){ + fun updateMyEmotionAndStatusMessage(id: Long, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) { val member = getById(id) member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion.code) member.updateStatusMessage(updateMyEmotionAndStatusMessageRequest.statusMessage) From f506182d04b8830c9907c3d5b48b4f0969920679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 18:38:35 +0900 Subject: [PATCH 195/357] =?UTF-8?q?build=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(=EB=B9=8C=EB=93=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8)=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle.kts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5df6b146..8b29a3de 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ src/test/resources/application-test.yml .env api-error-messages.properties +.json \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5466ff02..193b0901 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -71,10 +71,10 @@ dependencies { implementation("software.amazon.awssdk:s3:2.30.38") //okhttp3 - implementation("com.squareup.okhttp3:okhttp:4.9.3") + implementation("com.squareup.okhttp3:okhttp:4.12.0") //google auth - implementation("com.google.auth:google-auth-library-oauth2-http:1.18.0") + implementation("com.google.auth:google-auth-library-oauth2-http:1.33.1") implementation("org.springframework.boot:spring-boot-starter-validation") testImplementation("org.springframework.boot:spring-boot-starter-test") From 6c2edfc53f1987c57f22bc96b65466a9eb31af7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 18:48:24 +0900 Subject: [PATCH 196/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95)=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/UpdateMyEmotionAndStatusMessageRequest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt index 12ff7506..855d108c 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt @@ -5,8 +5,8 @@ import io.swagger.v3.oas.annotations.media.Schema data class UpdateMyEmotionAndStatusMessageRequest( - @Schema(description = "이모지(1 : 보고싶어요, 2: 기분 좋아요, 3 : 아무느낌 없어요, " + - "4 : 피곤해요, 5: 서운해요, 6 : 걱정돼요, 7 : 짜증나요)", example = "1") + @Schema(description = "이모지(MISS : 보고싶어요, GOOD: 기분 좋아요, NOTHING : 아무느낌 없어요, " + + "TIRED : 피곤해요, SAD: 서운해요, WORRY : 걱정돼요, ANNOY : 짜증나요)", example = "1") val emotion : Emotion, @Schema(description = "상태 메시지", example = "보고 싶어요") From 9cdc4f756811b20729b9bed8e077470f2ded2b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 21:36:04 +0900 Subject: [PATCH 197/357] =?UTF-8?q?fix=20:=20member=EC=9D=98=20emotion?= =?UTF-8?q?=ED=83=80=EC=9E=85=20enum=EC=9C=BC=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=A8=EC=97=90=20member=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alarm/service/StatusAlarmService.kt | 8 +++--- .../domain/service/CoupleInfoService.kt | 3 ++- .../dto/response/CoupleEmotionResponse.kt | 8 +++--- .../backend/member/domain/entity/Member.kt | 6 +++-- .../member/domain/service/MemberService.kt | 2 +- .../backend/member/domain/value/Emotion.kt | 6 +++++ .../UpdateMyEmotionAndStatusMessageRequest.kt | 9 ++++--- .../member/dto/response/MyEmotionResponse.kt | 3 ++- .../gomushin/backend/member/value/Emotion.kt | 26 ------------------- .../domain/service/MemberServiceTest.kt | 10 +++---- .../member/facade/MemberInfoFacadeTest.kt | 4 +-- 11 files changed, 37 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/member/domain/value/Emotion.kt delete mode 100644 src/main/kotlin/gomushin/backend/member/value/Emotion.kt diff --git a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt index 4432b629..6bf175f1 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt @@ -2,7 +2,7 @@ package gomushin.backend.alarm.service import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member -import gomushin.backend.member.value.Emotion +import gomushin.backend.member.domain.value.Emotion import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @@ -23,11 +23,11 @@ class StatusAlarmService ( "서운한 마음이 들었대요.+연인과 진심 어린 대화 어때요?", "연인의 마음이 무거운가봐요.+따뜻한 공감이 필요해요" ), - Emotion.GOOD to listOf( + Emotion.HAPPY to listOf( "기분 좋은 하루를 함께 나눠보세요.+당신의 행복이 전해질 거예요.", "좋은 일이 있었대요!+축하해주고 같이 기뻐해주세요💛" ), - Emotion.ANNOY to listOf( + Emotion.ANGRY to listOf( "연인이 짜증나는 일이 있었대요.+들어주는 것도 큰 위로가 될거예요", "OO님 오늘 좀 힘들었나 봐요.+오늘 전화통화 어때요?" ), @@ -35,7 +35,7 @@ class StatusAlarmService ( "당신을 걱정하는 마음이 담겼어요.+잠깐 안부를 전해주면 어떨까요?", "연인이 당신을 걱정해요.+연인의 마음 한쪽이 조금 무거웠대요." ), - Emotion.NOTHING to listOf( + Emotion.COMMON to listOf( "연인이 평범한 일상을 보냈대요.+안부를 전하는 일상이 관계를 지켜줄 거예요.", "감정의 큰 파도는 없지만,+OO님의 하루를 들어줄 누군가가 있다면 좋겠대요." ) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index d24f9559..cbdfefab 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -11,6 +11,7 @@ import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Emotion import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @@ -133,7 +134,7 @@ class CoupleInfoService( } @Transactional(readOnly = true) - fun getCoupleEmotion(id: Long): Int { + fun getCoupleEmotion(id: Long): Emotion { val coupleMember = findCoupleMember(id) return coupleMember.emotion ?: throw BadRequestException("sarangggun.member.not-exist-emoji") } diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleEmotionResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleEmotionResponse.kt index 31d66360..3442896a 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleEmotionResponse.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleEmotionResponse.kt @@ -1,11 +1,13 @@ package gomushin.backend.couple.dto.response +import gomushin.backend.member.domain.value.Emotion + data class CoupleEmotionResponse ( - val emotion : Int + val emotion : String ) { companion object{ - fun of(emotion: Int) = CoupleEmotionResponse( - emotion + fun of(emotion: Emotion) = CoupleEmotionResponse( + emotion.name ) } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index c1a6ff53..8058951e 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -1,6 +1,7 @@ package gomushin.backend.member.domain.entity import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import gomushin.backend.member.domain.value.Emotion import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role import jakarta.persistence.* @@ -42,8 +43,9 @@ class Member( @Column(name = "is_couple", nullable = false) var isCouple: Boolean = false, + @Enumerated(EnumType.STRING) @Column(name = "emotion") - var emotion : Int? = null, + var emotion : Emotion? = null, @Column(name = "fcm_token", nullable = false) var fcmToken: String = "", @@ -75,7 +77,7 @@ class Member( this.isCouple = !this.isCouple } - fun updateEmotion(emotion: Int) { + fun updateEmotion(emotion: Emotion) { this.emotion = emotion } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 40abdfd2..603534b2 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -28,7 +28,7 @@ class MemberService( @Transactional fun updateMyEmotionAndStatusMessage(id: Long, updateMyEmotionAndStatusMessageRequest: UpdateMyEmotionAndStatusMessageRequest) { val member = getById(id) - member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion.code) + member.updateEmotion(updateMyEmotionAndStatusMessageRequest.emotion) member.updateStatusMessage(updateMyEmotionAndStatusMessageRequest.statusMessage) } diff --git a/src/main/kotlin/gomushin/backend/member/domain/value/Emotion.kt b/src/main/kotlin/gomushin/backend/member/domain/value/Emotion.kt new file mode 100644 index 00000000..e9942f25 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/domain/value/Emotion.kt @@ -0,0 +1,6 @@ +package gomushin.backend.member.domain.value + + +enum class Emotion { + ANGRY, WORRY, HAPPY, MISS, COMMON, SAD, TIRED; +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt index 855d108c..8d29fbff 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt @@ -1,12 +1,15 @@ package gomushin.backend.member.dto.request -import gomushin.backend.member.value.Emotion +import gomushin.backend.member.domain.value.Emotion import io.swagger.v3.oas.annotations.media.Schema +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated data class UpdateMyEmotionAndStatusMessageRequest( - @Schema(description = "이모지(MISS : 보고싶어요, GOOD: 기분 좋아요, NOTHING : 아무느낌 없어요, " + - "TIRED : 피곤해요, SAD: 서운해요, WORRY : 걱정돼요, ANNOY : 짜증나요)", example = "1") + @Enumerated(EnumType.STRING) + @Schema(description = "이모지(MISS : 보고싶어요, GOOD: 기분 좋아요, COMMON : 아무느낌 없어요, " + + "TIRED : 피곤해요, SAD: 서운해요, WORRY : 걱정돼요, ANGRY : 짜증나요)", example = "1") val emotion : Emotion, @Schema(description = "상태 메시지", example = "보고 싶어요") diff --git a/src/main/kotlin/gomushin/backend/member/dto/response/MyEmotionResponse.kt b/src/main/kotlin/gomushin/backend/member/dto/response/MyEmotionResponse.kt index 3913fd3f..6fb9be58 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/response/MyEmotionResponse.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/response/MyEmotionResponse.kt @@ -1,9 +1,10 @@ package gomushin.backend.member.dto.response import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.value.Emotion data class MyEmotionResponse ( - val emotion : Int? + val emotion : Emotion? ) { companion object { fun of(member: Member) = MyEmotionResponse ( diff --git a/src/main/kotlin/gomushin/backend/member/value/Emotion.kt b/src/main/kotlin/gomushin/backend/member/value/Emotion.kt deleted file mode 100644 index 635f03c7..00000000 --- a/src/main/kotlin/gomushin/backend/member/value/Emotion.kt +++ /dev/null @@ -1,26 +0,0 @@ -package gomushin.backend.member.value - -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonValue -import gomushin.backend.core.infrastructure.exception.BadRequestException - -enum class Emotion (val code: Int) { - MISS(1), - GOOD(2), - NOTHING(3), - TIRED(4), - SAD(5), - WORRY(6), - ANNOY(7); - - companion object { - @JvmStatic - @JsonCreator - fun from(code: Int): Emotion = - entries.find { it.code == code } - ?: throw BadRequestException("sarangggun.member.not-exist-emoji") - } - - @JsonValue - fun toValue(): Int = code -} \ No newline at end of file diff --git a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt index 26290413..51e31c1b 100644 --- a/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/member/domain/service/MemberServiceTest.kt @@ -7,7 +7,7 @@ import gomushin.backend.member.domain.value.Role import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest -import gomushin.backend.member.value.Emotion +import gomushin.backend.member.domain.value.Emotion import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -66,7 +66,7 @@ class MemberServiceTest { profileImageUrl = null, provider = Provider.KAKAO, role = Role.GUEST, - emotion = 1, + emotion = Emotion.COMMON, statusMessage = "상태 변경전" ) val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(Emotion.SAD, "상태 변경후") @@ -74,7 +74,7 @@ class MemberServiceTest { `when`(memberRepository.findById(memberId)).thenReturn(Optional.of(expectedMember)) val result = memberService.updateMyEmotionAndStatusMessage(memberId, updateMyEmotionAndStatusMessageRequest) //then - assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion.code) + assertEquals(expectedMember.emotion, updateMyEmotionAndStatusMessageRequest.emotion) assertEquals(expectedMember.statusMessage, updateMyEmotionAndStatusMessageRequest.statusMessage) } @@ -92,7 +92,7 @@ class MemberServiceTest { profileImageUrl = null, provider = Provider.KAKAO, role = Role.GUEST, - emotion = 1, + emotion = Emotion.COMMON, statusMessage = "상태 변경전" ) val updateMyNickNameRequest = UpdateMyNickNameRequest("테스트 닉네임 수정") @@ -117,7 +117,7 @@ class MemberServiceTest { profileImageUrl = null, provider = Provider.KAKAO, role = Role.GUEST, - emotion = 1, + emotion = Emotion.COMMON, statusMessage = "상태 변경전", ) val updateMyBirthdayRequest = UpdateMyBirthdayRequest(LocalDate.of(2001, 3, 30)) diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index a440e1a8..41512ab5 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -13,7 +13,7 @@ import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest import gomushin.backend.member.dto.request.UpdateMyEmotionAndStatusMessageRequest import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import gomushin.backend.member.dto.request.UpdateMyNotificationRequest -import gomushin.backend.member.value.Emotion +import gomushin.backend.member.domain.value.Emotion import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -119,7 +119,7 @@ class MemberInfoFacadeTest { @Test fun updateMyEmotionAndStatusMessage() { //given - val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(Emotion.GOOD, "좋은 날씨야") + val updateMyEmotionAndStatusMessageRequest = UpdateMyEmotionAndStatusMessageRequest(Emotion.HAPPY, "좋은 날씨야") `when`(coupleInfoService.findCoupleMember(customUserDetails.getId())).thenReturn(memberCouple) `when`(notificationService.getByMemberId(memberCouple.id)).thenReturn(memberCoupleNotification) `when`(memberService.getById(customUserDetails.getId())).thenReturn(member) From cca708204cf1734920681bced33bb057896b10b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 21:41:53 +0900 Subject: [PATCH 198/357] =?UTF-8?q?fix=20:=20emotion=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B0=94=EA=BF=88=EC=97=90=20=EB=94=B0=EB=9D=BC=EC=84=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=EC=95=88=EB=90=98=EB=8A=94=EA=B1=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/service/CoupleInfoServiceTest.kt | 3 ++- .../gomushin/backend/member/facade/CoupleFacadeTest.kt | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index c5598663..92347b35 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -8,6 +8,7 @@ import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Emotion import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role import org.junit.jupiter.api.DisplayName @@ -410,7 +411,7 @@ class CoupleInfoServiceTest { role= Role.MEMBER, isCouple= true, statusMessage = "기분이 좋아용", - emotion = 2 + emotion = Emotion.HAPPY ) `when`(coupleRepository.findByMemberId(userId)).thenReturn(couple) `when`(memberRepository.findById(coupleUserId)).thenReturn(Optional.of(coupleUser)) diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 03c0cd3b..082d3cf1 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -12,6 +12,7 @@ import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.value.Emotion import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role import org.junit.jupiter.api.BeforeEach @@ -168,10 +169,10 @@ class CoupleFacadeTest { @DisplayName("이모지 조회 - 정상응답") @Test fun getEmotion() { - `when`(coupleInfoService.getCoupleEmotion(customUserDetails.getId())).thenReturn(1) + `when`(coupleInfoService.getCoupleEmotion(customUserDetails.getId())).thenReturn(Emotion.HAPPY) val result = coupleFacade.getCoupleEmotion(customUserDetails) verify(coupleInfoService).getCoupleEmotion(1L) - assertEquals(1, result.emotion) + assertEquals(Emotion.HAPPY.name, result.emotion) } } From 1521c66f343fcb4db67aa6fef8b62ecf2a1f9c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 29 Apr 2025 21:47:13 +0900 Subject: [PATCH 199/357] =?UTF-8?q?chore=20:=20swagger=20request=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=88=98=EC=A0=95(emotion=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=B4=20=EB=B0=94=EB=80=8C=EC=96=B4=EC=84=9C=20?= =?UTF-8?q?=EA=B7=B8=EA=B1=B0=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EB=A6=AC=ED=80=98=EC=8A=A4=ED=8A=B8=EB=8F=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95)=20#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/UpdateMyEmotionAndStatusMessageRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt index 8d29fbff..acb3445f 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/request/UpdateMyEmotionAndStatusMessageRequest.kt @@ -9,7 +9,7 @@ import jakarta.persistence.Enumerated data class UpdateMyEmotionAndStatusMessageRequest( @Enumerated(EnumType.STRING) @Schema(description = "이모지(MISS : 보고싶어요, GOOD: 기분 좋아요, COMMON : 아무느낌 없어요, " + - "TIRED : 피곤해요, SAD: 서운해요, WORRY : 걱정돼요, ANGRY : 짜증나요)", example = "1") + "TIRED : 피곤해요, SAD: 서운해요, WORRY : 걱정돼요, ANGRY : 짜증나요)", example = "COMMON") val emotion : Emotion, @Schema(description = "상태 메시지", example = "보고 싶어요") From 2718dee0d15ad0041df5174df68c1c2398b74a31 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 30 Apr 2025 17:36:31 +0900 Subject: [PATCH 200/357] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=B0=98=ED=99=98=20=EC=8B=9C=20=EC=BB=A4?= =?UTF-8?q?=ED=94=8C=20=EC=97=AC=EB=B6=80=20,=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=EC=97=AC=EB=B6=80=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/dto/response/MyInfoResponse.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt b/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt index 2a2442c7..83c0b960 100644 --- a/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt +++ b/src/main/kotlin/gomushin/backend/member/dto/response/MyInfoResponse.kt @@ -1,15 +1,21 @@ package gomushin.backend.member.dto.response import gomushin.backend.member.domain.entity.Member +import io.swagger.v3.oas.annotations.media.Schema data class MyInfoResponse( + @Schema(description = "내 닉네임", example = "닉네임") val nickname: String, + @Schema(description = "커플 여부", example = "true") val isCouple: Boolean, + @Schema(description = "온보딩 여부 (GUEST : 온보딩 X , MEMBER : 온보딩 O)", example = "MEMBER") + val role: String ) { companion object { fun of(member: Member) = MyInfoResponse( member.nickname, - member.isCouple + member.isCouple, + member.role.name ) } } From 1087c68f4e7f7fc11026283517e8b2f20debda0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 1 May 2025 00:48:16 +0900 Subject: [PATCH 201/357] =?UTF-8?q?feat=20:=20=EA=B8=B0=EB=85=90=EC=9D=BC?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20api=20=EC=A0=9C=EC=9E=91=20#61?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/entity/Anniversary.kt | 22 ++++++++++-- .../domain/service/AnniversaryCalculator.kt | 4 +-- .../domain/service/AnniversaryService.kt | 11 ++++++ .../couple/domain/value/AnniversaryEmoji.kt | 5 +++ .../dto/request/GenerateAnniversaryRequest.kt | 14 ++++++++ .../backend/couple/facade/CoupleFacade.kt | 9 ++--- .../presentation/AnniversaryController.kt | 34 +++++++++++++++++++ .../backend/couple/presentation/ApiPath.kt | 1 + 8 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/domain/value/AnniversaryEmoji.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/request/GenerateAnniversaryRequest.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt index dc1b8bb4..49259f0b 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt @@ -1,6 +1,7 @@ package gomushin.backend.couple.domain.entity import gomushin.backend.core.infrastructure.jpa.shared.BaseEntity +import gomushin.backend.couple.domain.value.AnniversaryEmoji import jakarta.persistence.* import java.time.LocalDate @@ -21,16 +22,31 @@ class Anniversary( var anniversaryDate: LocalDate, @Column(name = "anniversary_property", nullable = false) - var anniversaryProperty: Int + var anniversaryProperty: Int, + + @Enumerated(EnumType.STRING) + @Column(name = "anniversary_emoji") + var emoji : AnniversaryEmoji? = null ) : BaseEntity() { companion object { - fun create(coupleId: Long, title: String, anniversaryDate: LocalDate): Anniversary { + fun autoCreate(coupleId: Long, title: String, anniversaryDate: LocalDate): Anniversary { return Anniversary( coupleId = coupleId, title = title, anniversaryDate = anniversaryDate, - anniversaryProperty = 0 + anniversaryProperty = 0, ) } + + fun manualCreate(coupleId: Long, title: String, anniversaryDate: LocalDate, emoji: AnniversaryEmoji) : Anniversary { + return Anniversary( + coupleId = coupleId, + title = title, + anniversaryDate = anniversaryDate, + anniversaryProperty = 1, + emoji = emoji + ) + } + } } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt index 527af92b..f7c2b37f 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt @@ -58,7 +58,7 @@ class AnniversaryCalculator { break } else { val title = "${anniversaryYear}주년" - val anniversary = Anniversary.create(coupleId, title, anniversaryDate) + val anniversary = Anniversary.autoCreate(coupleId, title, anniversaryDate) anniversaryList.add(anniversary) anniversaryYear++ } @@ -82,7 +82,7 @@ class AnniversaryCalculator { break } else if (anniversaryDate.isAfter(militaryStartDate) && anniversaryDate.isBefore(militaryEndDate)) { val title = "${anniversaryDay}일" - val anniversary = Anniversary.create(coupleId, title, anniversaryDate) + val anniversary = Anniversary.autoCreate(coupleId, title, anniversaryDate) anniversaryList.add(anniversary) } } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index b1445569..334eb0c6 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest +import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import org.springframework.stereotype.Service @@ -67,6 +68,16 @@ class AnniversaryService( return anniversaryRepository.deleteAllByCoupleId(coupleId) } + @Transactional + fun generateAnniversary(couple: Couple, generateAnniversaryRequest: GenerateAnniversaryRequest) { + anniversaryRepository.save(Anniversary.manualCreate( + couple.id, + generateAnniversaryRequest.title, + generateAnniversaryRequest.date, + generateAnniversaryRequest.emoji + )) + } + private fun checkUserInCouple(userId: Long, couple: Couple) { if (!couple.containsUser(userId)) { throw BadRequestException("sarangggun.couple.not-in-couple") diff --git a/src/main/kotlin/gomushin/backend/couple/domain/value/AnniversaryEmoji.kt b/src/main/kotlin/gomushin/backend/couple/domain/value/AnniversaryEmoji.kt new file mode 100644 index 00000000..5b1e4825 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/domain/value/AnniversaryEmoji.kt @@ -0,0 +1,5 @@ +package gomushin.backend.couple.domain.value + +enum class AnniversaryEmoji { + HEART, CALENDAR, CAKE, TRAVEL +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/GenerateAnniversaryRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/GenerateAnniversaryRequest.kt new file mode 100644 index 00000000..2bfa93f8 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/GenerateAnniversaryRequest.kt @@ -0,0 +1,14 @@ +package gomushin.backend.couple.dto.request + +import gomushin.backend.couple.domain.value.AnniversaryEmoji +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +data class GenerateAnniversaryRequest( + @Schema(description = "제목", example = "전역일") + val title : String, + @Schema(description = "이모지", example = "HEART, CALENDAR, CAKE, TRAVEL") + val emoji : AnniversaryEmoji, + @Schema(description = "날짜", example = "2025-05-01") + val date : LocalDate +) diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 40a6d80a..e0898291 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -3,12 +3,9 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService -import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.domain.service.CoupleService -import gomushin.backend.couple.dto.request.CoupleConnectRequest -import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest -import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest +import gomushin.backend.couple.dto.request.* import gomushin.backend.couple.dto.response.* import org.springframework.stereotype.Component @@ -72,4 +69,8 @@ class CoupleFacade( val emotion = coupleInfoService.getCoupleEmotion(customUserDetails.getId()) return CoupleEmotionResponse.of(emotion) } + + fun generateAnniversary(customUserDetails: CustomUserDetails, generateAnniversaryRequest: GenerateAnniversaryRequest) { + anniversaryService.generateAnniversary(customUserDetails.getCouple(), generateAnniversaryRequest) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt new file mode 100644 index 00000000..b7c26ce4 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt @@ -0,0 +1,34 @@ +package gomushin.backend.couple.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest +import gomushin.backend.couple.facade.CoupleFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "기념일 생성", description = "AnniversaryController") +class AnniversaryController( + private val coupleFacade: CoupleFacade +) { + @ResponseStatus(HttpStatus.CREATED) + @PostMapping(ApiPath.ANNIVERSARY_GENERATE) + @Operation( + summary = "기념일 생성", + description = "generateAnniversary" + ) + fun generateAnniversary( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @RequestBody generateAnniversaryRequest: GenerateAnniversaryRequest + ): ApiResponse { + coupleFacade.generateAnniversary(customUserDetails, generateAnniversaryRequest) + return ApiResponse.success(true) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index a4f913d5..eabb36ff 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -12,4 +12,5 @@ object ApiPath { const val COUPLE_UPDATE_MILITARY_DATE = "/v1/couple/military-date" const val COUPLE_UPDATE_RELATIONSHIP_DATE = "/v1/couple/relationship-start-date" const val COUPLE_EMOJI = "/v1/couple/emotion" + const val ANNIVERSARY_GENERATE = "/v1/couple/new-anniversary" } From f44579b2752ed5bf6149355572cedf91eb857219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 1 May 2025 02:13:02 +0900 Subject: [PATCH 202/357] =?UTF-8?q?feat=20:=20=EA=B8=B0=EB=85=90=EC=9D=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20api=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20#61?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/ScheduleRepository.kt | 1 + .../dto/response/MonthlySchedulesResponse.kt | 1 + .../backend/member/facade/CoupleFacadeTest.kt | 30 ++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt index 5c95bdda..0447c440 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt @@ -17,6 +17,7 @@ interface ScheduleRepository : JpaRepository { @Query( """ SELECT new gomushin.backend.schedule.dto.response.MonthlySchedulesResponse( + s.id, s.title, s.startDate, s.endDate, diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt index dcb98f5f..a16a7bfd 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesResponse.kt @@ -3,6 +3,7 @@ package gomushin.backend.schedule.dto.response import java.time.LocalDateTime data class MonthlySchedulesResponse( + val id : Long, val title: String, val startDate: LocalDateTime, val endDate: LocalDateTime, diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt index 082d3cf1..43ebba40 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt @@ -6,6 +6,8 @@ import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.domain.service.CoupleService +import gomushin.backend.couple.domain.value.AnniversaryEmoji +import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.couple.dto.response.DdayResponse @@ -83,12 +85,12 @@ class CoupleFacadeTest { customUserDetails = Mockito.mock(CustomUserDetails::class.java) - `when`(customUserDetails.getId()).thenReturn(1L) } @DisplayName("grade 조회") @Test fun getGradeInfo(){ + `when`(customUserDetails.getId()).thenReturn(1L) `when`(coupleInfoService.getGrade(customUserDetails.getId())).thenReturn(1) val result = coupleFacade.getGradeInfo(customUserDetails) verify(coupleInfoService).getGrade(1L) @@ -98,6 +100,7 @@ class CoupleFacadeTest { @DisplayName("커플 연동 여부 조회") @Test fun checkConnect(){ + `when`(customUserDetails.getId()).thenReturn(1L) `when`(coupleInfoService.checkCouple(customUserDetails.getId())).thenReturn(true) val result = coupleFacade.checkConnect(customUserDetails) verify(coupleInfoService).checkCouple(1L) @@ -107,6 +110,7 @@ class CoupleFacadeTest { @DisplayName("디데이 조회 - 정상응답") @Test fun getDday(){ + `when`(customUserDetails.getId()).thenReturn(1L) `when`(coupleInfoService.getDday(customUserDetails.getId())).thenReturn(DdayResponse(100, 200, -345)) val result = coupleFacade.getDday(customUserDetails) verify(coupleInfoService).getDday(1L) @@ -118,6 +122,7 @@ class CoupleFacadeTest { @DisplayName("디데이 조회 - 날짜 정보 안 들어갔을 때") @Test fun getDdayNull(){ + `when`(customUserDetails.getId()).thenReturn(1L) `when`(coupleInfoService.getDday(customUserDetails.getId())).thenReturn(DdayResponse(null, null, null)) val result = coupleFacade.getDday(customUserDetails) verify(coupleInfoService).getDday(1L) @@ -129,6 +134,7 @@ class CoupleFacadeTest { @DisplayName("닉네임 조회 - 정상응답") @Test fun nickName(){ + `when`(customUserDetails.getId()).thenReturn(1L) `when`(coupleInfoService.getNickName(customUserDetails.getId())).thenReturn(NicknameResponse("김영록", "김영록 여친")) val result = coupleFacade.nickName(customUserDetails) verify(coupleInfoService).getNickName(1L) @@ -139,6 +145,7 @@ class CoupleFacadeTest { @DisplayName("상태 메시지 조회 - 정상응답") @Test fun statusMessage(){ + `when`(customUserDetails.getId()).thenReturn(1L) `when`(coupleInfoService.getStatusMessage(customUserDetails.getId())).thenReturn("기분이 좋아용") val result = coupleFacade.statusMessage(customUserDetails) verify(coupleInfoService).getStatusMessage(1L) @@ -148,6 +155,7 @@ class CoupleFacadeTest { @DisplayName("입대일, 전역일 수정 - 정상응답") @Test fun updateMilitaryDate() { + `when`(customUserDetails.getId()).thenReturn(1L) val updateMilitaryDateRequest = UpdateMilitaryDateRequest( LocalDate.of(2022, 5, 24), LocalDate.of(2023,11,23) @@ -159,6 +167,7 @@ class CoupleFacadeTest { @DisplayName("만난날 수정 - 정상응답") @Test fun updateRelationshipStartDate() { + `when`(customUserDetails.getId()).thenReturn(1L) val updateRelationshipStartDateRequest = UpdateRelationshipStartDateRequest( LocalDate.of(2022, 5, 24), ) @@ -169,10 +178,29 @@ class CoupleFacadeTest { @DisplayName("이모지 조회 - 정상응답") @Test fun getEmotion() { + `when`(customUserDetails.getId()).thenReturn(1L) `when`(coupleInfoService.getCoupleEmotion(customUserDetails.getId())).thenReturn(Emotion.HAPPY) val result = coupleFacade.getCoupleEmotion(customUserDetails) verify(coupleInfoService).getCoupleEmotion(1L) assertEquals(Emotion.HAPPY.name, result.emotion) } + @DisplayName("기념일 생성 - 정상응답") + @Test + fun generateAnniversary() { + //given + val generateAnniversaryRequest = GenerateAnniversaryRequest( + "전역일", + AnniversaryEmoji.CAKE, + LocalDate.of(2025, 5, 1) + ) + `when`(customUserDetails.getCouple()).thenReturn(couple) +// `when`(anniversaryService.generateAnniversary(customUserDetails.getCouple(),generateAnniversaryRequest)).thenReturn( +// Mockito.mock(Unit::class.java) +// ) + //when + coupleFacade.generateAnniversary(customUserDetails, generateAnniversaryRequest) + //then + verify(anniversaryService).generateAnniversary(couple, generateAnniversaryRequest) + } } From 59ff288b3cebf7ecce51dbcf5c68dfc925a1bd2b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 14:21:40 +0900 Subject: [PATCH 203/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=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 --- .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/ReadCoupleController.kt | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/presentation/ReadCoupleController.kt diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index eabb36ff..ff9ddffb 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -1,6 +1,7 @@ package gomushin.backend.couple.presentation object ApiPath { + const val COUPLE = "/v1/couple" const val COUPLE_CODE_GENERATE = "/v1/couple/code-generate" const val COUPLE_CONNECT = "/v1/couple/connect" const val COUPLE_ANNIVERSARY = "/v1/couple/anniversary" diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ReadCoupleController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ReadCoupleController.kt new file mode 100644 index 00000000..20f7c9cc --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ReadCoupleController.kt @@ -0,0 +1,29 @@ +package gomushin.backend.couple.presentation + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.couple.dto.response.CoupleInfoResponse +import gomushin.backend.couple.facade.CoupleFacade +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "커플 조회", description = "ReadCoupleController") +class ReadCoupleController( + private val coupleFacade: CoupleFacade, +) { + + @GetMapping(ApiPath.COUPLE) + @Operation( + summary = "커플 정보 조회", + description = "getCoupleInfo" + ) + fun getCoupleInfo( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ): ApiResponse { + return ApiResponse.success(coupleFacade.getInfo(customUserDetails)) + } +} From 6e199ae56b56e6ae81cd1bd78205de47d1613f8e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 14:22:48 +0900 Subject: [PATCH 204/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=ED=8C=8C=EC=82=AC=EB=93=9C=EC=99=80=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8F=B0=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/dto/response/CoupleInfoResponse.kt | 25 +++++++++++++++++ .../backend/couple/facade/CoupleFacade.kt | 28 ++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/CoupleInfoResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleInfoResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleInfoResponse.kt new file mode 100644 index 00000000..87667a21 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleInfoResponse.kt @@ -0,0 +1,25 @@ +package gomushin.backend.couple.dto.response + +import gomushin.backend.couple.domain.entity.Couple +import io.swagger.v3.oas.annotations.media.Schema + +data class CoupleInfoResponse( + @Schema(description = "커플 ID", example = "1") + val coupleId: Long, + + @Schema(description = "속해있는 군", example = "MARINE") + val military: String, + + @Schema(description = "커플 기념일 초기화 되었는지 여부", example = "false") + val isInit: Boolean = false, +) { + companion object { + fun of(couple: Couple): CoupleInfoResponse { + return CoupleInfoResponse( + coupleId = couple.id, + military = couple.military.toString(), + isInit = couple.isInit, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index e0898291..146b8f14 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -1,12 +1,14 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleConnectService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.domain.service.CoupleService import gomushin.backend.couple.dto.request.* import gomushin.backend.couple.dto.response.* +import gomushin.backend.member.domain.service.MemberService import org.springframework.stereotype.Component @Component @@ -14,9 +16,21 @@ class CoupleFacade( private val coupleConnectService: CoupleConnectService, private val anniversaryService: AnniversaryService, private val coupleInfoService: CoupleInfoService, - private val coupleService: CoupleService + private val coupleService: CoupleService, + private val memberService: MemberService, ) { + fun getInfo(customUserDetails: CustomUserDetails): CoupleInfoResponse { + val member = memberService.getById(customUserDetails.getId()) + + if (!member.checkIsCouple()) { + throw BadRequestException("saranggun.couple.not-connected") + } + + val couple = coupleService.getById(customUserDetails.getCouple().id) + return CoupleInfoResponse.of(couple) + } + fun requestCoupleCodeGeneration(customUserDetails: CustomUserDetails) = coupleConnectService.generateCoupleCode(customUserDetails.getId()) @@ -46,7 +60,7 @@ class CoupleFacade( return coupleInfoService.getDday(customUserDetails.getId()) } - fun nickName(customUserDetails: CustomUserDetails) : NicknameResponse { + fun nickName(customUserDetails: CustomUserDetails): NicknameResponse { return coupleInfoService.getNickName(customUserDetails.getId()) } @@ -60,7 +74,10 @@ class CoupleFacade( coupleInfoService.updateMilitaryDate(couple, updateMilitaryDateRequest) } - fun updateRelationshipStartDate(customUserDetails: CustomUserDetails, updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest) { + fun updateRelationshipStartDate( + customUserDetails: CustomUserDetails, + updateRelationshipStartDateRequest: UpdateRelationshipStartDateRequest + ) { val couple = coupleService.getByMemberId(customUserDetails.getId()) coupleInfoService.updateRelationshipStartDate(couple, updateRelationshipStartDateRequest) } @@ -70,7 +87,10 @@ class CoupleFacade( return CoupleEmotionResponse.of(emotion) } - fun generateAnniversary(customUserDetails: CustomUserDetails, generateAnniversaryRequest: GenerateAnniversaryRequest) { + fun generateAnniversary( + customUserDetails: CustomUserDetails, + generateAnniversaryRequest: GenerateAnniversaryRequest + ) { anniversaryService.generateAnniversary(customUserDetails.getCouple(), generateAnniversaryRequest) } } From 0e94f23ce8221210bd4a10456916b05ddd43b387 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 14:23:18 +0900 Subject: [PATCH 205/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=85=90=EC=9D=BC=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/entity/Anniversary.kt | 10 ++++++++-- .../gomushin/backend/couple/domain/entity/Couple.kt | 7 +++++++ .../couple/domain/service/AnniversaryCalculator.kt | 2 +- .../couple/domain/service/AnniversaryService.kt | 9 +++++++++ .../couple/dto/request/CoupleAnniversaryRequest.kt | 2 +- .../gomushin/backend/member/domain/entity/Member.kt | 10 +++++++--- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt index 49259f0b..229dc13c 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt @@ -26,7 +26,7 @@ class Anniversary( @Enumerated(EnumType.STRING) @Column(name = "anniversary_emoji") - var emoji : AnniversaryEmoji? = null + var emoji: AnniversaryEmoji? = null, ) : BaseEntity() { companion object { fun autoCreate(coupleId: Long, title: String, anniversaryDate: LocalDate): Anniversary { @@ -35,10 +35,16 @@ class Anniversary( title = title, anniversaryDate = anniversaryDate, anniversaryProperty = 0, + emoji = AnniversaryEmoji.HEART ) } - fun manualCreate(coupleId: Long, title: String, anniversaryDate: LocalDate, emoji: AnniversaryEmoji) : Anniversary { + fun manualCreate( + coupleId: Long, + title: String, + anniversaryDate: LocalDate, + emoji: AnniversaryEmoji + ): Anniversary { return Anniversary( coupleId = coupleId, title = title, diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt index d5d8703e..f4fe3785 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt @@ -31,6 +31,9 @@ class Couple( @Enumerated(EnumType.STRING) var military: Military? = null, + @Column(name = "is_init") + var isInit: Boolean = false, + ) : BaseEntity() { companion object { fun of( @@ -62,4 +65,8 @@ class Couple( fun containsUser(userId: Long): Boolean = userId == invitorId || userId == inviteeId + + fun initAnniversaries() { + this.isInit = true + } } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt index f7c2b37f..9b974718 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt @@ -72,7 +72,7 @@ class AnniversaryCalculator { militaryEndDate: LocalDate, anniversaryList: MutableList, ) { - val plusDay = 100L + val plusDay = 101L var anniversaryDay = 0L var anniversaryDate = relationShipStartDate while (true) { diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 334eb0c6..274fd195 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -36,6 +36,7 @@ class AnniversaryService( ) { val couple = coupleService.getById(request.coupleId) checkUserInCouple(userId, couple) + checkCoupleAnniversaryIsInit(couple) couple.updateMilitary(request.military) @@ -55,6 +56,8 @@ class AnniversaryService( anniversaries ) + couple.initAnniversaries() + saveAll(anniversaries) } @@ -83,4 +86,10 @@ class AnniversaryService( throw BadRequestException("sarangggun.couple.not-in-couple") } } + + private fun checkCoupleAnniversaryIsInit(couple: Couple) { + if (couple.isInit) { + throw BadRequestException("sarangggun.couple.already-init") + } + } } diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt index f0fd9242..cb553a35 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/CoupleAnniversaryRequest.kt @@ -17,5 +17,5 @@ data class CoupleAnniversaryRequest( val militaryEndDate: LocalDate, @Schema(description = "부대", example = "ARMY | NAVY | AIR_FORCE | MARINE 중 택 1 ") - val military: String, + val military: String, ) diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index 8058951e..31dc82ed 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -27,7 +27,7 @@ class Member( var birthDate: LocalDate? = null, @Column(name = "status_message") - var statusMessage : String? = null, + var statusMessage: String? = null, @Column(name = "profile_image_url") var profileImageUrl: String?, @@ -45,7 +45,7 @@ class Member( @Enumerated(EnumType.STRING) @Column(name = "emotion") - var emotion : Emotion? = null, + var emotion: Emotion? = null, @Column(name = "fcm_token", nullable = false) var fcmToken: String = "", @@ -69,6 +69,10 @@ class Member( } } + fun checkIsCouple(): Boolean { + return isCouple + } + fun updateFcmToken(fcmToken: String) { this.fcmToken = fcmToken } @@ -89,7 +93,7 @@ class Member( this.nickname = nickname } - fun updateBirthday(birthDate : LocalDate) { + fun updateBirthday(birthDate: LocalDate) { this.birthDate = birthDate } } From a49ddf400d9492a81931cfe17c4e682b4b6be89f Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 14:26:09 +0900 Subject: [PATCH 206/357] =?UTF-8?q?refactor:=20CoupleFacadeTest=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/{member => couple}/facade/CoupleFacadeTest.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) rename src/test/kotlin/gomushin/backend/{member => couple}/facade/CoupleFacadeTest.kt (97%) diff --git a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt similarity index 97% rename from src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt rename to src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt index 43ebba40..2c8ce928 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt @@ -1,4 +1,4 @@ -package gomushin.backend.member.facade +package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple @@ -12,8 +12,8 @@ import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.couple.dto.response.NicknameResponse -import gomushin.backend.couple.facade.CoupleFacade import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.value.Emotion import gomushin.backend.member.domain.value.Provider import gomushin.backend.member.domain.value.Role @@ -44,6 +44,9 @@ class CoupleFacadeTest { @Mock private lateinit var coupleService: CoupleService + @Mock + private lateinit var memberService: MemberService + @InjectMocks private lateinit var coupleFacade: CoupleFacade From 55db75dc8472fa171d80dd936e7f16b28e8dd3cb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 17:36:03 +0900 Subject: [PATCH 207/357] =?UTF-8?q?refactor:=20CoupleService=EC=97=90=20?= =?UTF-8?q?=EC=84=9E=EC=97=AC=EC=9E=88=EB=8A=94=20=EA=B8=B0=EB=85=90?= =?UTF-8?q?=EC=9D=BC=20=EB=93=B1=EB=A1=9D=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?CoupleFacade=20=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/AnniversaryService.kt | 64 +++---------------- .../backend/couple/facade/CoupleFacade.kt | 53 ++++++++++++--- .../backend/couple/facade/CoupleFacadeTest.kt | 8 +-- 3 files changed, 58 insertions(+), 67 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 274fd195..c23a46f6 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -1,10 +1,8 @@ package gomushin.backend.couple.domain.service -import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository -import gomushin.backend.couple.dto.request.CoupleAnniversaryRequest import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse @@ -15,8 +13,6 @@ import java.time.LocalDate @Service class AnniversaryService( private val anniversaryRepository: AnniversaryRepository, - private val coupleService: CoupleService, - private val anniversaryCalculator: AnniversaryCalculator ) { @Transactional(readOnly = true) @@ -29,67 +25,25 @@ class AnniversaryService( return anniversaryRepository.findByCoupleIdAndDate(couple.id, date) } - @Transactional - fun registerAnniversary( - userId: Long, - request: CoupleAnniversaryRequest - ) { - val couple = coupleService.getById(request.coupleId) - checkUserInCouple(userId, couple) - checkCoupleAnniversaryIsInit(couple) - - couple.updateMilitary(request.military) - - couple.updateAnniversary( - relationshipStartDate = request.relationshipStartDate, - militaryStartDate = request.militaryStartDate, - militaryEndDate = request.militaryEndDate, - ) - - val anniversaries: MutableList = mutableListOf() - - anniversaryCalculator.calculateInitAnniversaries( - couple.id, - request.relationshipStartDate, - request.militaryStartDate, - request.militaryEndDate, - anniversaries - ) - - couple.initAnniversaries() - - saveAll(anniversaries) - } - @Transactional fun saveAll(anniversaries: List): List { return anniversaryRepository.saveAll(anniversaries) } @Transactional - fun deleteAllByCoupleId(coupleId : Long) { + fun deleteAllByCoupleId(coupleId: Long) { return anniversaryRepository.deleteAllByCoupleId(coupleId) } @Transactional fun generateAnniversary(couple: Couple, generateAnniversaryRequest: GenerateAnniversaryRequest) { - anniversaryRepository.save(Anniversary.manualCreate( - couple.id, - generateAnniversaryRequest.title, - generateAnniversaryRequest.date, - generateAnniversaryRequest.emoji - )) - } - - private fun checkUserInCouple(userId: Long, couple: Couple) { - if (!couple.containsUser(userId)) { - throw BadRequestException("sarangggun.couple.not-in-couple") - } - } - - private fun checkCoupleAnniversaryIsInit(couple: Couple) { - if (couple.isInit) { - throw BadRequestException("sarangggun.couple.already-init") - } + anniversaryRepository.save( + Anniversary.manualCreate( + couple.id, + generateAnniversaryRequest.title, + generateAnniversaryRequest.date, + generateAnniversaryRequest.emoji + ) + ) } } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 146b8f14..85f1aacc 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -2,14 +2,14 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.infrastructure.exception.BadRequestException -import gomushin.backend.couple.domain.service.AnniversaryService -import gomushin.backend.couple.domain.service.CoupleConnectService -import gomushin.backend.couple.domain.service.CoupleInfoService -import gomushin.backend.couple.domain.service.CoupleService +import gomushin.backend.couple.domain.entity.Anniversary +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.service.* import gomushin.backend.couple.dto.request.* import gomushin.backend.couple.dto.response.* import gomushin.backend.member.domain.service.MemberService import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional @Component class CoupleFacade( @@ -18,6 +18,7 @@ class CoupleFacade( private val coupleInfoService: CoupleInfoService, private val coupleService: CoupleService, private val memberService: MemberService, + private val anniversaryCalculator: AnniversaryCalculator ) { fun getInfo(customUserDetails: CustomUserDetails): CoupleInfoResponse { @@ -39,13 +40,37 @@ class CoupleFacade( request: CoupleConnectRequest ) = coupleConnectService.connectCouple(customUserDetails.getId(), request.coupleCode) + @Transactional fun registerAnniversary( customUserDetails: CustomUserDetails, request: CoupleAnniversaryRequest - ) = anniversaryService.registerAnniversary( - customUserDetails.getId(), - request - ) + ) { + val couple = coupleService.getById(request.coupleId) + checkUserInCouple(customUserDetails.getId(), couple) + checkCoupleAnniversaryIsInit(couple) + + couple.updateMilitary(request.military) + + couple.updateAnniversary( + relationshipStartDate = request.relationshipStartDate, + militaryStartDate = request.militaryStartDate, + militaryEndDate = request.militaryEndDate, + ) + + val anniversaries: MutableList = mutableListOf() + + anniversaryCalculator.calculateInitAnniversaries( + couple.id, + request.relationshipStartDate, + request.militaryStartDate, + request.militaryEndDate, + anniversaries + ) + + couple.initAnniversaries() + + anniversaryService.saveAll(anniversaries) + } fun getGradeInfo(customUserDetails: CustomUserDetails): CoupleGradeResponse { val grade = coupleInfoService.getGrade(customUserDetails.getId()) @@ -93,4 +118,16 @@ class CoupleFacade( ) { anniversaryService.generateAnniversary(customUserDetails.getCouple(), generateAnniversaryRequest) } + + private fun checkUserInCouple(userId: Long, couple: Couple) { + if (!couple.containsUser(userId)) { + throw BadRequestException("sarangggun.couple.not-in-couple") + } + } + + private fun checkCoupleAnniversaryIsInit(couple: Couple) { + if (couple.isInit) { + throw BadRequestException("sarangggun.couple.already-init") + } + } } diff --git a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt index 2c8ce928..3f8ef9e4 100644 --- a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt @@ -2,10 +2,7 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple -import gomushin.backend.couple.domain.service.AnniversaryService -import gomushin.backend.couple.domain.service.CoupleConnectService -import gomushin.backend.couple.domain.service.CoupleInfoService -import gomushin.backend.couple.domain.service.CoupleService +import gomushin.backend.couple.domain.service.* import gomushin.backend.couple.domain.value.AnniversaryEmoji import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest @@ -47,6 +44,9 @@ class CoupleFacadeTest { @Mock private lateinit var memberService: MemberService + @Mock + private lateinit var anniversaryCalculator: AnniversaryCalculator + @InjectMocks private lateinit var coupleFacade: CoupleFacade From 2736bd57803a997abb6f2638d4b67776191809d9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 17:39:06 +0900 Subject: [PATCH 208/357] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=85=90=EC=9D=BC?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=97=AC=EB=B6=80=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/couple/domain/entity/Couple.kt | 6 +++--- .../backend/couple/dto/response/CoupleInfoResponse.kt | 4 ++-- .../kotlin/gomushin/backend/couple/facade/CoupleFacade.kt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt index f4fe3785..92b59903 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Couple.kt @@ -31,8 +31,8 @@ class Couple( @Enumerated(EnumType.STRING) var military: Military? = null, - @Column(name = "is_init") - var isInit: Boolean = false, + @Column(name = "is_anniversaries_registered") + var isAnniversariesRegistered: Boolean = false, ) : BaseEntity() { companion object { @@ -67,6 +67,6 @@ class Couple( userId == invitorId || userId == inviteeId fun initAnniversaries() { - this.isInit = true + this.isAnniversariesRegistered = true } } diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleInfoResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleInfoResponse.kt index 87667a21..e0e232a1 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleInfoResponse.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleInfoResponse.kt @@ -11,14 +11,14 @@ data class CoupleInfoResponse( val military: String, @Schema(description = "커플 기념일 초기화 되었는지 여부", example = "false") - val isInit: Boolean = false, + val isAnniversariesRegistered: Boolean = false, ) { companion object { fun of(couple: Couple): CoupleInfoResponse { return CoupleInfoResponse( coupleId = couple.id, military = couple.military.toString(), - isInit = couple.isInit, + isAnniversariesRegistered = couple.isAnniversariesRegistered, ) } } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 85f1aacc..929d0aea 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -126,7 +126,7 @@ class CoupleFacade( } private fun checkCoupleAnniversaryIsInit(couple: Couple) { - if (couple.isInit) { + if (couple.isAnniversariesRegistered) { throw BadRequestException("sarangggun.couple.already-init") } } From 5e32babfdc8d0a6f0b21eccc49a23d44a2cdcff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 1 May 2025 23:10:06 +0900 Subject: [PATCH 209/357] =?UTF-8?q?feat=20:=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20#65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ScheduleDetailResponse.kt | 26 +++++++++++++++++++ .../schedule/facade/ReadScheduleFacade.kt | 18 +++++++++++++ .../backend/schedule/presentation/ApiPath.kt | 1 + .../presentation/ReadScheduleController.kt | 12 +++++++++ 4 files changed, 57 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt new file mode 100644 index 00000000..ee33c9e5 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt @@ -0,0 +1,26 @@ +package gomushin.backend.schedule.dto.response + +import gomushin.backend.schedule.domain.entity.Schedule +import java.time.LocalDateTime + +data class ScheduleDetailResponse( + val id : Long, + val title : String, + val fatigue : String, + val startDate : LocalDateTime, + val endDate : LocalDateTime, + val letters : List +) { + companion object { + fun of(schedule : Schedule, letters: List) : ScheduleDetailResponse{ + return ScheduleDetailResponse( + schedule.id, + schedule.title, + schedule.fatigue, + schedule.startDate, + schedule.endDate, + letters + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index c7ea8a72..4ddf7f89 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -2,9 +2,13 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.response.DailySchedulesAndAnniversariesResponse +import gomushin.backend.schedule.dto.response.LetterPreviewResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse +import gomushin.backend.schedule.dto.response.ScheduleDetailResponse import org.springframework.stereotype.Component import java.time.LocalDate @@ -12,6 +16,8 @@ import java.time.LocalDate class ReadScheduleFacade( private val scheduleService: ScheduleService, private val anniversaryService: AnniversaryService, + private val letterService: LetterService, + private val pictureService: PictureService ) { fun getList( @@ -30,4 +36,16 @@ class ReadScheduleFacade( val dailyAnniversaries = anniversaryService.findByDate(customUserDetails.getCouple(), date) return DailySchedulesAndAnniversariesResponse.of(dailySchedules, dailyAnniversaries) } + + fun getScheduleDetail(customUserDetails: CustomUserDetails, scheduleId: Long): ScheduleDetailResponse { + val schedule = scheduleService.getById(scheduleId) + val letters = letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule) + val letterIds = letters.map { it.id } + val picturesByLetterId = pictureService.findAllByLetterIds(letterIds) + .associateBy { it.letterId } + val letterPreviews = letters.map { letter -> + LetterPreviewResponse.of(letter, picturesByLetterId[letter.id]) + } + return ScheduleDetailResponse.of(schedule, letterPreviews) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt index 66e28b1e..f2ca9862 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt @@ -4,6 +4,7 @@ object ApiPath { const val SCHEDULES = "/v1/schedules" const val SCHEDULE = "/v1/schedules/{scheduleId}" const val SCHEDULES_BY_DATE = "/v1/schedules/date" + const val SCHEDULE_DETAIL = "/v1/schedules/detail/{scheduleId}" const val LETTERS = "/v1/schedules/letters" const val LETTER = "/v1/schedules/{scheduleId}/letters/{letterId}" diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt index bff0b060..6a43499a 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt @@ -4,11 +4,13 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.schedule.dto.response.DailySchedulesAndAnniversariesResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse +import gomushin.backend.schedule.dto.response.ScheduleDetailResponse import gomushin.backend.schedule.facade.ReadScheduleFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import java.time.LocalDate @@ -39,4 +41,14 @@ class ReadScheduleController( val schedules = readScheduleFacade.get(customUserDetails, date) return ApiResponse.success(schedules) } + + @GetMapping(ApiPath.SCHEDULE_DETAIL) + @Operation(summary = "특정 스케쥴 상세조회", description = "getScheduleDetail") + fun getScheduleDetail( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable scheduleId: Long + ): ApiResponse{ + val scheduleDetails = readScheduleFacade.getScheduleDetail(customUserDetails, scheduleId) + return ApiResponse.success(scheduleDetails) + } } From c6ce42f9113ab85eed807a957b777e72eab7a52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 1 May 2025 23:48:11 +0900 Subject: [PATCH 210/357] =?UTF-8?q?test=20:=20=EC=9D=BC=EC=A0=95=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20api=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=20#65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/facade/ReadScheduleFacadeTest.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt index d942ff41..1cb38d35 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt @@ -4,6 +4,11 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse +import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.entity.Picture +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.response.DailyScheduleResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse @@ -17,6 +22,7 @@ import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension import java.time.LocalDate +import java.time.LocalDateTime @ExtendWith(MockitoExtension::class) class ReadScheduleFacadeTest { @@ -27,6 +33,12 @@ class ReadScheduleFacadeTest { @Mock private lateinit var anniversaryService: AnniversaryService + @Mock + private lateinit var letterService: LetterService + + @Mock + private lateinit var pictureService: PictureService + @InjectMocks private lateinit var readScheduleFacade: ReadScheduleFacade @@ -73,4 +85,36 @@ class ReadScheduleFacadeTest { verify(scheduleService, times(1)).findByDate(customUserDetails.getCouple(), date) } + @DisplayName("getScheduleDetail - 성공") + @Test + fun getScheduleDetail_success() { + //given + val scheduleId = 1L + val letterId = 2L + val mockLetter = mock(Letter::class.java) + val mockPicture = mock(Picture::class.java) + + val schedule = Schedule( + id = scheduleId, + title = "일정 제목", + fatigue = "VERT_TIRED", + startDate = LocalDateTime.of(2025, 5, 1, 7, 0,0), + endDate = LocalDateTime.of(2025, 5, 2,20,0,0) + ) + + //when + `when`(mockLetter.id).thenReturn(letterId) + `when`(mockPicture.letterId).thenReturn(letterId) + `when`(scheduleService.getById(scheduleId)).thenReturn(schedule) + `when`(letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule)).thenReturn(listOf(mockLetter)) + `when`(pictureService.findAllByLetterIds(listOf(letterId))).thenReturn(listOf(mockPicture)) + + readScheduleFacade.getScheduleDetail(customUserDetails, scheduleId) + + //then + verify(scheduleService).getById(scheduleId) + verify(letterService).findByCoupleAndSchedule(customUserDetails.getCouple(), schedule) + verify(pictureService).findAllByLetterIds(listOf(letterId)) + } + } From a72039442811c44ff2994fdf7f00ad97cd15c354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 2 May 2025 00:22:45 +0900 Subject: [PATCH 211/357] =?UTF-8?q?refactor=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(=EC=BD=94=EB=93=9C=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95)=20#65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/dto/response/ScheduleDetailResponse.kt | 2 +- .../backend/schedule/presentation/ReadScheduleController.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt index ee33c9e5..36328f88 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt @@ -12,7 +12,7 @@ data class ScheduleDetailResponse( val letters : List ) { companion object { - fun of(schedule : Schedule, letters: List) : ScheduleDetailResponse{ + fun of(schedule : Schedule, letters: List) : ScheduleDetailResponse { return ScheduleDetailResponse( schedule.id, schedule.title, diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt index 6a43499a..3784f688 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt @@ -47,7 +47,7 @@ class ReadScheduleController( fun getScheduleDetail( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @PathVariable scheduleId: Long - ): ApiResponse{ + ): ApiResponse { val scheduleDetails = readScheduleFacade.getScheduleDetail(customUserDetails, scheduleId) return ApiResponse.success(scheduleDetails) } From de4a6c763ad18c9ae2fcfb88cb6debcdf0c0c39d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 23:27:40 +0900 Subject: [PATCH 212/357] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20=EB=94=94=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=EC=99=80=20=EC=95=9E=EC=9C=BC=EB=A1=9C=20=EC=9D=BC=EC=A3=BC?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=BC=EC=A0=95,=20=EB=82=98=EC=97=90=EA=B2=8C?= =?UTF-8?q?=20=EC=98=A8=20=ED=8E=B8=EC=A7=80=20=EC=B5=9C=EA=B7=BC=205?= =?UTF-8?q?=EA=B0=9C=20API=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/AnniversaryController.kt | 23 ++++++++++++++----- .../backend/couple/presentation/ApiPath.kt | 1 + .../backend/schedule/presentation/ApiPath.kt | 2 ++ .../presentation/ReadLetterController.kt | 11 +++++++++ .../presentation/ReadScheduleController.kt | 10 ++++++++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt index b7c26ce4..ffd2776c 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt @@ -3,20 +3,20 @@ package gomushin.backend.couple.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest +import gomushin.backend.couple.dto.response.MainAnniversaryResponse +import gomushin.backend.couple.facade.AnniversaryFacade import gomushin.backend.couple.facade.CoupleFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.ResponseStatus -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @Tag(name = "기념일 생성", description = "AnniversaryController") class AnniversaryController( - private val coupleFacade: CoupleFacade + private val coupleFacade: CoupleFacade, + private val anniversaryFacade: AnniversaryFacade ) { @ResponseStatus(HttpStatus.CREATED) @PostMapping(ApiPath.ANNIVERSARY_GENERATE) @@ -31,4 +31,15 @@ class AnniversaryController( coupleFacade.generateAnniversary(customUserDetails, generateAnniversaryRequest) return ApiResponse.success(true) } -} \ No newline at end of file + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.ANNIVERSARY_MAIN) + @Operation( + summary = "메인 - 가까운 3개의 기념일 조회", + description = "getAnniversariesMain" + ) + fun getAnniversariesMain( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ): ApiResponse> = + ApiResponse.success(anniversaryFacade.getAnniversaryListMain(customUserDetails)) +} diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index ff9ddffb..4e2bfb94 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -14,4 +14,5 @@ object ApiPath { const val COUPLE_UPDATE_RELATIONSHIP_DATE = "/v1/couple/relationship-start-date" const val COUPLE_EMOJI = "/v1/couple/emotion" const val ANNIVERSARY_GENERATE = "/v1/couple/new-anniversary" + const val ANNIVERSARY_MAIN = "/v1/anniversary/main" } diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt index f2ca9862..6d0d262a 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt @@ -4,12 +4,14 @@ object ApiPath { const val SCHEDULES = "/v1/schedules" const val SCHEDULE = "/v1/schedules/{scheduleId}" const val SCHEDULES_BY_DATE = "/v1/schedules/date" + const val SCHEDULES_BY_WEEK = "/v1/schedules/week" const val SCHEDULE_DETAIL = "/v1/schedules/detail/{scheduleId}" const val LETTERS = "/v1/schedules/letters" const val LETTER = "/v1/schedules/{scheduleId}/letters/{letterId}" const val LETTERS_TO_ME = "/v1/schedules/letters/to-me" const val LETTERS_BY_SCHEDULE = "/v1/schedules/{scheduleId}" + const val LETTERS_MAIN = "/v1/schedules/letters/main" const val COMMENTS = "/v1/schedules/letters/{letterId}/comments" const val COMMENT = "/v1/schedules/letters/{letterId}/comments/{commentId}" diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index be958b32..091bf42e 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -6,6 +6,7 @@ import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.schedule.dto.request.ReadLettersToMePaginationRequest import gomushin.backend.schedule.dto.response.LetterDetailResponse import gomushin.backend.schedule.dto.response.LetterPreviewResponse +import gomushin.backend.schedule.dto.response.MainLetterPreviewResponse import gomushin.backend.schedule.facade.ReadLetterFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag @@ -51,4 +52,14 @@ class ReadLetterController( val letters = readLetterFacade.getLetterListToMe(customUserDetails,readLettersToMePaginationRequest) return ApiResponse.success(letters) } + + @GetMapping(ApiPath.LETTERS_MAIN) + @Operation(summary = "메인화면 편지 리스트 가져오기", description = "getLetterListMain") + fun getLetterListMain( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + ): ApiResponse> { + val letters = readLetterFacade.getLetterListMain(customUserDetails) + return ApiResponse.success(letters) + } + } diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt index 3784f688..3bddc821 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt @@ -3,6 +3,7 @@ package gomushin.backend.schedule.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.schedule.dto.response.DailySchedulesAndAnniversariesResponse +import gomushin.backend.schedule.dto.response.MainSchedulesAndAnniversariesResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse import gomushin.backend.schedule.dto.response.ScheduleDetailResponse import gomushin.backend.schedule.facade.ReadScheduleFacade @@ -51,4 +52,13 @@ class ReadScheduleController( val scheduleDetails = readScheduleFacade.getScheduleDetail(customUserDetails, scheduleId) return ApiResponse.success(scheduleDetails) } + + @GetMapping(ApiPath.SCHEDULES_BY_WEEK) + @Operation(summary = "메인 - 오늘 부터 일주일간의 일정 및 기념일 가져오기", description = "getScheduleByWeek") + fun getScheduleByWeek( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + ): ApiResponse { + val schedules = readScheduleFacade.getListByWeek(customUserDetails) + return ApiResponse.success(schedules) + } } From 467bb1bdf7fdd32d026472d64fd31a5638c6718a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 23:27:52 +0900 Subject: [PATCH 213/357] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20=EB=94=94=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=EC=99=80=20=EC=95=9E=EC=9C=BC=EB=A1=9C=20=EC=9D=BC=EC=A3=BC?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=BC=EC=A0=95,=20=EB=82=98=EC=97=90=EA=B2=8C?= =?UTF-8?q?=20=EC=98=A8=20=ED=8E=B8=EC=A7=80=20=EC=B5=9C=EA=B7=BC=205?= =?UTF-8?q?=EA=B0=9C=20=EB=A6=AC=EC=8A=A4=ED=8F=B0=EC=8A=A4=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 --- .../dto/response/MainAnniversaryResponse.kt | 24 +++++++++++ .../dto/response/MainAnniversariesResponse.kt | 7 ++++ .../dto/response/MainLetterPreviewResponse.kt | 40 +++++++++++++++++++ .../MainSchedulesAndAnniversariesResponse.kt | 16 ++++++++ .../dto/response/MainSchedulesResponse.kt | 11 +++++ 5 files changed, 98 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/MainAnniversaryResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/MainAnniversariesResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/MainSchedulesAndAnniversariesResponse.kt create mode 100644 src/main/kotlin/gomushin/backend/schedule/dto/response/MainSchedulesResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/MainAnniversaryResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/MainAnniversaryResponse.kt new file mode 100644 index 00000000..7df035dd --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/MainAnniversaryResponse.kt @@ -0,0 +1,24 @@ +package gomushin.backend.couple.dto.response + +import gomushin.backend.couple.domain.entity.Anniversary +import java.time.LocalDate + +data class MainAnniversaryResponse( + val id: Long, + val emoji: String, + val title: String, + val anniversaryDate: LocalDate, +) { + companion object { + fun of( + anniversary: Anniversary + ): MainAnniversaryResponse { + return MainAnniversaryResponse( + id = anniversary.id, + emoji = anniversary.emoji!!.name, + title = anniversary.title, + anniversaryDate = anniversary.anniversaryDate + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainAnniversariesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainAnniversariesResponse.kt new file mode 100644 index 00000000..59df3366 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainAnniversariesResponse.kt @@ -0,0 +1,7 @@ +package gomushin.backend.schedule.dto.response + +data class MainAnniversariesResponse( + val id: Long, + val title: String, + val anniversaryDate: String, +) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt new file mode 100644 index 00000000..d552020d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt @@ -0,0 +1,40 @@ +package gomushin.backend.schedule.dto.response + +import gomushin.backend.schedule.domain.entity.Letter +import gomushin.backend.schedule.domain.entity.Picture +import gomushin.backend.schedule.domain.entity.Schedule +import java.time.LocalDateTime + +data class MainLetterPreviewResponse( + val letterId: Long?, + val title: String?, + val content: String?, + val pictureUrl: String?, + val schedule: String?, + val createdAt: LocalDateTime?, +) { + companion object { + private const val MAX_CONTENT_LENGTH = 30 + private const val PREVIEW_CONTENT_LENGTH = 27 + + fun of( + letter: Letter?, + picture: Picture?, + schedule: Schedule?, + ): MainLetterPreviewResponse { + val previewContent = if (letter?.content != null && letter.content.length > MAX_CONTENT_LENGTH) { + letter.content.take(PREVIEW_CONTENT_LENGTH) + "..." + } else { + letter?.content + } + return MainLetterPreviewResponse( + letterId = letter?.id, + title = letter?.title, + content = previewContent, + pictureUrl = picture?.pictureUrl, + schedule = schedule?.title, + createdAt = letter?.createdAt + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainSchedulesAndAnniversariesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainSchedulesAndAnniversariesResponse.kt new file mode 100644 index 00000000..91992e10 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainSchedulesAndAnniversariesResponse.kt @@ -0,0 +1,16 @@ +package gomushin.backend.schedule.dto.response + +data class MainSchedulesAndAnniversariesResponse( + val schedules: List, + val anniversaries: List, +) { + companion object { + fun of( + schedules: List, + anniversaries: List, + ): MainSchedulesAndAnniversariesResponse { + return MainSchedulesAndAnniversariesResponse(schedules, anniversaries) + } + } +} + diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainSchedulesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainSchedulesResponse.kt new file mode 100644 index 00000000..f2ff9755 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainSchedulesResponse.kt @@ -0,0 +1,11 @@ +package gomushin.backend.schedule.dto.response + +import java.time.LocalDateTime + +data class MainSchedulesResponse( + val id: Long, + val title: String, + val startDate: LocalDateTime, + val endDate: LocalDateTime, + val fatigue: String, +) From 32304d0934e074e564c61549ceaf42e3418528fa Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 23:38:09 +0900 Subject: [PATCH 214/357] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20=EB=94=94=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=EC=99=80=20=EC=95=9E=EC=9C=BC=EB=A1=9C=20=EC=9D=BC=EC=A3=BC?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=BC=EC=A0=95,=20=EB=82=98=EC=97=90=EA=B2=8C?= =?UTF-8?q?=20=EC=98=A8=20=ED=8E=B8=EC=A7=80=20=EC=B5=9C=EA=B7=BC=205?= =?UTF-8?q?=EA=B0=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/JwtAuthenticationFilter.kt | 1 + .../repository/AnniversaryRepository.kt | 38 ++++++++++++++++++- .../domain/service/AnniversaryService.kt | 12 ++++++ .../couple/facade/AnniversaryFacade.kt | 16 ++++++++ .../domain/repository/LetterRepository.kt | 2 + .../domain/repository/ScheduleRepository.kt | 22 +++++++++++ .../schedule/domain/service/LetterService.kt | 5 +++ .../domain/service/ScheduleService.kt | 16 +++++++- .../schedule/facade/ReadLetterFacade.kt | 11 ++++++ .../schedule/facade/ReadScheduleFacade.kt | 22 +++++++++++ 10 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index c0e20e87..9c25fd5b 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -45,6 +45,7 @@ class JwtAuthenticationFilter( } else -> { + // TODO: 에러 응답 처리 SecurityContextHolder.clearContext() } } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index 52df4583..af3aab18 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -3,11 +3,13 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse +import gomushin.backend.schedule.dto.response.MainAnniversariesResponse import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import java.time.LocalDate +import java.time.LocalDateTime interface AnniversaryRepository : JpaRepository { @Modifying @@ -51,6 +53,40 @@ interface AnniversaryRepository : JpaRepository { AND function('DATE', a.anniversaryDate) = :startDate """ ) - fun findByCoupleIdAndDate(coupleId: Long, date: LocalDate): List + fun findByCoupleIdAndDate(coupleId: Long, startDate: LocalDate): List + + @Query( + """ + SELECT new gomushin.backend.schedule.dto.response.DailyAnniversaryResponse( + a.id, + a.title, + a.anniversaryDate + ) + FROM Anniversary a + WHERE a.coupleId = :coupleId + AND a.anniversaryDate BETWEEN :startDate AND :endDate + """ + ) + fun findByCoupleIdAndDateBetween( + coupleId: Long, + startDate: LocalDate, + endDate: LocalDate + ): List + + + @Query( + value = + """ + SELECT * FROM anniversary + WHERE couple_id = :coupleId + AND anniversary_date > CURRENT_DATE + ORDER BY anniversary_date ASC + LIMIT 3 + """, + nativeQuery = true + ) + fun findTop3UpcomingAnniversaries( + @Param("coupleId") coupleId: Long + ): List } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index c23a46f6..e81b471b 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -6,15 +6,22 @@ import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse +import gomushin.backend.schedule.dto.response.MainAnniversariesResponse import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate +import java.time.LocalDateTime @Service class AnniversaryService( private val anniversaryRepository: AnniversaryRepository, ) { + @Transactional(readOnly = true) + fun findByCoupleIdAndDateBetween(couple: Couple, startDate: LocalDate, endDate: LocalDate): List { + return anniversaryRepository.findByCoupleIdAndDateBetween(couple.id, startDate, endDate) + } + @Transactional(readOnly = true) fun findByCoupleIdAndYearAndMonth(couple: Couple, year: Int, month: Int): List { return anniversaryRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) @@ -46,4 +53,9 @@ class AnniversaryService( ) ) } + + @Transactional(readOnly = true) + fun getUpcomingTop3Anniversaries(couple: Couple): List { + return anniversaryRepository.findTop3UpcomingAnniversaries(couple.id) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt new file mode 100644 index 00000000..7391ad97 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -0,0 +1,16 @@ +package gomushin.backend.couple.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.dto.response.MainAnniversaryResponse +import org.springframework.stereotype.Component + +@Component +class AnniversaryFacade( + private val anniversaryService: AnniversaryService, +) { + fun getAnniversaryListMain(customUserDetails: CustomUserDetails): List { + val anniversaries = anniversaryService.getUpcomingTop3Anniversaries(customUserDetails.getCouple()) + return anniversaries.map { MainAnniversaryResponse.of(it) } + } +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index 3959688b..d8487199 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -36,4 +36,6 @@ interface LetterRepository : JpaRepository { @Param("key") key: Long, @Param("take") take: Long, ): List + + fun findTop5ByCoupleIdOrderByCreatedAtDesc(coupleId: Long): List } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt index 0447c440..72a38794 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt @@ -2,12 +2,14 @@ package gomushin.backend.schedule.domain.repository import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.dto.response.DailyScheduleResponse +import gomushin.backend.schedule.dto.response.MainSchedulesResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import java.time.LocalDate +import java.time.LocalDateTime interface ScheduleRepository : JpaRepository { @Modifying @@ -47,4 +49,24 @@ interface ScheduleRepository : JpaRepository { """ ) fun findByCoupleIdAndStartDate(coupleId: Long, startDate: LocalDate): List + + @Query( + """ + SELECT new gomushin.backend.schedule.dto.response.MainSchedulesResponse( + s.id, + s.title, + s.startDate, + s.endDate, + s.fatigue + ) + FROM Schedule s + WHERE s.coupleId = :coupleId + AND s.startDate BETWEEN :startDate AND :endDate + """ + ) + fun findByCoupleIdAndStartDateBetween( + coupleId: Long, + startDate: LocalDateTime, + endDate: LocalDateTime + ): List } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index 66ae82c1..07b9e582 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -90,4 +90,9 @@ class LetterService( readLettersToMePaginationRequest.take, ) } + + @Transactional(readOnly = true) + fun findTop5ByCreatedDateDesc(couple: Couple): List { + return letterRepository.findTop5ByCoupleIdOrderByCreatedAtDesc(couple.id) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt index ba784400..885b10b9 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt @@ -6,6 +6,7 @@ import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.ScheduleRepository import gomushin.backend.schedule.dto.request.UpsertScheduleRequest import gomushin.backend.schedule.dto.response.DailyScheduleResponse +import gomushin.backend.schedule.dto.response.MainSchedulesResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -21,6 +22,19 @@ class ScheduleService( return scheduleRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) } + @Transactional(readOnly = true) + fun findByCoupleIdAndDateBetween( + couple: Couple, + startDate: LocalDate, + endDate: LocalDate + ): List { + return scheduleRepository.findByCoupleIdAndStartDateBetween( + couple.id, + startDate.atStartOfDay(), + endDate.atTime(23, 59, 59) + ) + } + @Transactional(readOnly = true) fun findByDate(couple: Couple, date: LocalDate): List { return scheduleRepository.findByCoupleIdAndStartDate(couple.id, date) @@ -58,7 +72,7 @@ class ScheduleService( } @Transactional - fun deleteAllByMember(memberId : Long) { + fun deleteAllByMember(memberId: Long) { scheduleRepository.deleteAllByUserId(memberId) } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 2cf28340..14ed1f22 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -103,4 +103,15 @@ class ReadLetterFacade( isLastPage = isLastPage ) } + + fun getLetterListMain( + customUserDetails: CustomUserDetails, + ): List { + val letters = letterService.findTop5ByCreatedDateDesc(customUserDetails.getCouple()) + return letters.map { letter -> + val picture = pictureService.findFirstByLetterId(letter.id) + val schedule = scheduleService.findById(letter.scheduleId) + MainLetterPreviewResponse.of(letter, picture, schedule) + } + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index 4ddf7f89..a1842448 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -6,11 +6,14 @@ import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.response.DailySchedulesAndAnniversariesResponse +import gomushin.backend.schedule.dto.response.MainSchedulesAndAnniversariesResponse +import gomushin.backend.schedule.dto.response.MainSchedulesResponse import gomushin.backend.schedule.dto.response.LetterPreviewResponse import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse import gomushin.backend.schedule.dto.response.ScheduleDetailResponse import org.springframework.stereotype.Component import java.time.LocalDate +import java.time.LocalDateTime @Component class ReadScheduleFacade( @@ -48,4 +51,23 @@ class ReadScheduleFacade( } return ScheduleDetailResponse.of(schedule, letterPreviews) } + + fun getListByWeek(customUserDetails: CustomUserDetails): MainSchedulesAndAnniversariesResponse { + val today = LocalDate.now() + val schedules = scheduleService.findByCoupleIdAndDateBetween( + customUserDetails.getCouple(), + today, + today.plusDays(6) + ) + val anniversaries = anniversaryService.findByCoupleIdAndDateBetween( + customUserDetails.getCouple(), + today, + today.plusDays(6) + ) + + return MainSchedulesAndAnniversariesResponse.of( + schedules, + anniversaries + ) + } } From c0ee4ab9b161cda4acce29e7922d06bbfdf06271 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 1 May 2025 23:38:24 +0900 Subject: [PATCH 215/357] =?UTF-8?q?fix:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=BC=EC=9E=90=201=EC=9D=BC=20=EC=B0=A8=EC=9D=B4=EB=82=98?= =?UTF-8?q?=EB=8A=94=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/service/AnniversaryCalculator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt index 9b974718..e43df239 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryCalculator.kt @@ -72,9 +72,9 @@ class AnniversaryCalculator { militaryEndDate: LocalDate, anniversaryList: MutableList, ) { - val plusDay = 101L + val plusDay = 100L var anniversaryDay = 0L - var anniversaryDate = relationShipStartDate + var anniversaryDate = relationShipStartDate.minusDays(1) while (true) { anniversaryDate = anniversaryDate.plusDays(plusDay) anniversaryDay += plusDay From d585b00fd260bfa38b3350edd3b8f86e6d1908d4 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 2 May 2025 00:29:55 +0900 Subject: [PATCH 216/357] =?UTF-8?q?fix:=20=EC=BF=BC=EB=A6=AC=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/repository/AnniversaryRepository.kt | 2 +- .../schedule/dto/response/MainAnniversariesResponse.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index af3aab18..a9fd6e0b 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -58,7 +58,7 @@ interface AnniversaryRepository : JpaRepository { @Query( """ - SELECT new gomushin.backend.schedule.dto.response.DailyAnniversaryResponse( + SELECT new gomushin.backend.schedule.dto.response.MainAnniversariesResponse( a.id, a.title, a.anniversaryDate diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainAnniversariesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainAnniversariesResponse.kt index 59df3366..16433e13 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainAnniversariesResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainAnniversariesResponse.kt @@ -1,7 +1,9 @@ package gomushin.backend.schedule.dto.response +import java.time.LocalDate + data class MainAnniversariesResponse( val id: Long, val title: String, - val anniversaryDate: String, + val anniversaryDate: LocalDate, ) From d41abc67ab65d10ba88307200ddc9ce3c753c192 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 2 May 2025 00:33:46 +0900 Subject: [PATCH 217/357] =?UTF-8?q?fix:=20=EC=B6=A9=EB=8F=8C=20=EB=B3=91?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/service/AnniversaryService.kt | 1 - .../backend/schedule/facade/ReadScheduleFacade.kt | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index e81b471b..73a0d7c1 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -10,7 +10,6 @@ import gomushin.backend.schedule.dto.response.MainAnniversariesResponse import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate -import java.time.LocalDateTime @Service class AnniversaryService( diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index a1842448..acc05aca 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -5,15 +5,9 @@ import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService -import gomushin.backend.schedule.dto.response.DailySchedulesAndAnniversariesResponse -import gomushin.backend.schedule.dto.response.MainSchedulesAndAnniversariesResponse -import gomushin.backend.schedule.dto.response.MainSchedulesResponse -import gomushin.backend.schedule.dto.response.LetterPreviewResponse -import gomushin.backend.schedule.dto.response.MonthlySchedulesAndAnniversariesResponse -import gomushin.backend.schedule.dto.response.ScheduleDetailResponse +import gomushin.backend.schedule.dto.response.* import org.springframework.stereotype.Component import java.time.LocalDate -import java.time.LocalDateTime @Component class ReadScheduleFacade( From 1bc5ea8e167d71393f00dc35e0c77f163f669a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 2 May 2025 02:29:22 +0900 Subject: [PATCH 218/357] =?UTF-8?q?build=20:=20=EC=BD=94=EB=A3=A8=ED=8B=B4?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 193b0901..3af37697 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -76,6 +76,9 @@ dependencies { //google auth implementation("com.google.auth:google-auth-library-oauth2-http:1.33.1") + //coroutine + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.springframework.boot:spring-boot-starter-validation") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") From f0ab0e7b7ef538402c5760416fa7e989112108f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 2 May 2025 02:30:22 +0900 Subject: [PATCH 219/357] =?UTF-8?q?feat=20:=2019=EC=8B=9C=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20=EB=AA=A8=EB=93=A0=20=EC=BB=A4=ED=94=8C=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=97=90=EA=B2=8C=20=EC=A7=88=EB=AC=B8=ED=98=95=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=B3=B4=EB=82=B4=EA=B8=B0=20#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/BackendApplication.kt | 2 + .../alarm/service/QuestionAlarmService.kt | 46 +++++++++++++++++++ .../alarm/service/StatusAlarmService.kt | 12 ++--- .../backend/alarm/util/MessageParsingUtil.kt | 10 ++++ .../domain/repository/MemberRepository.kt | 3 ++ .../member/domain/service/MemberService.kt | 5 ++ 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt create mode 100644 src/main/kotlin/gomushin/backend/alarm/util/MessageParsingUtil.kt diff --git a/src/main/kotlin/gomushin/backend/BackendApplication.kt b/src/main/kotlin/gomushin/backend/BackendApplication.kt index 3afdf3a8..63282034 100644 --- a/src/main/kotlin/gomushin/backend/BackendApplication.kt +++ b/src/main/kotlin/gomushin/backend/BackendApplication.kt @@ -4,10 +4,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication import org.springframework.scheduling.annotation.EnableAsync +import org.springframework.scheduling.annotation.EnableScheduling @ConfigurationPropertiesScan @SpringBootApplication @EnableAsync +@EnableScheduling class BackendApplication fun main(args: Array) { diff --git a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt new file mode 100644 index 00000000..2d5410d6 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt @@ -0,0 +1,46 @@ +package gomushin.backend.alarm.service + +import gomushin.backend.alarm.util.MessageParsingUtil +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.member.domain.service.MemberService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import org.hibernate.query.sqm.tree.SqmNode.log +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.time.LocalDateTime + +@Service +class QuestionAlarmService ( + private val fcmService: FCMService, + private val memberService: MemberService +) { + private val questionMessages = listOf( + "오늘 하루는 어땠나요?+연인에게도 안부를 전해보세요", + "사랑은 매일 채우는 것+오늘도 연인과 한 조각 채워보세요", + "네가 있어서 참 좋아+연인에게 고마움을 전해보세요", + "매일 사랑이 자라요+가벼운 전화통화 어때요?" + ) + + @Scheduled(cron = "0 0 19 * * *", zone = "Asia/Seoul") + fun sendQuestionAlarms() { + val coupleMembers = memberService.getAllCoupledMember() + runBlocking { + coupleMembers.map { member -> + async(Dispatchers.IO) { + try { + val notificationContent = questionMessages.random() + val (title, sendContent) = MessageParsingUtil.parse(notificationContent) + log.info("질문형 메시지 전송 : 수신자 {${member.name}}, 제목 {${title}}, 내용{${sendContent}}, 전송시각{${LocalDateTime.now()}}\n") + fcmService.sendMessageTo(member.fcmToken, title, sendContent) + } catch (e: Exception) { + log.error("질문형 메시지 전송오류 : 수신자 {${member.name}}, 전송시각{${LocalDateTime.now()}}\n") + throw BadRequestException("sarangggun.alarm.fail-issue-accesstoken") + } + } + }.awaitAll() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt index 6bf175f1..023c9acc 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt @@ -1,5 +1,6 @@ package gomushin.backend.alarm.service +import gomushin.backend.alarm.util.MessageParsingUtil import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.value.Emotion @@ -46,14 +47,13 @@ class StatusAlarmService ( val notificationContent = statusMessage[emotion]?.random() ?: throw BadRequestException("sarangggun.member.not-exist-emoji") - val parts = notificationContent.split("+") - val title = parts[0] - val content = if (parts[1].contains("OO")) { - parts[1].replace("OO", sender.nickname) + val (title, content) = MessageParsingUtil.parse(notificationContent) + val sendContent = if (content.contains("OO")) { + content.replace("OO", sender.nickname) } else { - parts[1] + content } val token = receiver.fcmToken - fcmService.sendMessageTo(token, title, content) + fcmService.sendMessageTo(token, title, sendContent) } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/alarm/util/MessageParsingUtil.kt b/src/main/kotlin/gomushin/backend/alarm/util/MessageParsingUtil.kt new file mode 100644 index 00000000..cce92fc2 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/alarm/util/MessageParsingUtil.kt @@ -0,0 +1,10 @@ +package gomushin.backend.alarm.util + +object MessageParsingUtil { + fun parse(notificationContent : String) : Pair { + val parts = notificationContent.split("+") + val title = parts[0] + val content = parts[1] + return title to content + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt b/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt index 7e86a7ab..f0bb48d0 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt @@ -2,7 +2,10 @@ package gomushin.backend.member.domain.repository import gomushin.backend.member.domain.entity.Member import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query interface MemberRepository : JpaRepository { fun findByEmail(email: String): Member? + @Query("SELECT m FROM Member m WHERE m.isCouple = true") + fun findCoupledMembers(): List } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 603534b2..c18883bc 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -48,4 +48,9 @@ class MemberService( fun deleteMember(id : Long) { memberRepository.deleteById(id) } + + @Transactional(readOnly = true) + fun getAllCoupledMember() : List{ + return memberRepository.findCoupledMembers() + } } From f5b984a3d25c38fadf5c5412b86f8db01c5fc595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 2 May 2025 02:40:23 +0900 Subject: [PATCH 220/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(Logger=EA=B0=9D=EC=B2=B4=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9,=20=EC=A0=84=EC=86=A1=EC=8B=A4=ED=8C=A8=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=97=90=EB=9F=AC=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=82=BD=EC=9E=85=EB=90=9C=EA=B1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95)=20#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/alarm/service/QuestionAlarmService.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt index 2d5410d6..64335b5e 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt @@ -7,7 +7,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking -import org.hibernate.query.sqm.tree.SqmNode.log +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import java.time.LocalDateTime @@ -17,6 +18,7 @@ class QuestionAlarmService ( private val fcmService: FCMService, private val memberService: MemberService ) { + private val log: Logger = LoggerFactory.getLogger(QuestionAlarmService::class.java) private val questionMessages = listOf( "오늘 하루는 어땠나요?+연인에게도 안부를 전해보세요", "사랑은 매일 채우는 것+오늘도 연인과 한 조각 채워보세요", @@ -37,7 +39,7 @@ class QuestionAlarmService ( fcmService.sendMessageTo(member.fcmToken, title, sendContent) } catch (e: Exception) { log.error("질문형 메시지 전송오류 : 수신자 {${member.name}}, 전송시각{${LocalDateTime.now()}}\n") - throw BadRequestException("sarangggun.alarm.fail-issue-accesstoken") + throw BadRequestException("sarangggun.alarm.fail-send-alarm") } } }.awaitAll() From 4ab8e82c7e1b7065b4e80cd9833cbd428d828bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 2 May 2025 02:42:24 +0900 Subject: [PATCH 221/357] =?UTF-8?q?fix=20:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81(=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EC=99=B8=20:=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=9C=EC=83=9D=20=EC=8B=9C=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4=20=EC=A4=91=EB=8B=A8?= =?UTF-8?q?=EB=90=98=EA=B8=B0=20=EB=95=8C=EB=AC=B8)=20#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/alarm/service/QuestionAlarmService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt index 64335b5e..e8ad6374 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt @@ -39,7 +39,6 @@ class QuestionAlarmService ( fcmService.sendMessageTo(member.fcmToken, title, sendContent) } catch (e: Exception) { log.error("질문형 메시지 전송오류 : 수신자 {${member.name}}, 전송시각{${LocalDateTime.now()}}\n") - throw BadRequestException("sarangggun.alarm.fail-send-alarm") } } }.awaitAll() From a3c7b78464b60727a6a16ce102cad9f1b041e158 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 2 May 2025 01:45:51 +0900 Subject: [PATCH 222/357] =?UTF-8?q?feat:=20=EB=94=94=EB=8D=B0=EC=9D=B4(?= =?UTF-8?q?=EA=B8=B0=EB=85=90=EC=9D=BC)=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20API=20,=20dto=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 --- .../dto/request/ReadAnniversariesRequest.kt | 7 ++++++ .../dto/response/TotalAnniversaryResponse.kt | 24 +++++++++++++++++++ .../presentation/AnniversaryController.kt | 18 ++++++++++++++ .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/ReadLetterController.kt | 6 ++--- 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/request/ReadAnniversariesRequest.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/TotalAnniversaryResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/ReadAnniversariesRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/ReadAnniversariesRequest.kt new file mode 100644 index 00000000..99e2b07f --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/ReadAnniversariesRequest.kt @@ -0,0 +1,7 @@ +package gomushin.backend.couple.dto.request + +data class ReadAnniversariesRequest( + val key: Long = Long.MAX_VALUE, + val orderCreatedAt: String = "DESC", + val take: Long = 10L, +) diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/TotalAnniversaryResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/TotalAnniversaryResponse.kt new file mode 100644 index 00000000..cf161143 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/TotalAnniversaryResponse.kt @@ -0,0 +1,24 @@ +package gomushin.backend.couple.dto.response + +import gomushin.backend.couple.domain.entity.Anniversary +import java.time.LocalDate + +data class TotalAnniversaryResponse( + val id: Long?, + val title: String?, + val anniversaryDate: LocalDate?, + val emoji: String?, +) { + companion object { + fun of( + anniversary: Anniversary? + ): TotalAnniversaryResponse { + return TotalAnniversaryResponse( + id = anniversary?.id, + title = anniversary?.title, + anniversaryDate = anniversary?.anniversaryDate, + emoji = anniversary?.emoji.toString() + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt index ffd2776c..b71496e1 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt @@ -1,13 +1,17 @@ package gomushin.backend.couple.presentation import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.PageResponse import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest +import gomushin.backend.couple.dto.request.ReadAnniversariesRequest import gomushin.backend.couple.dto.response.MainAnniversaryResponse +import gomushin.backend.couple.dto.response.TotalAnniversaryResponse import gomushin.backend.couple.facade.AnniversaryFacade import gomushin.backend.couple.facade.CoupleFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.springdoc.core.annotations.ParameterObject import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.* @@ -42,4 +46,18 @@ class AnniversaryController( @AuthenticationPrincipal customUserDetails: CustomUserDetails ): ApiResponse> = ApiResponse.success(anniversaryFacade.getAnniversaryListMain(customUserDetails)) + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.ANNIVERSARIES) + @Operation( + summary = "기념일 리스트 조회", + description = "getAnniversaries" + ) + fun getAnniversaryList( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @ParameterObject readAnniversariesRequest: ReadAnniversariesRequest, + ): PageResponse { + val anniversaries = anniversaryFacade.getAnniversaryList(customUserDetails, readAnniversariesRequest) + return anniversaries + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index 4e2bfb94..5f584856 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -15,4 +15,5 @@ object ApiPath { const val COUPLE_EMOJI = "/v1/couple/emotion" const val ANNIVERSARY_GENERATE = "/v1/couple/new-anniversary" const val ANNIVERSARY_MAIN = "/v1/anniversary/main" + const val ANNIVERSARIES = "/v1/anniversaries" } diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index 091bf42e..f31b1478 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -48,9 +48,9 @@ class ReadLetterController( fun getLetterListToMe( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @ParameterObject readLettersToMePaginationRequest: ReadLettersToMePaginationRequest - ): ApiResponse> { - val letters = readLetterFacade.getLetterListToMe(customUserDetails,readLettersToMePaginationRequest) - return ApiResponse.success(letters) + ): PageResponse { + val letters = readLetterFacade.getLetterListToMe(customUserDetails, readLettersToMePaginationRequest) + return letters } @GetMapping(ApiPath.LETTERS_MAIN) From 3243e62d8f6874e3cac0fb0d9a01553159bbc610 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 2 May 2025 01:46:00 +0900 Subject: [PATCH 223/357] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AnniversaryRepository.kt | 19 +++++++++++ .../domain/service/AnniversaryService.kt | 16 ++++++++- .../couple/facade/AnniversaryFacade.kt | 34 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index a9fd6e0b..0bf1b19c 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -89,4 +89,23 @@ interface AnniversaryRepository : JpaRepository { fun findTop3UpcomingAnniversaries( @Param("coupleId") coupleId: Long ): List + + + @Query( + value = + """ + SELECT * + FROM anniversary a + WHERE a.couple_id = :coupleId + AND a.id < :key + ORDER BY anniversary_date DESC + LIMIT :take + """, + nativeQuery = true + ) + fun findAnniversaries( + @Param("coupleId") coupleId: Long, + @Param("key") key: Long, + @Param("take") take: Long + ): List } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 73a0d7c1..779a6a29 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -4,6 +4,7 @@ import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest +import gomushin.backend.couple.dto.request.ReadAnniversariesRequest import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import gomushin.backend.schedule.dto.response.MainAnniversariesResponse @@ -17,7 +18,20 @@ class AnniversaryService( ) { @Transactional(readOnly = true) - fun findByCoupleIdAndDateBetween(couple: Couple, startDate: LocalDate, endDate: LocalDate): List { + fun findAnniversaries(couple: Couple, readAnniversariesRequest: ReadAnniversariesRequest): List { + return anniversaryRepository.findAnniversaries( + couple.id, + readAnniversariesRequest.key, + readAnniversariesRequest.take, + ) + } + + @Transactional(readOnly = true) + fun findByCoupleIdAndDateBetween( + couple: Couple, + startDate: LocalDate, + endDate: LocalDate + ): List { return anniversaryRepository.findByCoupleIdAndDateBetween(couple.id, startDate, endDate) } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt index 7391ad97..8f1acf31 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -1,16 +1,50 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.web.PageResponse import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.dto.request.ReadAnniversariesRequest import gomushin.backend.couple.dto.response.MainAnniversaryResponse +import gomushin.backend.couple.dto.response.TotalAnniversaryResponse +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component @Component class AnniversaryFacade( private val anniversaryService: AnniversaryService, + @Value("\${server.url}") + private val baseUrl: String, ) { fun getAnniversaryListMain(customUserDetails: CustomUserDetails): List { val anniversaries = anniversaryService.getUpcomingTop3Anniversaries(customUserDetails.getCouple()) return anniversaries.map { MainAnniversaryResponse.of(it) } } + + fun getAnniversaryList( + customUserDetails: CustomUserDetails, + readAnniversariesRequest: ReadAnniversariesRequest, + ): PageResponse { + val anniversaries = + anniversaryService.findAnniversaries(customUserDetails.getCouple(), readAnniversariesRequest) + val anniversaryResponses = anniversaries.map { anniversary -> + TotalAnniversaryResponse.of(anniversary) + } + val isLastPage = anniversaryResponses.size < readAnniversariesRequest.take + + val hasData = anniversaryResponses.isNotEmpty() + + val nextUrl = if (!isLastPage && hasData) { + "${baseUrl}/v1/anniversaries?key=${anniversaryResponses.last().id}&take=${readAnniversariesRequest.take}&page=${readAnniversariesRequest.take}" + } else { + null + } + + return PageResponse.of( + data = anniversaryResponses, + after = if (hasData) anniversaryResponses.last().id else null, + count = anniversaryResponses.size, + next = nextUrl, + isLastPage = isLastPage + ) + } } From 0a152cdc4fbb221b67c89d5b3675ed195a04a21b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 2 May 2025 13:38:15 +0900 Subject: [PATCH 224/357] =?UTF-8?q?refactor:=20url=20=EC=97=90=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt index 8f1acf31..49e4fc4e 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -34,7 +34,7 @@ class AnniversaryFacade( val hasData = anniversaryResponses.isNotEmpty() val nextUrl = if (!isLastPage && hasData) { - "${baseUrl}/v1/anniversaries?key=${anniversaryResponses.last().id}&take=${readAnniversariesRequest.take}&page=${readAnniversariesRequest.take}" + "${baseUrl}/v1/anniversaries?key=${anniversaryResponses.last().id}&take=${readAnniversariesRequest.take}" } else { null } From 02b7db018fb5930f0f8479896f542b553658dd5d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 13:33:00 +0900 Subject: [PATCH 225/357] =?UTF-8?q?refactor:=20multipart=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/presentation/UpsertAndDeleteLetterController.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt index 38f7ea11..b271c59e 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt @@ -19,13 +19,12 @@ class UpsertAndDeleteLetterController( ) { @ResponseStatus(HttpStatus.CREATED) @PostMapping( - ApiPath.LETTERS, - consumes = [MediaType.MULTIPART_FORM_DATA_VALUE] + ApiPath.LETTERS ) @Operation(summary = "편지 수정하거나 추가하기", description = "upsertLetter") fun upsertLetter( @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @RequestPart("data") upsertLetterRequest: UpsertLetterRequest, + @RequestPart upsertLetterRequest: UpsertLetterRequest, @RequestPart("pictures", required = false) pictures: List?, ): ApiResponse { upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) From 4f67301b2be5792006468c185b2f168fec908e4d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 14:08:32 +0900 Subject: [PATCH 226/357] =?UTF-8?q?fix:=20=EC=BF=BC=EB=A6=AC=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=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 --- .../backend/schedule/domain/repository/LetterRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index d8487199..2d9e4e45 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -21,7 +21,7 @@ interface LetterRepository : JpaRepository { @Query( """ SELECT * - FROM Letter l + FROM letter l WHERE l.couple_id = :coupleId AND l.author_id = :partnerPk AND l.id <:key From 6a6ec43ba965baf1451ed3f8cedc52dbfe37e55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 3 May 2025 14:24:34 +0900 Subject: [PATCH 227/357] =?UTF-8?q?feat=20:=20=EB=94=94=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#72?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/alarm/service/DdayAlarmSerivce.kt | 40 +++++++++++++++++++ .../repository/AnniversaryRepository.kt | 15 +++++++ .../domain/service/AnniversaryService.kt | 6 +++ .../response/AnniversaryNotificationInfo.kt | 7 ++++ 4 files changed, 68 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/AnniversaryNotificationInfo.kt diff --git a/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt b/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt new file mode 100644 index 00000000..89b0ed13 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt @@ -0,0 +1,40 @@ +package gomushin.backend.alarm.service + +import gomushin.backend.couple.domain.service.AnniversaryService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.LocalDateTime + +@Service +class DdayAlarmSerivce( + private val fcmService: FCMService, + private val anniversaryService: AnniversaryService +) { + private val log: Logger = LoggerFactory.getLogger(QuestionAlarmService::class.java) + private val alarmTitle = "오늘의 디데이가 도착했어요" + + @Scheduled(cron = "0 21 14 * * *", zone = "Asia/Seoul") + fun sendDdayAlarms() { + val sendDdayAlarmContents = anniversaryService.getTodayAnniversaryMemberFcmTokens(LocalDate.now()) + log.info("전송 크기 : ${sendDdayAlarmContents.size}") + runBlocking { + sendDdayAlarmContents.map { content -> + async(Dispatchers.IO) { + try { + log.info("디데이 메시지 전송 : 수신자 {${content.memberId}}, 제목 {${alarmTitle}}, 내용{${content.title}}, 전송시각{${LocalDateTime.now()}}\n") + fcmService.sendMessageTo(content.fcmToken, alarmTitle, content.title) + } catch (e: Exception) { + log.error("디데이 메시지 전송오류 : 수신자 {${content.memberId}}, 전송시각{${LocalDateTime.now()}}\n") + } + } + }.awaitAll() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index 0bf1b19c..dfb83dc6 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -1,6 +1,7 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Anniversary +import gomushin.backend.couple.dto.response.AnniversaryNotificationInfo import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import gomushin.backend.schedule.dto.response.MainAnniversariesResponse @@ -108,4 +109,18 @@ interface AnniversaryRepository : JpaRepository { @Param("key") key: Long, @Param("take") take: Long ): List + + @Query( + """ + SELECT a.title AS title, m.id AS memberId, m.fcm_token AS fcmToken + FROM anniversary a + JOIN couple c ON a.couple_id = c.id + JOIN member m ON m.id = c.invitor_id OR m.id = c.invitee_id + JOIN notification n ON n.member_id = m.id + WHERE DATE(a.anniversary_date) = :nowDate + AND n.dday = true + """, + nativeQuery = true + ) + fun findTodayAnniversaryMemberFcmTokens(@Param("nowDate")date: LocalDate): List } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 779a6a29..2367298b 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -5,6 +5,7 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest import gomushin.backend.couple.dto.request.ReadAnniversariesRequest +import gomushin.backend.couple.dto.response.AnniversaryNotificationInfo import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import gomushin.backend.schedule.dto.response.MainAnniversariesResponse @@ -71,4 +72,9 @@ class AnniversaryService( fun getUpcomingTop3Anniversaries(couple: Couple): List { return anniversaryRepository.findTop3UpcomingAnniversaries(couple.id) } + + @Transactional(readOnly = true) + fun getTodayAnniversaryMemberFcmTokens(date : LocalDate) : List { + return anniversaryRepository.findTodayAnniversaryMemberFcmTokens(date) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/AnniversaryNotificationInfo.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/AnniversaryNotificationInfo.kt new file mode 100644 index 00000000..50df8c71 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/AnniversaryNotificationInfo.kt @@ -0,0 +1,7 @@ +package gomushin.backend.couple.dto.response + +interface AnniversaryNotificationInfo { + val title: String + val memberId: Long + val fcmToken: String +} \ No newline at end of file From 762ae732299caa61aec3c86a4a9a3d44cbd87cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 3 May 2025 14:32:39 +0900 Subject: [PATCH 228/357] =?UTF-8?q?chore=20:=20cron=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD(=EC=9B=90=EB=9E=98=20=EA=B8=B0=ED=9A=8D=EC=9D=98?= =?UTF-8?q?=EB=8F=84=EC=9D=B8=200=EC=8B=9C=EB=A7=88=EB=8B=A4=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=EB=90=98=EA=B2=8C)=20#72?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt b/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt index 89b0ed13..60350ee5 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt @@ -20,7 +20,7 @@ class DdayAlarmSerivce( private val log: Logger = LoggerFactory.getLogger(QuestionAlarmService::class.java) private val alarmTitle = "오늘의 디데이가 도착했어요" - @Scheduled(cron = "0 21 14 * * *", zone = "Asia/Seoul") + @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") fun sendDdayAlarms() { val sendDdayAlarmContents = anniversaryService.getTodayAnniversaryMemberFcmTokens(LocalDate.now()) log.info("전송 크기 : ${sendDdayAlarmContents.size}") From 2453f1ebfa8cf4bc59da8229a9e6740527ae2ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 3 May 2025 15:09:38 +0900 Subject: [PATCH 229/357] =?UTF-8?q?fix=20:=20AL=5F01=20=EC=A7=88=EB=AC=B8?= =?UTF-8?q?=ED=98=95=20=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=95=BD=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20#78?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/alarm/service/QuestionAlarmService.kt | 4 ++-- .../member/domain/repository/MemberRepository.kt | 11 +++++++++-- .../backend/member/domain/service/MemberService.kt | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt index e8ad6374..2ca0c464 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt @@ -28,14 +28,14 @@ class QuestionAlarmService ( @Scheduled(cron = "0 0 19 * * *", zone = "Asia/Seoul") fun sendQuestionAlarms() { - val coupleMembers = memberService.getAllCoupledMember() + val coupleMembers = memberService.getAllCoupledMemberWithEnabledNotification() runBlocking { coupleMembers.map { member -> async(Dispatchers.IO) { try { val notificationContent = questionMessages.random() val (title, sendContent) = MessageParsingUtil.parse(notificationContent) - log.info("질문형 메시지 전송 : 수신자 {${member.name}}, 제목 {${title}}, 내용{${sendContent}}, 전송시각{${LocalDateTime.now()}}\n") + log.info("질문형 메시지 전송 : 수신자 {${member.id}}, 제목 {${title}}, 내용{${sendContent}}, 전송시각{${LocalDateTime.now()}}\n") fcmService.sendMessageTo(member.fcmToken, title, sendContent) } catch (e: Exception) { log.error("질문형 메시지 전송오류 : 수신자 {${member.name}}, 전송시각{${LocalDateTime.now()}}\n") diff --git a/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt b/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt index f0bb48d0..d2a66c79 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt @@ -6,6 +6,13 @@ import org.springframework.data.jpa.repository.Query interface MemberRepository : JpaRepository { fun findByEmail(email: String): Member? - @Query("SELECT m FROM Member m WHERE m.isCouple = true") - fun findCoupledMembers(): List + @Query( + """ + SELECT m FROM Member m + JOIN Notification n ON n.memberId = m.id + WHERE m.isCouple = true + AND NOT (n.dday = false AND n.partnerStatus = false) + """ + ) + fun findCoupleMembersWithEnabledNotification(): List } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index c18883bc..4704bf96 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -50,7 +50,7 @@ class MemberService( } @Transactional(readOnly = true) - fun getAllCoupledMember() : List{ - return memberRepository.findCoupledMembers() + fun getAllCoupledMemberWithEnabledNotification() : List{ + return memberRepository.findCoupleMembersWithEnabledNotification() } } From 9535771f62f5c4bcc0f85de46ca918ed60110be6 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 14:34:18 +0900 Subject: [PATCH 230/357] =?UTF-8?q?fix:=20=EC=9D=BC=EC=A0=95=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20emoji=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/repository/AnniversaryRepository.kt | 1 + .../backend/schedule/dto/response/DailyAnniversaryResponse.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index dfb83dc6..7a4140f1 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -47,6 +47,7 @@ interface AnniversaryRepository : JpaRepository { SELECT new gomushin.backend.schedule.dto.response.DailyAnniversaryResponse( a.id, a.title, + a.emoji, a.anniversaryDate ) FROM Anniversary a diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyAnniversaryResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyAnniversaryResponse.kt index ac381f03..3c026f78 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyAnniversaryResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/DailyAnniversaryResponse.kt @@ -1,9 +1,11 @@ package gomushin.backend.schedule.dto.response +import gomushin.backend.couple.domain.value.AnniversaryEmoji import java.time.LocalDate data class DailyAnniversaryResponse( val id: Long, val title: String, + val emoji: AnniversaryEmoji, val anniversaryDate: LocalDate, ) From a7bd944364e9d896c566079e230e45560805abf3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 14:34:40 +0900 Subject: [PATCH 231/357] =?UTF-8?q?fix:=20PageResponse=EC=97=90=20next=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/core/common/web/PageResponse.kt | 8 ++++---- .../backend/couple/facade/AnniversaryFacade.kt | 12 ++++++------ .../backend/schedule/facade/ReadLetterFacade.kt | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt b/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt index dcc0d96b..c7bb610f 100644 --- a/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt +++ b/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt @@ -9,8 +9,8 @@ data class PageResponse( val after: Long?, @Schema(description = "데이터 수") val count: Int, - @Schema(description = "다음 페이지 URL") - val next: String?, +// @Schema(description = "다음 페이지 URL") +// val next: String?, @Schema(description = "마지막 페이지 여부") val isLastPage: Boolean, ) { @@ -19,14 +19,14 @@ data class PageResponse( data: List, after: Long?, count: Int, - next: String?, +// next: String?, isLastPage: Boolean, ): PageResponse { return PageResponse( data = data, after = after, count = count, - next = next, +// next = next, isLastPage = isLastPage ) } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt index 49e4fc4e..961656f1 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -33,17 +33,17 @@ class AnniversaryFacade( val hasData = anniversaryResponses.isNotEmpty() - val nextUrl = if (!isLastPage && hasData) { - "${baseUrl}/v1/anniversaries?key=${anniversaryResponses.last().id}&take=${readAnniversariesRequest.take}" - } else { - null - } +// val nextUrl = if (!isLastPage && hasData) { +// "${baseUrl}/v1/anniversaries?key=${anniversaryResponses.last().id}&take=${readAnniversariesRequest.take}" +// } else { +// null +// } return PageResponse.of( data = anniversaryResponses, after = if (hasData) anniversaryResponses.last().id else null, count = anniversaryResponses.size, - next = nextUrl, +// next = nextUrl, isLastPage = isLastPage ) } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 14ed1f22..51fb5853 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -89,17 +89,17 @@ class ReadLetterFacade( val hasData = letterPreviewResponses.isNotEmpty() - val nextUrl = if (!isLastPage && hasData) { - "${baseUrl}/v1/schedules/letters/to-me?key=${letterPreviewResponses.last().letterId}&orderCreatedAt=DESC&take=${readLettersToMePaginationRequest.take}" - } else { - null - } +// val nextUrl = if (!isLastPage && hasData) { +// "${baseUrl}/v1/schedules/letters/to-me?key=${letterPreviewResponses.last().letterId}&orderCreatedAt=DESC&take=${readLettersToMePaginationRequest.take}" +// } else { +// null +// } return PageResponse.of( data = letterPreviewResponses, after = if (hasData) letterPreviewResponses.last().letterId else null, count = letterPreviewResponses.size, - next = nextUrl, +// next = nextUrl, isLastPage = isLastPage ) } From 4936d6deda585c51f015d1800285895b080fd6cd Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 14:51:57 +0900 Subject: [PATCH 232/357] =?UTF-8?q?fix:=20PageResponse=EC=97=90=20next=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ .../gomushin/backend/schedule/facade/ReadLetterFacade.kt | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 60e5b4cd..1711a21b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] + pull_request: + branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 51fb5853..586eb1e4 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -48,6 +48,10 @@ class ReadLetterFacade( schedule, letterId, ) + + println("schedule = $schedule") + println("letter = $letter") + val letterResponse = LetterResponse.of(letter) val pictures = pictureService.findAllByLetter(letter) From 04afaed2ac2d4edb64188c3e65fb32fef9d6dd67 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 14:56:40 +0900 Subject: [PATCH 233/357] =?UTF-8?q?fix:=20PageResponse=EC=97=90=20next=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/presentation/ReadLetterController.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index f31b1478..e06adf3f 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -39,6 +39,8 @@ class ReadLetterController( @PathVariable scheduleId: Long, @PathVariable letterId: Long, ): ApiResponse { + println("scheduleId = $scheduleId") + println("letterId = $letterId") val letter = readLetterFacade.get(customUserDetails, scheduleId, letterId) return ApiResponse.success(letter) } From 5579038c010e68638ee40f669f291862baa3cd58 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 15:06:39 +0900 Subject: [PATCH 234/357] =?UTF-8?q?fix:=20PageResponse=EC=97=90=20next=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 -- .../gomushin/backend/schedule/domain/service/LetterService.kt | 2 +- .../gomushin/backend/schedule/facade/ReadLetterFacade.kt | 3 --- .../backend/schedule/presentation/ReadLetterController.kt | 2 -- 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1711a21b..60e5b4cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,6 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index 07b9e582..18d6def2 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -53,7 +53,7 @@ class LetterService( @Transactional(readOnly = true) fun findByCoupleIdAndScheduleIdAndId(couple: Couple, scheduleId: Long, letterId: Long) = - letterRepository.findByCoupleIdAndScheduleIdAndId(couple.id, letterId, scheduleId) + letterRepository.findByCoupleIdAndScheduleIdAndId(couple.id, scheduleId, letterId) @Transactional(readOnly = true) fun findByCoupleAndSchedule(couple: Couple, schedule: Schedule) = diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 586eb1e4..9f7b679a 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -49,9 +49,6 @@ class ReadLetterFacade( letterId, ) - println("schedule = $schedule") - println("letter = $letter") - val letterResponse = LetterResponse.of(letter) val pictures = pictureService.findAllByLetter(letter) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index e06adf3f..f31b1478 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -39,8 +39,6 @@ class ReadLetterController( @PathVariable scheduleId: Long, @PathVariable letterId: Long, ): ApiResponse { - println("scheduleId = $scheduleId") - println("letterId = $letterId") val letter = readLetterFacade.get(customUserDetails, scheduleId, letterId) return ApiResponse.success(letter) } From ccc9da12b52e6a07bf2b8554344756762f421b7e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 16:01:07 +0900 Subject: [PATCH 235/357] =?UTF-8?q?fix:=20=ED=8E=B8=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=9D=B4=EB=A6=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/dto/response/LetterPreviewResponse.kt | 4 ++++ .../gomushin/backend/schedule/facade/ReadLetterFacade.kt | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt index 3735f424..4b1bfbf4 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt @@ -2,10 +2,12 @@ package gomushin.backend.schedule.dto.response import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.entity.Picture +import gomushin.backend.schedule.domain.entity.Schedule import java.time.LocalDateTime data class LetterPreviewResponse( val letterId: Long?, + val scheduleTitle: String?, val title: String?, val content: String?, val pictureUrl: String?, @@ -17,6 +19,7 @@ data class LetterPreviewResponse( fun of( letter: Letter?, + schedule: Schedule?, picture: Picture? ): LetterPreviewResponse { val previewContent = if (letter?.content != null && letter.content.length > MAX_CONTENT_LENGTH) { @@ -26,6 +29,7 @@ data class LetterPreviewResponse( } return LetterPreviewResponse( letterId = letter?.id, + scheduleTitle = schedule?.title, title = letter?.title, content = previewContent, pictureUrl = picture?.pictureUrl, diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 9f7b679a..a540c8e8 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -33,7 +33,7 @@ class ReadLetterFacade( ) return letters.map { letter -> val picture = pictureService.findFirstByLetterId(letter.id) - LetterPreviewResponse.of(letter, picture) + LetterPreviewResponse.of(letter, schedule, picture) } } @@ -79,11 +79,13 @@ class ReadLetterFacade( customUserDetails.getCouple().invitorId } - val letters = letterService.findArrivedToMe(customUserDetails.getCouple(), partnerPk, readLettersToMePaginationRequest) + val letters = + letterService.findArrivedToMe(customUserDetails.getCouple(), partnerPk, readLettersToMePaginationRequest) val letterPreviewResponses = letters.map { letter -> val picture = letter?.let { pictureService.findFirstByLetterId(it.id) } - LetterPreviewResponse.of(letter, picture) + val schedule = letter?.let { scheduleService.getById(it.scheduleId) } + LetterPreviewResponse.of(letter, schedule, picture) } val isLastPage = letterPreviewResponses.size < readLettersToMePaginationRequest.take From 23d0486fc834f766f8817087b3443176f3aa184c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 3 May 2025 16:06:50 +0900 Subject: [PATCH 236/357] =?UTF-8?q?fix:=20=EB=82=98=EC=97=90=EA=B2=8C=20?= =?UTF-8?q?=EC=98=A8=20=ED=8E=B8=EC=A7=80=EA=B0=80=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=EC=BB=A4=ED=94=8C=20=EC=A0=84=EC=B2=B4=20=ED=8E=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/LetterRepository.kt | 2 ++ .../schedule/domain/service/LetterService.kt | 2 ++ .../backend/schedule/facade/ReadLetterFacade.kt | 15 ++++----------- .../backend/schedule/facade/ReadScheduleFacade.kt | 2 +- .../schedule/presentation/ReadLetterController.kt | 6 +++--- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index 2d9e4e45..bd2d0a34 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -7,6 +7,8 @@ import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param interface LetterRepository : JpaRepository { + fun findByCoupleId(coupleId: Long): List + fun findByCoupleIdAndScheduleId(coupleId: Long, scheduleId: Long): List fun findByCoupleIdAndScheduleIdAndId(coupleId: Long, scheduleId: Long, letterId: Long): Letter? diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index 18d6def2..a05b0c5c 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -42,6 +42,8 @@ class LetterService( @Transactional(readOnly = true) fun findById(id: Long) = letterRepository.findByIdOrNull(id) + @Transactional(readOnly = true) + fun findByCouple(couple: Couple) = letterRepository.findByCoupleId(couple.id) @Transactional(readOnly = true) fun getByCoupleAndScheduleAndId( diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index a540c8e8..2d092d54 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -68,23 +68,16 @@ class ReadLetterFacade( ) } - fun getLetterListToMe( + fun getLetterListToCouple( customUserDetails: CustomUserDetails, readLettersToMePaginationRequest: ReadLettersToMePaginationRequest, ): PageResponse { - val partnerPk = if (customUserDetails.getCouple().invitorId == customUserDetails.getId()) { - customUserDetails.getCouple().inviteeId - } else { - customUserDetails.getCouple().invitorId - } - - val letters = - letterService.findArrivedToMe(customUserDetails.getCouple(), partnerPk, readLettersToMePaginationRequest) + val letters = letterService.findByCouple(customUserDetails.getCouple()) val letterPreviewResponses = letters.map { letter -> - val picture = letter?.let { pictureService.findFirstByLetterId(it.id) } - val schedule = letter?.let { scheduleService.getById(it.scheduleId) } + val picture = letter.let { pictureService.findFirstByLetterId(it.id) } + val schedule = letter.let { scheduleService.getById(it.scheduleId) } LetterPreviewResponse.of(letter, schedule, picture) } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index acc05aca..9f2a917c 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -41,7 +41,7 @@ class ReadScheduleFacade( val picturesByLetterId = pictureService.findAllByLetterIds(letterIds) .associateBy { it.letterId } val letterPreviews = letters.map { letter -> - LetterPreviewResponse.of(letter, picturesByLetterId[letter.id]) + LetterPreviewResponse.of(letter, schedule, picturesByLetterId[letter.id]) } return ScheduleDetailResponse.of(schedule, letterPreviews) } diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index f31b1478..c54a3f8c 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -43,13 +43,13 @@ class ReadLetterController( return ApiResponse.success(letter) } - @GetMapping(ApiPath.LETTERS_TO_ME) - @Operation(summary = "내가 받은 편지 리스트 가져오기", description = "getLetterListToMe") + @GetMapping(ApiPath.LETTERS) + @Operation(summary = "커플 전체 편지 리스트 가져오기", description = "getLetterListToMe") fun getLetterListToMe( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @ParameterObject readLettersToMePaginationRequest: ReadLettersToMePaginationRequest ): PageResponse { - val letters = readLetterFacade.getLetterListToMe(customUserDetails, readLettersToMePaginationRequest) + val letters = readLetterFacade.getLetterListToCouple(customUserDetails, readLettersToMePaginationRequest) return letters } From 362761eeef3aea8a3f2377a9bc6aef504de5bfdb Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 4 May 2025 15:02:09 +0900 Subject: [PATCH 237/357] =?UTF-8?q?refactor:=20=EC=95=88=EC=93=B0=EB=8A=94?= =?UTF-8?q?=20url=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/schedule/presentation/ApiPath.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt index 6d0d262a..e36623ad 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ApiPath.kt @@ -9,7 +9,6 @@ object ApiPath { const val LETTERS = "/v1/schedules/letters" const val LETTER = "/v1/schedules/{scheduleId}/letters/{letterId}" - const val LETTERS_TO_ME = "/v1/schedules/letters/to-me" const val LETTERS_BY_SCHEDULE = "/v1/schedules/{scheduleId}" const val LETTERS_MAIN = "/v1/schedules/letters/main" From df38f3e6d34b8066018c6913bede205511320bde Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 4 May 2025 15:02:24 +0900 Subject: [PATCH 238/357] =?UTF-8?q?refactor:=20LetterPreviewResponse=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=97=90=20scheduleId=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/dto/response/LetterPreviewResponse.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt index 4b1bfbf4..1252e2b9 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt @@ -7,6 +7,7 @@ import java.time.LocalDateTime data class LetterPreviewResponse( val letterId: Long?, + val scheduleId: Long?, val scheduleTitle: String?, val title: String?, val content: String?, @@ -29,6 +30,7 @@ data class LetterPreviewResponse( } return LetterPreviewResponse( letterId = letter?.id, + scheduleId = schedule?.id, scheduleTitle = schedule?.title, title = letter?.title, content = previewContent, From 7d1efc9b6af1ccecc43a21684c32b32869657187 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 4 May 2025 15:03:38 +0900 Subject: [PATCH 239/357] =?UTF-8?q?refactor:=20=EB=82=98=EC=97=90=EA=B2=8C?= =?UTF-8?q?=20=EC=98=A8=20=ED=8E=B8=EC=A7=80=20=EC=9E=98=EB=AA=BB=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/domain/repository/LetterRepository.kt | 6 ++---- .../backend/schedule/domain/service/LetterService.kt | 8 +++----- .../gomushin/backend/schedule/facade/ReadLetterFacade.kt | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index bd2d0a34..5350911a 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -25,19 +25,17 @@ interface LetterRepository : JpaRepository { SELECT * FROM letter l WHERE l.couple_id = :coupleId - AND l.author_id = :partnerPk AND l.id <:key ORDER BY l.created_at DESC LIMIT :take """, nativeQuery = true ) - fun findByLettersToMe( + fun findAllToCouple( @Param("coupleId") coupleId: Long, - @Param("partnerPk") partnerPk: Long, @Param("key") key: Long, @Param("take") take: Long, - ): List + ): List fun findTop5ByCoupleIdOrderByCreatedAtDesc(coupleId: Long): List } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index a05b0c5c..6884aac1 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -80,14 +80,12 @@ class LetterService( } @Transactional(readOnly = true) - fun findArrivedToMe( + fun findAllToCouple( couple: Couple, - partnerPk: Long, readLettersToMePaginationRequest: ReadLettersToMePaginationRequest - ): List { - return letterRepository.findByLettersToMe( + ): List { + return letterRepository.findAllToCouple( couple.id, - partnerPk, readLettersToMePaginationRequest.key, readLettersToMePaginationRequest.take, ) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 2d092d54..7e37a74e 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -73,7 +73,7 @@ class ReadLetterFacade( readLettersToMePaginationRequest: ReadLettersToMePaginationRequest, ): PageResponse { - val letters = letterService.findByCouple(customUserDetails.getCouple()) + val letters = letterService.findAllToCouple(customUserDetails.getCouple(), readLettersToMePaginationRequest) val letterPreviewResponses = letters.map { letter -> val picture = letter.let { pictureService.findFirstByLetterId(it.id) } From 5c0acf860fcf067c5721fbdb7606d5ecbd57cf40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 5 May 2025 14:58:32 +0900 Subject: [PATCH 240/357] =?UTF-8?q?fix=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95(=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?=EC=8B=9D=EC=9D=B4=20=EC=95=84=EB=8B=88=EB=9D=BC=20true?= =?UTF-8?q?=EB=A9=B4=20=EC=95=8C=EB=A6=BC=20on,=20false=EB=A9=B4=20off?= =?UTF-8?q?=EC=9D=B4=EA=B2=8C=EB=81=94=20->=20=EC=A7=81=EA=B4=80=EC=A0=81?= =?UTF-8?q?=EC=9D=B4=EA=B2=8C=20=EC=88=98=EC=A0=95)=20#83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/member/domain/entity/Notification.kt | 8 ++++++++ .../gomushin/backend/member/facade/MemberInfoFacade.kt | 4 ++-- .../backend/member/facade/MemberInfoFacadeTest.kt | 8 +++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt index 834889b4..82c26bd8 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt @@ -37,4 +37,12 @@ class Notification( fun updatePartnerStatus() { this.partnerStatus = !this.partnerStatus } + + fun changeDday(dday: Boolean) { + this.dday = dday + } + + fun changePartnerStatus(partnerStatus: Boolean) { + this.partnerStatus = partnerStatus + } } diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 15de191b..28c10a30 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -56,8 +56,8 @@ class MemberInfoFacade( @Transactional fun updateMyNotification(customUserDetails: CustomUserDetails, updateMyNotificationRequest: UpdateMyNotificationRequest) { notificationService.getByMemberId(customUserDetails.getId()).apply { - if (updateMyNotificationRequest.dday) updateDday() - if (updateMyNotificationRequest.partnerStatus) updatePartnerStatus() + changeDday(updateMyNotificationRequest.dday) + changePartnerStatus(updateMyNotificationRequest.partnerStatus) } } } diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 41512ab5..e3e84e0a 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -172,14 +172,12 @@ class MemberInfoFacadeTest { @Test fun updateNotification() { //given - val updateMyNotificationRequest = UpdateMyNotificationRequest(true, true) + val updateMyNotificationRequest = UpdateMyNotificationRequest(true, false) `when`(notificationService.getByMemberId(customUserDetails.getId())).thenReturn(notification) - val previousDay = notification.dday - val previousSuperstate = notification.partnerStatus //when val result = memberInfoFacade.updateMyNotification(customUserDetails, updateMyNotificationRequest) //then - assertEquals(notification.dday, !previousDay) - assertEquals(notification.partnerStatus, !previousSuperstate) + assertEquals(notification.dday, updateMyNotificationRequest.dday) + assertEquals(notification.partnerStatus, updateMyNotificationRequest.partnerStatus) } } From 6a6bd452025f4b89b47b59cb0fd72a5ab74decb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 5 May 2025 15:14:35 +0900 Subject: [PATCH 241/357] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20#83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ .../dto/response/MyNotificationResponse.kt | 18 ++++++++++++++++++ .../backend/member/facade/MemberInfoFacade.kt | 6 ++++++ .../backend/member/presentation/ApiPath.kt | 1 + .../presentation/MemberInfoController.kt | 11 +++++++++++ 5 files changed, 38 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/member/dto/response/MyNotificationResponse.kt diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 60e5b4cd..1711a21b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] + pull_request: + branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/member/dto/response/MyNotificationResponse.kt b/src/main/kotlin/gomushin/backend/member/dto/response/MyNotificationResponse.kt new file mode 100644 index 00000000..e2db13a5 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/member/dto/response/MyNotificationResponse.kt @@ -0,0 +1,18 @@ +package gomushin.backend.member.dto.response + +import gomushin.backend.member.domain.entity.Notification +import io.swagger.v3.oas.annotations.media.Schema + +data class MyNotificationResponse( + @Schema(description = "디데이 알림 설정 정보", example = "true") + val dday : Boolean, + @Schema(description = "상태 알림 설정 정보", example = "false") + val partnerStatus : Boolean +) { + companion object { + fun of(notification: Notification) = MyNotificationResponse ( + notification.dday, + notification.partnerStatus + ) + } +} diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 28c10a30..a810f4b5 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -11,6 +11,7 @@ import gomushin.backend.member.dto.request.UpdateMyNickNameRequest import gomushin.backend.member.dto.request.UpdateMyNotificationRequest import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.dto.response.MyInfoResponse +import gomushin.backend.member.dto.response.MyNotificationResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -60,4 +61,9 @@ class MemberInfoFacade( changePartnerStatus(updateMyNotificationRequest.partnerStatus) } } + + fun getMyNotification(customUserDetails: CustomUserDetails): MyNotificationResponse { + val notification = notificationService.getByMemberId(customUserDetails.getId()) + return MyNotificationResponse.of(notification) + } } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index ed127c9b..93729f99 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -10,4 +10,5 @@ object ApiPath { const val UPDATE_MY_BIRTHDAY = "/v1/member/my-birthday" const val UPDATE_MY_NOTIFICATION_POLICY = "/v1/member/my-notification" const val DELETE_ALL_MY_DATA ="/v1/member/all-my-data" + const val MY_NOTIFICATION = "/v1/member/my-notification" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 833fb46f..8d629a34 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -9,6 +9,7 @@ import gomushin.backend.member.dto.request.UpdateMyNotificationRequest import gomushin.backend.member.dto.response.MyEmotionResponse import gomushin.backend.member.facade.MemberInfoFacade import gomushin.backend.member.dto.response.MyInfoResponse +import gomushin.backend.member.dto.response.MyNotificationResponse import gomushin.backend.member.dto.response.MyStatusMessageResponse import gomushin.backend.member.facade.LeaveFacade import io.swagger.v3.oas.annotations.Operation @@ -112,4 +113,14 @@ class MemberInfoController( leaveFacade.leave(customUserDetails) return ApiResponse.success(true) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.MY_NOTIFICATION) + @Operation(summary = "나의 알림 정책 조회", description = "getMyNotification") + fun getMyNotification( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ) : ApiResponse { + val myNotification = memberInfoFacade.getMyNotification(customUserDetails) + return ApiResponse.success(myNotification) + } } From e79a5b7b4a7a05039c562e398b21378883ac1b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 5 May 2025 15:14:45 +0900 Subject: [PATCH 242/357] =?UTF-8?q?test=20:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20#83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/facade/MemberInfoFacadeTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index e3e84e0a..dd18f8a8 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -180,4 +180,16 @@ class MemberInfoFacadeTest { assertEquals(notification.dday, updateMyNotificationRequest.dday) assertEquals(notification.partnerStatus, updateMyNotificationRequest.partnerStatus) } + + @DisplayName("알림정책 조회") + @Test + fun getMyNotification() { + //given + `when`(notificationService.getByMemberId(customUserDetails.getId())).thenReturn(notification) + //when + val result = memberInfoFacade.getMyNotification(customUserDetails) + //then + assertEquals(notification.dday, result.dday) + assertEquals(notification.partnerStatus, result.partnerStatus) + } } From 276631922ef004b288b85ef4414807de774cdb10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 5 May 2025 15:29:22 +0900 Subject: [PATCH 243/357] =?UTF-8?q?fix=20:=20=EC=95=8C=EB=A6=BC=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20updateDday()=EB=9E=91?= =?UTF-8?q?=20updatePartnerStatus()=EB=A1=9C=20=ED=95=B4=EA=B2=B0=ED=95=98?= =?UTF-8?q?=EA=B2=8C=EB=81=94=20=EC=88=98=EC=A0=95(=EB=94=B0=EB=A1=9C=20ch?= =?UTF-8?q?angeDday/PartenerStatus=EC=99=80=20=EA=B0=99=EC=9D=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EC=A0=81=EC=9D=B8=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=95=88=20=EB=A7=8C=EB=93=A4=EA=B2=8C)=20#83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/domain/entity/Notification.kt | 12 ++---------- .../member/domain/service/NotificationService.kt | 4 ++-- .../backend/member/facade/MemberInfoFacade.kt | 4 ++-- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt index 82c26bd8..4e96156c 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Notification.kt @@ -30,19 +30,11 @@ class Notification( } } - fun updateDday() { - this.dday = !this.dday - } - - fun updatePartnerStatus() { - this.partnerStatus = !this.partnerStatus - } - - fun changeDday(dday: Boolean) { + fun updateDday(dday: Boolean) { this.dday = dday } - fun changePartnerStatus(partnerStatus: Boolean) { + fun updatePartnerStatus(partnerStatus: Boolean) { this.partnerStatus = partnerStatus } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt index b228e19d..26d9a7fc 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt @@ -15,8 +15,8 @@ class NotificationService( fun initNotification(memberId: Long, isNotification: Boolean) { val notification = Notification.create(memberId) if (isNotification) { - notification.updateDday() - notification.updatePartnerStatus() + notification.updateDday(true) + notification.updatePartnerStatus(true) } save(notification) } diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index a810f4b5..27c27877 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -57,8 +57,8 @@ class MemberInfoFacade( @Transactional fun updateMyNotification(customUserDetails: CustomUserDetails, updateMyNotificationRequest: UpdateMyNotificationRequest) { notificationService.getByMemberId(customUserDetails.getId()).apply { - changeDday(updateMyNotificationRequest.dday) - changePartnerStatus(updateMyNotificationRequest.partnerStatus) + updateDday(updateMyNotificationRequest.dday) + updatePartnerStatus(updateMyNotificationRequest.partnerStatus) } } From 95c50432f9ec6b50bde719ea9d5e8c07ebe9ebca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 5 May 2025 15:41:36 +0900 Subject: [PATCH 244/357] =?UTF-8?q?refactor=20:=20if=EB=AC=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20#83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 -- .../backend/member/domain/service/NotificationService.kt | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1711a21b..60e5b4cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,6 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt index 26d9a7fc..3c5b7d96 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/NotificationService.kt @@ -14,10 +14,8 @@ class NotificationService( @Transactional fun initNotification(memberId: Long, isNotification: Boolean) { val notification = Notification.create(memberId) - if (isNotification) { - notification.updateDday(true) - notification.updatePartnerStatus(true) - } + notification.updateDday(isNotification) + notification.updatePartnerStatus(isNotification) save(notification) } From bbbe746b2298b52073bfbd270ddb8ed81c2e8ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 5 May 2025 16:14:41 +0900 Subject: [PATCH 245/357] =?UTF-8?q?feat=20:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EC=83=9D=EB=85=84=EC=9B=94=EC=9D=BC=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20api=20=EA=B5=AC=ED=98=84=20#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ .../dto/response/CoupleBirthDayResponse.kt | 19 +++++++++++++++++++ .../backend/couple/facade/CoupleFacade.kt | 6 ++++++ .../backend/couple/presentation/ApiPath.kt | 1 + .../presentation/CoupleInfoController.kt | 10 ++++++++++ 5 files changed, 38 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/CoupleBirthDayResponse.kt diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 60e5b4cd..1711a21b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] + pull_request: + branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleBirthDayResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleBirthDayResponse.kt new file mode 100644 index 00000000..a0a842c3 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleBirthDayResponse.kt @@ -0,0 +1,19 @@ +package gomushin.backend.couple.dto.response + +import gomushin.backend.member.domain.entity.Member +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDate + +data class CoupleBirthDayResponse ( + @Schema(description = "커플 생년월일", example = "2001-04-01") + val coupleBirthDay : LocalDate?, + @Schema(description = "내 생년월일", example = "2001-03-01") + val myBirthDay : LocalDate? +) { + companion object { + fun of(coupleMember : Member, myMember : Member) = CoupleBirthDayResponse( + coupleMember.birthDate, + myMember.birthDate + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 929d0aea..0f4f819c 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -130,4 +130,10 @@ class CoupleFacade( throw BadRequestException("sarangggun.couple.already-init") } } + + fun getCoupleBirthDay(customUserDetails: CustomUserDetails): CoupleBirthDayResponse { + val couple = coupleInfoService.findCoupleMember(customUserDetails.getId()) + val member = memberService.getById(customUserDetails.getId()) + return CoupleBirthDayResponse.of(couple, member) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index 5f584856..71854751 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -16,4 +16,5 @@ object ApiPath { const val ANNIVERSARY_GENERATE = "/v1/couple/new-anniversary" const val ANNIVERSARY_MAIN = "/v1/anniversary/main" const val ANNIVERSARIES = "/v1/anniversaries" + const val COUPLE_BIRTHDAY = "/v1/couple/birthday" } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt index d0acaa44..819227ca 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/CoupleInfoController.kt @@ -71,4 +71,14 @@ class CoupleInfoController ( ):ApiResponse{ return ApiResponse.success(coupleFacade.getCoupleEmotion(customUserDetails)) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.COUPLE_BIRTHDAY) + @Operation(summary = "커플 생년월일 조회 api", description = "getCoupleBirthDay") + fun getCoupleBirthDay( + @AuthenticationPrincipal customUserDetails: CustomUserDetails + ) :ApiResponse{ + val coupleBirthDay = coupleFacade.getCoupleBirthDay(customUserDetails) + return ApiResponse.success(coupleBirthDay) + } } \ No newline at end of file From 498787ac4e7b1020c17b7d750958221e95b4e7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 5 May 2025 16:15:07 +0900 Subject: [PATCH 246/357] =?UTF-8?q?test=20:=20=EC=BB=A4=ED=94=8C=20?= =?UTF-8?q?=EC=83=9D=EB=85=84=EC=9B=94=EC=9D=BC=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20api=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/facade/CoupleFacadeTest.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt index 3f8ef9e4..31dae5a6 100644 --- a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt @@ -62,7 +62,7 @@ class CoupleFacadeTest { name = "곰신", nickname = "곰신닉네임", email = "test1@test.com", - birthDate = null, + birthDate = LocalDate.of(2001, 3, 1), profileImageUrl = null, provider = Provider.KAKAO, role = Role.MEMBER, @@ -73,7 +73,7 @@ class CoupleFacadeTest { name = "꽃신", nickname = "꽃신닉네임", email = "test2@test.com", - birthDate = null, + birthDate = LocalDate.of(2001, 4, 1), profileImageUrl = null, provider = Provider.KAKAO, role = Role.MEMBER, @@ -206,4 +206,18 @@ class CoupleFacadeTest { //then verify(anniversaryService).generateAnniversary(couple, generateAnniversaryRequest) } + + @DisplayName("생년월일 조회 - 정상응답") + @Test + fun getCoupleBirthDay() { + //given + `when`(memberService.getById(customUserDetails.getId())).thenReturn(member1) + `when`(coupleInfoService.findCoupleMember(customUserDetails.getId())).thenReturn(member2) + //when + val result = coupleFacade.getCoupleBirthDay(customUserDetails) + //then + verify(coupleInfoService).findCoupleMember(customUserDetails.getId()) + assertEquals(result.myBirthDay, member1.birthDate) + assertEquals(result.coupleBirthDay, member2.birthDate) + } } From 1fe5193bbda7102230ce7e25a36dc1ed886dd19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 6 May 2025 00:01:11 +0900 Subject: [PATCH 247/357] =?UTF-8?q?refactor=20:=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=88=98=EC=A0=95(coupleBirthday=EB=B3=B4?= =?UTF-8?q?=EB=8B=A4=EB=8A=94=20partnerBirthday=EB=A1=9C=20=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B2=8C=20=EC=83=81=EB=8C=80=EB=B0=A9(=EC=95=A0?= =?UTF-8?q?=EC=9D=B8)=EC=9D=98=20=EC=83=9D=EC=9D=BC=EC=9E=84=EC=9D=84=20?= =?UTF-8?q?=EB=8D=94=20=EC=9E=98=20=EB=82=98=ED=83=80=EB=82=B4=EC=A3=BC?= =?UTF-8?q?=EB=8A=94=20=EA=B2=83=20=EA=B0=99=EC=95=84=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95)=20#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 -- .../backend/couple/dto/response/CoupleBirthDayResponse.kt | 2 +- .../kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1711a21b..60e5b4cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,6 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleBirthDayResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleBirthDayResponse.kt index a0a842c3..59a74c5b 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleBirthDayResponse.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/CoupleBirthDayResponse.kt @@ -6,7 +6,7 @@ import java.time.LocalDate data class CoupleBirthDayResponse ( @Schema(description = "커플 생년월일", example = "2001-04-01") - val coupleBirthDay : LocalDate?, + val partnerBirthday : LocalDate?, @Schema(description = "내 생년월일", example = "2001-03-01") val myBirthDay : LocalDate? ) { diff --git a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt index 31dae5a6..1cc65e2a 100644 --- a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt @@ -218,6 +218,6 @@ class CoupleFacadeTest { //then verify(coupleInfoService).findCoupleMember(customUserDetails.getId()) assertEquals(result.myBirthDay, member1.birthDate) - assertEquals(result.coupleBirthDay, member2.birthDate) + assertEquals(result.partnerBirthday, member2.birthDate) } } From 17af8ffbafcbb3c4849c895e93e59bc1906408e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 6 May 2025 15:28:56 +0900 Subject: [PATCH 248/357] =?UTF-8?q?feat=20:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=A0=84=EC=86=A1=20=ED=95=A0=20=EB=95=8C?= =?UTF-8?q?=20=EB=A7=88=EB=8B=A4=20=EC=95=8C=EB=A6=BC=20=EB=82=B4=EC=9A=A9?= =?UTF-8?q?=20redis=EC=97=90=20=EC=A0=80=EC=9E=A5=20#80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/alarm/dto/SaveAlarmMessage.kt | 16 ++++++++++++++++ .../backend/alarm/service/DdayAlarmSerivce.kt | 5 ++++- .../alarm/service/QuestionAlarmService.kt | 6 ++++-- .../alarm/service/StatusAlarmService.kt | 5 ++++- .../configuration/redis/RedisConfiguration.kt | 14 ++++++++++++++ .../core/configuration/redis/RedisKey.kt | 13 +++++++++++++ .../core/configuration/redis/RedisService.kt | 18 ++++++++++++++++++ 7 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/alarm/dto/SaveAlarmMessage.kt create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/redis/RedisConfiguration.kt create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/redis/RedisKey.kt create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt diff --git a/src/main/kotlin/gomushin/backend/alarm/dto/SaveAlarmMessage.kt b/src/main/kotlin/gomushin/backend/alarm/dto/SaveAlarmMessage.kt new file mode 100644 index 00000000..5816f8f2 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/alarm/dto/SaveAlarmMessage.kt @@ -0,0 +1,16 @@ +package gomushin.backend.alarm.dto + +import java.time.LocalDateTime + +data class SaveAlarmMessage( + val title: String, + val content: String, + val timestamp: String = LocalDateTime.now().toString() +) { + companion object { + fun of(title: String, content: String) = SaveAlarmMessage( + title, + content, + ) + } +} diff --git a/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt b/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt index 60350ee5..e0c4094e 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt @@ -1,5 +1,6 @@ package gomushin.backend.alarm.service +import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.couple.domain.service.AnniversaryService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -15,7 +16,8 @@ import java.time.LocalDateTime @Service class DdayAlarmSerivce( private val fcmService: FCMService, - private val anniversaryService: AnniversaryService + private val anniversaryService: AnniversaryService, + private val redisService: RedisService ) { private val log: Logger = LoggerFactory.getLogger(QuestionAlarmService::class.java) private val alarmTitle = "오늘의 디데이가 도착했어요" @@ -29,6 +31,7 @@ class DdayAlarmSerivce( async(Dispatchers.IO) { try { log.info("디데이 메시지 전송 : 수신자 {${content.memberId}}, 제목 {${alarmTitle}}, 내용{${content.title}}, 전송시각{${LocalDateTime.now()}}\n") + redisService.saveAlarm(alarmTitle, content.title, content.memberId) fcmService.sendMessageTo(content.fcmToken, alarmTitle, content.title) } catch (e: Exception) { log.error("디데이 메시지 전송오류 : 수신자 {${content.memberId}}, 전송시각{${LocalDateTime.now()}}\n") diff --git a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt index 2ca0c464..d660c4cd 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt @@ -1,7 +1,7 @@ package gomushin.backend.alarm.service import gomushin.backend.alarm.util.MessageParsingUtil -import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.member.domain.service.MemberService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -16,7 +16,8 @@ import java.time.LocalDateTime @Service class QuestionAlarmService ( private val fcmService: FCMService, - private val memberService: MemberService + private val memberService: MemberService, + private val redisService: RedisService ) { private val log: Logger = LoggerFactory.getLogger(QuestionAlarmService::class.java) private val questionMessages = listOf( @@ -36,6 +37,7 @@ class QuestionAlarmService ( val notificationContent = questionMessages.random() val (title, sendContent) = MessageParsingUtil.parse(notificationContent) log.info("질문형 메시지 전송 : 수신자 {${member.id}}, 제목 {${title}}, 내용{${sendContent}}, 전송시각{${LocalDateTime.now()}}\n") + redisService.saveAlarm(title, sendContent, member.id) fcmService.sendMessageTo(member.fcmToken, title, sendContent) } catch (e: Exception) { log.error("질문형 메시지 전송오류 : 수신자 {${member.name}}, 전송시각{${LocalDateTime.now()}}\n") diff --git a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt index 023c9acc..e0d8bb5c 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt @@ -1,6 +1,7 @@ package gomushin.backend.alarm.service import gomushin.backend.alarm.util.MessageParsingUtil +import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.value.Emotion @@ -9,7 +10,8 @@ import org.springframework.stereotype.Service @Service class StatusAlarmService ( - private val fcmService: FCMService + private val fcmService: FCMService, + private val redisService: RedisService ) { private val statusMessage: Map> = mapOf( Emotion.MISS to listOf( @@ -54,6 +56,7 @@ class StatusAlarmService ( content } val token = receiver.fcmToken + redisService.saveAlarm(title, sendContent, receiver.id) fcmService.sendMessageTo(token, title, sendContent) } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisConfiguration.kt new file mode 100644 index 00000000..5d82743e --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisConfiguration.kt @@ -0,0 +1,14 @@ +package gomushin.backend.core.configuration.redis + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.core.StringRedisTemplate + +@Configuration +class RedisConfiguration { + @Bean + fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): StringRedisTemplate { + return StringRedisTemplate(redisConnectionFactory) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisKey.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisKey.kt new file mode 100644 index 00000000..e65ab376 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisKey.kt @@ -0,0 +1,13 @@ +package gomushin.backend.core.configuration.redis + +enum class RedisKey( + private val tag : String +) { + ALARM("alarm:"); + + companion object { + fun getRedisAlarmKey(receiverId : Long) : String { + return ALARM.tag + receiverId + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt new file mode 100644 index 00000000..88665eb8 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt @@ -0,0 +1,18 @@ +package gomushin.backend.core.configuration.redis + +import com.fasterxml.jackson.databind.ObjectMapper +import gomushin.backend.alarm.dto.SaveAlarmMessage +import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.stereotype.Service + +@Service +class RedisService ( + private val redisTemplate: StringRedisTemplate +) { + fun saveAlarm(title : String, content : String, receiverId : Long) { + val savedAlarmMessage = SaveAlarmMessage.of(title, content) + val key = RedisKey.getRedisAlarmKey(receiverId) + val json = ObjectMapper().writeValueAsString(savedAlarmMessage) + redisTemplate.opsForList().leftPush(key, json) + } +} \ No newline at end of file From 7b91107bb5df51a9446b0786d84d7cab8d4035e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 6 May 2025 20:57:43 +0900 Subject: [PATCH 249/357] =?UTF-8?q?build=20:=20Kotlin=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EC=9D=98=20=EC=A7=81=EB=A0=AC=ED=99=94/?= =?UTF-8?q?=EC=97=AD=EC=A7=81=EB=A0=AC=ED=99=94=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EB=AA=A8=EB=93=88=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?#80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 3af37697..e7edd43b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,6 +84,9 @@ dependencies { testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + //jackson + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") } kotlin { From 78e0f56007e750ffb4298bc4a8b38f48625f76a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 6 May 2025 21:04:35 +0900 Subject: [PATCH 250/357] =?UTF-8?q?config=20:=20objectMapper=EB=8A=94=20?= =?UTF-8?q?=EC=9E=AC=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EA=B8=B0=EC=97=90=20bean=EC=9C=BC=EB=A1=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=ED=95=B4=EC=84=9C=20=EA=B4=80=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B2=83=EC=9D=B4=20=ED=8E=B8=ED=95=98=EA=B2=A0?= =?UTF-8?q?=EB=8B=A4=EB=8A=94=20=ED=8C=90=EB=8B=A8=EC=9D=B4=20=EB=93=A4?= =?UTF-8?q?=EC=97=88=EC=9D=8C=20#80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/jackson/JacksonConfig.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt diff --git a/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt b/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt new file mode 100644 index 00000000..0d14e560 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt @@ -0,0 +1,14 @@ +package gomushin.backend.core.configuration.jackson + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class JacksonConfig { + @Bean + fun objectMapper() : ObjectMapper { + return jacksonObjectMapper() + } +} \ No newline at end of file From 9841460b18d3a20687de640cfd8fb00a255ed675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 6 May 2025 21:05:18 +0900 Subject: [PATCH 251/357] =?UTF-8?q?feat=20:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EA=B5=AC=ED=98=84=20#80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/redis/RedisService.kt | 33 +++++++++++++++++-- .../backend/member/facade/MemberInfoFacade.kt | 9 ++++- .../backend/member/presentation/ApiPath.kt | 1 + .../presentation/MemberInfoController.kt | 13 ++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt index 88665eb8..0d8ac845 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt @@ -4,15 +4,44 @@ import com.fasterxml.jackson.databind.ObjectMapper import gomushin.backend.alarm.dto.SaveAlarmMessage import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter @Service class RedisService ( - private val redisTemplate: StringRedisTemplate + private val redisTemplate: StringRedisTemplate, + private val objectMapper: ObjectMapper ) { fun saveAlarm(title : String, content : String, receiverId : Long) { val savedAlarmMessage = SaveAlarmMessage.of(title, content) val key = RedisKey.getRedisAlarmKey(receiverId) - val json = ObjectMapper().writeValueAsString(savedAlarmMessage) + val json = objectMapper.writeValueAsString(savedAlarmMessage) redisTemplate.opsForList().leftPush(key, json) } + + fun getAlarms(memberId : Long, recentDays : Long) : List { + val key = RedisKey.getRedisAlarmKey(memberId) + val allJson = redisTemplate.opsForList().range(key, 0, -1) ?: return emptyList() + val now = LocalDateTime.now() + val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME + val cutoff = now.minusDays(recentDays) + + val validAlarms = mutableListOf() + + allJson.forEach { json -> + val alarm = runCatching { + objectMapper.readValue(json, SaveAlarmMessage::class.java) + }.getOrNull() ?: return@forEach + + runCatching { LocalDateTime.parse(alarm.timestamp, formatter) } + .getOrNull() + ?.takeIf { it.isAfter(cutoff) } + ?.let { validAlarms.add(alarm) } + ?: run { + redisTemplate.opsForList().remove(key, 1, json) + } + } + + return validAlarms + } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 27c27877..0f83a3ac 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -1,7 +1,9 @@ package gomushin.backend.member.facade +import gomushin.backend.alarm.dto.SaveAlarmMessage import gomushin.backend.alarm.service.StatusAlarmService import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.service.NotificationService @@ -21,7 +23,8 @@ class MemberInfoFacade( private val memberService: MemberService, private val notificationService: NotificationService, private val statusAlarmService: StatusAlarmService, - private val coupleInfoService: CoupleInfoService + private val coupleInfoService: CoupleInfoService, + private val redisService: RedisService ) { fun getMemberInfo(customUserDetails: CustomUserDetails): MyInfoResponse { val member = memberService.getById(customUserDetails.getId()) @@ -66,4 +69,8 @@ class MemberInfoFacade( val notification = notificationService.getByMemberId(customUserDetails.getId()) return MyNotificationResponse.of(notification) } + + fun getMyReceivedNotification(customUserDetails: CustomUserDetails, recentDays: Long): List { + return redisService.getAlarms(customUserDetails.getId(), recentDays) + } } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt index 93729f99..a6c12924 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt @@ -11,4 +11,5 @@ object ApiPath { const val UPDATE_MY_NOTIFICATION_POLICY = "/v1/member/my-notification" const val DELETE_ALL_MY_DATA ="/v1/member/all-my-data" const val MY_NOTIFICATION = "/v1/member/my-notification" + const val MY_ALARM = "/v1/member/my-alarm/{recentDays}" } diff --git a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt index 8d629a34..60e78fd8 100644 --- a/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt +++ b/src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt @@ -1,5 +1,6 @@ package gomushin.backend.member.presentation +import gomushin.backend.alarm.dto.SaveAlarmMessage import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.member.dto.request.UpdateMyBirthdayRequest @@ -18,6 +19,7 @@ import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.ResponseStatus @@ -123,4 +125,15 @@ class MemberInfoController( val myNotification = memberInfoFacade.getMyNotification(customUserDetails) return ApiResponse.success(myNotification) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.MY_ALARM) + @Operation(summary = "알림 수신 내역 조회", description = "getMyReceivedNotification") + fun getMyReceivedNotification( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable recentDays : Long + ) : ApiResponse>{ + val saveAlarmMessages = memberInfoFacade.getMyReceivedNotification(customUserDetails, recentDays) + return ApiResponse.success(saveAlarmMessages) + } } From ef84519b514816f5bc5c979a02e52baf826f1dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 6 May 2025 21:09:55 +0900 Subject: [PATCH 252/357] =?UTF-8?q?test=20:=20redisService=20=EB=AA=A8?= =?UTF-8?q?=ED=82=B9=20=EC=B6=94=EA=B0=80=20#80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/member/facade/MemberInfoFacadeTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index dd18f8a8..4095eb94 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -2,6 +2,7 @@ package gomushin.backend.member.facade import gomushin.backend.alarm.service.StatusAlarmService import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.entity.Notification @@ -35,6 +36,8 @@ class MemberInfoFacadeTest { private lateinit var statusAlarmService: StatusAlarmService @Mock private lateinit var coupleInfoService: CoupleInfoService + @Mock + private lateinit var redisService: RedisService @InjectMocks private lateinit var memberInfoFacade: MemberInfoFacade From b74a01fb8acdbb936a9ad8a94644d70f9b2acf1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 6 May 2025 21:13:54 +0900 Subject: [PATCH 253/357] =?UTF-8?q?build=20:=20=EC=A4=91=EB=B3=B5=EB=90=9C?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20#80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e7edd43b..3af37697 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,9 +84,6 @@ dependencies { testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - - //jackson - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") } kotlin { From 7fc5cb5631ad4161e6a2d37a7f7a02e53a3ed6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 6 May 2025 23:33:30 +0900 Subject: [PATCH 254/357] =?UTF-8?q?refactor=20:=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0(re?= =?UTF-8?q?disConfiguration=EC=97=86=EC=96=B4=EB=8F=84=20=EC=A0=95?= =?UTF-8?q?=EC=83=81=EB=8F=99=EC=9E=91=ED=95=A8),=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=AA=85=20=EB=B3=80=EA=B2=BD(=EC=97=AC=EB=9F=AC=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B1=B8=EB=A1=9C=20=EB=B4=90=EC=84=9C?= =?UTF-8?q?=EB=8A=94=20Facade=EA=B0=80=20=EC=A0=81=EC=A0=88=ED=95=9C=20?= =?UTF-8?q?=EB=84=A4=EC=9E=84=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EC=9E=84)=20#8?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DdayAlarmFacade.kt} | 7 ++++--- .../QuestionAlarmFacade.kt} | 7 ++++--- .../core/configuration/redis/RedisConfiguration.kt | 14 -------------- 3 files changed, 8 insertions(+), 20 deletions(-) rename src/main/kotlin/gomushin/backend/alarm/{service/DdayAlarmSerivce.kt => facade/DdayAlarmFacade.kt} (93%) rename src/main/kotlin/gomushin/backend/alarm/{service/QuestionAlarmService.kt => facade/QuestionAlarmFacade.kt} (94%) delete mode 100644 src/main/kotlin/gomushin/backend/core/configuration/redis/RedisConfiguration.kt diff --git a/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt similarity index 93% rename from src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt rename to src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt index e0c4094e..1b45a097 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/DdayAlarmSerivce.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt @@ -1,5 +1,6 @@ -package gomushin.backend.alarm.service +package gomushin.backend.alarm.facade +import gomushin.backend.alarm.service.FCMService import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.couple.domain.service.AnniversaryService import kotlinx.coroutines.Dispatchers @@ -14,12 +15,12 @@ import java.time.LocalDate import java.time.LocalDateTime @Service -class DdayAlarmSerivce( +class DdayAlarmFacade( private val fcmService: FCMService, private val anniversaryService: AnniversaryService, private val redisService: RedisService ) { - private val log: Logger = LoggerFactory.getLogger(QuestionAlarmService::class.java) + private val log: Logger = LoggerFactory.getLogger(QuestionAlarmFacade::class.java) private val alarmTitle = "오늘의 디데이가 도착했어요" @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") diff --git a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt similarity index 94% rename from src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt rename to src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt index d660c4cd..f0825583 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/QuestionAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt @@ -1,5 +1,6 @@ -package gomushin.backend.alarm.service +package gomushin.backend.alarm.facade +import gomushin.backend.alarm.service.FCMService import gomushin.backend.alarm.util.MessageParsingUtil import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.member.domain.service.MemberService @@ -14,12 +15,12 @@ import org.springframework.stereotype.Service import java.time.LocalDateTime @Service -class QuestionAlarmService ( +class QuestionAlarmFacade ( private val fcmService: FCMService, private val memberService: MemberService, private val redisService: RedisService ) { - private val log: Logger = LoggerFactory.getLogger(QuestionAlarmService::class.java) + private val log: Logger = LoggerFactory.getLogger(QuestionAlarmFacade::class.java) private val questionMessages = listOf( "오늘 하루는 어땠나요?+연인에게도 안부를 전해보세요", "사랑은 매일 채우는 것+오늘도 연인과 한 조각 채워보세요", diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisConfiguration.kt deleted file mode 100644 index 5d82743e..00000000 --- a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisConfiguration.kt +++ /dev/null @@ -1,14 +0,0 @@ -package gomushin.backend.core.configuration.redis - -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.data.redis.connection.RedisConnectionFactory -import org.springframework.data.redis.core.StringRedisTemplate - -@Configuration -class RedisConfiguration { - @Bean - fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): StringRedisTemplate { - return StringRedisTemplate(redisConnectionFactory) - } -} \ No newline at end of file From 175c64d75a4ffdf3cd621e5f33343495ef5ae280 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 7 May 2025 15:04:14 +0900 Subject: [PATCH 255/357] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=BB=A4=EC=8A=A4=ED=85=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/jackson/JacksonConfig.kt | 15 ++++++-- .../security/SecurityConfiguration.kt | 5 ++- .../filter/CustomAuthenticationEntryPoint.kt | 37 +++++++++++++++++++ .../handler/CustomAccessDeniedHandler.kt | 8 ++++ 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/core/infrastructure/filter/CustomAuthenticationEntryPoint.kt diff --git a/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt b/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt index 0d14e560..d0d2df95 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt @@ -1,6 +1,10 @@ package gomushin.backend.core.configuration.jackson +import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -8,7 +12,12 @@ import org.springframework.context.annotation.Configuration @Configuration class JacksonConfig { @Bean - fun objectMapper() : ObjectMapper { - return jacksonObjectMapper() + fun objectMapper(): ObjectMapper { + return jacksonObjectMapper().apply { + registerModules(JavaTimeModule()) + registerModules(KotlinModule.Builder().build()) + configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false) + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 1cf4c044..c1627eb7 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -1,5 +1,6 @@ package gomushin.backend.core.configuration.security +import gomushin.backend.core.infrastructure.filter.CustomAuthenticationEntryPoint import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter import gomushin.backend.core.jwt.JwtTokenProvider import gomushin.backend.core.oauth.handler.CustomAccessDeniedHandler @@ -31,7 +32,8 @@ class SecurityConfiguration( fun filterChain( http: HttpSecurity, corsConfiguration: CustomCorsConfiguration, customOAuth2UserService: CustomOAuth2UserService, coupleRepository: CoupleRepository, - customAccessDeniedHandler: CustomAccessDeniedHandler + customAccessDeniedHandler: CustomAccessDeniedHandler, + customAuthenticationEntryPoint: CustomAuthenticationEntryPoint, ): SecurityFilterChain { http .csrf { @@ -68,6 +70,7 @@ class SecurityConfiguration( } .exceptionHandling { it.accessDeniedHandler(customAccessDeniedHandler) + it.authenticationEntryPoint(customAuthenticationEntryPoint) } .authorizeHttpRequests { it.requestMatchers( diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/CustomAuthenticationEntryPoint.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/CustomAuthenticationEntryPoint.kt new file mode 100644 index 00000000..437c6336 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/CustomAuthenticationEntryPoint.kt @@ -0,0 +1,37 @@ +package gomushin.backend.core.infrastructure.filter + +import com.fasterxml.jackson.databind.ObjectMapper +import gomushin.backend.core.common.web.response.ExtendedHttpStatus +import gomushin.backend.core.common.web.response.exception.ErrorCodeResolvingApiErrorException +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.MediaType +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.stereotype.Component + +@Component +class CustomAuthenticationEntryPoint: AuthenticationEntryPoint { + override fun commence( + request: HttpServletRequest?, + response: HttpServletResponse?, + authException: AuthenticationException? + ) { + if (response != null) { + if (response.isCommitted) return + } + + response?.contentType = MediaType.APPLICATION_JSON_VALUE + response?.status = HttpServletResponse.SC_UNAUTHORIZED + response?.characterEncoding = "UTF-8" + + val exception = ErrorCodeResolvingApiErrorException( + ExtendedHttpStatus.UNAUTHORIZED, + "sarangggun.auth.unauthorized" + ) + + response?.writer?.write( + ObjectMapper().writeValueAsString(exception.error) + ) + } +} diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt index 851e517b..109d029d 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt @@ -33,6 +33,14 @@ class CustomAccessDeniedHandler : AccessDeniedHandler { response.writer.write( ObjectMapper().writeValueAsString(exception.error) ) + } else { + val exception = ErrorCodeResolvingApiErrorException( + ExtendedHttpStatus.FORBIDDEN, + "sarangggun.auth.member-only" + ) + response.writer.write( + ObjectMapper().writeValueAsString(exception.error) + ) } } From d8e4d5ac8d54940d9ff38e4a293b2151dea6fe2e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 7 May 2025 15:17:36 +0900 Subject: [PATCH 256/357] =?UTF-8?q?refactor:=20ObjectMapper=20=EB=B9=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A3=BC=EC=9E=85=20=EB=B0=9B=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/CustomAuthenticationEntryPoint.kt | 9 +++--- .../handler/CustomAccessDeniedHandler.kt | 28 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/CustomAuthenticationEntryPoint.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/CustomAuthenticationEntryPoint.kt index 437c6336..8cf5e04f 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/CustomAuthenticationEntryPoint.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/CustomAuthenticationEntryPoint.kt @@ -11,15 +11,14 @@ import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.stereotype.Component @Component -class CustomAuthenticationEntryPoint: AuthenticationEntryPoint { +class CustomAuthenticationEntryPoint(private val objectMapper: ObjectMapper): AuthenticationEntryPoint { override fun commence( request: HttpServletRequest?, response: HttpServletResponse?, authException: AuthenticationException? ) { - if (response != null) { - if (response.isCommitted) return - } + + if(response?.isCommitted == true) return response?.contentType = MediaType.APPLICATION_JSON_VALUE response?.status = HttpServletResponse.SC_UNAUTHORIZED @@ -31,7 +30,7 @@ class CustomAuthenticationEntryPoint: AuthenticationEntryPoint { ) response?.writer?.write( - ObjectMapper().writeValueAsString(exception.error) + objectMapper.writeValueAsString(exception.error) ) } } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt index 109d029d..0f960c47 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt @@ -12,7 +12,7 @@ import org.springframework.stereotype.Component @Component -class CustomAccessDeniedHandler : AccessDeniedHandler { +class CustomAccessDeniedHandler(private val objectMapper: ObjectMapper) : AccessDeniedHandler { override fun handle( request: HttpServletRequest, response: HttpServletResponse, @@ -25,24 +25,20 @@ class CustomAccessDeniedHandler : AccessDeniedHandler { response.characterEncoding = "UTF-8" - if (request.requestURI.contains("/v1/member/onboarding")) { - val exception = ErrorCodeResolvingApiErrorException( - ExtendedHttpStatus.FORBIDDEN, - "sarangggun.auth.guest-only" - ) - response.writer.write( - ObjectMapper().writeValueAsString(exception.error) - ) + val errorCode = if (request.requestURI.contains("/v1/member/onboarding")) { + "sarangggun.auth.guest-only" } else { - val exception = ErrorCodeResolvingApiErrorException( - ExtendedHttpStatus.FORBIDDEN, - "sarangggun.auth.member-only" - ) - response.writer.write( - ObjectMapper().writeValueAsString(exception.error) - ) + "sarangggun.auth.member-only" } + val exception = ErrorCodeResolvingApiErrorException( + ExtendedHttpStatus.FORBIDDEN, + errorCode + ) + response.writer.write( + objectMapper.writeValueAsString(exception.error) + ) + } } From 61b756fe9ca259054259eea73d9310d08bc8d267 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 7 May 2025 15:46:43 +0900 Subject: [PATCH 257/357] =?UTF-8?q?fix:=20=EC=9D=BC=EC=A0=95=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=98=AC=20=EB=95=8C=20=EB=82=A0=EC=A7=9C=EA=B0=80=20?= =?UTF-8?q?=EA=B2=B9=EC=B3=90=EC=9E=88=EB=8A=94=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=EB=8F=84=20=EC=A0=84=EB=B6=80=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/domain/repository/ScheduleRepository.kt | 9 +++++---- .../backend/schedule/domain/service/ScheduleService.kt | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt index 72a38794..0629c7bd 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt @@ -45,10 +45,10 @@ interface ScheduleRepository : JpaRepository { ) FROM Schedule s WHERE s.coupleId = :coupleId - AND function('DATE', s.startDate) = :startDate + AND :date BETWEEN FUNCTION('DATE', s.startDate) AND FUNCTION('DATE', s.endDate) """ ) - fun findByCoupleIdAndStartDate(coupleId: Long, startDate: LocalDate): List + fun findByCoupleIdAndDate(coupleId: Long, date: LocalDate): List @Query( """ @@ -61,10 +61,11 @@ interface ScheduleRepository : JpaRepository { ) FROM Schedule s WHERE s.coupleId = :coupleId - AND s.startDate BETWEEN :startDate AND :endDate + AND s.startDate <= :endDate + AND s.endDate >= :startDate """ ) - fun findByCoupleIdAndStartDateBetween( + fun findByCoupleIdAndStartDateAndEndDateBetween( coupleId: Long, startDate: LocalDateTime, endDate: LocalDateTime diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt index 885b10b9..c8c58270 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt @@ -28,7 +28,7 @@ class ScheduleService( startDate: LocalDate, endDate: LocalDate ): List { - return scheduleRepository.findByCoupleIdAndStartDateBetween( + return scheduleRepository.findByCoupleIdAndStartDateAndEndDateBetween( couple.id, startDate.atStartOfDay(), endDate.atTime(23, 59, 59) @@ -37,7 +37,7 @@ class ScheduleService( @Transactional(readOnly = true) fun findByDate(couple: Couple, date: LocalDate): List { - return scheduleRepository.findByCoupleIdAndStartDate(couple.id, date) + return scheduleRepository.findByCoupleIdAndDate(couple.id, date) } @Transactional From 4ec897209505b8e60d3b68d3867ed10dedcc7d9c Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 7 May 2025 23:32:46 +0900 Subject: [PATCH 258/357] =?UTF-8?q?fix:=20jackson=20config=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20serializer=20=EC=84=A4=EC=A0=95=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 --- .../configuration/jackson/JacksonConfig.kt | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt b/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt index d0d2df95..b87eb4e8 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/jackson/JacksonConfig.kt @@ -4,18 +4,40 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter @Configuration class JacksonConfig { + companion object { + private const val DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" + private const val DATE_FORMAT = "yyyy-MM-dd" + private const val TIME_FORMAT = "HH:mm:ss.SSS" + private val LOCAL_DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT) + private val LOCAL_DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT) + private val LOCAL_TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_FORMAT) + } + @Bean fun objectMapper(): ObjectMapper { + val javaTimeModule = JavaTimeModule().apply { + addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer(LOCAL_DATETIME_FORMATTER)) + addSerializer(LocalDate::class.java, LocalDateSerializer(LOCAL_DATE_FORMATTER)) + addSerializer(LocalTime::class.java, LocalTimeSerializer(LOCAL_TIME_FORMATTER)) + } + return jacksonObjectMapper().apply { - registerModules(JavaTimeModule()) - registerModules(KotlinModule.Builder().build()) + registerModules(javaTimeModule, KotlinModule.Builder().build()) + configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false) configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } From 21786d072782a97c1f64c0478edc729baf4e2af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 9 May 2025 01:39:31 +0900 Subject: [PATCH 259/357] =?UTF-8?q?feat=20:=20=EC=97=B0=EA=B2=B0=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=EC=8B=9C=20=EC=83=81=EB=8C=80=EB=B0=A9=EC=9D=98=20isC?= =?UTF-8?q?oupled=ED=95=84=EB=93=9C=EB=8F=84=20false=EB=A1=9C=20=EB=90=98?= =?UTF-8?q?=EB=B0=94=EA=BF=94=EC=A3=BC=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/member/domain/entity/Member.kt | 4 ++++ .../kotlin/gomushin/backend/member/facade/LeaveFacade.kt | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index 31dc82ed..0a0d2ce9 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -96,4 +96,8 @@ class Member( fun updateBirthday(birthDate: LocalDate) { this.birthDate = birthDate } + + fun updateIsCouple(isCouple: Boolean) { + this.isCouple = isCouple + } } diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt index 49f788f4..703690c4 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -3,6 +3,7 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.service.S3Service import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.domain.service.CoupleService import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.service.NotificationService @@ -24,11 +25,13 @@ class LeaveFacade( private val pictureService: PictureService, private val scheduleService: ScheduleService, private val s3Service: S3Service, + private val coupleInfoService: CoupleInfoService ) { @Transactional fun leave(customUserDetails: CustomUserDetails) { val memberId = customUserDetails.getId() val coupleId = customUserDetails.getCouple().id + val partner = coupleInfoService.findCoupleMember(memberId) anniversaryService.deleteAllByCoupleId(coupleId) commentService.deleteAllByMemberId(memberId) coupleService.deleteByMemberId(memberId) @@ -45,5 +48,7 @@ class LeaveFacade( pictureService.deleteAllByLetterIds(letters) letterService.deleteAllByMemberId(memberId) memberService.deleteMember(memberId) + + partner.updateIsCouple(false) } } \ No newline at end of file From dcd2c705fe4f7193b5ea922ca4349eeb225c5cef Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 11 May 2025 17:24:41 +0900 Subject: [PATCH 260/357] =?UTF-8?q?fix:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C=20=EC=9D=91=EB=8B=B5=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 --- .../backend/core/common/web/PageResponse.kt | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt b/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt index c7bb610f..2a5a2ecf 100644 --- a/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt +++ b/src/main/kotlin/gomushin/backend/core/common/web/PageResponse.kt @@ -1,34 +1,23 @@ package gomushin.backend.core.common.web import io.swagger.v3.oas.annotations.media.Schema +import org.springframework.data.domain.Page data class PageResponse( @Schema(description = "페이지 응답") - val data: List?, - @Schema(description = "다음 페이지를 위한 커서 키") - val after: Long?, - @Schema(description = "데이터 수") - val count: Int, -// @Schema(description = "다음 페이지 URL") -// val next: String?, + val data: List, + @Schema(description = "전체 페이지 수") + val totalPages: Int, @Schema(description = "마지막 페이지 여부") val isLastPage: Boolean, ) { companion object { - fun of( - data: List, - after: Long?, - count: Int, -// next: String?, - isLastPage: Boolean, - ): PageResponse { - return PageResponse( - data = data, - after = after, - count = count, -// next = next, - isLastPage = isLastPage - ) - } + fun from( + page: Page + ): PageResponse = PageResponse( + data = page.content, + totalPages = page.totalPages, + isLastPage = page.isLast + ) } } From 4243f94a31345a56fc278ed291740c23b4e99edd Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 11 May 2025 17:25:17 +0900 Subject: [PATCH 261/357] =?UTF-8?q?fix:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=9D=91=EB=8B=B5=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 --- .../domain/service/AnniversaryService.kt | 10 +++---- .../couple/facade/AnniversaryFacade.kt | 25 +++++------------ .../presentation/AnniversaryController.kt | 7 +++-- .../schedule/domain/service/LetterService.kt | 10 +++---- .../schedule/facade/ReadLetterFacade.kt | 27 +++++-------------- .../presentation/ReadLetterController.kt | 8 +++--- 6 files changed, 29 insertions(+), 58 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 2367298b..33228145 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -4,11 +4,12 @@ import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest -import gomushin.backend.couple.dto.request.ReadAnniversariesRequest import gomushin.backend.couple.dto.response.AnniversaryNotificationInfo import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import gomushin.backend.schedule.dto.response.MainAnniversariesResponse +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @@ -19,11 +20,10 @@ class AnniversaryService( ) { @Transactional(readOnly = true) - fun findAnniversaries(couple: Couple, readAnniversariesRequest: ReadAnniversariesRequest): List { + fun findAnniversaries(couple: Couple, pageRequest: PageRequest): Page { return anniversaryRepository.findAnniversaries( couple.id, - readAnniversariesRequest.key, - readAnniversariesRequest.take, + pageRequest ) } @@ -74,7 +74,7 @@ class AnniversaryService( } @Transactional(readOnly = true) - fun getTodayAnniversaryMemberFcmTokens(date : LocalDate) : List { + fun getTodayAnniversaryMemberFcmTokens(date: LocalDate): List { return anniversaryRepository.findTodayAnniversaryMemberFcmTokens(date) } } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt index 961656f1..e6dff894 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -3,10 +3,10 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.PageResponse import gomushin.backend.couple.domain.service.AnniversaryService -import gomushin.backend.couple.dto.request.ReadAnniversariesRequest import gomushin.backend.couple.dto.response.MainAnniversaryResponse import gomushin.backend.couple.dto.response.TotalAnniversaryResponse import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Component @Component @@ -22,29 +22,16 @@ class AnniversaryFacade( fun getAnniversaryList( customUserDetails: CustomUserDetails, - readAnniversariesRequest: ReadAnniversariesRequest, + page: Int, + size: Int, ): PageResponse { + val pageRequest = PageRequest.of(page, size) val anniversaries = - anniversaryService.findAnniversaries(customUserDetails.getCouple(), readAnniversariesRequest) + anniversaryService.findAnniversaries(customUserDetails.getCouple(), pageRequest) val anniversaryResponses = anniversaries.map { anniversary -> TotalAnniversaryResponse.of(anniversary) } - val isLastPage = anniversaryResponses.size < readAnniversariesRequest.take - val hasData = anniversaryResponses.isNotEmpty() - -// val nextUrl = if (!isLastPage && hasData) { -// "${baseUrl}/v1/anniversaries?key=${anniversaryResponses.last().id}&take=${readAnniversariesRequest.take}" -// } else { -// null -// } - - return PageResponse.of( - data = anniversaryResponses, - after = if (hasData) anniversaryResponses.last().id else null, - count = anniversaryResponses.size, -// next = nextUrl, - isLastPage = isLastPage - ) + return PageResponse.from(anniversaryResponses) } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt index b71496e1..06c286f9 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt @@ -4,14 +4,12 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.PageResponse import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest -import gomushin.backend.couple.dto.request.ReadAnniversariesRequest import gomushin.backend.couple.dto.response.MainAnniversaryResponse import gomushin.backend.couple.dto.response.TotalAnniversaryResponse import gomushin.backend.couple.facade.AnniversaryFacade import gomushin.backend.couple.facade.CoupleFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag -import org.springdoc.core.annotations.ParameterObject import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.* @@ -55,9 +53,10 @@ class AnniversaryController( ) fun getAnniversaryList( @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @ParameterObject readAnniversariesRequest: ReadAnniversariesRequest, + @RequestParam(defaultValue = "0") page: Int, + @RequestParam(defaultValue = "10") size: Int, ): PageResponse { - val anniversaries = anniversaryFacade.getAnniversaryList(customUserDetails, readAnniversariesRequest) + val anniversaries = anniversaryFacade.getAnniversaryList(customUserDetails, page, size) return anniversaries } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index 6884aac1..b25aec29 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -5,8 +5,9 @@ import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.LetterRepository -import gomushin.backend.schedule.dto.request.ReadLettersToMePaginationRequest import gomushin.backend.schedule.dto.request.UpsertLetterRequest +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -82,12 +83,11 @@ class LetterService( @Transactional(readOnly = true) fun findAllToCouple( couple: Couple, - readLettersToMePaginationRequest: ReadLettersToMePaginationRequest - ): List { + pageable: Pageable + ): Page { return letterRepository.findAllToCouple( couple.id, - readLettersToMePaginationRequest.key, - readLettersToMePaginationRequest.take, + pageable ) } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 7e37a74e..8710c7b4 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -6,9 +6,9 @@ import gomushin.backend.schedule.domain.service.CommentService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService -import gomushin.backend.schedule.dto.request.ReadLettersToMePaginationRequest import gomushin.backend.schedule.dto.response.* import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Component @Component @@ -70,10 +70,11 @@ class ReadLetterFacade( fun getLetterListToCouple( customUserDetails: CustomUserDetails, - readLettersToMePaginationRequest: ReadLettersToMePaginationRequest, + page: Int, + size: Int, ): PageResponse { - - val letters = letterService.findAllToCouple(customUserDetails.getCouple(), readLettersToMePaginationRequest) + val pageRequest = PageRequest.of(page, size) + val letters = letterService.findAllToCouple(customUserDetails.getCouple(), pageRequest) val letterPreviewResponses = letters.map { letter -> val picture = letter.let { pictureService.findFirstByLetterId(it.id) } @@ -81,23 +82,7 @@ class ReadLetterFacade( LetterPreviewResponse.of(letter, schedule, picture) } - val isLastPage = letterPreviewResponses.size < readLettersToMePaginationRequest.take - - val hasData = letterPreviewResponses.isNotEmpty() - -// val nextUrl = if (!isLastPage && hasData) { -// "${baseUrl}/v1/schedules/letters/to-me?key=${letterPreviewResponses.last().letterId}&orderCreatedAt=DESC&take=${readLettersToMePaginationRequest.take}" -// } else { -// null -// } - - return PageResponse.of( - data = letterPreviewResponses, - after = if (hasData) letterPreviewResponses.last().letterId else null, - count = letterPreviewResponses.size, -// next = nextUrl, - isLastPage = isLastPage - ) + return PageResponse.from(letterPreviewResponses) } fun getLetterListMain( diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index c54a3f8c..d6116d9d 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -3,17 +3,16 @@ package gomushin.backend.schedule.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.PageResponse import gomushin.backend.core.common.web.response.ApiResponse -import gomushin.backend.schedule.dto.request.ReadLettersToMePaginationRequest import gomushin.backend.schedule.dto.response.LetterDetailResponse import gomushin.backend.schedule.dto.response.LetterPreviewResponse import gomushin.backend.schedule.dto.response.MainLetterPreviewResponse import gomushin.backend.schedule.facade.ReadLetterFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag -import org.springdoc.core.annotations.ParameterObject import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -47,9 +46,10 @@ class ReadLetterController( @Operation(summary = "커플 전체 편지 리스트 가져오기", description = "getLetterListToMe") fun getLetterListToMe( @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @ParameterObject readLettersToMePaginationRequest: ReadLettersToMePaginationRequest + @RequestParam(defaultValue = "0") page: Int, + @RequestParam(defaultValue = "10") size: Int, ): PageResponse { - val letters = readLetterFacade.getLetterListToCouple(customUserDetails, readLettersToMePaginationRequest) + val letters = readLetterFacade.getLetterListToCouple(customUserDetails, page, size); return letters } From a8c12ba92b1c009cf8d2774c699aafd14506e324 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 11 May 2025 17:26:03 +0900 Subject: [PATCH 262/357] =?UTF-8?q?fix:=20Pageable=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AnniversaryRepository.kt | 23 ++++++++----------- .../domain/repository/LetterRepository.kt | 17 ++++++-------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index 7a4140f1..17af1c41 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -5,12 +5,13 @@ import gomushin.backend.couple.dto.response.AnniversaryNotificationInfo import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import gomushin.backend.schedule.dto.response.MainAnniversariesResponse +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import java.time.LocalDate -import java.time.LocalDateTime interface AnniversaryRepository : JpaRepository { @Modifying @@ -94,22 +95,16 @@ interface AnniversaryRepository : JpaRepository { @Query( - value = """ - SELECT * - FROM anniversary a - WHERE a.couple_id = :coupleId - AND a.id < :key - ORDER BY anniversary_date DESC - LIMIT :take - """, - nativeQuery = true + SELECT a FROM Anniversary a + WHERE a.coupleId = :coupleId + ORDER BY a.anniversaryDate DESC + """ ) fun findAnniversaries( @Param("coupleId") coupleId: Long, - @Param("key") key: Long, - @Param("take") take: Long - ): List + pageable: Pageable + ): Page @Query( """ @@ -123,5 +118,5 @@ interface AnniversaryRepository : JpaRepository { """, nativeQuery = true ) - fun findTodayAnniversaryMemberFcmTokens(@Param("nowDate")date: LocalDate): List + fun findTodayAnniversaryMemberFcmTokens(@Param("nowDate") date: LocalDate): List } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt index 5350911a..9317e9cf 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/LetterRepository.kt @@ -1,6 +1,8 @@ package gomushin.backend.schedule.domain.repository import gomushin.backend.schedule.domain.entity.Letter +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query @@ -22,20 +24,15 @@ interface LetterRepository : JpaRepository { @Query( """ - SELECT * - FROM letter l - WHERE l.couple_id = :coupleId - AND l.id <:key - ORDER BY l.created_at DESC - LIMIT :take + SELECT l FROM Letter l + WHERE l.coupleId = :coupleId + ORDER BY l.createdAt DESC """, - nativeQuery = true ) fun findAllToCouple( @Param("coupleId") coupleId: Long, - @Param("key") key: Long, - @Param("take") take: Long, - ): List + pageable: Pageable + ): Page fun findTop5ByCoupleIdOrderByCreatedAtDesc(coupleId: Long): List } From 8a9762f6e2bf83d0f9432e5f3d3be7f138411070 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 11 May 2025 23:29:15 +0900 Subject: [PATCH 263/357] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=8B=9C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EB=8F=99=EC=8B=9C?= =?UTF-8?q?=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20Lock=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/repository/CoupleRepository.kt | 13 +++++++++++-- .../backend/couple/domain/service/CoupleService.kt | 10 ++++++++++ .../gomushin/backend/couple/facade/CoupleFacade.kt | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt index 33cc7add..a122535f 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/CoupleRepository.kt @@ -1,14 +1,18 @@ package gomushin.backend.couple.domain.repository import gomushin.backend.couple.domain.entity.Couple +import jakarta.persistence.LockModeType +import jakarta.persistence.QueryHint import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query +import org.springframework.data.jpa.repository.QueryHints import org.springframework.data.repository.query.Param interface CoupleRepository : JpaRepository { - fun findByInvitorId(invitorId: Long) : Couple? - fun findByInviteeId(inviteeId: Long) : Couple? + fun findByInvitorId(invitorId: Long): Couple? + fun findByInviteeId(inviteeId: Long): Couple? @Query("SELECT c FROM Couple c WHERE c.invitorId = :memberId OR c.inviteeId = :memberId") fun findByMemberId(@Param("memberId") memberId: Long): Couple? @@ -16,4 +20,9 @@ interface CoupleRepository : JpaRepository { @Modifying @Query("DELETE FROM Couple c WHERE c.invitorId = :memberId OR c.inviteeId = :memberId") fun deleteByMemberId(@Param("memberId") memberId: Long) + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints(QueryHint(name = "javax.persistence.lock.timeout", value = "3000")) + @Query("SELECT c FROM Couple c WHERE c.id = :id") + fun findByIdWithLock(@Param("id") id: Long): Couple? } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt index a377adf1..db63ac86 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleService.kt @@ -16,6 +16,16 @@ class CoupleService( return findById(id) ?: throw BadRequestException("sarangggun.couple.not-found") } + @Transactional(readOnly = true) + fun getByIdWithLock(id: Long): Couple { + return findByIdWithLock(id) ?: throw BadRequestException("sarangggun.couple.not-found") + } + + @Transactional(readOnly = true) + fun findByIdWithLock(id: Long): Couple? { + return coupleRepository.findByIdWithLock(id) + } + @Transactional(readOnly = true) fun findById(id: Long): Couple? { return coupleRepository.findByIdOrNull(id) diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 0f4f819c..882a1e49 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -45,7 +45,7 @@ class CoupleFacade( customUserDetails: CustomUserDetails, request: CoupleAnniversaryRequest ) { - val couple = coupleService.getById(request.coupleId) + val couple = coupleService.getByIdWithLock(request.coupleId) checkUserInCouple(customUserDetails.getId(), couple) checkCoupleAnniversaryIsInit(couple) From 9f7fc4c6628935d5e6e8ad9e3808331b1a00e284 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 12 May 2025 21:26:14 +0900 Subject: [PATCH 264/357] =?UTF-8?q?fix:=20minio=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0f9b2532..0cc31548 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,11 +44,14 @@ services: environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + MINIO_SERVER_URL: ${MINIO_SERVER_URL} + MINIO_BROWSER_REDIRECT_URL: ${MINIO_BROWSER_REDIRECT_URL} + volumes: - minio_data:/data ports: - - "9000:9000" - - "9001:9001" + - "9002:9000" + - "9003:9001" networks: - sarang-backend-network restart: unless-stopped From 9cb211bb397317e7cd6ff6a0e09c548ced7864dc Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 12 May 2025 22:55:43 +0900 Subject: [PATCH 265/357] =?UTF-8?q?fix:=20JwtTokenProviderImpl=EB=A5=BC=20?= =?UTF-8?q?TokenService=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 --- .../security/SecurityConfiguration.kt | 8 ++++---- .../filter/JwtAuthenticationFilter.kt | 8 ++++---- .../gomushin/backend/core/jwt/JwtTokenProvider.kt | 7 ------- .../{JwtTokenProviderImpl.kt => TokenService.kt} | 15 +++++++-------- .../core/oauth/handler/CustomSuccessHandler.kt | 8 ++++---- 5 files changed, 19 insertions(+), 27 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt rename src/main/kotlin/gomushin/backend/core/jwt/infrastructure/{JwtTokenProviderImpl.kt => TokenService.kt} (80%) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index c1627eb7..23229b9b 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -2,7 +2,7 @@ package gomushin.backend.core.configuration.security import gomushin.backend.core.infrastructure.filter.CustomAuthenticationEntryPoint import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter -import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.jwt.infrastructure.TokenService import gomushin.backend.core.oauth.handler.CustomAccessDeniedHandler import gomushin.backend.core.oauth.handler.CustomSuccessHandler import gomushin.backend.core.oauth.service.CustomOAuth2UserService @@ -22,7 +22,7 @@ import org.springframework.web.cors.CorsUtils @Configuration @EnableWebSecurity class SecurityConfiguration( - private val jwtTokenProvider: JwtTokenProvider, + private val tokenService: TokenService, private val memberRepository: MemberRepository, @Value("\${redirect-url}") private val redirectUrl: String, @Value("\${cookie.domain}") private val cookieDomain: String @@ -61,7 +61,7 @@ class SecurityConfiguration( } .successHandler( CustomSuccessHandler( - jwtTokenProvider, + tokenService, memberRepository, redirectUrl, cookieDomain @@ -95,7 +95,7 @@ class SecurityConfiguration( } .addFilterBefore( JwtAuthenticationFilter( - jwtTokenProvider, + tokenService, CustomUserDetailsService( memberRepository, coupleRepository, diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt index 9c25fd5b..a16843a4 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt @@ -1,6 +1,6 @@ package gomushin.backend.core.infrastructure.filter -import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.jwt.infrastructure.TokenService import gomushin.backend.core.service.CustomUserDetailsService import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest @@ -12,7 +12,7 @@ import org.springframework.web.filter.OncePerRequestFilter @Component class JwtAuthenticationFilter( - private val tokenProvider: JwtTokenProvider, + private val tokenService: TokenService, private val customUserDetailsService: CustomUserDetailsService ) : OncePerRequestFilter() { @@ -40,7 +40,7 @@ class JwtAuthenticationFilter( val accessToken = getCookieValue(request, AT_IN_COOKIE) ?: getAccessTokenFromHeader(request) when { - accessToken != null && tokenProvider.validateToken(accessToken) -> { + accessToken != null && tokenService.validateToken(accessToken) -> { applyAuthentication(accessToken) } @@ -54,7 +54,7 @@ class JwtAuthenticationFilter( } private fun applyAuthentication(token: String) { - val userId = tokenProvider.getMemberIdFromToken(token) + val userId = tokenService.getMemberIdFromToken(token) val userDetails = customUserDetailsService.loadUserById(userId) val auth = UsernamePasswordAuthenticationToken( diff --git a/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt b/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt deleted file mode 100644 index dd6b927c..00000000 --- a/src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt +++ /dev/null @@ -1,7 +0,0 @@ -package gomushin.backend.core.jwt - -interface JwtTokenProvider { - fun provideAccessToken(userId: Long, role: String): String - fun getMemberIdFromToken(token: String): Long - fun validateToken(token: String): Boolean -} diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt similarity index 80% rename from src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt rename to src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt index 2ee97798..d1f3d5b8 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt @@ -1,7 +1,6 @@ package gomushin.backend.core.jwt.infrastructure -import gomushin.backend.core.jwt.JwtTokenProvider -import io.jsonwebtoken.* +import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger @@ -9,11 +8,11 @@ import org.springframework.stereotype.Component import java.util.* @Component -class JwtTokenProviderImpl( +class TokenService( jwtProperties: JwtProperties -) : JwtTokenProvider { +) { companion object { - private val logger: Logger = LogManager.getLogger(JwtTokenProviderImpl::class.java) + private val logger: Logger = LogManager.getLogger(TokenService::class.java) } val ISSUER = jwtProperties.issuer @@ -22,16 +21,16 @@ class JwtTokenProviderImpl( val ACCESS_TOKEN_EXPIRATION = jwtProperties.accessTokenExpiration val REFRESH_TOKEN_EXPIRATION = jwtProperties.refreshTokenExpiration - override fun provideAccessToken(userId: Long, role: String): String { + fun provideAccessToken(userId: Long, role: String): String { return createToken(userId, role, ACCESS_TOKEN_EXPIRATION, Type.ACCESS) } - override fun getMemberIdFromToken(token: String): Long { + fun getMemberIdFromToken(token: String): Long { val subject = getSubject(token) return subject.toLong() } - override fun validateToken(token: String): Boolean { + fun validateToken(token: String): Boolean { try { Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token) return true diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 34ef76a2..29229264 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -1,7 +1,7 @@ package gomushin.backend.core.oauth.handler import gomushin.backend.core.infrastructure.exception.BadRequestException -import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.jwt.infrastructure.TokenService import gomushin.backend.core.oauth.CustomOAuth2User import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository @@ -17,7 +17,7 @@ import org.springframework.stereotype.Component @Component class CustomSuccessHandler( - private val jwtTokenProvider: JwtTokenProvider, + private val tokenService: TokenService, private val memberRepository: MemberRepository, @Value("\${redirect-url}") private val redirectUrl: String, @Value("\${cookie.domain}") private val cookieDomain: String, @@ -37,9 +37,9 @@ class CustomSuccessHandler( var accessToken = "" getMemberByEmail(principal.getEmail())?.let { - accessToken = jwtTokenProvider.provideAccessToken(it.id, it.role.name) + accessToken = tokenService.provideAccessToken(it.id, it.role.name) } ?: run { - accessToken = jwtTokenProvider.provideAccessToken(principal.getUserId(), principal.getRole()) + accessToken = tokenService.provideAccessToken(principal.getUserId(), principal.getRole()) } val cookie = createCookie("access_token", accessToken) From d9d2e940afef6f8e6fd75c03eb98f3db348b3bca Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 12 May 2025 23:29:30 +0900 Subject: [PATCH 266/357] =?UTF-8?q?fix:=20s3=20file=20url=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 --- src/main/kotlin/gomushin/backend/core/service/S3Service.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt index 13f61ee4..81e87910 100644 --- a/src/main/kotlin/gomushin/backend/core/service/S3Service.kt +++ b/src/main/kotlin/gomushin/backend/core/service/S3Service.kt @@ -13,8 +13,8 @@ import java.util.* @Service class S3Service( private val s3Client: S3Client, - @Value("\${aws.s3.endpoint}") val endpoint: String, @Value("\${aws.s3.bucket}") private val bucket: String, + @Value("\${aws.s3.base-url}") private val baseUrl: String, ) { fun uploadFile(multipartFile: MultipartFile): String { @@ -42,7 +42,7 @@ class S3Service( } private fun getFileUrl(fileName: String): String { - val normalizedEndpoint = endpoint.removeSuffix("/") + val normalizedEndpoint = baseUrl.removeSuffix("/") return "$normalizedEndpoint/$bucket/$fileName" } From 72c4b604b7a02cdb29d6cea5280396ff6dd6d393 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 13 May 2025 20:42:53 +0900 Subject: [PATCH 267/357] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B9=85=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 --- .github/workflows/deploy.yml | 2 ++ .../backend/core/oauth/handler/CustomAccessDeniedHandler.kt | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 60e5b4cd..1711a21b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] + pull_request: + branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt index 0f960c47..1b350504 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt @@ -5,6 +5,7 @@ import gomushin.backend.core.common.web.response.ExtendedHttpStatus import gomushin.backend.core.common.web.response.exception.ErrorCodeResolvingApiErrorException import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import org.slf4j.LoggerFactory import org.springframework.http.MediaType import org.springframework.security.access.AccessDeniedException import org.springframework.security.web.access.AccessDeniedHandler @@ -13,6 +14,7 @@ import org.springframework.stereotype.Component @Component class CustomAccessDeniedHandler(private val objectMapper: ObjectMapper) : AccessDeniedHandler { + private val logger = LoggerFactory.getLogger(CustomAccessDeniedHandler::class.java) override fun handle( request: HttpServletRequest, response: HttpServletResponse, @@ -24,6 +26,8 @@ class CustomAccessDeniedHandler(private val objectMapper: ObjectMapper) : Access response.status = HttpServletResponse.SC_FORBIDDEN response.characterEncoding = "UTF-8" + // 정확히 어떤 이유로 발생한 에러인지 보려면 어떻게 해야하나 + logger.error("Access Denied: ${accessDeniedException.message}") val errorCode = if (request.requestURI.contains("/v1/member/onboarding")) { "sarangggun.auth.guest-only" From 8b3cb28922e6825aa37da56506655e4674845b17 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 13 May 2025 22:37:25 +0900 Subject: [PATCH 268/357] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=80,=20=EB=8C=93=EA=B8=80,=20=EC=82=AC=EC=A7=84=20?= =?UTF-8?q?=EC=A0=84=EB=B6=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/CommentRepository.kt | 4 ++++ .../domain/repository/PictureRepository.kt | 5 ++++- .../schedule/domain/service/CommentService.kt | 5 +++++ .../schedule/domain/service/LetterService.kt | 5 +++++ .../facade/UpsertAndDeleteScheduleFacade.kt | 21 +++++++++++++++++++ .../presentation/ReadLetterController.kt | 4 ++-- 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt index 381dfa7e..da5c45b4 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/CommentRepository.kt @@ -12,4 +12,8 @@ interface CommentRepository : JpaRepository { @Modifying @Query("DELETE FROM Comment c WHERE c.authorId = :authorId") fun deleteAllByAuthorId(@Param("authorId") authorId: Long) + + @Modifying + @Query("DELETE FROM Comment c WHERE c.letterId = :letterId") + fun deleteAllByLetterId(letterId: Long) } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt index 09e18afb..535d9e18 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/PictureRepository.kt @@ -8,10 +8,13 @@ import org.springframework.data.repository.query.Param interface PictureRepository : JpaRepository { fun findAllByPictureUrlIn(urls: List): List - fun deleteAllByLetterId(letterId: Long) fun findAllByLetterId(letterId: Long): List fun findFirstByLetterIdOrderByIdAsc(letterId: Long): Picture? + @Modifying + @Query("DELETE FROM Picture p WHERE p.letterId = :letterId") + fun deleteAllByLetterId(letterId: Long) + @Query("SELECT p FROM Picture p WHERE p.letterId IN :letterIds") fun findAllByLetterIdIn(@Param("letterIds") letterIds: List): List diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt index 4ec56a5b..5465ec5e 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt @@ -67,4 +67,9 @@ class CommentService( fun deleteAllByMemberId(memberId: Long) { commentRepository.deleteAllByAuthorId(memberId) } + + @Transactional + fun deleteAllByLetterId(letterId: Long) { + commentRepository.deleteAllByLetterId(letterId) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt index b25aec29..27ae3d15 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/LetterService.kt @@ -37,6 +37,11 @@ class LetterService( ) } + @Transactional(readOnly = true) + fun findByCoupleIdAndScheduleId(couple: Couple, scheduleId: Long): List { + return letterRepository.findByCoupleIdAndScheduleId(couple.id, scheduleId) + } + @Transactional(readOnly = true) fun getById(id: Long) = findById(id) ?: throw BadRequestException("sarangggun.letter.not-exist") diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt index ef562461..a7e9d751 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt @@ -1,13 +1,22 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.service.S3Service +import gomushin.backend.schedule.domain.service.CommentService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.request.UpsertScheduleRequest import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional @Component class UpsertAndDeleteScheduleFacade( private val scheduleService: ScheduleService, + private val letterService: LetterService, + private val commentService: CommentService, + private val pictureService: PictureService, + private val s3Service: S3Service, ) { fun upsert(customUserDetails: CustomUserDetails, upsertScheduleRequest: UpsertScheduleRequest) { @@ -19,7 +28,19 @@ class UpsertAndDeleteScheduleFacade( ) } + @Transactional fun delete(customUserDetails: CustomUserDetails, scheduleId: Long) { + val schedule = scheduleService.getById(scheduleId) + letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule).forEach { letter -> + pictureService.findAllByLetter(letter) + .takeIf { it.isNotEmpty() } + ?.forEach { picture -> + s3Service.deleteFile(picture.pictureUrl) + } + pictureService.deleteAllByLetterId(letter.id) + commentService.deleteAllByLetterId(letter.id) + letterService.delete(letter.id) + } scheduleService.delete(customUserDetails.getCouple().id, customUserDetails.getId(), scheduleId) } } diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index d6116d9d..dc3d3381 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -46,10 +46,10 @@ class ReadLetterController( @Operation(summary = "커플 전체 편지 리스트 가져오기", description = "getLetterListToMe") fun getLetterListToMe( @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @RequestParam(defaultValue = "0") page: Int, + @RequestParam(defaultValue = "1") page: Int, @RequestParam(defaultValue = "10") size: Int, ): PageResponse { - val letters = readLetterFacade.getLetterListToCouple(customUserDetails, page, size); + val letters = readLetterFacade.getLetterListToCouple(customUserDetails, page - 1, size); return letters } From 2a5212fa34dc7b00452021c8317f9fafc01c45e8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 13 May 2025 23:44:25 +0900 Subject: [PATCH 269/357] =?UTF-8?q?fix:=20=EC=8A=A4=EC=BC=80=EC=A5=B4=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EC=8B=9C=20=EB=94=94=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=EB=8F=84=20=ED=95=A8=EA=BB=98=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 -- .../dto/response/MonthlySchedulesAndAnniversariesResponse.kt | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1711a21b..60e5b4cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,6 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt index 647563e9..c21e2f42 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MonthlySchedulesAndAnniversariesResponse.kt @@ -4,14 +4,14 @@ import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse data class MonthlySchedulesAndAnniversariesResponse( val schedules: List, -// val anniversaries: List, + val anniversaries: List, ) { companion object { fun of( schedules: List, anniversaries: List, ): MonthlySchedulesAndAnniversariesResponse { - return MonthlySchedulesAndAnniversariesResponse(schedules) + return MonthlySchedulesAndAnniversariesResponse(schedules, anniversaries) } } } From dfd03262f57df1501d45df4666e799894f9bfcbf Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Tue, 13 May 2025 23:53:29 +0900 Subject: [PATCH 270/357] =?UTF-8?q?fix=20:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=EC=8B=9C=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/presentation/AnniversaryController.kt | 5 +++-- .../backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt | 1 + .../backend/schedule/presentation/ReadLetterController.kt | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt index 06c286f9..1eaab412 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt @@ -53,10 +53,11 @@ class AnniversaryController( ) fun getAnniversaryList( @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @RequestParam(defaultValue = "0") page: Int, + @RequestParam(defaultValue = "1") page: Int, @RequestParam(defaultValue = "10") size: Int, ): PageResponse { - val anniversaries = anniversaryFacade.getAnniversaryList(customUserDetails, page, size) + val safePage = if (page < 1) 0 else page - 1 + val anniversaries = anniversaryFacade.getAnniversaryList(customUserDetails, safePage, size) return anniversaries } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt index a7e9d751..ecb4a2e5 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt @@ -32,6 +32,7 @@ class UpsertAndDeleteScheduleFacade( fun delete(customUserDetails: CustomUserDetails, scheduleId: Long) { val schedule = scheduleService.getById(scheduleId) letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule).forEach { letter -> + // TODO: S3 삭제 로직 트랜잭션 커밋 이후로 분리 + 이벤트 발행으로 처리하기 pictureService.findAllByLetter(letter) .takeIf { it.isNotEmpty() } ?.forEach { picture -> diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt index dc3d3381..f50fd1d0 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadLetterController.kt @@ -49,7 +49,8 @@ class ReadLetterController( @RequestParam(defaultValue = "1") page: Int, @RequestParam(defaultValue = "10") size: Int, ): PageResponse { - val letters = readLetterFacade.getLetterListToCouple(customUserDetails, page - 1, size); + val safePage = if (page < 1) 0 else page - 1 + val letters = readLetterFacade.getLetterListToCouple(customUserDetails, safePage, size) return letters } From c3bac6cd853150ab849080284dcee13885bc9649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 16 May 2025 12:32:39 +0900 Subject: [PATCH 271/357] =?UTF-8?q?feat=20:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=89=AC=20=ED=86=A0=ED=81=B0=20=EB=8F=84=EC=9E=85(=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=8B=9C=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=89=AC=20=ED=86=A0=ED=81=B0=EB=8F=84=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=EC=97=90=20=EA=B0=99=EC=9D=B4=20=EB=B0=9C=EA=B8=89=ED=95=B4?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0)=20#90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/redis/RedisKey.kt | 6 +++++- .../core/configuration/redis/RedisService.kt | 6 ++++++ .../security/SecurityConfiguration.kt | 5 ++++- .../core/oauth/handler/CustomSuccessHandler.kt | 18 +++++++++++++++--- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisKey.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisKey.kt index e65ab376..c81f24de 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisKey.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisKey.kt @@ -3,11 +3,15 @@ package gomushin.backend.core.configuration.redis enum class RedisKey( private val tag : String ) { - ALARM("alarm:"); + ALARM("alarm:"), + REFRESH_TOKEN("refresh_token:"); companion object { fun getRedisAlarmKey(receiverId : Long) : String { return ALARM.tag + receiverId } + fun getRedisRefreshKey(refreshToken : String) : String { + return REFRESH_TOKEN.tag + refreshToken + } } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt index 0d8ac845..0402b2c8 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import gomushin.backend.alarm.dto.SaveAlarmMessage import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Service +import java.time.Duration import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -44,4 +45,9 @@ class RedisService ( return validAlarms } + + fun upsertRefresh(userId : Long, refreshToken : String) { + val key = RedisKey.getRedisRefreshKey(refreshToken) + redisTemplate.opsForValue().set(key, userId.toString(), Duration.ofDays(1)) + } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 23229b9b..842684ee 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -1,5 +1,6 @@ package gomushin.backend.core.configuration.security +import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.core.infrastructure.filter.CustomAuthenticationEntryPoint import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter import gomushin.backend.core.jwt.infrastructure.TokenService @@ -24,6 +25,7 @@ import org.springframework.web.cors.CorsUtils class SecurityConfiguration( private val tokenService: TokenService, private val memberRepository: MemberRepository, + private val redisService: RedisService, @Value("\${redirect-url}") private val redirectUrl: String, @Value("\${cookie.domain}") private val cookieDomain: String ) { @@ -63,8 +65,9 @@ class SecurityConfiguration( CustomSuccessHandler( tokenService, memberRepository, + redisService, + cookieDomain, redirectUrl, - cookieDomain ) ) } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 29229264..ada0f4a5 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -1,5 +1,6 @@ package gomushin.backend.core.oauth.handler +import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.core.jwt.infrastructure.TokenService import gomushin.backend.core.oauth.CustomOAuth2User @@ -19,6 +20,7 @@ import org.springframework.stereotype.Component class CustomSuccessHandler( private val tokenService: TokenService, private val memberRepository: MemberRepository, + private val redisService: RedisService, @Value("\${redirect-url}") private val redirectUrl: String, @Value("\${cookie.domain}") private val cookieDomain: String, ) : SimpleUrlAuthenticationSuccessHandler() { @@ -36,14 +38,17 @@ class CustomSuccessHandler( } var accessToken = "" + val refreshToken = makeRefreshToken() getMemberByEmail(principal.getEmail())?.let { accessToken = tokenService.provideAccessToken(it.id, it.role.name) } ?: run { accessToken = tokenService.provideAccessToken(principal.getUserId(), principal.getRole()) } - val cookie = createCookie("access_token", accessToken) - response!!.addHeader("Set-Cookie", cookie.toString()) + val accessCookie = createCookie("access_token", accessToken) + val refreshCookie = createCookie("refresh_token", refreshToken) + response!!.addHeader("Set-Cookie", accessCookie.toString()) + response.addHeader("Set-Cookie", refreshCookie.toString()) response.sendRedirect(redirectUrl) } @@ -53,7 +58,7 @@ class CustomSuccessHandler( .httpOnly(true) .secure(true) .sameSite("None") - .domain(cookieDomain) + .domain("localhost") //Todo push할때 cookie domain으로 바꾸기 .maxAge(432000) .build() } @@ -61,4 +66,11 @@ class CustomSuccessHandler( private fun getMemberByEmail(email: String): Member? { return memberRepository.findByEmail(email) } + + private fun makeRefreshToken() : String { + val chars = ('0'..'9') + ('a'..'z') + ('A'..'Z') + return (1..20) + .map { chars.random() } + .joinToString("") + } } From 0e6a9beba1b1216abb5e9aa796d643517de7b5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 16 May 2025 12:37:42 +0900 Subject: [PATCH 272/357] =?UTF-8?q?refactor=20:=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=89=AC=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C=20tokenSerive?= =?UTF-8?q?=EB=A1=9C=20=EC=98=AE=EA=B9=80=20#90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/jwt/infrastructure/TokenService.kt | 14 ++++++++++++++ .../core/oauth/handler/CustomSuccessHandler.kt | 8 +------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt index d1f3d5b8..e3d451c2 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt @@ -5,6 +5,7 @@ import io.jsonwebtoken.security.Keys import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import org.springframework.stereotype.Component +import java.time.Duration import java.util.* @Component @@ -39,6 +40,19 @@ class TokenService( } } + override fun provideRefreshToken() : String { + return createToken(0, "", REFRESH_TOKEN_EXPIRATION, Type.REFRESH) + } + + override fun getTokenDuration(token: String): Duration { + val now = Date(System.currentTimeMillis()) + return Duration.between(now.toInstant(), getTokenExpiration(token).toInstant()) + } + + private fun getTokenExpiration(token: String) : Date { + return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).payload.expiration + } + private fun createToken(userId: Long, role: String, expiration: Long, type: Type): String { val expirationMs = expiration * 60 * 1000 val expiryDate = Date(System.currentTimeMillis() + expirationMs) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index ada0f4a5..a6e53f20 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -38,7 +38,7 @@ class CustomSuccessHandler( } var accessToken = "" - val refreshToken = makeRefreshToken() + val refreshToken = jwtTokenProvider.provideRefreshToken() getMemberByEmail(principal.getEmail())?.let { accessToken = tokenService.provideAccessToken(it.id, it.role.name) } ?: run { @@ -67,10 +67,4 @@ class CustomSuccessHandler( return memberRepository.findByEmail(email) } - private fun makeRefreshToken() : String { - val chars = ('0'..'9') + ('a'..'z') + ('A'..'Z') - return (1..20) - .map { chars.random() } - .joinToString("") - } } From 8fbadb061ab7a2a868cb106d91fa2aa6c52d8cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 12 May 2025 03:47:38 +0900 Subject: [PATCH 273/357] =?UTF-8?q?refactor=20:=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=EB=A7=8C=EB=93=9C=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=94=B0=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EA=B4=80=EB=A6=AC(=ED=99=95=EC=9E=A5=EC=84=B1=20?= =?UTF-8?q?=EC=9C=84=ED=95=A8,=20reissueFacade=EC=97=90=EC=84=9C=EB=8F=84?= =?UTF-8?q?=20=EC=BF=A0=ED=82=A4=20=EB=A7=8C=EB=93=9C=EB=8A=94=EA=B2=8C=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=A8)=20#90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/cookie/CookieService.kt | 21 +++++++++++++++++++ .../security/SecurityConfiguration.kt | 7 ++++--- .../oauth/handler/CustomSuccessHandler.kt | 19 ++++------------- 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/core/configuration/cookie/CookieService.kt diff --git a/src/main/kotlin/gomushin/backend/core/configuration/cookie/CookieService.kt b/src/main/kotlin/gomushin/backend/core/configuration/cookie/CookieService.kt new file mode 100644 index 00000000..dd6d184a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/configuration/cookie/CookieService.kt @@ -0,0 +1,21 @@ +package gomushin.backend.core.configuration.cookie + +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.ResponseCookie +import org.springframework.stereotype.Service + +@Service +class CookieService( + @Value("\${cookie.domain}") private val cookieDomain: String +){ + fun createCookie(key: String, value: String): ResponseCookie { + return ResponseCookie.from(key, value) + .path("/") + .httpOnly(true) + .secure(true) + .sameSite("None") + .domain("localhost") //Todo push할때 cookie domain으로 바꾸기 + .maxAge(432000) + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 842684ee..82aaaae4 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -1,5 +1,6 @@ package gomushin.backend.core.configuration.security +import gomushin.backend.core.configuration.cookie.CookieService import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.core.infrastructure.filter.CustomAuthenticationEntryPoint import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter @@ -26,8 +27,8 @@ class SecurityConfiguration( private val tokenService: TokenService, private val memberRepository: MemberRepository, private val redisService: RedisService, - @Value("\${redirect-url}") private val redirectUrl: String, - @Value("\${cookie.domain}") private val cookieDomain: String + private val cookieService: CookieService, + @Value("\${redirect-url}") private val redirectUrl: String ) { @Bean @@ -66,7 +67,7 @@ class SecurityConfiguration( tokenService, memberRepository, redisService, - cookieDomain, + cookieService, redirectUrl, ) ) diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index a6e53f20..6a0522d0 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -1,5 +1,6 @@ package gomushin.backend.core.oauth.handler +import gomushin.backend.core.configuration.cookie.CookieService import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.core.jwt.infrastructure.TokenService @@ -11,7 +12,6 @@ import jakarta.servlet.ServletException import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.beans.factory.annotation.Value -import org.springframework.http.ResponseCookie import org.springframework.security.core.Authentication import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler import org.springframework.stereotype.Component @@ -21,8 +21,8 @@ class CustomSuccessHandler( private val tokenService: TokenService, private val memberRepository: MemberRepository, private val redisService: RedisService, + private val cookieService: CookieService, @Value("\${redirect-url}") private val redirectUrl: String, - @Value("\${cookie.domain}") private val cookieDomain: String, ) : SimpleUrlAuthenticationSuccessHandler() { @Throws(IOException::class, ServletException::class) @@ -45,24 +45,13 @@ class CustomSuccessHandler( accessToken = tokenService.provideAccessToken(principal.getUserId(), principal.getRole()) } - val accessCookie = createCookie("access_token", accessToken) - val refreshCookie = createCookie("refresh_token", refreshToken) + val accessCookie = cookieService.createCookie("access_token", accessToken) + val refreshCookie = cookieService.createCookie("refresh_token", refreshToken) response!!.addHeader("Set-Cookie", accessCookie.toString()) response.addHeader("Set-Cookie", refreshCookie.toString()) response.sendRedirect(redirectUrl) } - private fun createCookie(key: String, value: String): ResponseCookie { - return ResponseCookie.from(key, value) - .path("/") - .httpOnly(true) - .secure(true) - .sameSite("None") - .domain("localhost") //Todo push할때 cookie domain으로 바꾸기 - .maxAge(432000) - .build() - } - private fun getMemberByEmail(email: String): Member? { return memberRepository.findByEmail(email) } From 47597caf0096a02c53f3c20c85b52640ffcdf0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 12 May 2025 04:19:04 +0900 Subject: [PATCH 274/357] =?UTF-8?q?feat=20:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/auth/ApiPath.kt | 5 +++ .../backend/auth/ReissueController.kt | 25 +++++++++++++++ .../gomushin/backend/auth/ReissueFacade.kt | 32 +++++++++++++++++++ .../configuration/cookie/CookieService.kt | 2 +- .../core/configuration/redis/RedisService.kt | 17 ++++++++-- 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/auth/ApiPath.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/ReissueController.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt diff --git a/src/main/kotlin/gomushin/backend/auth/ApiPath.kt b/src/main/kotlin/gomushin/backend/auth/ApiPath.kt new file mode 100644 index 00000000..38bd8286 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/ApiPath.kt @@ -0,0 +1,5 @@ +package gomushin.backend.auth + +object ApiPath { + const val REISSUE = "/v1/auth/reissue" +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/auth/ReissueController.kt b/src/main/kotlin/gomushin/backend/auth/ReissueController.kt new file mode 100644 index 00000000..989fab44 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/ReissueController.kt @@ -0,0 +1,25 @@ +package gomushin.backend.auth + +import gomushin.backend.core.common.web.response.ApiResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + +@RestController +@Tag(name = "토큰 재발급", description = "ReissueController") +class ReissueController ( + private val reissueFacade: ReissueFacade +) { + @ResponseStatus(HttpStatus.CREATED) + @PostMapping(ApiPath.REISSUE) + @Operation(summary = "토큰 재발급", description = "reissue") + fun reissue( + @CookieValue("refresh_token") refreshToken: String, + response : HttpServletResponse + ): ApiResponse { + reissueFacade.reissue(refreshToken, response) + return ApiResponse.success(true) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt b/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt new file mode 100644 index 00000000..9331e23e --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt @@ -0,0 +1,32 @@ +package gomushin.backend.auth + +import gomushin.backend.core.configuration.cookie.CookieService +import gomushin.backend.core.configuration.redis.RedisKey +import gomushin.backend.core.configuration.redis.RedisService +import gomushin.backend.core.jwt.JwtTokenProvider +import gomushin.backend.core.jwt.infrastructure.JwtProperties +import gomushin.backend.member.domain.service.MemberService +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component + +@Component +class ReissueFacade( + private val redisService: RedisService, + private val jwtTokenProvider: JwtTokenProvider, + private val memberService: MemberService, + private val cookieService: CookieService +){ + fun reissue(refreshToken : String, response: HttpServletResponse) { + jwtTokenProvider.validateToken(refreshToken) + val userId = redisService.getRefreshTokenValue(refreshToken) + redisService.deleteRefreshToken(refreshToken) + val user = memberService.getById(userId) + val newAccessToken = jwtTokenProvider.provideAccessToken(userId, user.role.name) + val newRefreshToken = jwtTokenProvider.provideRefreshToken() + redisService.upsertRefresh(userId, newRefreshToken, jwtTokenProvider.getTokenDuration(newAccessToken)) + val accessCookie = cookieService.createCookie("access_token", newAccessToken) + val refreshCookie = cookieService.createCookie("refresh_token", newRefreshToken) + response.addHeader("Set-Cookie", accessCookie.toString()) + response.addHeader("Set-Cookie", refreshCookie.toString()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/configuration/cookie/CookieService.kt b/src/main/kotlin/gomushin/backend/core/configuration/cookie/CookieService.kt index dd6d184a..7df45496 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/cookie/CookieService.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/cookie/CookieService.kt @@ -14,7 +14,7 @@ class CookieService( .httpOnly(true) .secure(true) .sameSite("None") - .domain("localhost") //Todo push할때 cookie domain으로 바꾸기 + .domain(cookieDomain) .maxAge(432000) .build() } diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt index 0402b2c8..dda09015 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt @@ -2,6 +2,7 @@ package gomushin.backend.core.configuration.redis import com.fasterxml.jackson.databind.ObjectMapper import gomushin.backend.alarm.dto.SaveAlarmMessage +import gomushin.backend.core.infrastructure.exception.BadRequestException import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Service import java.time.Duration @@ -46,8 +47,20 @@ class RedisService ( return validAlarms } - fun upsertRefresh(userId : Long, refreshToken : String) { + fun upsertRefresh(userId : Long, refreshToken : String, duration: Duration) { val key = RedisKey.getRedisRefreshKey(refreshToken) - redisTemplate.opsForValue().set(key, userId.toString(), Duration.ofDays(1)) + redisTemplate.opsForValue().set(key, userId.toString(), duration) + } + + fun getRefreshTokenValue(refreshToken: String) : Long { + val key = RedisKey.getRedisRefreshKey(refreshToken) + val value = redisTemplate.opsForValue().get(key) + ?: throw BadRequestException("sarangggun.auth.unauth.refresh") + return value.toLong() + } + + fun deleteRefreshToken(refreshToken: String) { + val key = RedisKey.getRedisRefreshKey(refreshToken) + redisTemplate.delete(key) } } \ No newline at end of file From 2b4b978e914bec3103bc75de9116d49def28f1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 16 May 2025 12:42:50 +0900 Subject: [PATCH 275/357] =?UTF-8?q?refactor=20:=20refreshToken=20redis?= =?UTF-8?q?=EC=97=90=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20tokenSerive=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99=20#90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/auth/ReissueFacade.kt | 20 +++++++--------- .../core/configuration/redis/RedisService.kt | 19 --------------- .../security/SecurityConfiguration.kt | 3 --- .../core/jwt/infrastructure/TokenService.kt | 23 ++++++++++++++++++- .../oauth/handler/CustomSuccessHandler.kt | 4 ++-- 5 files changed, 32 insertions(+), 37 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt b/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt index 9331e23e..053f616a 100644 --- a/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt +++ b/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt @@ -1,29 +1,25 @@ package gomushin.backend.auth import gomushin.backend.core.configuration.cookie.CookieService -import gomushin.backend.core.configuration.redis.RedisKey -import gomushin.backend.core.configuration.redis.RedisService -import gomushin.backend.core.jwt.JwtTokenProvider -import gomushin.backend.core.jwt.infrastructure.JwtProperties +import gomushin.backend.core.jwt.infrastructure.TokenService import gomushin.backend.member.domain.service.MemberService import jakarta.servlet.http.HttpServletResponse import org.springframework.stereotype.Component @Component class ReissueFacade( - private val redisService: RedisService, - private val jwtTokenProvider: JwtTokenProvider, + private val tokenService: TokenService, private val memberService: MemberService, private val cookieService: CookieService ){ fun reissue(refreshToken : String, response: HttpServletResponse) { - jwtTokenProvider.validateToken(refreshToken) - val userId = redisService.getRefreshTokenValue(refreshToken) - redisService.deleteRefreshToken(refreshToken) + tokenService.validateToken(refreshToken) + val userId = tokenService.getRefreshTokenValue(refreshToken) + tokenService.deleteRefreshToken(refreshToken) val user = memberService.getById(userId) - val newAccessToken = jwtTokenProvider.provideAccessToken(userId, user.role.name) - val newRefreshToken = jwtTokenProvider.provideRefreshToken() - redisService.upsertRefresh(userId, newRefreshToken, jwtTokenProvider.getTokenDuration(newAccessToken)) + val newAccessToken = tokenService.provideAccessToken(userId, user.role.name) + val newRefreshToken = tokenService.provideRefreshToken() + tokenService.upsertRefresh(userId, newRefreshToken, tokenService.getTokenDuration(newAccessToken)) val accessCookie = cookieService.createCookie("access_token", newAccessToken) val refreshCookie = cookieService.createCookie("refresh_token", newRefreshToken) response.addHeader("Set-Cookie", accessCookie.toString()) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt index dda09015..0d8ac845 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt @@ -2,10 +2,8 @@ package gomushin.backend.core.configuration.redis import com.fasterxml.jackson.databind.ObjectMapper import gomushin.backend.alarm.dto.SaveAlarmMessage -import gomushin.backend.core.infrastructure.exception.BadRequestException import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Service -import java.time.Duration import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -46,21 +44,4 @@ class RedisService ( return validAlarms } - - fun upsertRefresh(userId : Long, refreshToken : String, duration: Duration) { - val key = RedisKey.getRedisRefreshKey(refreshToken) - redisTemplate.opsForValue().set(key, userId.toString(), duration) - } - - fun getRefreshTokenValue(refreshToken: String) : Long { - val key = RedisKey.getRedisRefreshKey(refreshToken) - val value = redisTemplate.opsForValue().get(key) - ?: throw BadRequestException("sarangggun.auth.unauth.refresh") - return value.toLong() - } - - fun deleteRefreshToken(refreshToken: String) { - val key = RedisKey.getRedisRefreshKey(refreshToken) - redisTemplate.delete(key) - } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 82aaaae4..5e493aa9 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -1,7 +1,6 @@ package gomushin.backend.core.configuration.security import gomushin.backend.core.configuration.cookie.CookieService -import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.core.infrastructure.filter.CustomAuthenticationEntryPoint import gomushin.backend.core.infrastructure.filter.JwtAuthenticationFilter import gomushin.backend.core.jwt.infrastructure.TokenService @@ -26,7 +25,6 @@ import org.springframework.web.cors.CorsUtils class SecurityConfiguration( private val tokenService: TokenService, private val memberRepository: MemberRepository, - private val redisService: RedisService, private val cookieService: CookieService, @Value("\${redirect-url}") private val redirectUrl: String ) { @@ -66,7 +64,6 @@ class SecurityConfiguration( CustomSuccessHandler( tokenService, memberRepository, - redisService, cookieService, redirectUrl, ) diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt index e3d451c2..c3d9ee2a 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt @@ -1,16 +1,20 @@ package gomushin.backend.core.jwt.infrastructure +import gomushin.backend.core.configuration.redis.RedisKey +import gomushin.backend.core.infrastructure.exception.BadRequestException import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger +import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Component import java.time.Duration import java.util.* @Component class TokenService( - jwtProperties: JwtProperties + jwtProperties: JwtProperties, + private val redisTemplate: StringRedisTemplate ) { companion object { private val logger: Logger = LogManager.getLogger(TokenService::class.java) @@ -72,4 +76,21 @@ class TokenService( fun getSubject(token: String): String { return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).payload.subject } + + fun upsertRefresh(userId : Long, refreshToken : String, duration: Duration) { + val key = RedisKey.getRedisRefreshKey(refreshToken) + redisTemplate.opsForValue().set(key, userId.toString(), duration) + } + + fun getRefreshTokenValue(refreshToken: String) : Long { + val key = RedisKey.getRedisRefreshKey(refreshToken) + val value = redisTemplate.opsForValue().get(key) + ?: throw BadRequestException("sarangggun.auth.unauth.refresh") + return value.toLong() + } + + fun deleteRefreshToken(refreshToken: String) { + val key = RedisKey.getRedisRefreshKey(refreshToken) + redisTemplate.delete(key) + } } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 6a0522d0..fed5c6d6 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -1,7 +1,6 @@ package gomushin.backend.core.oauth.handler import gomushin.backend.core.configuration.cookie.CookieService -import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.core.jwt.infrastructure.TokenService import gomushin.backend.core.oauth.CustomOAuth2User @@ -20,7 +19,6 @@ import org.springframework.stereotype.Component class CustomSuccessHandler( private val tokenService: TokenService, private val memberRepository: MemberRepository, - private val redisService: RedisService, private val cookieService: CookieService, @Value("\${redirect-url}") private val redirectUrl: String, ) : SimpleUrlAuthenticationSuccessHandler() { @@ -41,8 +39,10 @@ class CustomSuccessHandler( val refreshToken = jwtTokenProvider.provideRefreshToken() getMemberByEmail(principal.getEmail())?.let { accessToken = tokenService.provideAccessToken(it.id, it.role.name) + tokenService.upsertRefresh(it.id, refreshToken, tokenService.getTokenDuration(refreshToken)) } ?: run { accessToken = tokenService.provideAccessToken(principal.getUserId(), principal.getRole()) + tokenService.upsertRefresh(principal.getUserId(), refreshToken, tokenService.getTokenDuration(refreshToken)) } val accessCookie = cookieService.createCookie("access_token", accessToken) From 254a96d37c63c2548554a3d8ceb3bc97843d681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 15 May 2025 17:38:48 +0900 Subject: [PATCH 276/357] =?UTF-8?q?refactor=20:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=86=B5=EC=9D=BC=EA=B0=90=20?= =?UTF-8?q?=EC=9E=88=EA=B2=8C=20=EC=88=98=EC=A0=95(=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20=3D>=20invalid?= =?UTF-8?q?=EB=A1=9C)=20#90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/core/jwt/infrastructure/TokenService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt index c3d9ee2a..eb7ab17f 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt @@ -85,7 +85,7 @@ class TokenService( fun getRefreshTokenValue(refreshToken: String) : Long { val key = RedisKey.getRedisRefreshKey(refreshToken) val value = redisTemplate.opsForValue().get(key) - ?: throw BadRequestException("sarangggun.auth.unauth.refresh") + ?: throw BadRequestException("sarangggun.auth.invalid-refresh") return value.toLong() } From 1ec4e053ee262eb2728a8b1876bd109ea0159be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 16 May 2025 13:04:00 +0900 Subject: [PATCH 277/357] =?UTF-8?q?fix=20:=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95(?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20override=EC=97=B0?= =?UTF-8?q?=EC=82=B0=EC=9E=90,=20=EC=A7=80=EA=B8=88=EC=9D=80=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4(tokenProvider)=EC=82=AC=EC=9A=A9=ED=95=9C=EA=B1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95)=20#90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/core/jwt/infrastructure/TokenService.kt | 4 ++-- .../backend/core/oauth/handler/CustomSuccessHandler.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt index eb7ab17f..a7e65902 100644 --- a/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt +++ b/src/main/kotlin/gomushin/backend/core/jwt/infrastructure/TokenService.kt @@ -44,11 +44,11 @@ class TokenService( } } - override fun provideRefreshToken() : String { + fun provideRefreshToken() : String { return createToken(0, "", REFRESH_TOKEN_EXPIRATION, Type.REFRESH) } - override fun getTokenDuration(token: String): Duration { + fun getTokenDuration(token: String): Duration { val now = Date(System.currentTimeMillis()) return Duration.between(now.toInstant(), getTokenExpiration(token).toInstant()) } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index fed5c6d6..f277b19b 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -36,7 +36,7 @@ class CustomSuccessHandler( } var accessToken = "" - val refreshToken = jwtTokenProvider.provideRefreshToken() + val refreshToken = tokenService.provideRefreshToken() getMemberByEmail(principal.getEmail())?.let { accessToken = tokenService.provideAccessToken(it.id, it.role.name) tokenService.upsertRefresh(it.id, refreshToken, tokenService.getTokenDuration(refreshToken)) From ffecc6a697b44fa8b9e47d592119e358c5df02f8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 16 May 2025 18:00:41 +0900 Subject: [PATCH 278/357] =?UTF-8?q?fix=20:=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20=ED=95=98=EB=A3=A8?= =?UTF-8?q?=EC=A2=85=EC=9D=BC=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/ScheduleDetailResponse.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt index 36328f88..e2f2e921 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/ScheduleDetailResponse.kt @@ -4,22 +4,24 @@ import gomushin.backend.schedule.domain.entity.Schedule import java.time.LocalDateTime data class ScheduleDetailResponse( - val id : Long, - val title : String, - val fatigue : String, - val startDate : LocalDateTime, - val endDate : LocalDateTime, - val letters : List + val id: Long, + val title: String, + val fatigue: String, + val startDate: LocalDateTime, + val endDate: LocalDateTime, + val isAllDay: Boolean, + val letters: List, ) { companion object { - fun of(schedule : Schedule, letters: List) : ScheduleDetailResponse { + fun of(schedule: Schedule, letters: List): ScheduleDetailResponse { return ScheduleDetailResponse( schedule.id, schedule.title, schedule.fatigue, schedule.startDate, schedule.endDate, - letters + schedule.isAllDay, + letters, ) } } From a998d61db0e5e1058c8fb101c167d4986bfd6c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 16 May 2025 22:39:21 +0900 Subject: [PATCH 279/357] =?UTF-8?q?refactor=20:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20redis=EC=A0=80=EC=9E=A5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=94=B0=EB=A1=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=84=9C=20=EB=B6=84=EB=A6=AC=ED=95=98=EA=B8=B0=20#10?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt | 6 +++--- .../gomushin/backend/alarm/facade/QuestionAlarmFacade.kt | 6 +++--- .../service/NotificationRedisService.kt} | 5 +++-- .../gomushin/backend/alarm/service/StatusAlarmService.kt | 5 ++--- .../gomushin/backend/member/facade/MemberInfoFacade.kt | 6 +++--- .../gomushin/backend/member/facade/MemberInfoFacadeTest.kt | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) rename src/main/kotlin/gomushin/backend/{core/configuration/redis/RedisService.kt => alarm/service/NotificationRedisService.kt} (92%) diff --git a/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt index 1b45a097..dd77de2d 100644 --- a/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt @@ -1,7 +1,7 @@ package gomushin.backend.alarm.facade import gomushin.backend.alarm.service.FCMService -import gomushin.backend.core.configuration.redis.RedisService +import gomushin.backend.alarm.service.NotificationRedisService import gomushin.backend.couple.domain.service.AnniversaryService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -18,7 +18,7 @@ import java.time.LocalDateTime class DdayAlarmFacade( private val fcmService: FCMService, private val anniversaryService: AnniversaryService, - private val redisService: RedisService + private val notificationRedisService: NotificationRedisService ) { private val log: Logger = LoggerFactory.getLogger(QuestionAlarmFacade::class.java) private val alarmTitle = "오늘의 디데이가 도착했어요" @@ -32,7 +32,7 @@ class DdayAlarmFacade( async(Dispatchers.IO) { try { log.info("디데이 메시지 전송 : 수신자 {${content.memberId}}, 제목 {${alarmTitle}}, 내용{${content.title}}, 전송시각{${LocalDateTime.now()}}\n") - redisService.saveAlarm(alarmTitle, content.title, content.memberId) + notificationRedisService.saveAlarm(alarmTitle, content.title, content.memberId) fcmService.sendMessageTo(content.fcmToken, alarmTitle, content.title) } catch (e: Exception) { log.error("디데이 메시지 전송오류 : 수신자 {${content.memberId}}, 전송시각{${LocalDateTime.now()}}\n") diff --git a/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt index f0825583..d964d4ab 100644 --- a/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt @@ -2,7 +2,7 @@ package gomushin.backend.alarm.facade import gomushin.backend.alarm.service.FCMService import gomushin.backend.alarm.util.MessageParsingUtil -import gomushin.backend.core.configuration.redis.RedisService +import gomushin.backend.alarm.service.NotificationRedisService import gomushin.backend.member.domain.service.MemberService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -18,7 +18,7 @@ import java.time.LocalDateTime class QuestionAlarmFacade ( private val fcmService: FCMService, private val memberService: MemberService, - private val redisService: RedisService + private val notificationRedisService: NotificationRedisService ) { private val log: Logger = LoggerFactory.getLogger(QuestionAlarmFacade::class.java) private val questionMessages = listOf( @@ -38,7 +38,7 @@ class QuestionAlarmFacade ( val notificationContent = questionMessages.random() val (title, sendContent) = MessageParsingUtil.parse(notificationContent) log.info("질문형 메시지 전송 : 수신자 {${member.id}}, 제목 {${title}}, 내용{${sendContent}}, 전송시각{${LocalDateTime.now()}}\n") - redisService.saveAlarm(title, sendContent, member.id) + notificationRedisService.saveAlarm(title, sendContent, member.id) fcmService.sendMessageTo(member.fcmToken, title, sendContent) } catch (e: Exception) { log.error("질문형 메시지 전송오류 : 수신자 {${member.name}}, 전송시각{${LocalDateTime.now()}}\n") diff --git a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt b/src/main/kotlin/gomushin/backend/alarm/service/NotificationRedisService.kt similarity index 92% rename from src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt rename to src/main/kotlin/gomushin/backend/alarm/service/NotificationRedisService.kt index 0d8ac845..da635238 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/redis/RedisService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/NotificationRedisService.kt @@ -1,14 +1,15 @@ -package gomushin.backend.core.configuration.redis +package gomushin.backend.alarm.service import com.fasterxml.jackson.databind.ObjectMapper import gomushin.backend.alarm.dto.SaveAlarmMessage +import gomushin.backend.core.configuration.redis.RedisKey import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Service import java.time.LocalDateTime import java.time.format.DateTimeFormatter @Service -class RedisService ( +class NotificationRedisService ( private val redisTemplate: StringRedisTemplate, private val objectMapper: ObjectMapper ) { diff --git a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt index e0d8bb5c..d9aa0949 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt @@ -1,7 +1,6 @@ package gomushin.backend.alarm.service import gomushin.backend.alarm.util.MessageParsingUtil -import gomushin.backend.core.configuration.redis.RedisService import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.value.Emotion @@ -11,7 +10,7 @@ import org.springframework.stereotype.Service @Service class StatusAlarmService ( private val fcmService: FCMService, - private val redisService: RedisService + private val notificationRedisService: NotificationRedisService ) { private val statusMessage: Map> = mapOf( Emotion.MISS to listOf( @@ -56,7 +55,7 @@ class StatusAlarmService ( content } val token = receiver.fcmToken - redisService.saveAlarm(title, sendContent, receiver.id) + notificationRedisService.saveAlarm(title, sendContent, receiver.id) fcmService.sendMessageTo(token, title, sendContent) } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt index 0f83a3ac..025bb860 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/MemberInfoFacade.kt @@ -3,7 +3,7 @@ package gomushin.backend.member.facade import gomushin.backend.alarm.dto.SaveAlarmMessage import gomushin.backend.alarm.service.StatusAlarmService import gomushin.backend.core.CustomUserDetails -import gomushin.backend.core.configuration.redis.RedisService +import gomushin.backend.alarm.service.NotificationRedisService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.domain.service.NotificationService @@ -24,7 +24,7 @@ class MemberInfoFacade( private val notificationService: NotificationService, private val statusAlarmService: StatusAlarmService, private val coupleInfoService: CoupleInfoService, - private val redisService: RedisService + private val notificationRedisService: NotificationRedisService ) { fun getMemberInfo(customUserDetails: CustomUserDetails): MyInfoResponse { val member = memberService.getById(customUserDetails.getId()) @@ -71,6 +71,6 @@ class MemberInfoFacade( } fun getMyReceivedNotification(customUserDetails: CustomUserDetails, recentDays: Long): List { - return redisService.getAlarms(customUserDetails.getId(), recentDays) + return notificationRedisService.getAlarms(customUserDetails.getId(), recentDays) } } diff --git a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt index 4095eb94..092a2ee1 100644 --- a/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/member/facade/MemberInfoFacadeTest.kt @@ -2,7 +2,7 @@ package gomushin.backend.member.facade import gomushin.backend.alarm.service.StatusAlarmService import gomushin.backend.core.CustomUserDetails -import gomushin.backend.core.configuration.redis.RedisService +import gomushin.backend.alarm.service.NotificationRedisService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.entity.Notification @@ -37,7 +37,7 @@ class MemberInfoFacadeTest { @Mock private lateinit var coupleInfoService: CoupleInfoService @Mock - private lateinit var redisService: RedisService + private lateinit var notificationRedisService: NotificationRedisService @InjectMocks private lateinit var memberInfoFacade: MemberInfoFacade From d88bcb32096a0a0652d211876028133a723b9e29 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 18 May 2025 01:39:36 +0900 Subject: [PATCH 280/357] =?UTF-8?q?feat:=20mockK=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=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 --- build.gradle.kts | 3 +++ .../backend/core/oauth/handler/CustomAccessDeniedHandler.kt | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3af37697..302a0ef5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,8 @@ plugins { group = "gomushin" version = "0.0.1-SNAPSHOT" +val mockkVersion = "1.13.10" + java { toolchain { languageVersion = JavaLanguageVersion.of(17) @@ -83,6 +85,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("io.mockk:mockk:${mockkVersion}") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt index 1b350504..c4f08442 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt @@ -26,7 +26,6 @@ class CustomAccessDeniedHandler(private val objectMapper: ObjectMapper) : Access response.status = HttpServletResponse.SC_FORBIDDEN response.characterEncoding = "UTF-8" - // 정확히 어떤 이유로 발생한 에러인지 보려면 어떻게 해야하나 logger.error("Access Denied: ${accessDeniedException.message}") val errorCode = if (request.requestURI.contains("/v1/member/onboarding")) { From a8ba02a66b44a5df39c375c2c11578adf522c0ad Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 18 May 2025 01:40:09 +0900 Subject: [PATCH 281/357] =?UTF-8?q?feat:=20anniversaryService=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/couple/domain/service/AnniversaryService.kt | 6 +++--- .../gomushin/backend/couple/facade/AnniversaryFacade.kt | 3 --- .../gomushin/backend/schedule/facade/ReadScheduleFacade.kt | 6 +++--- .../schedule/domain/facade/ReadScheduleFacadeTest.kt | 4 ++-- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 33228145..7f881940 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -28,7 +28,7 @@ class AnniversaryService( } @Transactional(readOnly = true) - fun findByCoupleIdAndDateBetween( + fun findByCoupleAndDateBetween( couple: Couple, startDate: LocalDate, endDate: LocalDate @@ -37,12 +37,12 @@ class AnniversaryService( } @Transactional(readOnly = true) - fun findByCoupleIdAndYearAndMonth(couple: Couple, year: Int, month: Int): List { + fun findByCoupleAndYearAndMonth(couple: Couple, year: Int, month: Int): List { return anniversaryRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) } @Transactional(readOnly = true) - fun findByDate(couple: Couple, date: LocalDate): List { + fun findByCoupleAndDate(couple: Couple, date: LocalDate): List { return anniversaryRepository.findByCoupleIdAndDate(couple.id, date) } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt index e6dff894..1a4a5961 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -5,15 +5,12 @@ import gomushin.backend.core.common.web.PageResponse import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.dto.response.MainAnniversaryResponse import gomushin.backend.couple.dto.response.TotalAnniversaryResponse -import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Component @Component class AnniversaryFacade( private val anniversaryService: AnniversaryService, - @Value("\${server.url}") - private val baseUrl: String, ) { fun getAnniversaryListMain(customUserDetails: CustomUserDetails): List { val anniversaries = anniversaryService.getUpcomingTop3Anniversaries(customUserDetails.getCouple()) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index 9f2a917c..82c03481 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -24,13 +24,13 @@ class ReadScheduleFacade( ): MonthlySchedulesAndAnniversariesResponse { val monthlySchedules = scheduleService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) val monthlyAnniversaries = - anniversaryService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) + anniversaryService.findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month) return MonthlySchedulesAndAnniversariesResponse.of(monthlySchedules, monthlyAnniversaries) } fun get(customUserDetails: CustomUserDetails, date: LocalDate): DailySchedulesAndAnniversariesResponse { val dailySchedules = scheduleService.findByDate(customUserDetails.getCouple(), date) - val dailyAnniversaries = anniversaryService.findByDate(customUserDetails.getCouple(), date) + val dailyAnniversaries = anniversaryService.findByCoupleAndDate(customUserDetails.getCouple(), date) return DailySchedulesAndAnniversariesResponse.of(dailySchedules, dailyAnniversaries) } @@ -53,7 +53,7 @@ class ReadScheduleFacade( today, today.plusDays(6) ) - val anniversaries = anniversaryService.findByCoupleIdAndDateBetween( + val anniversaries = anniversaryService.findByCoupleAndDateBetween( customUserDetails.getCouple(), today, today.plusDays(6) diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt index 1cb38d35..aa7f613a 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt @@ -61,13 +61,13 @@ class ReadScheduleFacadeTest { // when `when`(scheduleService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month)) .thenReturn(listOf(monthlySchedulesResponse)) - `when`(anniversaryService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month)) + `when`(anniversaryService.findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month)) .thenReturn(listOf(monthlyAnniversariesResponse)) readScheduleFacade.getList(customUserDetails, year, month) // then verify(scheduleService, times(1)).findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) - verify(anniversaryService, times(1)).findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) + verify(anniversaryService, times(1)).findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month) } @DisplayName("get - 성공") From aed0ad2e92a5b9a5baa27ec1a645a2c2f1626775 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 18 May 2025 01:40:41 +0900 Subject: [PATCH 282/357] =?UTF-8?q?feat:=20AnniversaryFacadeTest,=20Annive?= =?UTF-8?q?rsaryServiceTest=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/AnniversaryServiceTest.kt | 164 ++++++++++++++++++ .../couple/facade/AnniversaryFacadeTest.kt | 88 ++++++++++ 2 files changed, 252 insertions(+) create mode 100644 src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt create mode 100644 src/test/kotlin/gomushin/backend/couple/facade/AnniversaryFacadeTest.kt diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt new file mode 100644 index 00000000..72e9707d --- /dev/null +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt @@ -0,0 +1,164 @@ +package gomushin.backend.couple.domain.service + +import gomushin.backend.couple.domain.entity.Anniversary +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.repository.AnniversaryRepository +import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse +import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse +import gomushin.backend.schedule.dto.response.MainAnniversariesResponse +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import java.time.LocalDate +import kotlin.test.assertTrue + +@ExtendWith(MockKExtension::class) +class AnniversaryServiceTest { + @MockK + lateinit var anniversaryRepository: AnniversaryRepository + + @InjectMockKs + lateinit var anniversaryService: AnniversaryService + + @DisplayName("findAnniversaries 는 Anniversary 엔티티를 Page 형태로 반환한다.") + @Test + fun findAnniversaries_success() { + // given + val couple = Couple(1L, 1L, 2L) + val pageRequest = PageRequest.of(0, 10) + val expectedPage = mockk>() + + every { + anniversaryRepository.findAnniversaries(couple.id, pageRequest) + } returns expectedPage + + // when + val result = anniversaryService.findAnniversaries(couple, pageRequest) + + // then + verify(exactly = 1) { + anniversaryRepository.findAnniversaries(couple.id, pageRequest) + } + assert(result == expectedPage) + } + + @DisplayName("findByCoupleAndDateBetween 는 Anniversary 엔티티를 List 형태로 반환한다.") + @Test + fun findByCoupleAndDateBetween_success() { + // given + val couple = Couple(1L, 1L, 2L) + val startDate = LocalDate.of(2025, 1, 1) + val endDate = LocalDate.of(2025, 12, 31) + val expectedList = listOf() + + every { + anniversaryRepository.findByCoupleIdAndDateBetween(couple.id, startDate, endDate) + } returns expectedList + + // when + val result = anniversaryService.findByCoupleAndDateBetween(couple, startDate, endDate) + + // then + verify(exactly = 1) { + anniversaryRepository.findByCoupleIdAndDateBetween(couple.id, startDate, endDate) + } + assert(result == expectedList) + } + + @DisplayName("findByCoupleAndYearAndMonth 는 Anniversary 엔티티를 List 형태로 반환한다.") + @Test + fun findByCoupleAndYearAndMonth_success() { + // given + val couple = Couple(1L, 1L, 2L) + val year = 2025 + val month = 1 + val expectedList = listOf() + + every { + anniversaryRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) + } returns expectedList + + // when + val result = anniversaryService.findByCoupleAndYearAndMonth(couple, year, month) + + // then + verify(exactly = 1) { + anniversaryRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) + } + assert(result == expectedList) + } + + @DisplayName("findByCoupleAndDate 는 Anniversary 엔티티를 List 형태로 반환한다.") + @Test + fun findByCoupleAndDate_success() { + // given + val couple = Couple(1L, 1L, 2L) + val date = LocalDate.of(2025, 1, 1) + val expectedList = listOf() + + every { + anniversaryRepository.findByCoupleIdAndDate(couple.id, date) + } returns expectedList + + // when + val result = anniversaryService.findByCoupleAndDate(couple, date) + + // then + verify(exactly = 1) { + anniversaryRepository.findByCoupleIdAndDate(couple.id, date) + } + assert(result == expectedList) + } + + @DisplayName("saveAll 은 Anniversary 엔티티를 List 형태로 저장한다.") + @Test + fun saveAll_success() { + // given + val anniversaries = listOf() + val expectedList = listOf() + every { + anniversaryRepository.saveAll(anniversaries) + } returns expectedList + + // when + val result = anniversaryService.saveAll(anniversaries) + + // then + verify(exactly = 1) { + anniversaryRepository.saveAll(anniversaries) + } + assert(result == expectedList) + } + + @DisplayName("getUpcomingTop3Anniversaries 는 Anniversary 엔티티를 List 형태로 반환하고, 최대 3개를 반환한다.") + @Test + fun getUpcomingTop3Anniversaries_success() { + // given + val couple = Couple(1L, 1L, 2L) + val expectedList = listOf( + Anniversary(1L, couple.id, "Anniversary 1", LocalDate.of(2025, 1, 1), 1), + Anniversary(2L, couple.id, "Anniversary 2", LocalDate.of(2025, 2, 1), 1), + Anniversary(3L, couple.id, "Anniversary 3", LocalDate.of(2025, 3, 1), 1), + Anniversary(4L, couple.id, "Anniversary 4", LocalDate.of(2025, 3, 1), 1) + ) + + every { + anniversaryRepository.findTop3UpcomingAnniversaries(couple.id) + } returns expectedList.take(3) + + // when + val result = anniversaryService.getUpcomingTop3Anniversaries(couple) + + // then + assertTrue(result.size <= 3, "반환된 기념일의 개수는 최대 3개여야 합니다.") + } +} + diff --git a/src/test/kotlin/gomushin/backend/couple/facade/AnniversaryFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/AnniversaryFacadeTest.kt new file mode 100644 index 00000000..1d675cd3 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/couple/facade/AnniversaryFacadeTest.kt @@ -0,0 +1,88 @@ +package gomushin.backend.couple.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.entity.Anniversary +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.schedule.dto.response.MainAnniversariesResponse +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import java.time.LocalDate + +@ExtendWith(MockKExtension::class) +class AnniversaryFacadeTest { + @MockK + lateinit var anniversaryService: AnniversaryService + + @InjectMockKs + lateinit var anniversaryFacade: AnniversaryFacade + + @DisplayName("getAnniversaryListMain 은 AnniversaryService 의 getUpcomingTop3Anniversaries 를 호출한다.") + @Test + fun getAnniversaryListMain_success() { + // given + val customUserDetails = mockk() + val couple = mockk() + val anniversaryList = listOf() + val anniversaryResponseList = listOf() + + every { + customUserDetails.getCouple() + } returns couple + + every { + anniversaryService.getUpcomingTop3Anniversaries(couple) + } returns anniversaryList + + // when + val result = anniversaryFacade.getAnniversaryListMain(customUserDetails) + + // then + verify(exactly = 1) { + anniversaryService.getUpcomingTop3Anniversaries(couple) + } + assert(result == anniversaryResponseList) + } + + @DisplayName("getAnniversaryList 은 AnniversaryService 의 findAnniversaries 를 호출한다.") + @Test + fun getAnniversaryList_success() { + // given + val customUserDetails = mockk() + val couple = mockk() + val page = 0 + val size = 10 + val pageRequest = PageRequest.of(page, size) + + val content = listOf( + Anniversary(1L, 1L, "Anniversary 1", LocalDate.of(2023, 10, 1), 1), + Anniversary(2L, 2L, "Anniversary 2", LocalDate.of(2023, 10, 2), 1) + ) + val anniversaries = PageImpl(content) + + every { customUserDetails.getCouple() } returns couple + every { + anniversaryService.findAnniversaries(couple, pageRequest) + } returns anniversaries + + // when + val result = anniversaryFacade.getAnniversaryList(customUserDetails, page, size) + + // then + verify(exactly = 1) { + anniversaryService.findAnniversaries(couple, pageRequest) + } + assertThat(result.data).hasSize(2) + assertThat(result.totalPages).isEqualTo(1) + } +} From 9f34ac937d197b1d60926bf2f3a5d366e02568c9 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 18 May 2025 16:39:45 +0900 Subject: [PATCH 283/357] =?UTF-8?q?fix:=20=EA=B2=80=EC=A6=9D=20=ED=9A=A8?= =?UTF-8?q?=EA=B3=BC=EA=B0=80=20=EC=97=86=EB=8A=94=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=9D=BC=EC=9D=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/AnniversaryServiceTest.kt | 44 ++++++++++++++----- .../couple/facade/AnniversaryFacadeTest.kt | 1 - 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt index 72e9707d..e34b6e58 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt @@ -18,6 +18,7 @@ import org.junit.jupiter.api.extension.ExtendWith import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import java.time.LocalDate +import kotlin.test.assertEquals import kotlin.test.assertTrue @ExtendWith(MockKExtension::class) @@ -41,13 +42,12 @@ class AnniversaryServiceTest { } returns expectedPage // when - val result = anniversaryService.findAnniversaries(couple, pageRequest) + anniversaryService.findAnniversaries(couple, pageRequest) // then verify(exactly = 1) { anniversaryRepository.findAnniversaries(couple.id, pageRequest) } - assert(result == expectedPage) } @DisplayName("findByCoupleAndDateBetween 는 Anniversary 엔티티를 List 형태로 반환한다.") @@ -64,13 +64,12 @@ class AnniversaryServiceTest { } returns expectedList // when - val result = anniversaryService.findByCoupleAndDateBetween(couple, startDate, endDate) + anniversaryService.findByCoupleAndDateBetween(couple, startDate, endDate) // then verify(exactly = 1) { anniversaryRepository.findByCoupleIdAndDateBetween(couple.id, startDate, endDate) } - assert(result == expectedList) } @DisplayName("findByCoupleAndYearAndMonth 는 Anniversary 엔티티를 List 형태로 반환한다.") @@ -87,13 +86,12 @@ class AnniversaryServiceTest { } returns expectedList // when - val result = anniversaryService.findByCoupleAndYearAndMonth(couple, year, month) + anniversaryService.findByCoupleAndYearAndMonth(couple, year, month) // then verify(exactly = 1) { anniversaryRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) } - assert(result == expectedList) } @DisplayName("findByCoupleAndDate 는 Anniversary 엔티티를 List 형태로 반환한다.") @@ -109,13 +107,12 @@ class AnniversaryServiceTest { } returns expectedList // when - val result = anniversaryService.findByCoupleAndDate(couple, date) + anniversaryService.findByCoupleAndDate(couple, date) // then verify(exactly = 1) { anniversaryRepository.findByCoupleIdAndDate(couple.id, date) } - assert(result == expectedList) } @DisplayName("saveAll 은 Anniversary 엔티티를 List 형태로 저장한다.") @@ -129,13 +126,12 @@ class AnniversaryServiceTest { } returns expectedList // when - val result = anniversaryService.saveAll(anniversaries) + anniversaryService.saveAll(anniversaries) // then verify(exactly = 1) { anniversaryRepository.saveAll(anniversaries) } - assert(result == expectedList) } @DisplayName("getUpcomingTop3Anniversaries 는 Anniversary 엔티티를 List 형태로 반환하고, 최대 3개를 반환한다.") @@ -160,5 +156,33 @@ class AnniversaryServiceTest { // then assertTrue(result.size <= 3, "반환된 기념일의 개수는 최대 3개여야 합니다.") } + + @DisplayName("getUpcomingTop3Anniversaries 는 Anniversary 엔티티를 List 형태로 반환하고, 시간 순으로 정렬한다.") + @Test + fun getUpcomingTop3Anniversaries_sortedByDate() { + // given + val couple = Couple(1L, 1L, 2L) + val expectedList = listOf( + Anniversary(1L, couple.id, "Anniversary 1", LocalDate.of(2025, 1, 1), 1), + Anniversary(2L, couple.id, "Anniversary 2", LocalDate.of(2025, 2, 1), 1), + Anniversary(3L, couple.id, "Anniversary 3", LocalDate.of(2025, 3, 1), 1), + Anniversary(4L, couple.id, "Anniversary 4", LocalDate.of(2025, 4, 1), 1), + ) + + every { + anniversaryRepository.findTop3UpcomingAnniversaries(couple.id) + } returns expectedList.take(3) + + // when + val result = anniversaryService.getUpcomingTop3Anniversaries(couple) + + // then + assertEquals(result, expectedList.take(3)) + assertEquals( + result.map { it.anniversaryDate }.sorted(), + result.map { it.anniversaryDate }, + "반환된 기념일은 시간 순으로 정렬되어야 합니다." + ) + } } diff --git a/src/test/kotlin/gomushin/backend/couple/facade/AnniversaryFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/AnniversaryFacadeTest.kt index 1d675cd3..7a3de726 100644 --- a/src/test/kotlin/gomushin/backend/couple/facade/AnniversaryFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/facade/AnniversaryFacadeTest.kt @@ -51,7 +51,6 @@ class AnniversaryFacadeTest { verify(exactly = 1) { anniversaryService.getUpcomingTop3Anniversaries(couple) } - assert(result == anniversaryResponseList) } @DisplayName("getAnniversaryList 은 AnniversaryService 의 findAnniversaries 를 호출한다.") From bcd0dca5005f134126b7587b5c4a92f124601b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 19 May 2025 00:26:10 +0900 Subject: [PATCH 284/357] =?UTF-8?q?fix=20:=20"/"=20api=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=A3=BC=EC=86=8C?= =?UTF-8?q?=EC=99=80=20=EA=B2=B9=EC=B9=A0=20=EC=9A=B0=EB=A0=A4=EC=9E=88?= =?UTF-8?q?=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/auth/MainController.kt | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/auth/MainController.kt diff --git a/src/main/kotlin/gomushin/backend/auth/MainController.kt b/src/main/kotlin/gomushin/backend/auth/MainController.kt deleted file mode 100644 index c313e101..00000000 --- a/src/main/kotlin/gomushin/backend/auth/MainController.kt +++ /dev/null @@ -1,16 +0,0 @@ -package gomushin.backend.auth - -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController - -@RestController -@RequestMapping("/") -class MainController { - - @GetMapping - fun index(): String { - return "곰신 서버" - } - -} From c91d4a41b0a8d03b3cd0b8ec0613f353a9937fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 21 May 2025 02:10:41 +0900 Subject: [PATCH 285/357] =?UTF-8?q?fix=20:=20=EC=82=AC=EA=B7=80=EA=B8=B0?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91=ED=95=9C=20=EB=82=A0=EC=A7=9C=EC=97=90=20?= =?UTF-8?q?+1=ED=95=B4=EC=A3=BC=EA=B8=B0=20#116?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/couple/domain/service/CoupleInfoService.kt | 2 +- .../backend/couple/domain/service/CoupleInfoServiceTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt index cbdfefab..9711a8ca 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleInfoService.kt @@ -59,7 +59,7 @@ class CoupleInfoService( ?: throw BadRequestException("saranggun.couple.not-connected") val today = LocalDate.now() val sinceLove: Int? = couple.relationshipStartDate?.let { startLove -> - computeDday(startLove, today) + computeDday(startLove, today) + 1 } val sinceMilitaryStart : Int? = couple.militaryStartDate?.let { startMilitary -> computeDday(startMilitary, today) diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index 92347b35..1461a28f 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -216,7 +216,7 @@ class CoupleInfoServiceTest { //then assertEquals(coupleInfoService.computeDday(militaryStartDate, today), response.sinceMilitaryStart) assertEquals(coupleInfoService.computeDday(militaryEndDate, today), response.militaryEndLeft) - assertEquals(coupleInfoService.computeDday(relationshipStartDate, today), response.sinceLove) + assertEquals(coupleInfoService.computeDday(relationshipStartDate, today) + 1, response.sinceLove) } @DisplayName("nickName-성공") From eeb52607ff487156bd27521158533286ae275568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Fri, 23 May 2025 16:08:17 +0900 Subject: [PATCH 286/357] =?UTF-8?q?chore=20:=20cors=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80(=EB=8F=84=EB=A9=94=EC=9D=B8=20sa?= =?UTF-8?q?rangkkun.site=EC=83=88=EB=A1=9C=20=EC=82=AC=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/CustomCorsConfiguration.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index 9d68aa98..7713bb4b 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -20,6 +20,7 @@ class CustomCorsConfiguration { "https://frontend-sarang.vercel.app", "https://vite.sarang-backend.o-r.kr:5173", "https://sarang-backend.o-r.kr", + "https://www.sarangkkun.site" ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") From ca3e00fdafe34e5382e810725702c95a84e768c2 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Fri, 23 May 2025 01:24:34 +0900 Subject: [PATCH 287/357] =?UTF-8?q?feat:=20=EB=94=94=EB=8D=B0=EC=9D=B4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/AnniversaryService.kt | 19 +++++++++++++++++++ .../couple/facade/AnniversaryFacade.kt | 4 ++++ .../presentation/AnniversaryController.kt | 14 ++++++++++++++ .../backend/couple/presentation/ApiPath.kt | 1 + 4 files changed, 38 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 7f881940..e06e5559 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -1,5 +1,6 @@ package gomushin.backend.couple.domain.service +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository @@ -10,6 +11,7 @@ import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import gomushin.backend.schedule.dto.response.MainAnniversariesResponse import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @@ -19,6 +21,14 @@ class AnniversaryService( private val anniversaryRepository: AnniversaryRepository, ) { + @Transactional(readOnly = true) + fun getById(id: Long): Anniversary { + return findById(id) ?: throw BadRequestException("sarangggun.anniversary.not-found") + } + + @Transactional(readOnly = true) + fun findById(id: Long) = anniversaryRepository.findByIdOrNull(id) + @Transactional(readOnly = true) fun findAnniversaries(couple: Couple, pageRequest: PageRequest): Page { return anniversaryRepository.findAnniversaries( @@ -77,4 +87,13 @@ class AnniversaryService( fun getTodayAnniversaryMemberFcmTokens(date: LocalDate): List { return anniversaryRepository.findTodayAnniversaryMemberFcmTokens(date) } + + @Transactional + fun delete(couple: Couple, anniversaryId: Long) { + val anniversary = getById(anniversaryId) + if (anniversary.coupleId != couple.id) { + throw BadRequestException("sarangggun.anniversary.unauthorized") + } + anniversaryRepository.deleteById(anniversaryId) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt index 1a4a5961..d9c8a489 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -31,4 +31,8 @@ class AnniversaryFacade( return PageResponse.from(anniversaryResponses) } + + fun delete(customUserDetails: CustomUserDetails, anniversaryId: Long) { + anniversaryService.delete(customUserDetails.getCouple(), anniversaryId) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt index 1eaab412..0fb1afc1 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt @@ -34,6 +34,20 @@ class AnniversaryController( return ApiResponse.success(true) } + @ResponseStatus(HttpStatus.OK) + @DeleteMapping(ApiPath.ANNIVERSARY) + @Operation( + summary = "기념일 삭제", + description = "deleteAnniversary" + ) + fun deleteAnniversary( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable anniversaryId: Long + ): ApiResponse { + anniversaryFacade.delete(customUserDetails, anniversaryId) + return ApiResponse.success(true) + } + @ResponseStatus(HttpStatus.OK) @GetMapping(ApiPath.ANNIVERSARY_MAIN) @Operation( diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt index 71854751..8eaf5569 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/ApiPath.kt @@ -16,5 +16,6 @@ object ApiPath { const val ANNIVERSARY_GENERATE = "/v1/couple/new-anniversary" const val ANNIVERSARY_MAIN = "/v1/anniversary/main" const val ANNIVERSARIES = "/v1/anniversaries" + const val ANNIVERSARY = "/v1/anniversary/{anniversaryId}" const val COUPLE_BIRTHDAY = "/v1/couple/birthday" } From 58f4ecb7e485e5e20fbaa7b803b8251cafec15af Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 24 May 2025 14:19:17 +0900 Subject: [PATCH 288/357] =?UTF-8?q?Feat:=20=EC=9C=A0=EC=A0=80=20Role=20?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=20=EC=A3=BC=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/CustomCorsConfiguration.kt | 3 +- .../security/SecurityConfiguration.kt | 6 ++-- .../oauth/handler/CustomSuccessHandler.kt | 28 +++++++++++++++---- .../backend/member/domain/value/Role.kt | 11 +++++++- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index 7713bb4b..d3682eb0 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -20,7 +20,8 @@ class CustomCorsConfiguration { "https://frontend-sarang.vercel.app", "https://vite.sarang-backend.o-r.kr:5173", "https://sarang-backend.o-r.kr", - "https://www.sarangkkun.site" + "https://www.sarangkkun.site", + "https://sarangkkun.site", ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 5e493aa9..e852bb3e 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -26,7 +26,8 @@ class SecurityConfiguration( private val tokenService: TokenService, private val memberRepository: MemberRepository, private val cookieService: CookieService, - @Value("\${redirect-url}") private val redirectUrl: String + @Value("\${member-redirect-url}") private val memberRedirectUrl: String, + @Value("\${guest-redirect-url}") private val guestRedirectUrl: String, ) { @Bean @@ -65,7 +66,8 @@ class SecurityConfiguration( tokenService, memberRepository, cookieService, - redirectUrl, + memberRedirectUrl, + guestRedirectUrl, ) ) } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index f277b19b..3841d51b 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -6,6 +6,7 @@ import gomushin.backend.core.jwt.infrastructure.TokenService import gomushin.backend.core.oauth.CustomOAuth2User import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository +import gomushin.backend.member.domain.value.Role import io.jsonwebtoken.io.IOException import jakarta.servlet.ServletException import jakarta.servlet.http.HttpServletRequest @@ -20,7 +21,8 @@ class CustomSuccessHandler( private val tokenService: TokenService, private val memberRepository: MemberRepository, private val cookieService: CookieService, - @Value("\${redirect-url}") private val redirectUrl: String, + @Value("\${member-redirect-url}") private val memberRedirectUrl: String, + @Value("\${guest-redirect-url}") private val guestRedirectUrl: String, ) : SimpleUrlAuthenticationSuccessHandler() { @Throws(IOException::class, ServletException::class) @@ -45,11 +47,25 @@ class CustomSuccessHandler( tokenService.upsertRefresh(principal.getUserId(), refreshToken, tokenService.getTokenDuration(refreshToken)) } - val accessCookie = cookieService.createCookie("access_token", accessToken) - val refreshCookie = cookieService.createCookie("refresh_token", refreshToken) - response!!.addHeader("Set-Cookie", accessCookie.toString()) - response.addHeader("Set-Cookie", refreshCookie.toString()) - response.sendRedirect(redirectUrl) + val member = getMemberByEmail(principal.getEmail()) + + when { + member == null -> { + response!!.sendRedirect(guestRedirectUrl) + } + + member.role == Role.GUEST -> { + response!!.sendRedirect(guestRedirectUrl) + } + + else -> { + val accessCookie = cookieService.createCookie("access_token", accessToken) + val refreshCookie = cookieService.createCookie("refresh_token", refreshToken) + response!!.addHeader("Set-Cookie", accessCookie.toString()) + response.addHeader("Set-Cookie", refreshCookie.toString()) + response.sendRedirect(memberRedirectUrl) + } + } } private fun getMemberByEmail(email: String): Member? { diff --git a/src/main/kotlin/gomushin/backend/member/domain/value/Role.kt b/src/main/kotlin/gomushin/backend/member/domain/value/Role.kt index 7225670e..41c7fa5d 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/value/Role.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/value/Role.kt @@ -1,6 +1,15 @@ package gomushin.backend.member.domain.value +import gomushin.backend.core.infrastructure.exception.BadRequestException + enum class Role { GUEST, - MEMBER, + MEMBER; + + companion object { + fun getByName(name: String): Role { + return entries.firstOrNull { it.name == name } + ?: throw BadRequestException("sarangggun.member.not-exist-role") + } + } } From 7651e35e9cd10f51ec59c8e746baf7ad5ee1f7e6 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 24 May 2025 14:53:45 +0900 Subject: [PATCH 289/357] =?UTF-8?q?fix:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ .../backend/core/oauth/handler/CustomSuccessHandler.kt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 60e5b4cd..1711a21b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,6 +3,8 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] + pull_request: + branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 3841d51b..c6674f12 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -49,6 +49,9 @@ class CustomSuccessHandler( val member = getMemberByEmail(principal.getEmail()) + println("member: ${member?.name}") + println("member.role: ${member?.role}") + when { member == null -> { response!!.sendRedirect(guestRedirectUrl) From 05510fc272a5088fd5c0f25eaaff222fa0d4ff86 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 24 May 2025 15:10:35 +0900 Subject: [PATCH 290/357] =?UTF-8?q?fix:=20my-info=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=ED=92=80=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SecurityConfiguration.kt | 5 ++-- .../handler/CustomAccessDeniedHandler.kt | 1 + .../oauth/handler/CustomSuccessHandler.kt | 28 ++++--------------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index e852bb3e..4e429db9 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -78,6 +78,7 @@ class SecurityConfiguration( .authorizeHttpRequests { it.requestMatchers( "/", + "/v1/member/my-info", "/v1/auth/**", "/v1/oauth/**", "/oauth2/**", @@ -90,9 +91,9 @@ class SecurityConfiguration( "/health", "/swagger-ui/index.html", "/favicon.ico", - "/error" + "/error", ).permitAll() - it.requestMatchers(CorsUtils::isPreFlightRequest).permitAll() + it.requestMatchers(CorsUtils::isPreFlightRequest,).permitAll() it.requestMatchers("/v1/member/onboarding").hasRole("GUEST") it.anyRequest().hasRole("MEMBER") } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt index c4f08442..eee9a876 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomAccessDeniedHandler.kt @@ -27,6 +27,7 @@ class CustomAccessDeniedHandler(private val objectMapper: ObjectMapper) : Access response.characterEncoding = "UTF-8" logger.error("Access Denied: ${accessDeniedException.message}") + accessDeniedException.printStackTrace() val errorCode = if (request.requestURI.contains("/v1/member/onboarding")) { "sarangggun.auth.guest-only" diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index c6674f12..5ee37b3f 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -39,6 +39,7 @@ class CustomSuccessHandler( var accessToken = "" val refreshToken = tokenService.provideRefreshToken() + getMemberByEmail(principal.getEmail())?.let { accessToken = tokenService.provideAccessToken(it.id, it.role.name) tokenService.upsertRefresh(it.id, refreshToken, tokenService.getTokenDuration(refreshToken)) @@ -47,28 +48,11 @@ class CustomSuccessHandler( tokenService.upsertRefresh(principal.getUserId(), refreshToken, tokenService.getTokenDuration(refreshToken)) } - val member = getMemberByEmail(principal.getEmail()) - - println("member: ${member?.name}") - println("member.role: ${member?.role}") - - when { - member == null -> { - response!!.sendRedirect(guestRedirectUrl) - } - - member.role == Role.GUEST -> { - response!!.sendRedirect(guestRedirectUrl) - } - - else -> { - val accessCookie = cookieService.createCookie("access_token", accessToken) - val refreshCookie = cookieService.createCookie("refresh_token", refreshToken) - response!!.addHeader("Set-Cookie", accessCookie.toString()) - response.addHeader("Set-Cookie", refreshCookie.toString()) - response.sendRedirect(memberRedirectUrl) - } - } + val accessCookie = cookieService.createCookie("access_token", accessToken) + val refreshCookie = cookieService.createCookie("refresh_token", refreshToken) + response!!.addHeader("Set-Cookie", accessCookie.toString()) + response.addHeader("Set-Cookie", refreshCookie.toString()) + response.sendRedirect(memberRedirectUrl) } private fun getMemberByEmail(email: String): Member? { From 47c5f3838544f565779ca2df34ce1c14e8c52f5e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 24 May 2025 15:36:13 +0900 Subject: [PATCH 291/357] =?UTF-8?q?fix:=20my-info=20filter=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 -- .../core/configuration/security/SecurityConfiguration.kt | 2 -- .../gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1711a21b..60e5b4cd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,6 @@ name: Sarang Backend CI/CD on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: ci: diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt index 4e429db9..9a23186b 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt @@ -27,7 +27,6 @@ class SecurityConfiguration( private val memberRepository: MemberRepository, private val cookieService: CookieService, @Value("\${member-redirect-url}") private val memberRedirectUrl: String, - @Value("\${guest-redirect-url}") private val guestRedirectUrl: String, ) { @Bean @@ -67,7 +66,6 @@ class SecurityConfiguration( memberRepository, cookieService, memberRedirectUrl, - guestRedirectUrl, ) ) } diff --git a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt index 5ee37b3f..a920c36b 100644 --- a/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt @@ -22,7 +22,6 @@ class CustomSuccessHandler( private val memberRepository: MemberRepository, private val cookieService: CookieService, @Value("\${member-redirect-url}") private val memberRedirectUrl: String, - @Value("\${guest-redirect-url}") private val guestRedirectUrl: String, ) : SimpleUrlAuthenticationSuccessHandler() { @Throws(IOException::class, ServletException::class) @@ -50,6 +49,7 @@ class CustomSuccessHandler( val accessCookie = cookieService.createCookie("access_token", accessToken) val refreshCookie = cookieService.createCookie("refresh_token", refreshToken) + response!!.addHeader("Set-Cookie", accessCookie.toString()) response.addHeader("Set-Cookie", refreshCookie.toString()) response.sendRedirect(memberRedirectUrl) From 993738d01eeffca82d82013ef5be62f5e3ad302d Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 25 May 2025 12:25:37 +0900 Subject: [PATCH 292/357] =?UTF-8?q?fix:=20=EB=A1=9C=EC=BB=AC=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20cors=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/configuration/security/CustomCorsConfiguration.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt index d3682eb0..c87f431b 100644 --- a/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt +++ b/src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt @@ -22,6 +22,7 @@ class CustomCorsConfiguration { "https://sarang-backend.o-r.kr", "https://www.sarangkkun.site", "https://sarangkkun.site", + "https://vite.sarangkkun.site:5173", ) configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowedHeaders = listOf("*") From d7e3c30c34ebaf20b3218c258807f4c53cc7fcae Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 21 May 2025 23:29:10 +0900 Subject: [PATCH 293/357] =?UTF-8?q?refactor:=20ScheduleService=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=AA=85=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/member/facade/LeaveFacade.kt | 4 ++-- .../backend/schedule/domain/service/ScheduleService.kt | 6 +++--- .../backend/schedule/facade/ReadScheduleFacade.kt | 6 +++--- .../schedule/presentation/ReadScheduleController.kt | 4 ++-- .../schedule/domain/facade/ReadScheduleFacadeTest.kt | 10 +++++----- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt index 703690c4..0ea2d9df 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -36,7 +36,7 @@ class LeaveFacade( commentService.deleteAllByMemberId(memberId) coupleService.deleteByMemberId(memberId) notificationService.deleteAllByMember(memberId) - scheduleService.deleteAllByMember(memberId) + scheduleService.deleteAllByMemberId(memberId) val letters = letterService.findAllByAuthorId(memberId) pictureService.findAllByLetterIds(letters) @@ -51,4 +51,4 @@ class LeaveFacade( partner.updateIsCouple(false) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt index c8c58270..dbfc1ce1 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/ScheduleService.kt @@ -18,12 +18,12 @@ class ScheduleService( private val scheduleRepository: ScheduleRepository, ) { @Transactional(readOnly = true) - fun findByCoupleIdAndYearAndMonth(couple: Couple, year: Int, month: Int): List { + fun findByCoupleAndYearAndMonth(couple: Couple, year: Int, month: Int): List { return scheduleRepository.findByCoupleIdAndYearAndMonth(couple.id, year, month) } @Transactional(readOnly = true) - fun findByCoupleIdAndDateBetween( + fun findByCoupleAndDateBetween( couple: Couple, startDate: LocalDate, endDate: LocalDate @@ -72,7 +72,7 @@ class ScheduleService( } @Transactional - fun deleteAllByMember(memberId: Long) { + fun deleteAllByMemberId(memberId: Long) { scheduleRepository.deleteAllByUserId(memberId) } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index 82c03481..ebd32f53 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -22,7 +22,7 @@ class ReadScheduleFacade( year: Int, month: Int ): MonthlySchedulesAndAnniversariesResponse { - val monthlySchedules = scheduleService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) + val monthlySchedules = scheduleService.findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month) val monthlyAnniversaries = anniversaryService.findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month) return MonthlySchedulesAndAnniversariesResponse.of(monthlySchedules, monthlyAnniversaries) @@ -34,7 +34,7 @@ class ReadScheduleFacade( return DailySchedulesAndAnniversariesResponse.of(dailySchedules, dailyAnniversaries) } - fun getScheduleDetail(customUserDetails: CustomUserDetails, scheduleId: Long): ScheduleDetailResponse { + fun getDetail(customUserDetails: CustomUserDetails, scheduleId: Long): ScheduleDetailResponse { val schedule = scheduleService.getById(scheduleId) val letters = letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule) val letterIds = letters.map { it.id } @@ -48,7 +48,7 @@ class ReadScheduleFacade( fun getListByWeek(customUserDetails: CustomUserDetails): MainSchedulesAndAnniversariesResponse { val today = LocalDate.now() - val schedules = scheduleService.findByCoupleIdAndDateBetween( + val schedules = scheduleService.findByCoupleAndDateBetween( customUserDetails.getCouple(), today, today.plusDays(6) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt index 3bddc821..be00f69d 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/ReadScheduleController.kt @@ -44,12 +44,12 @@ class ReadScheduleController( } @GetMapping(ApiPath.SCHEDULE_DETAIL) - @Operation(summary = "특정 스케쥴 상세조회", description = "getScheduleDetail") + @Operation(summary = "특정 스케쥴 상세조회", description = "getDetail") fun getScheduleDetail( @AuthenticationPrincipal customUserDetails: CustomUserDetails, @PathVariable scheduleId: Long ): ApiResponse { - val scheduleDetails = readScheduleFacade.getScheduleDetail(customUserDetails, scheduleId) + val scheduleDetails = readScheduleFacade.getDetail(customUserDetails, scheduleId) return ApiResponse.success(scheduleDetails) } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt index aa7f613a..a68b9ce0 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt @@ -59,14 +59,14 @@ class ReadScheduleFacadeTest { val monthlyAnniversariesResponse = mock(MonthlyAnniversariesResponse::class.java) // when - `when`(scheduleService.findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month)) + `when`(scheduleService.findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month)) .thenReturn(listOf(monthlySchedulesResponse)) `when`(anniversaryService.findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month)) .thenReturn(listOf(monthlyAnniversariesResponse)) readScheduleFacade.getList(customUserDetails, year, month) // then - verify(scheduleService, times(1)).findByCoupleIdAndYearAndMonth(customUserDetails.getCouple(), year, month) + verify(scheduleService, times(1)).findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month) verify(anniversaryService, times(1)).findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month) } @@ -85,9 +85,9 @@ class ReadScheduleFacadeTest { verify(scheduleService, times(1)).findByDate(customUserDetails.getCouple(), date) } - @DisplayName("getScheduleDetail - 성공") + @DisplayName("getDetail - 성공") @Test - fun getScheduleDetail_success() { + fun getDetail_success() { //given val scheduleId = 1L val letterId = 2L @@ -109,7 +109,7 @@ class ReadScheduleFacadeTest { `when`(letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule)).thenReturn(listOf(mockLetter)) `when`(pictureService.findAllByLetterIds(listOf(letterId))).thenReturn(listOf(mockPicture)) - readScheduleFacade.getScheduleDetail(customUserDetails, scheduleId) + readScheduleFacade.getDetail(customUserDetails, scheduleId) //then verify(scheduleService).getById(scheduleId) From 912ba6b1ba0d5589a78e80610a2577d95800bda8 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 21 May 2025 23:30:53 +0900 Subject: [PATCH 294/357] =?UTF-8?q?refactor:=20=EC=9D=BC=EC=A3=BC=EC=9D=BC?= =?UTF-8?q?=20=EA=B3=84=EC=82=B0=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A7=A4?= =?UTF-8?q?=EC=A7=81=EB=84=98=EB=B2=84=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/facade/ReadScheduleFacade.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index ebd32f53..e3e6947f 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -17,6 +17,10 @@ class ReadScheduleFacade( private val pictureService: PictureService ) { + companion object { + const val WEEK_DAYS = 6L + } + fun getList( customUserDetails: CustomUserDetails, year: Int, @@ -51,12 +55,12 @@ class ReadScheduleFacade( val schedules = scheduleService.findByCoupleAndDateBetween( customUserDetails.getCouple(), today, - today.plusDays(6) + today.plusDays(WEEK_DAYS) ) val anniversaries = anniversaryService.findByCoupleAndDateBetween( customUserDetails.getCouple(), today, - today.plusDays(6) + today.plusDays(WEEK_DAYS) ) return MainSchedulesAndAnniversariesResponse.of( From 5749e7331e538d3ef5cd2a7dff619b038f6d2608 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Wed, 21 May 2025 23:59:24 +0900 Subject: [PATCH 295/357] =?UTF-8?q?refactor:=20S3=20=EB=82=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=EC=83=9D=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/event/dto/S3DeleteEvent.kt | 5 +++++ .../listener/S3PicturesDeleteEventListener.kt | 22 +++++++++++++++++++ .../backend/member/facade/LeaveFacade.kt | 17 ++++++++++---- .../facade/UpsertAndDeleteScheduleFacade.kt | 17 ++++++++++---- 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/core/event/dto/S3DeleteEvent.kt create mode 100644 src/main/kotlin/gomushin/backend/core/event/listener/S3PicturesDeleteEventListener.kt diff --git a/src/main/kotlin/gomushin/backend/core/event/dto/S3DeleteEvent.kt b/src/main/kotlin/gomushin/backend/core/event/dto/S3DeleteEvent.kt new file mode 100644 index 00000000..9f935b0f --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/event/dto/S3DeleteEvent.kt @@ -0,0 +1,5 @@ +package gomushin.backend.core.event.dto + +data class S3DeleteEvent( + val pictureUrls: List +) diff --git a/src/main/kotlin/gomushin/backend/core/event/listener/S3PicturesDeleteEventListener.kt b/src/main/kotlin/gomushin/backend/core/event/listener/S3PicturesDeleteEventListener.kt new file mode 100644 index 00000000..5b28e24b --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/event/listener/S3PicturesDeleteEventListener.kt @@ -0,0 +1,22 @@ +package gomushin.backend.core.event.listener + +import gomushin.backend.core.service.S3Service +import gomushin.backend.core.event.dto.S3DeleteEvent +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class S3PicturesDeleteEventListener( + private val s3Service: S3Service, +) { + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun handle(event: S3DeleteEvent) { + event.pictureUrls.forEach { pictureUrl -> + s3Service.deleteFile(pictureUrl) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt index 0ea2d9df..ad876386 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -1,7 +1,7 @@ package gomushin.backend.member.facade import gomushin.backend.core.CustomUserDetails -import gomushin.backend.core.service.S3Service +import gomushin.backend.core.event.dto.S3DeleteEvent import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.domain.service.CoupleInfoService import gomushin.backend.couple.domain.service.CoupleService @@ -11,6 +11,7 @@ import gomushin.backend.schedule.domain.service.CommentService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -24,8 +25,8 @@ class LeaveFacade( private val notificationService: NotificationService, private val pictureService: PictureService, private val scheduleService: ScheduleService, - private val s3Service: S3Service, - private val coupleInfoService: CoupleInfoService + private val coupleInfoService: CoupleInfoService, + private val applicationEventPublisher: ApplicationEventPublisher, ) { @Transactional fun leave(customUserDetails: CustomUserDetails) { @@ -39,10 +40,11 @@ class LeaveFacade( scheduleService.deleteAllByMemberId(memberId) val letters = letterService.findAllByAuthorId(memberId) + val pictureUrlsToDelete = mutableListOf() pictureService.findAllByLetterIds(letters) .takeIf { it.isNotEmpty() } ?.forEach { picture -> - s3Service.deleteFile(picture.pictureUrl) + pictureUrlsToDelete.add(picture.pictureUrl) } pictureService.deleteAllByLetterIds(letters) @@ -50,5 +52,12 @@ class LeaveFacade( memberService.deleteMember(memberId) partner.updateIsCouple(false) + if (pictureUrlsToDelete.isNotEmpty()) { + applicationEventPublisher.publishEvent( + S3DeleteEvent( + pictureUrls = pictureUrlsToDelete + ) + ) + } } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt index ecb4a2e5..5f1b442a 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteScheduleFacade.kt @@ -1,12 +1,13 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails -import gomushin.backend.core.service.S3Service +import gomushin.backend.core.event.dto.S3DeleteEvent import gomushin.backend.schedule.domain.service.CommentService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.request.UpsertScheduleRequest +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -16,7 +17,7 @@ class UpsertAndDeleteScheduleFacade( private val letterService: LetterService, private val commentService: CommentService, private val pictureService: PictureService, - private val s3Service: S3Service, + private val applicationEventPublisher: ApplicationEventPublisher, ) { fun upsert(customUserDetails: CustomUserDetails, upsertScheduleRequest: UpsertScheduleRequest) { @@ -31,17 +32,25 @@ class UpsertAndDeleteScheduleFacade( @Transactional fun delete(customUserDetails: CustomUserDetails, scheduleId: Long) { val schedule = scheduleService.getById(scheduleId) + val pictureUrlsToDelete = mutableListOf() letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule).forEach { letter -> - // TODO: S3 삭제 로직 트랜잭션 커밋 이후로 분리 + 이벤트 발행으로 처리하기 pictureService.findAllByLetter(letter) .takeIf { it.isNotEmpty() } ?.forEach { picture -> - s3Service.deleteFile(picture.pictureUrl) + pictureUrlsToDelete.add(picture.pictureUrl) } pictureService.deleteAllByLetterId(letter.id) commentService.deleteAllByLetterId(letter.id) letterService.delete(letter.id) } scheduleService.delete(customUserDetails.getCouple().id, customUserDetails.getId(), scheduleId) + + if (pictureUrlsToDelete.isNotEmpty()) { + applicationEventPublisher.publishEvent( + S3DeleteEvent( + pictureUrls = pictureUrlsToDelete + ) + ) + } } } From fa48f62bfcecc014157f2fca63fe3f7d43fb6548 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 22 May 2025 01:21:43 +0900 Subject: [PATCH 296/357] =?UTF-8?q?refactor:=20ReadScheduleFacade=20LocalD?= =?UTF-8?q?ate.now()=20=EB=A5=BC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?-=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=AA=A8=ED=82=B9=EC=9D=84?= =?UTF-8?q?=20=EC=89=BD=EA=B2=8C=20=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/facade/ReadScheduleFacade.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index e3e6947f..9697b753 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -50,17 +50,17 @@ class ReadScheduleFacade( return ScheduleDetailResponse.of(schedule, letterPreviews) } - fun getListByWeek(customUserDetails: CustomUserDetails): MainSchedulesAndAnniversariesResponse { - val today = LocalDate.now() + fun getListByWeek(customUserDetails: CustomUserDetails, baseDate : LocalDate = LocalDate.now()): MainSchedulesAndAnniversariesResponse { + val endDate = baseDate.plusDays(WEEK_DAYS) val schedules = scheduleService.findByCoupleAndDateBetween( customUserDetails.getCouple(), - today, - today.plusDays(WEEK_DAYS) + baseDate, + endDate ) val anniversaries = anniversaryService.findByCoupleAndDateBetween( customUserDetails.getCouple(), - today, - today.plusDays(WEEK_DAYS) + baseDate, + endDate ) return MainSchedulesAndAnniversariesResponse.of( From 2438e8a228bb8adf6e6a8736f8b4b458e9b03cfe Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 22 May 2025 01:22:02 +0900 Subject: [PATCH 297/357] =?UTF-8?q?test:=20MockK=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facade/ReadScheduleFacadeMockkTest.kt | 92 +++++++++++++++++++ .../domain/facade/ReadScheduleFacadeTest.kt | 11 ++- 2 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt new file mode 100644 index 00000000..c96bf2e3 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt @@ -0,0 +1,92 @@ +package gomushin.backend.schedule.domain.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.domain.service.ScheduleService +import gomushin.backend.schedule.dto.response.MainAnniversariesResponse +import gomushin.backend.schedule.dto.response.MainSchedulesResponse +import gomushin.backend.schedule.facade.ReadScheduleFacade +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.time.LocalDate + +@ExtendWith(MockKExtension::class) +class ReadScheduleFacadeMockkTest { + + @MockK + lateinit var scheduleService: ScheduleService + + @MockK + lateinit var anniversaryService: AnniversaryService + + @MockK + lateinit var letterService: LetterService + + @MockK + lateinit var pictureService: PictureService + + @InjectMockKs + lateinit var readScheduleFacade: ReadScheduleFacade + + private val customUserDetails = mockk() + + private val couple = mockk() + + init { + every { customUserDetails.getCouple() } returns couple + every { customUserDetails.getId() } returns 1L + } + + @DisplayName("getListByWeek - 성공") + @Test + fun getListByWeek_success() { + // given + val date = LocalDate.of(2025, 5, 22) + val mainSchedulesResponse = mockk() + val mainAnniversariesResponse = mockk() + + // when + every { + scheduleService.findByCoupleAndDateBetween( + couple, + date, + date.plusDays(6) + ) + } returns listOf(mainSchedulesResponse) + + every { + anniversaryService.findByCoupleAndDateBetween( + couple, + date, + date.plusDays(6) + ) + } returns listOf(mainAnniversariesResponse) + + + readScheduleFacade.getListByWeek(customUserDetails, date) + + // then + verify(exactly = 1) { + scheduleService.findByCoupleAndDateBetween( + couple, + date, + date.plusDays(6) + ) + anniversaryService.findByCoupleAndDateBetween( + couple, + date, + date.plusDays(6) + ) + } + } +} diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt index a68b9ce0..68261edc 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt @@ -98,15 +98,19 @@ class ReadScheduleFacadeTest { id = scheduleId, title = "일정 제목", fatigue = "VERT_TIRED", - startDate = LocalDateTime.of(2025, 5, 1, 7, 0,0), - endDate = LocalDateTime.of(2025, 5, 2,20,0,0) + startDate = LocalDateTime.of(2025, 5, 1, 7, 0, 0), + endDate = LocalDateTime.of(2025, 5, 2, 20, 0, 0) ) //when `when`(mockLetter.id).thenReturn(letterId) `when`(mockPicture.letterId).thenReturn(letterId) `when`(scheduleService.getById(scheduleId)).thenReturn(schedule) - `when`(letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule)).thenReturn(listOf(mockLetter)) + `when`(letterService.findByCoupleAndSchedule(customUserDetails.getCouple(), schedule)).thenReturn( + listOf( + mockLetter + ) + ) `when`(pictureService.findAllByLetterIds(listOf(letterId))).thenReturn(listOf(mockPicture)) readScheduleFacade.getDetail(customUserDetails, scheduleId) @@ -116,5 +120,4 @@ class ReadScheduleFacadeTest { verify(letterService).findByCoupleAndSchedule(customUserDetails.getCouple(), schedule) verify(pictureService).findAllByLetterIds(listOf(letterId)) } - } From f9a6eb14974e5de4d84f83f94c0fff48342c7161 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 22 May 2025 02:18:28 +0900 Subject: [PATCH 298/357] =?UTF-8?q?test:=20=EC=8B=A4=ED=8C=A8=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1=20-=20MockK=EC=99=80=20?= =?UTF-8?q?Mockito=20=EB=A5=BC=20=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EC=97=90=20=EC=9E=91=EC=84=B1=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=97=86=EC=96=B4,=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ScheduleServiceMockKTest.kt | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceMockKTest.kt diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceMockKTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceMockKTest.kt new file mode 100644 index 00000000..7a306469 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceMockKTest.kt @@ -0,0 +1,86 @@ +package gomushin.backend.schedule.domain.service + +import gomushin.backend.core.common.support.SpringContextHolder +import gomushin.backend.core.configuration.env.AppEnv +import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.domain.repository.ScheduleRepository +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.data.repository.findByIdOrNull +import kotlin.test.assertEquals + +@ExtendWith(MockKExtension::class) +class ScheduleServiceMockKTest { + + @MockK + lateinit var scheduleRepository: ScheduleRepository + + private lateinit var scheduleService: ScheduleService + + @BeforeEach + fun setUp() { + // 확장함수 모킹 + mockkStatic("org.springframework.data.repository.CrudRepositoryExtensionsKt") + + // 서비스 인스턴스 직접 생성 + // 확장함수를 사용하는 경우, @InjectMockks가 아닌 직접 생성해야 함 + scheduleService = ScheduleService(scheduleRepository) + + // SpringContextHolder 모킹 + mockkObject(SpringContextHolder) + val mockAppEnv = mockk() + every { SpringContextHolder.getBean(AppEnv::class.java) } returns mockAppEnv + every { mockAppEnv.getId() } returns "test" + } + + + @DisplayName("getById() 경우, 존재하지 않는 id로 조회 시 BadRequestException 발생") + @Test + fun getById_notExistId_throwBadRequestException() { + // given + val id = 1L + every { + scheduleRepository.findByIdOrNull(id) + } returns null + + // when & then + val exception = assertThrows { + scheduleService.getById(id) + } + val errorMessage = exception.error.element.message.resolved + + assertEquals("해당 일정이 존재하지 않아요.", errorMessage) + } + + @DisplayName("커플 멤버가 아닌 멤버가 일정을 삭제하려 하면, 에러 발생") + @Test + fun delete_unauthorized_throwBadRequestException() { + // given + val coupleId = 1L + val userId = 2L + val scheduleId = 3L + val schedule = mockk() + every { scheduleRepository.findByIdOrNull(scheduleId) } returns schedule + every { schedule.coupleId } returns 4L + every { schedule.userId } returns 5L + + // when & then + val exception = assertThrows { + scheduleService.delete(coupleId, userId, scheduleId) + } + val errorMessage = exception.error.element.message.resolved + + assertEquals("해당 일정에 대한 권한이 없어요.", errorMessage) + } + +} From eaaab57c4f5557ddad6f0ad527f5ef17528e47bc Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Thu, 22 May 2025 13:52:36 +0900 Subject: [PATCH 299/357] =?UTF-8?q?refactor:=20ScheduleServiceTest=20?= =?UTF-8?q?=EC=A0=84=EB=B6=80=20MockK=EB=A1=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ScheduleServiceMockKTest.kt | 86 -------- .../domain/service/ScheduleServiceTest.kt | 203 ++++++++++++------ 2 files changed, 136 insertions(+), 153 deletions(-) delete mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceMockKTest.kt diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceMockKTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceMockKTest.kt deleted file mode 100644 index 7a306469..00000000 --- a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceMockKTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package gomushin.backend.schedule.domain.service - -import gomushin.backend.core.common.support.SpringContextHolder -import gomushin.backend.core.configuration.env.AppEnv -import gomushin.backend.core.infrastructure.exception.BadRequestException -import gomushin.backend.schedule.domain.entity.Schedule -import gomushin.backend.schedule.domain.repository.ScheduleRepository -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.junit5.MockKExtension -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.springframework.data.repository.findByIdOrNull -import kotlin.test.assertEquals - -@ExtendWith(MockKExtension::class) -class ScheduleServiceMockKTest { - - @MockK - lateinit var scheduleRepository: ScheduleRepository - - private lateinit var scheduleService: ScheduleService - - @BeforeEach - fun setUp() { - // 확장함수 모킹 - mockkStatic("org.springframework.data.repository.CrudRepositoryExtensionsKt") - - // 서비스 인스턴스 직접 생성 - // 확장함수를 사용하는 경우, @InjectMockks가 아닌 직접 생성해야 함 - scheduleService = ScheduleService(scheduleRepository) - - // SpringContextHolder 모킹 - mockkObject(SpringContextHolder) - val mockAppEnv = mockk() - every { SpringContextHolder.getBean(AppEnv::class.java) } returns mockAppEnv - every { mockAppEnv.getId() } returns "test" - } - - - @DisplayName("getById() 경우, 존재하지 않는 id로 조회 시 BadRequestException 발생") - @Test - fun getById_notExistId_throwBadRequestException() { - // given - val id = 1L - every { - scheduleRepository.findByIdOrNull(id) - } returns null - - // when & then - val exception = assertThrows { - scheduleService.getById(id) - } - val errorMessage = exception.error.element.message.resolved - - assertEquals("해당 일정이 존재하지 않아요.", errorMessage) - } - - @DisplayName("커플 멤버가 아닌 멤버가 일정을 삭제하려 하면, 에러 발생") - @Test - fun delete_unauthorized_throwBadRequestException() { - // given - val coupleId = 1L - val userId = 2L - val scheduleId = 3L - val schedule = mockk() - every { scheduleRepository.findByIdOrNull(scheduleId) } returns schedule - every { schedule.coupleId } returns 4L - every { schedule.userId } returns 5L - - // when & then - val exception = assertThrows { - scheduleService.delete(coupleId, userId, scheduleId) - } - val errorMessage = exception.error.element.message.resolved - - assertEquals("해당 일정에 대한 권한이 없어요.", errorMessage) - } - -} diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt index ed0569ac..3bb353bc 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt @@ -1,86 +1,155 @@ package gomushin.backend.schedule.domain.service +import gomushin.backend.core.common.support.SpringContextHolder +import gomushin.backend.core.configuration.env.AppEnv +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.ScheduleRepository import gomushin.backend.schedule.dto.request.UpsertScheduleRequest -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import org.junit.jupiter.api.* import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.InjectMocks -import org.mockito.Mock -import org.mockito.Mockito.* -import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.data.repository.findByIdOrNull import java.time.LocalDateTime -import java.util.* +import kotlin.test.assertEquals -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockKExtension::class) class ScheduleServiceTest { - @Mock - private lateinit var scheduleRepository: ScheduleRepository + @MockK + lateinit var scheduleRepository: ScheduleRepository - @InjectMocks private lateinit var scheduleService: ScheduleService - @DisplayName("일정 저장 테스트") - @Test - fun insert_success() { - // given - val coupleId = 1L - val userId = 1L - val upsertScheduleRequest = mock(UpsertScheduleRequest::class.java) - `when`(upsertScheduleRequest.toEntity(coupleId, userId)).thenReturn(mock(Schedule::class.java)) - `when`(scheduleRepository.save(any(Schedule::class.java))).thenReturn(mock(Schedule::class.java)) - - // when - scheduleService.upsert(null, coupleId, userId, upsertScheduleRequest) - - // then - verify(scheduleRepository).save(any(Schedule::class.java)) + @BeforeEach + fun setUp() { + // 확장함수 모킹 + mockkStatic("org.springframework.data.repository.CrudRepositoryExtensionsKt") + + // 서비스 인스턴스 직접 생성 + // 확장함수를 사용하는 경우, @InjectMockks가 아닌 직접 생성해야 함 + scheduleService = ScheduleService(scheduleRepository) + + // SpringContextHolder 모킹 + mockkObject(SpringContextHolder) + val mockAppEnv = mockk() + every { SpringContextHolder.getBean(AppEnv::class.java) } returns mockAppEnv + every { mockAppEnv.getId() } returns "test" + } + + @Nested + inner class UpsertTest { + @DisplayName("일정 저장 테스트 - 성공") + @Test + fun insert_success() { + // given + val coupleId = 1L + val userId = 1L + val upsertScheduleRequest = mockk() + every { upsertScheduleRequest.toEntity(coupleId, userId) } returns mockk() + every { scheduleRepository.save(any()) } returns mockk() + + // when + scheduleService.upsert(null, coupleId, userId, upsertScheduleRequest) + + // then + verify { scheduleRepository.save(any()) } + } + + @DisplayName("일정 수정 테스트 - 성공") + @Test + fun update_success() { + // given + val id = 1L + val scheduleId = 1L + val coupleId = 1L + val userId = 1L + val upsertScheduleRequest = mockk() + val schedule = Schedule( + id = scheduleId, + coupleId = coupleId, + userId = userId, + startDate = LocalDateTime.of(2023, 1, 1, 0, 0), + endDate = LocalDateTime.of(2023, 1, 2, 0, 0), + title = "Test Schedule", + isAllDay = false, + fatigue = "VERY_TIRED", + ) + + + every { upsertScheduleRequest.title } returns "Updated Schedule" + every { upsertScheduleRequest.fatigue } returns "TIRED" + every { upsertScheduleRequest.isAllDay } returns true + every { upsertScheduleRequest.id } returns id + every { upsertScheduleRequest.startDate } returns LocalDateTime.of(2023, 1, 1, 0, 0) + every { upsertScheduleRequest.endDate } returns LocalDateTime.of(2023, 1, 2, 0, 0) + + every { scheduleRepository.findByIdOrNull(id) } returns schedule + every { scheduleRepository.save(schedule) } returns schedule + + // when + scheduleService.upsert(id, coupleId, userId, upsertScheduleRequest) + + // then + verify { scheduleRepository.findByIdOrNull(id) } + verify(exactly = 0) { + scheduleRepository.save(any()) + } + } } - @DisplayName("일정 수정 테스트") - @Test - fun update_success() { - // given - val id = 1L - val coupleId = 1L - val userId = 1L - val upsertScheduleRequest = mock(UpsertScheduleRequest::class.java) - val mockSchedule = mock(Schedule::class.java) - `when`(scheduleRepository.findById(id)).thenReturn(Optional.of(mockSchedule)) - - // when - scheduleService.upsert(id, coupleId, userId, upsertScheduleRequest) - - // then - verify(scheduleRepository).findById(id) - verify(scheduleRepository, never()).save(any(Schedule::class.java)) + @Nested + inner class ReadTest { + @DisplayName("getById() 경우, 존재하지 않는 id로 조회 시 BadRequestException 발생") + @Test + fun getById_notExistId_throwBadRequestException() { + // given + val id = 1L + every { + scheduleRepository.findByIdOrNull(id) + } returns null + + // when & then + val exception = assertThrows { + scheduleService.getById(id) + } + val errorMessage = exception.error.element.message.resolved + + assertEquals("해당 일정이 존재하지 않아요.", errorMessage) + } } - @DisplayName("일정 삭제 테스트") - @Test - fun delete_success() { - // given - val coupleId = 1L - val userId = 1L - val scheduleId = 1L - val mockSchedule = Schedule( - id = scheduleId, - coupleId = coupleId, - userId = userId, - startDate = LocalDateTime.of(2023, 1, 1, 0, 0), - endDate = LocalDateTime.of(2023, 1, 2, 0, 0), - title = "Test Schedule", - isAllDay = false, - fatigue = "VERY_TIRED", - ) - `when`(scheduleRepository.findById(scheduleId)).thenReturn(Optional.of(mockSchedule)) - - // when - scheduleService.delete(coupleId, userId, scheduleId) - - // then - verify(scheduleRepository).deleteById(scheduleId) + @Nested + inner class DeleteTest { + @DisplayName("일정 삭제 테스트 - 성공") + @Test + fun delete_success() { + // given + val coupleId = 1L + val userId = 1L + val scheduleId = 1L + val schedule = Schedule( + id = scheduleId, + coupleId = coupleId, + userId = userId, + startDate = LocalDateTime.of(2023, 1, 1, 0, 0), + endDate = LocalDateTime.of(2023, 1, 2, 0, 0), + title = "Test Schedule", + isAllDay = false, + fatigue = "VERY_TIRED", + ) + + every { scheduleRepository.findByIdOrNull(scheduleId) } returns schedule + justRun { scheduleRepository.deleteById(scheduleId) } + + // when + scheduleService.delete(coupleId, userId, scheduleId) + + // then + verify { scheduleRepository.deleteById(scheduleId) } + } + } } From 826d6bf545ff660ed4c60d3b320f46de945f6100 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 25 May 2025 16:04:56 +0900 Subject: [PATCH 300/357] =?UTF-8?q?test:=20MockK=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facade/ReadScheduleFacadeMockkTest.kt | 58 ++++++++++++++++++- .../domain/facade/ReadScheduleFacadeTest.kt | 36 ------------ 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt index c96bf2e3..8e4d7df1 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt @@ -3,11 +3,11 @@ package gomushin.backend.schedule.domain.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService -import gomushin.backend.schedule.dto.response.MainAnniversariesResponse -import gomushin.backend.schedule.dto.response.MainSchedulesResponse +import gomushin.backend.schedule.dto.response.* import gomushin.backend.schedule.facade.ReadScheduleFacade import io.mockk.every import io.mockk.impl.annotations.InjectMockKs @@ -47,6 +47,40 @@ class ReadScheduleFacadeMockkTest { every { customUserDetails.getId() } returns 1L } + @DisplayName("getList - 성공") + @Test + fun getList_success() { + // given + val year = 2025 + val month = 4 + val monthlySchedulesResponse = mockk() + val monthlyAnniversariesResponse = mockk() + + // when + every { + scheduleService.findByCoupleAndYearAndMonth( + couple, + year, + month + ) + } returns listOf(monthlySchedulesResponse) + + every { + anniversaryService.findByCoupleAndYearAndMonth( + couple, + year, + month + ) + } returns listOf(monthlyAnniversariesResponse) + readScheduleFacade.getList(customUserDetails, year, month) + + // then + verify(exactly = 1) { + scheduleService.findByCoupleAndYearAndMonth(couple, year, month) + anniversaryService.findByCoupleAndYearAndMonth(couple, year, month) + } + } + @DisplayName("getListByWeek - 성공") @Test fun getListByWeek_success() { @@ -89,4 +123,24 @@ class ReadScheduleFacadeMockkTest { ) } } + + @DisplayName("get - 성공") + @Test + fun get_success() { + // given + val date = LocalDate.of(2025, 4, 1) + val mockSchedules = listOf(mockk()) + val mockAnniversaries = listOf(mockk()) + + // when + every { scheduleService.findByDate(couple, date) } returns mockSchedules + every { anniversaryService.findByCoupleAndDate(couple, date) } returns mockAnniversaries + readScheduleFacade.get(customUserDetails, date) + + // then + verify(exactly = 1) { + scheduleService.findByDate(couple, date) + anniversaryService.findByCoupleAndDate(couple, date) + } + } } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt index 68261edc..9f43b59e 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt @@ -49,42 +49,6 @@ class ReadScheduleFacadeTest { `when`(customUserDetails.getCouple()).thenReturn(mock(Couple::class.java)) } - @DisplayName("getList - 성공") - @Test - fun getList_success() { - // given - val year = 2025 - val month = 4 - val monthlySchedulesResponse = mock(MonthlySchedulesResponse::class.java) - val monthlyAnniversariesResponse = mock(MonthlyAnniversariesResponse::class.java) - - // when - `when`(scheduleService.findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month)) - .thenReturn(listOf(monthlySchedulesResponse)) - `when`(anniversaryService.findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month)) - .thenReturn(listOf(monthlyAnniversariesResponse)) - readScheduleFacade.getList(customUserDetails, year, month) - - // then - verify(scheduleService, times(1)).findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month) - verify(anniversaryService, times(1)).findByCoupleAndYearAndMonth(customUserDetails.getCouple(), year, month) - } - - @DisplayName("get - 성공") - @Test - fun get_success() { - // given - val date = LocalDate.of(2025, 4, 1) - val mockSchedules = listOf(mock(DailyScheduleResponse::class.java)) - - // when - `when`(scheduleService.findByDate(customUserDetails.getCouple(), date)).thenReturn(mockSchedules) - readScheduleFacade.get(customUserDetails, date) - - // then - verify(scheduleService, times(1)).findByDate(customUserDetails.getCouple(), date) - } - @DisplayName("getDetail - 성공") @Test fun getDetail_success() { From 6c3a1f0ae445307918e3c7bd4c010393d270a25b Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 25 May 2025 12:43:41 +0900 Subject: [PATCH 301/357] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/auth/ApiPath.kt | 5 ---- .../auth/domain/service/AuthService.kt | 30 +++++++++++++++++++ .../backend/auth/facade/LogoutFacade.kt | 26 ++++++++++++++++ .../auth/{ => facade}/ReissueFacade.kt | 4 +-- .../backend/auth/presentation/ApiPath.kt | 7 +++++ .../auth/presentation/LogoutController.kt | 30 +++++++++++++++++++ .../{ => presentation}/ReissueController.kt | 5 ++-- 7 files changed, 98 insertions(+), 9 deletions(-) delete mode 100644 src/main/kotlin/gomushin/backend/auth/ApiPath.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/domain/service/AuthService.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/facade/LogoutFacade.kt rename src/main/kotlin/gomushin/backend/auth/{ => facade}/ReissueFacade.kt (97%) create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt create mode 100644 src/main/kotlin/gomushin/backend/auth/presentation/LogoutController.kt rename src/main/kotlin/gomushin/backend/auth/{ => presentation}/ReissueController.kt (89%) diff --git a/src/main/kotlin/gomushin/backend/auth/ApiPath.kt b/src/main/kotlin/gomushin/backend/auth/ApiPath.kt deleted file mode 100644 index 38bd8286..00000000 --- a/src/main/kotlin/gomushin/backend/auth/ApiPath.kt +++ /dev/null @@ -1,5 +0,0 @@ -package gomushin.backend.auth - -object ApiPath { - const val REISSUE = "/v1/auth/reissue" -} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/auth/domain/service/AuthService.kt b/src/main/kotlin/gomushin/backend/auth/domain/service/AuthService.kt new file mode 100644 index 00000000..ad8b8e0a --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/domain/service/AuthService.kt @@ -0,0 +1,30 @@ +package gomushin.backend.auth.domain.service + +import jakarta.servlet.http.Cookie +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Service + +@Service +class AuthService { + companion object { + private const val AT_PREFIX = "access_token" + private const val RT_PREFIX = "refresh_token" + } + + fun logout(response: HttpServletResponse) { + val expiredAccess = Cookie(AT_PREFIX, "").apply { + path = "/" + isHttpOnly = true + maxAge = 0 + } + + val expiredRefresh = Cookie(RT_PREFIX, "").apply { + path = "/" + isHttpOnly = true + maxAge = 0 + } + + response.addCookie(expiredAccess) + response.addCookie(expiredRefresh) + } +} diff --git a/src/main/kotlin/gomushin/backend/auth/facade/LogoutFacade.kt b/src/main/kotlin/gomushin/backend/auth/facade/LogoutFacade.kt new file mode 100644 index 00000000..423768ff --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/facade/LogoutFacade.kt @@ -0,0 +1,26 @@ +package gomushin.backend.auth.facade + +import gomushin.backend.auth.domain.service.AuthService +import gomushin.backend.core.jwt.infrastructure.TokenService +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component + +@Component +class LogoutFacade( + private val authService: AuthService, + private val tokenService: TokenService, +) { + fun logout( + request: HttpServletRequest, + response: HttpServletResponse + ) { + val refreshToken = request.cookies?.find { it.name == "refresh_token" }?.value + + if (refreshToken != null) { + tokenService.deleteRefreshToken(refreshToken) + } + + authService.logout(response) + } +} diff --git a/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt b/src/main/kotlin/gomushin/backend/auth/facade/ReissueFacade.kt similarity index 97% rename from src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt rename to src/main/kotlin/gomushin/backend/auth/facade/ReissueFacade.kt index 053f616a..101d3c03 100644 --- a/src/main/kotlin/gomushin/backend/auth/ReissueFacade.kt +++ b/src/main/kotlin/gomushin/backend/auth/facade/ReissueFacade.kt @@ -1,4 +1,4 @@ -package gomushin.backend.auth +package gomushin.backend.auth.facade import gomushin.backend.core.configuration.cookie.CookieService import gomushin.backend.core.jwt.infrastructure.TokenService @@ -25,4 +25,4 @@ class ReissueFacade( response.addHeader("Set-Cookie", accessCookie.toString()) response.addHeader("Set-Cookie", refreshCookie.toString()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt new file mode 100644 index 00000000..828f99e7 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/presentation/ApiPath.kt @@ -0,0 +1,7 @@ +package gomushin.backend.auth.presentation + +object ApiPath { + const val REISSUE = "/v1/auth/reissue" + + const val LOGOUT = "/v1/auth/logout" +} diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/LogoutController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/LogoutController.kt new file mode 100644 index 00000000..0049e88e --- /dev/null +++ b/src/main/kotlin/gomushin/backend/auth/presentation/LogoutController.kt @@ -0,0 +1,30 @@ +package gomushin.backend.auth.presentation + +import gomushin.backend.auth.facade.LogoutFacade +import gomushin.backend.core.common.web.response.ApiResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController + +@RestController +@Tag(name = "로그아웃", description = "LogoutController") +class LogoutController( + private val logoutFacade: LogoutFacade +) { + + @ResponseStatus(HttpStatus.NO_CONTENT) + @PostMapping + @Operation(summary = "로그아웃", description = "logout") + fun logout( + request: HttpServletRequest, + response: HttpServletResponse, + ):ApiResponse { + logoutFacade.logout(request, response) + return ApiResponse.success(true) + } +} diff --git a/src/main/kotlin/gomushin/backend/auth/ReissueController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/ReissueController.kt similarity index 89% rename from src/main/kotlin/gomushin/backend/auth/ReissueController.kt rename to src/main/kotlin/gomushin/backend/auth/presentation/ReissueController.kt index 989fab44..d7d5ed02 100644 --- a/src/main/kotlin/gomushin/backend/auth/ReissueController.kt +++ b/src/main/kotlin/gomushin/backend/auth/presentation/ReissueController.kt @@ -1,5 +1,6 @@ -package gomushin.backend.auth +package gomushin.backend.auth.presentation +import gomushin.backend.auth.facade.ReissueFacade import gomushin.backend.core.common.web.response.ApiResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag @@ -22,4 +23,4 @@ class ReissueController ( reissueFacade.reissue(refreshToken, response) return ApiResponse.success(true) } -} \ No newline at end of file +} From 3fe3efe9ce536eff80d41c69d7252776f6c3a2af Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 25 May 2025 12:53:22 +0900 Subject: [PATCH 302/357] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20ApiPath=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/auth/presentation/LogoutController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/auth/presentation/LogoutController.kt b/src/main/kotlin/gomushin/backend/auth/presentation/LogoutController.kt index 0049e88e..ca391c35 100644 --- a/src/main/kotlin/gomushin/backend/auth/presentation/LogoutController.kt +++ b/src/main/kotlin/gomushin/backend/auth/presentation/LogoutController.kt @@ -18,7 +18,7 @@ class LogoutController( ) { @ResponseStatus(HttpStatus.NO_CONTENT) - @PostMapping + @PostMapping(ApiPath.LOGOUT) @Operation(summary = "로그아웃", description = "logout") fun logout( request: HttpServletRequest, From 9f3126e8eda85bc3e7ec03db1dd79fbc77b33914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 25 May 2025 18:52:21 +0900 Subject: [PATCH 303/357] =?UTF-8?q?chore=20:=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=8E=B8=EC=A7=80=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=EA=B1=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=8A=A4=EC=BC=80=EC=A4=84=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EB=94=94=20=EA=B0=99=EC=9D=B4=20=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0=20#132?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/dto/response/MainLetterPreviewResponse.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt index d552020d..18cde22d 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt @@ -11,6 +11,7 @@ data class MainLetterPreviewResponse( val content: String?, val pictureUrl: String?, val schedule: String?, + val scheduleId : Long?, val createdAt: LocalDateTime?, ) { companion object { @@ -33,6 +34,7 @@ data class MainLetterPreviewResponse( content = previewContent, pictureUrl = picture?.pictureUrl, schedule = schedule?.title, + scheduleId = schedule?.id, createdAt = letter?.createdAt ) } From 2873b77cd698ccc5d613db01bc70bef3d7cc64dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 25 May 2025 19:13:44 +0900 Subject: [PATCH 304/357] =?UTF-8?q?chore=20:=20=ED=8E=B8=EC=A7=80=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=97=90=EC=84=9C=20=EB=82=B4=EA=B0=80=20=EC=8D=BC?= =?UTF-8?q?=EB=8A=94=EC=A7=80=20=EC=97=AC=EB=B6=80=EB=8F=84=20=EA=B0=99?= =?UTF-8?q?=EC=9D=B4=20=EB=82=B4=EC=A3=BC=EA=B2=8C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?#132?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/dto/response/LetterDetailResponse.kt | 4 ++-- .../schedule/dto/response/LetterPreviewResponse.kt | 5 ++++- .../backend/schedule/dto/response/LetterResponse.kt | 4 +++- .../dto/response/MainLetterPreviewResponse.kt | 3 +++ .../backend/schedule/facade/ReadLetterFacade.kt | 12 ++++++++---- .../backend/schedule/facade/ReadScheduleFacade.kt | 3 ++- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt index e21a77ed..3f715703 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt @@ -3,7 +3,7 @@ package gomushin.backend.schedule.dto.response data class LetterDetailResponse( val letter: LetterResponse, val pictures: List, - val comments: List, + val comments: List ) { companion object { fun of( @@ -14,7 +14,7 @@ data class LetterDetailResponse( return LetterDetailResponse( letter = letter, pictures = pictures, - comments = comments, + comments = comments ) } } diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt index 1252e2b9..38454bc5 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt @@ -12,6 +12,7 @@ data class LetterPreviewResponse( val title: String?, val content: String?, val pictureUrl: String?, + val isWrittenByMe : Boolean?, val createdAt: LocalDateTime?, ) { companion object { @@ -21,7 +22,8 @@ data class LetterPreviewResponse( fun of( letter: Letter?, schedule: Schedule?, - picture: Picture? + picture: Picture?, + memberId : Long? ): LetterPreviewResponse { val previewContent = if (letter?.content != null && letter.content.length > MAX_CONTENT_LENGTH) { letter.content.take(PREVIEW_CONTENT_LENGTH) + "..." @@ -35,6 +37,7 @@ data class LetterPreviewResponse( title = letter?.title, content = previewContent, pictureUrl = picture?.pictureUrl, + isWrittenByMe = memberId == letter?.authorId, createdAt = letter?.createdAt ) } diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterResponse.kt index 01e03d7d..f06a8b70 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterResponse.kt @@ -8,15 +8,17 @@ data class LetterResponse( val title: String, val content: String, val author: String, + val isWrittenByMe : Boolean, val createdAt: LocalDateTime, ) { companion object { - fun of(letter: Letter): LetterResponse { + fun of(letter: Letter, memberId : Long): LetterResponse { return LetterResponse( id = letter.id, title = letter.title, content = letter.content, author = letter.author, + isWrittenByMe = letter.authorId == memberId, createdAt = letter.createdAt, ) } diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt index 18cde22d..9090d484 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt @@ -12,6 +12,7 @@ data class MainLetterPreviewResponse( val pictureUrl: String?, val schedule: String?, val scheduleId : Long?, + val isWrittenByMe : Boolean?, val createdAt: LocalDateTime?, ) { companion object { @@ -22,6 +23,7 @@ data class MainLetterPreviewResponse( letter: Letter?, picture: Picture?, schedule: Schedule?, + memberId : Long? ): MainLetterPreviewResponse { val previewContent = if (letter?.content != null && letter.content.length > MAX_CONTENT_LENGTH) { letter.content.take(PREVIEW_CONTENT_LENGTH) + "..." @@ -35,6 +37,7 @@ data class MainLetterPreviewResponse( pictureUrl = picture?.pictureUrl, schedule = schedule?.title, scheduleId = schedule?.id, + isWrittenByMe = memberId == letter?.authorId, createdAt = letter?.createdAt ) } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 8710c7b4..d06d708d 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -26,6 +26,7 @@ class ReadLetterFacade( customUserDetails: CustomUserDetails, scheduleId: Long, ): List { + val memberId = customUserDetails.getId() val schedule = scheduleService.getById(scheduleId) val letters = letterService.findByCoupleAndSchedule( customUserDetails.getCouple(), @@ -33,7 +34,7 @@ class ReadLetterFacade( ) return letters.map { letter -> val picture = pictureService.findFirstByLetterId(letter.id) - LetterPreviewResponse.of(letter, schedule, picture) + LetterPreviewResponse.of(letter, schedule, picture, memberId) } } @@ -42,6 +43,7 @@ class ReadLetterFacade( scheduleId: Long, letterId: Long, ): LetterDetailResponse { + val memberId = customUserDetails.getId() val schedule = scheduleService.getById(scheduleId) val letter = letterService.getByCoupleAndScheduleAndId( customUserDetails.getCouple(), @@ -49,7 +51,7 @@ class ReadLetterFacade( letterId, ) - val letterResponse = LetterResponse.of(letter) + val letterResponse = LetterResponse.of(letter, memberId) val pictures = pictureService.findAllByLetter(letter) val pictureResponses = pictures.map { picture -> @@ -74,12 +76,13 @@ class ReadLetterFacade( size: Int, ): PageResponse { val pageRequest = PageRequest.of(page, size) + val memberId = customUserDetails.getId() val letters = letterService.findAllToCouple(customUserDetails.getCouple(), pageRequest) val letterPreviewResponses = letters.map { letter -> val picture = letter.let { pictureService.findFirstByLetterId(it.id) } val schedule = letter.let { scheduleService.getById(it.scheduleId) } - LetterPreviewResponse.of(letter, schedule, picture) + LetterPreviewResponse.of(letter, schedule, picture, memberId) } return PageResponse.from(letterPreviewResponses) @@ -89,10 +92,11 @@ class ReadLetterFacade( customUserDetails: CustomUserDetails, ): List { val letters = letterService.findTop5ByCreatedDateDesc(customUserDetails.getCouple()) + val memberId = customUserDetails.getId() return letters.map { letter -> val picture = pictureService.findFirstByLetterId(letter.id) val schedule = scheduleService.findById(letter.scheduleId) - MainLetterPreviewResponse.of(letter, picture, schedule) + MainLetterPreviewResponse.of(letter, picture, schedule, memberId) } } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index 9697b753..333ea398 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -44,8 +44,9 @@ class ReadScheduleFacade( val letterIds = letters.map { it.id } val picturesByLetterId = pictureService.findAllByLetterIds(letterIds) .associateBy { it.letterId } + val memberId = customUserDetails.getId() val letterPreviews = letters.map { letter -> - LetterPreviewResponse.of(letter, schedule, picturesByLetterId[letter.id]) + LetterPreviewResponse.of(letter, schedule, picturesByLetterId[letter.id], memberId) } return ScheduleDetailResponse.of(schedule, letterPreviews) } From c2cb43c59c7d8c33434a0a9527702627cd3e6581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 25 May 2025 21:52:35 +0900 Subject: [PATCH 305/357] =?UTF-8?q?refactor=20:=20[=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=B0=98=EC=98=81]=20=EB=A9=94=EC=84=9C=EB=93=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=EC=84=B1=20=EC=9C=84=ED=95=B4=EC=84=9C=20memberId?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20member=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EC=A7=80=EC=A0=95=20#132?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/dto/response/LetterDetailResponse.kt | 4 ++-- .../schedule/dto/response/LetterPreviewResponse.kt | 5 +++-- .../dto/response/MainLetterPreviewResponse.kt | 5 +++-- .../backend/schedule/facade/ReadLetterFacade.kt | 14 ++++++++------ .../backend/schedule/facade/ReadScheduleFacade.kt | 9 ++++++--- .../schedule/domain/facade/ReadLetterFacadeTest.kt | 5 +++++ .../domain/facade/ReadScheduleFacadeMockkTest.kt | 5 +++++ .../domain/facade/ReadScheduleFacadeTest.kt | 4 ++++ 8 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt index 3f715703..e21a77ed 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterDetailResponse.kt @@ -3,7 +3,7 @@ package gomushin.backend.schedule.dto.response data class LetterDetailResponse( val letter: LetterResponse, val pictures: List, - val comments: List + val comments: List, ) { companion object { fun of( @@ -14,7 +14,7 @@ data class LetterDetailResponse( return LetterDetailResponse( letter = letter, pictures = pictures, - comments = comments + comments = comments, ) } } diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt index 38454bc5..d3ec9de4 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/LetterPreviewResponse.kt @@ -1,5 +1,6 @@ package gomushin.backend.schedule.dto.response +import gomushin.backend.member.domain.entity.Member import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.entity.Picture import gomushin.backend.schedule.domain.entity.Schedule @@ -23,7 +24,7 @@ data class LetterPreviewResponse( letter: Letter?, schedule: Schedule?, picture: Picture?, - memberId : Long? + member : Member?, ): LetterPreviewResponse { val previewContent = if (letter?.content != null && letter.content.length > MAX_CONTENT_LENGTH) { letter.content.take(PREVIEW_CONTENT_LENGTH) + "..." @@ -37,7 +38,7 @@ data class LetterPreviewResponse( title = letter?.title, content = previewContent, pictureUrl = picture?.pictureUrl, - isWrittenByMe = memberId == letter?.authorId, + isWrittenByMe = member?.id == letter?.authorId, createdAt = letter?.createdAt ) } diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt index 9090d484..d30acb51 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/response/MainLetterPreviewResponse.kt @@ -1,5 +1,6 @@ package gomushin.backend.schedule.dto.response +import gomushin.backend.member.domain.entity.Member import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.entity.Picture import gomushin.backend.schedule.domain.entity.Schedule @@ -23,7 +24,7 @@ data class MainLetterPreviewResponse( letter: Letter?, picture: Picture?, schedule: Schedule?, - memberId : Long? + member : Member?, ): MainLetterPreviewResponse { val previewContent = if (letter?.content != null && letter.content.length > MAX_CONTENT_LENGTH) { letter.content.take(PREVIEW_CONTENT_LENGTH) + "..." @@ -37,7 +38,7 @@ data class MainLetterPreviewResponse( pictureUrl = picture?.pictureUrl, schedule = schedule?.title, scheduleId = schedule?.id, - isWrittenByMe = memberId == letter?.authorId, + isWrittenByMe = member?.id == letter?.authorId, createdAt = letter?.createdAt ) } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index d06d708d..8188ebcc 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -2,6 +2,7 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.PageResponse +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.schedule.domain.service.CommentService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService @@ -17,6 +18,7 @@ class ReadLetterFacade( private val scheduleService: ScheduleService, private val pictureService: PictureService, private val commentService: CommentService, + private val memberService: MemberService, @Value("\${server.url}") private val baseUrl: String, ) { @@ -26,7 +28,7 @@ class ReadLetterFacade( customUserDetails: CustomUserDetails, scheduleId: Long, ): List { - val memberId = customUserDetails.getId() + val member = memberService.getById(customUserDetails.getId()) val schedule = scheduleService.getById(scheduleId) val letters = letterService.findByCoupleAndSchedule( customUserDetails.getCouple(), @@ -34,7 +36,7 @@ class ReadLetterFacade( ) return letters.map { letter -> val picture = pictureService.findFirstByLetterId(letter.id) - LetterPreviewResponse.of(letter, schedule, picture, memberId) + LetterPreviewResponse.of(letter, schedule, picture, member) } } @@ -76,13 +78,13 @@ class ReadLetterFacade( size: Int, ): PageResponse { val pageRequest = PageRequest.of(page, size) - val memberId = customUserDetails.getId() + val member = memberService.getById(customUserDetails.getId()) val letters = letterService.findAllToCouple(customUserDetails.getCouple(), pageRequest) val letterPreviewResponses = letters.map { letter -> val picture = letter.let { pictureService.findFirstByLetterId(it.id) } val schedule = letter.let { scheduleService.getById(it.scheduleId) } - LetterPreviewResponse.of(letter, schedule, picture, memberId) + LetterPreviewResponse.of(letter, schedule, picture, member) } return PageResponse.from(letterPreviewResponses) @@ -92,11 +94,11 @@ class ReadLetterFacade( customUserDetails: CustomUserDetails, ): List { val letters = letterService.findTop5ByCreatedDateDesc(customUserDetails.getCouple()) - val memberId = customUserDetails.getId() + val member = memberService.getById(customUserDetails.getId()) return letters.map { letter -> val picture = pictureService.findFirstByLetterId(letter.id) val schedule = scheduleService.findById(letter.scheduleId) - MainLetterPreviewResponse.of(letter, picture, schedule, memberId) + MainLetterPreviewResponse.of(letter, picture, schedule, member) } } } diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt index 333ea398..29a2a318 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadScheduleFacade.kt @@ -2,10 +2,12 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.response.* +import kotlinx.coroutines.flow.merge import org.springframework.stereotype.Component import java.time.LocalDate @@ -14,7 +16,8 @@ class ReadScheduleFacade( private val scheduleService: ScheduleService, private val anniversaryService: AnniversaryService, private val letterService: LetterService, - private val pictureService: PictureService + private val pictureService: PictureService, + private val memberService: MemberService, ) { companion object { @@ -44,9 +47,9 @@ class ReadScheduleFacade( val letterIds = letters.map { it.id } val picturesByLetterId = pictureService.findAllByLetterIds(letterIds) .associateBy { it.letterId } - val memberId = customUserDetails.getId() + val member = memberService.getById(customUserDetails.getId()) val letterPreviews = letters.map { letter -> - LetterPreviewResponse.of(letter, schedule, picturesByLetterId[letter.id], memberId) + LetterPreviewResponse.of(letter, schedule, picturesByLetterId[letter.id], member) } return ScheduleDetailResponse.of(schedule, letterPreviews) } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt index 651a76b3..fa273d23 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadLetterFacadeTest.kt @@ -2,6 +2,7 @@ package gomushin.backend.schedule.domain.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.entity.Picture import gomushin.backend.schedule.domain.entity.Schedule @@ -33,6 +34,9 @@ class ReadLetterFacadeTest { @Mock lateinit var pictureService: PictureService + @Mock + lateinit var memberService: MemberService + private lateinit var readLetterFacade: ReadLetterFacade @@ -44,6 +48,7 @@ class ReadLetterFacadeTest { scheduleService, pictureService, commentService, + memberService, baseUrl ) } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt index 8e4d7df1..5ec8cd84 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeMockkTest.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService @@ -18,6 +19,7 @@ import io.mockk.verify import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock import java.time.LocalDate @ExtendWith(MockKExtension::class) @@ -35,6 +37,9 @@ class ReadScheduleFacadeMockkTest { @MockK lateinit var pictureService: PictureService + @MockK + lateinit var memberService: MemberService + @InjectMockKs lateinit var readScheduleFacade: ReadScheduleFacade diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt index 9f43b59e..d7fd9831 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/ReadScheduleFacadeTest.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.service.AnniversaryService import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse +import gomushin.backend.member.domain.service.MemberService import gomushin.backend.schedule.domain.entity.Letter import gomushin.backend.schedule.domain.entity.Picture import gomushin.backend.schedule.domain.entity.Schedule @@ -39,6 +40,9 @@ class ReadScheduleFacadeTest { @Mock private lateinit var pictureService: PictureService + @Mock + private lateinit var memberService: MemberService + @InjectMocks private lateinit var readScheduleFacade: ReadScheduleFacade From 91e3dd8eccabd76c8ab62ad75c8dbbfb91955e68 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 25 May 2025 15:23:27 +0900 Subject: [PATCH 306/357] =?UTF-8?q?feat:=20=ED=83=88=ED=87=B4=20=EC=8B=9C?= =?UTF-8?q?=20=EC=97=B0=EA=B4=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=84?= =?UTF-8?q?=EB=B6=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/member/facade/LeaveFacade.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt index ad876386..660dae60 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -35,9 +35,13 @@ class LeaveFacade( val partner = coupleInfoService.findCoupleMember(memberId) anniversaryService.deleteAllByCoupleId(coupleId) commentService.deleteAllByMemberId(memberId) + commentService.deleteAllByMemberId(partner.id) coupleService.deleteByMemberId(memberId) + coupleService.deleteByMemberId(partner.id) notificationService.deleteAllByMember(memberId) + notificationService.deleteAllByMember(partner.id) scheduleService.deleteAllByMemberId(memberId) + scheduleService.deleteAllByMemberId(partner.id) val letters = letterService.findAllByAuthorId(memberId) val pictureUrlsToDelete = mutableListOf() @@ -49,8 +53,8 @@ class LeaveFacade( pictureService.deleteAllByLetterIds(letters) letterService.deleteAllByMemberId(memberId) + letterService.deleteAllByMemberId(partner.id) memberService.deleteMember(memberId) - partner.updateIsCouple(false) if (pictureUrlsToDelete.isNotEmpty()) { applicationEventPublisher.publishEvent( @@ -61,3 +65,4 @@ class LeaveFacade( } } } + From 9bbc1873011c1859aecade1cc109ced3cdeebf33 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 25 May 2025 23:47:16 +0900 Subject: [PATCH 307/357] =?UTF-8?q?fix:=20=EC=A4=91=EB=B3=B5=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EB=B3=B4=EC=9E=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=9C=EC=84=9C=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/gomushin/backend/member/facade/LeaveFacade.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt index 660dae60..35468253 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -37,7 +37,6 @@ class LeaveFacade( commentService.deleteAllByMemberId(memberId) commentService.deleteAllByMemberId(partner.id) coupleService.deleteByMemberId(memberId) - coupleService.deleteByMemberId(partner.id) notificationService.deleteAllByMember(memberId) notificationService.deleteAllByMember(partner.id) scheduleService.deleteAllByMemberId(memberId) @@ -54,8 +53,8 @@ class LeaveFacade( pictureService.deleteAllByLetterIds(letters) letterService.deleteAllByMemberId(memberId) letterService.deleteAllByMemberId(partner.id) - memberService.deleteMember(memberId) partner.updateIsCouple(false) + memberService.deleteMember(memberId) if (pictureUrlsToDelete.isNotEmpty()) { applicationEventPublisher.publishEvent( S3DeleteEvent( From e43c02424447dc84aa7737ef417e38bc4704e544 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 25 May 2025 23:59:38 +0900 Subject: [PATCH 308/357] =?UTF-8?q?feat:=20=ED=8C=8C=ED=8A=B8=EB=84=88?= =?UTF-8?q?=EA=B0=80=20=EC=93=B4=20=ED=8E=B8=EC=A7=80=EC=9D=98=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=EB=8F=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/facade/LeaveFacade.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt index 35468253..aae35066 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -42,18 +42,26 @@ class LeaveFacade( scheduleService.deleteAllByMemberId(memberId) scheduleService.deleteAllByMemberId(partner.id) - val letters = letterService.findAllByAuthorId(memberId) + val memberLetters = letterService.findAllByAuthorId(memberId) + val partnerLetters = letterService.findAllByAuthorId(partner.id) + val pictureUrlsToDelete = mutableListOf() - pictureService.findAllByLetterIds(letters) + + pictureService.findAllByLetterIds(memberLetters) + .takeIf { it.isNotEmpty() } + ?.forEach { picture -> pictureUrlsToDelete.add(picture.pictureUrl) } + pictureService.findAllByLetterIds(partnerLetters) .takeIf { it.isNotEmpty() } - ?.forEach { picture -> - pictureUrlsToDelete.add(picture.pictureUrl) - } + ?.forEach { picture -> pictureUrlsToDelete.add(picture.pictureUrl) } + + pictureService.deleteAllByLetterIds(memberLetters) + pictureService.deleteAllByLetterIds(partnerLetters) - pictureService.deleteAllByLetterIds(letters) letterService.deleteAllByMemberId(memberId) letterService.deleteAllByMemberId(partner.id) + partner.updateIsCouple(false) + memberService.deleteMember(memberId) if (pictureUrlsToDelete.isNotEmpty()) { applicationEventPublisher.publishEvent( From 906d1b97ee19fdce9b108c61c743440d0d44c276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 25 May 2025 22:59:41 +0900 Subject: [PATCH 309/357] =?UTF-8?q?feat=20:=20=EC=9E=85=EB=8C=80=EC=9D=BC,?= =?UTF-8?q?=20=EC=A0=84=EC=97=AD=EC=9D=BC=20=EC=88=98=EC=A0=95=20=EC=8B=9C?= =?UTF-8?q?=20Dday=EC=B4=88=EA=B8=B0=EC=84=A4=EC=A0=95=ED=95=9C=EA=B1=B0?= =?UTF-8?q?=20=EB=8B=A4=EC=8B=9C=20=EC=9E=AC=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20#134?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/couple/domain/entity/Anniversary.kt | 6 +++++- .../couple/domain/repository/AnniversaryRepository.kt | 4 ++++ .../backend/couple/domain/service/AnniversaryService.kt | 5 +++++ .../kotlin/gomushin/backend/couple/facade/CoupleFacade.kt | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt index 229dc13c..9bc62f3e 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt @@ -27,6 +27,9 @@ class Anniversary( @Enumerated(EnumType.STRING) @Column(name = "anniversary_emoji") var emoji: AnniversaryEmoji? = null, + + @Column(name = "is_auto_insert") + var isAutoInsert: Boolean = false ) : BaseEntity() { companion object { fun autoCreate(coupleId: Long, title: String, anniversaryDate: LocalDate): Anniversary { @@ -35,7 +38,8 @@ class Anniversary( title = title, anniversaryDate = anniversaryDate, anniversaryProperty = 0, - emoji = AnniversaryEmoji.HEART + emoji = AnniversaryEmoji.HEART, + isAutoInsert = true ) } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt index 17af1c41..9eeda508 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/repository/AnniversaryRepository.kt @@ -119,4 +119,8 @@ interface AnniversaryRepository : JpaRepository { nativeQuery = true ) fun findTodayAnniversaryMemberFcmTokens(@Param("nowDate") date: LocalDate): List + + @Modifying + @Query("DELETE FROM Anniversary a WHERE a.coupleId = :coupleId AND a.isAutoInsert = true") + fun deleteAllByCoupleIdAndAutoInsertTrue(@Param("coupleId") coupleId: Long) } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index e06e5559..1ac95af6 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -96,4 +96,9 @@ class AnniversaryService( } anniversaryRepository.deleteById(anniversaryId) } + + @Transactional + fun deleteAllByCoupleIdAndAutoInsert(couple: Couple) { + return anniversaryRepository.deleteAllByCoupleIdAndAutoInsertTrue(couple.id) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 882a1e49..1215d51a 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -96,6 +96,7 @@ class CoupleFacade( fun updateMilitaryDate(customUserDetails: CustomUserDetails, updateMilitaryDateRequest: UpdateMilitaryDateRequest) { val couple = coupleService.getByMemberId(customUserDetails.getId()) + anniversaryService.deleteAllByCoupleIdAndAutoInsert(customUserDetails.getCouple()) coupleInfoService.updateMilitaryDate(couple, updateMilitaryDateRequest) } From b00eb4f1cb3bd63394300ce86f0051e1ddf588b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sun, 25 May 2025 23:09:34 +0900 Subject: [PATCH 310/357] =?UTF-8?q?test=20:=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=85=90=EC=9D=BC=20=EC=82=AD=EC=A0=9C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B1=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#134?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/AnniversaryServiceTest.kt | 20 ++++++++++++++++--- .../backend/couple/facade/CoupleFacadeTest.kt | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt index e34b6e58..4bd66b83 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt @@ -6,12 +6,10 @@ import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse import gomushin.backend.schedule.dto.response.MainAnniversariesResponse -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.junit5.MockKExtension -import io.mockk.mockk -import io.mockk.verify import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -184,5 +182,21 @@ class AnniversaryServiceTest { "반환된 기념일은 시간 순으로 정렬되어야 합니다." ) } + + @DisplayName("deleteAllByCoupleIdAndAutoInsert는 anniversaryRepository의 deleteAllByCoupleIdAndAutoInsertTrue메서드 호출") + @Test + fun deleteAllByCoupleIdAndAutoInsert() { + //given + val couple = Couple(1L, 1L, 2L) + every { + anniversaryRepository.deleteAllByCoupleIdAndAutoInsertTrue(couple.id) + } just Runs + //when + anniversaryService.deleteAllByCoupleIdAndAutoInsert(couple) + //then + verify(exactly = 1) { + anniversaryRepository.deleteAllByCoupleIdAndAutoInsertTrue(couple.id) + } + } } diff --git a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt index 1cc65e2a..68fd3758 100644 --- a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt @@ -165,6 +165,7 @@ class CoupleFacadeTest { ) val result = coupleFacade.updateMilitaryDate(customUserDetails, updateMilitaryDateRequest) verify(coupleInfoService).updateMilitaryDate(customUserDetails.getCouple(), updateMilitaryDateRequest) + verify(anniversaryService).deleteAllByCoupleIdAndAutoInsert(customUserDetails.getCouple()) } @DisplayName("만난날 수정 - 정상응답") From ddc87ee84fc75b7d748e9f593032196d7d26129c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 26 May 2025 00:04:55 +0900 Subject: [PATCH 311/357] =?UTF-8?q?refactor=20:=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=BC=EB=A7=81=20=EC=BD=A4=EB=A7=88=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#134?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gomushin/backend/couple/domain/entity/Anniversary.kt | 4 ++-- .../backend/couple/domain/service/AnniversaryService.kt | 2 +- .../kotlin/gomushin/backend/couple/facade/CoupleFacade.kt | 2 +- .../backend/couple/domain/service/AnniversaryServiceTest.kt | 2 +- .../kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt index 9bc62f3e..75e9acff 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt @@ -29,7 +29,7 @@ class Anniversary( var emoji: AnniversaryEmoji? = null, @Column(name = "is_auto_insert") - var isAutoInsert: Boolean = false + var isAutoInsert: Boolean = false, ) : BaseEntity() { companion object { fun autoCreate(coupleId: Long, title: String, anniversaryDate: LocalDate): Anniversary { @@ -39,7 +39,7 @@ class Anniversary( anniversaryDate = anniversaryDate, anniversaryProperty = 0, emoji = AnniversaryEmoji.HEART, - isAutoInsert = true + isAutoInsert = true, ) } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index 1ac95af6..ce1fed30 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -98,7 +98,7 @@ class AnniversaryService( } @Transactional - fun deleteAllByCoupleIdAndAutoInsert(couple: Couple) { + fun deleteAllByCoupleAndAutoInsert(couple: Couple) { return anniversaryRepository.deleteAllByCoupleIdAndAutoInsertTrue(couple.id) } } diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 1215d51a..7edda5a7 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -96,7 +96,7 @@ class CoupleFacade( fun updateMilitaryDate(customUserDetails: CustomUserDetails, updateMilitaryDateRequest: UpdateMilitaryDateRequest) { val couple = coupleService.getByMemberId(customUserDetails.getId()) - anniversaryService.deleteAllByCoupleIdAndAutoInsert(customUserDetails.getCouple()) + anniversaryService.deleteAllByCoupleAndAutoInsert(couple) coupleInfoService.updateMilitaryDate(couple, updateMilitaryDateRequest) } diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt index 4bd66b83..6e240cc1 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/AnniversaryServiceTest.kt @@ -192,7 +192,7 @@ class AnniversaryServiceTest { anniversaryRepository.deleteAllByCoupleIdAndAutoInsertTrue(couple.id) } just Runs //when - anniversaryService.deleteAllByCoupleIdAndAutoInsert(couple) + anniversaryService.deleteAllByCoupleAndAutoInsert(couple) //then verify(exactly = 1) { anniversaryRepository.deleteAllByCoupleIdAndAutoInsertTrue(couple.id) diff --git a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt index 68fd3758..6b643fb6 100644 --- a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt @@ -165,7 +165,7 @@ class CoupleFacadeTest { ) val result = coupleFacade.updateMilitaryDate(customUserDetails, updateMilitaryDateRequest) verify(coupleInfoService).updateMilitaryDate(customUserDetails.getCouple(), updateMilitaryDateRequest) - verify(anniversaryService).deleteAllByCoupleIdAndAutoInsert(customUserDetails.getCouple()) + verify(anniversaryService).deleteAllByCoupleAndAutoInsert(customUserDetails.getCouple()) } @DisplayName("만난날 수정 - 정상응답") From 2f6f5d05b27db7df187be986bdf6ee41dc5ef5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 26 May 2025 00:05:43 +0900 Subject: [PATCH 312/357] =?UTF-8?q?chore=20:=20Transactional=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20#134?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt index 7edda5a7..204b2428 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/CoupleFacade.kt @@ -94,6 +94,7 @@ class CoupleFacade( return StatusMessageResponse.of(statusMessage) } + @Transactional fun updateMilitaryDate(customUserDetails: CustomUserDetails, updateMilitaryDateRequest: UpdateMilitaryDateRequest) { val couple = coupleService.getByMemberId(customUserDetails.getId()) anniversaryService.deleteAllByCoupleAndAutoInsert(couple) From 1ddd9fc8900130c3930ef3161f0aa80bfb01b732 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 26 May 2025 00:40:29 +0900 Subject: [PATCH 313/357] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/AnniversaryDetailResponse.kt | 20 +++++++++++++++++++ .../couple/facade/AnniversaryFacade.kt | 12 +++++++++++ .../presentation/AnniversaryController.kt | 19 +++++++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/response/AnniversaryDetailResponse.kt diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/AnniversaryDetailResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/AnniversaryDetailResponse.kt new file mode 100644 index 00000000..817d64e8 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/AnniversaryDetailResponse.kt @@ -0,0 +1,20 @@ +package gomushin.backend.couple.dto.response + +import gomushin.backend.couple.domain.entity.Anniversary +import java.time.LocalDate + +data class AnniversaryDetailResponse( + val id: Long, + val title: String, + val anniversaryDate: LocalDate, +) { + companion object { + fun of(anniversary: Anniversary): AnniversaryDetailResponse { + return AnniversaryDetailResponse( + id = anniversary.id, + title = anniversary.title, + anniversaryDate = anniversary.anniversaryDate, + ) + } + } +} diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt index d9c8a489..9d80cf2e 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -2,7 +2,9 @@ package gomushin.backend.couple.facade import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.PageResponse +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.dto.response.AnniversaryDetailResponse import gomushin.backend.couple.dto.response.MainAnniversaryResponse import gomushin.backend.couple.dto.response.TotalAnniversaryResponse import org.springframework.data.domain.PageRequest @@ -32,7 +34,17 @@ class AnniversaryFacade( return PageResponse.from(anniversaryResponses) } + fun get(customUserDetails: CustomUserDetails, anniversaryId: Long): AnniversaryDetailResponse { + val anniversary = anniversaryService.getById(anniversaryId) + if (anniversary.coupleId != customUserDetails.getCouple().id) { + throw BadRequestException("sarangggun.anniversary.unauthorized") + } + return AnniversaryDetailResponse.of(anniversary) + } + fun delete(customUserDetails: CustomUserDetails, anniversaryId: Long) { anniversaryService.delete(customUserDetails.getCouple(), anniversaryId) } + + } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt index 0fb1afc1..86c703d5 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.PageResponse import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest +import gomushin.backend.couple.dto.response.AnniversaryDetailResponse import gomushin.backend.couple.dto.response.MainAnniversaryResponse import gomushin.backend.couple.dto.response.TotalAnniversaryResponse import gomushin.backend.couple.facade.AnniversaryFacade @@ -15,7 +16,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.* @RestController -@Tag(name = "기념일 생성", description = "AnniversaryController") +@Tag(name = "기념일", description = "AnniversaryController") class AnniversaryController( private val coupleFacade: CoupleFacade, private val anniversaryFacade: AnniversaryFacade @@ -74,4 +75,20 @@ class AnniversaryController( val anniversaries = anniversaryFacade.getAnniversaryList(customUserDetails, safePage, size) return anniversaries } + + @ResponseStatus(HttpStatus.OK) + @GetMapping(ApiPath.ANNIVERSARY) + @Operation( + summary = "기념일 상세 조회", + description = "getAnniversary" + ) + fun getAnniversary( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable anniversaryId: Long + ): ApiResponse { + val anniversary = anniversaryFacade.get(customUserDetails, anniversaryId) + return ApiResponse.success(anniversary) + } + + } From a94ecf720ced86e142286ebca367607b64dbfa74 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 26 May 2025 00:24:09 +0900 Subject: [PATCH 314/357] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/dto/request/UpdateAnniversaryRequest.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/couple/dto/request/UpdateAnniversaryRequest.kt diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateAnniversaryRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateAnniversaryRequest.kt new file mode 100644 index 00000000..c582fed1 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/UpdateAnniversaryRequest.kt @@ -0,0 +1,9 @@ +package gomushin.backend.couple.dto.request + +import java.time.LocalDate + +data class UpdateAnniversaryRequest( + val title: String? = null, + val emoji: String? = null, + val anniversaryDate: LocalDate? = null +) From 1da54685ad8693c1cf765d762d9c3f588b388cf1 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 26 May 2025 00:24:34 +0900 Subject: [PATCH 315/357] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=A9=94=EC=84=9C=EB=93=9C=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 --- .../gomushin/backend/couple/domain/entity/Anniversary.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt index 75e9acff..caa4607f 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/entity/Anniversary.kt @@ -57,6 +57,11 @@ class Anniversary( emoji = emoji ) } + } + fun update(title: String?, anniversaryDate: LocalDate?, emoji: AnniversaryEmoji?) { + title?.let { this.title = it } + anniversaryDate?.let { this.anniversaryDate = it } + emoji?.let { this.emoji = it } } } From 467e60c1d9108608fdea1c7e1db13a2112b887dd Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 26 May 2025 00:24:48 +0900 Subject: [PATCH 316/357] =?UTF-8?q?feat:=20=EA=B8=B0=EB=85=90=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/AnniversaryService.kt | 15 +++++++++++++++ .../couple/domain/value/AnniversaryEmoji.kt | 10 ++++++++-- .../backend/couple/facade/AnniversaryFacade.kt | 9 ++++++++- .../couple/presentation/AnniversaryController.kt | 16 ++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt index ce1fed30..4977611d 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/AnniversaryService.kt @@ -4,7 +4,9 @@ import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.entity.Anniversary import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.couple.domain.repository.AnniversaryRepository +import gomushin.backend.couple.domain.value.AnniversaryEmoji import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest +import gomushin.backend.couple.dto.request.UpdateAnniversaryRequest import gomushin.backend.couple.dto.response.AnniversaryNotificationInfo import gomushin.backend.couple.dto.response.MonthlyAnniversariesResponse import gomushin.backend.schedule.dto.response.DailyAnniversaryResponse @@ -101,4 +103,17 @@ class AnniversaryService( fun deleteAllByCoupleAndAutoInsert(couple: Couple) { return anniversaryRepository.deleteAllByCoupleIdAndAutoInsertTrue(couple.id) } + + @Transactional + fun update(couple: Couple, anniversaryId: Long, updateAnniversaryRequest: UpdateAnniversaryRequest) { + val anniversary = getById(anniversaryId) + if (anniversary.coupleId != couple.id) { + throw BadRequestException("sarangggun.anniversary.unauthorized") + } + anniversary.update( + title = updateAnniversaryRequest.title, + anniversaryDate = updateAnniversaryRequest.anniversaryDate, + emoji = AnniversaryEmoji.getByName(updateAnniversaryRequest.emoji) + ) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/domain/value/AnniversaryEmoji.kt b/src/main/kotlin/gomushin/backend/couple/domain/value/AnniversaryEmoji.kt index 5b1e4825..b1f64655 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/value/AnniversaryEmoji.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/value/AnniversaryEmoji.kt @@ -1,5 +1,11 @@ package gomushin.backend.couple.domain.value enum class AnniversaryEmoji { - HEART, CALENDAR, CAKE, TRAVEL -} \ No newline at end of file + HEART, CALENDAR, CAKE, TRAVEL; + + companion object { + fun getByName(name: String?): AnniversaryEmoji? { + return entries.find { it.name.equals(name, ignoreCase = true) } + } + } +} diff --git a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt index 9d80cf2e..d3224413 100644 --- a/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt +++ b/src/main/kotlin/gomushin/backend/couple/facade/AnniversaryFacade.kt @@ -4,6 +4,7 @@ import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.PageResponse import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.dto.request.UpdateAnniversaryRequest import gomushin.backend.couple.dto.response.AnniversaryDetailResponse import gomushin.backend.couple.dto.response.MainAnniversaryResponse import gomushin.backend.couple.dto.response.TotalAnniversaryResponse @@ -46,5 +47,11 @@ class AnniversaryFacade( anniversaryService.delete(customUserDetails.getCouple(), anniversaryId) } - + fun updateAnniversary( + customUserDetails: CustomUserDetails, + anniversaryId: Long, + updateAnniversaryRequest: UpdateAnniversaryRequest, + ) { + anniversaryService.update(customUserDetails.getCouple(), anniversaryId, updateAnniversaryRequest) + } } diff --git a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt index 86c703d5..aa12f75c 100644 --- a/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt +++ b/src/main/kotlin/gomushin/backend/couple/presentation/AnniversaryController.kt @@ -5,6 +5,7 @@ import gomushin.backend.core.common.web.PageResponse import gomushin.backend.core.common.web.response.ApiResponse import gomushin.backend.couple.dto.request.GenerateAnniversaryRequest import gomushin.backend.couple.dto.response.AnniversaryDetailResponse +import gomushin.backend.couple.dto.request.UpdateAnniversaryRequest import gomushin.backend.couple.dto.response.MainAnniversaryResponse import gomushin.backend.couple.dto.response.TotalAnniversaryResponse import gomushin.backend.couple.facade.AnniversaryFacade @@ -35,6 +36,21 @@ class AnniversaryController( return ApiResponse.success(true) } + @ResponseStatus(HttpStatus.OK) + @PutMapping(ApiPath.ANNIVERSARY) + @Operation( + summary = "기념일 수정", + description = "updateAnniversary" + ) + fun updateAnniversary( + @AuthenticationPrincipal customUserDetails: CustomUserDetails, + @PathVariable anniversaryId: Long, + @RequestBody updateAnniversaryRequest: UpdateAnniversaryRequest, + ): ApiResponse { + anniversaryFacade.updateAnniversary(customUserDetails, anniversaryId, updateAnniversaryRequest) + return ApiResponse.success(true) + } + @ResponseStatus(HttpStatus.OK) @DeleteMapping(ApiPath.ANNIVERSARY) @Operation( From 92a76dafe6372a10eb128cdea912ee7784c3d387 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 26 May 2025 00:29:32 +0900 Subject: [PATCH 317/357] =?UTF-8?q?refactor:=20dto=EC=97=90=20=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=BC=EB=A7=81=20=EC=BD=A4=EB=A7=88=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 --- .../backend/couple/dto/request/GenerateAnniversaryRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/couple/dto/request/GenerateAnniversaryRequest.kt b/src/main/kotlin/gomushin/backend/couple/dto/request/GenerateAnniversaryRequest.kt index 2bfa93f8..ccf8ff88 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/request/GenerateAnniversaryRequest.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/request/GenerateAnniversaryRequest.kt @@ -10,5 +10,5 @@ data class GenerateAnniversaryRequest( @Schema(description = "이모지", example = "HEART, CALENDAR, CAKE, TRAVEL") val emoji : AnniversaryEmoji, @Schema(description = "날짜", example = "2025-05-01") - val date : LocalDate + val date : LocalDate, ) From cda9833238e0adeb3c02d1d62e86ae632fc0805d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 26 May 2025 02:18:30 +0900 Subject: [PATCH 318/357] =?UTF-8?q?fix=20:=20=EB=94=94=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EC=96=91?= =?UTF-8?q?=EC=88=98=EC=9D=B4=EB=A9=B4=20+=EB=B6=99=EC=9D=B4=EA=B3=A0,=200?= =?UTF-8?q?=EC=9D=B4=EB=A9=B4=20-DAY=EB=A1=9C=20=ED=91=9C=EA=B8=B0?= =?UTF-8?q?=EB=90=98=EA=B2=8C=EB=81=94=20=EC=88=98=EC=A0=95=20#140?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/dto/response/DdayResponse.kt | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt b/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt index 8e08c78b..d599bce4 100644 --- a/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt +++ b/src/main/kotlin/gomushin/backend/couple/dto/response/DdayResponse.kt @@ -1,17 +1,24 @@ package gomushin.backend.couple.dto.response data class DdayResponse ( - val sinceLove : Int?, - val sinceMilitaryStart : Int?, - val militaryEndLeft : Int? + val sinceLove : String?, + val sinceMilitaryStart : String?, + val militaryEndLeft : String? ){ companion object{ - fun of(sinceLove: Int?, - sinceMilitaryStart: Int?, - militaryEndLeft: Int?) = DdayResponse ( - sinceLove, - sinceMilitaryStart, - militaryEndLeft - ) + fun of( + sinceLove: Int?, + sinceMilitaryStart: Int?, + militaryEndLeft: Int? + ): DdayResponse { + fun format(day: Int?): String? = + day?.let { if (it > 0) "+$it" else if (it == 0) "-DAY" else it.toString() } + + return DdayResponse( + sinceLove = format(sinceLove), + sinceMilitaryStart = format(sinceMilitaryStart), + militaryEndLeft = format(militaryEndLeft) + ) + } } } \ No newline at end of file From cd0dff8f619f255aff6d9bb1d2814e6a2a28cb4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 26 May 2025 02:18:52 +0900 Subject: [PATCH 319/357] =?UTF-8?q?test=20:=20=EC=9D=91=EB=8B=B5=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EA=B0=80=20=EB=8B=AC=EB=9D=BC=EC=A7=90=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20#140?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleInfoServiceTest.kt | 13 ++++++++++--- .../backend/couple/facade/CoupleFacadeTest.kt | 12 +++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt index 1461a28f..7a7dd71c 100644 --- a/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/domain/service/CoupleInfoServiceTest.kt @@ -6,6 +6,7 @@ import gomushin.backend.couple.domain.repository.AnniversaryRepository import gomushin.backend.couple.domain.repository.CoupleRepository import gomushin.backend.couple.dto.request.UpdateMilitaryDateRequest import gomushin.backend.couple.dto.request.UpdateRelationshipStartDateRequest +import gomushin.backend.couple.dto.response.DdayResponse import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.repository.MemberRepository import gomushin.backend.member.domain.value.Emotion @@ -208,15 +209,21 @@ class CoupleInfoServiceTest { val militaryStartDate = couple.militaryStartDate!! val militaryEndDate = couple.militaryEndDate!! val relationshipStartDate = couple.relationshipStartDate!! + val expectedResponse = DdayResponse.of( + coupleInfoService.computeDday(relationshipStartDate, today) + 1, + coupleInfoService.computeDday(militaryStartDate, today), + coupleInfoService.computeDday(militaryEndDate, today), + ) + `when`(coupleRepository.findByMemberId(invitorId)).thenReturn(couple) //when val response = coupleInfoService.getDday(invitorId) //then - assertEquals(coupleInfoService.computeDday(militaryStartDate, today), response.sinceMilitaryStart) - assertEquals(coupleInfoService.computeDday(militaryEndDate, today), response.militaryEndLeft) - assertEquals(coupleInfoService.computeDday(relationshipStartDate, today) + 1, response.sinceLove) + assertEquals(expectedResponse.sinceMilitaryStart, response.sinceMilitaryStart) + assertEquals(expectedResponse.militaryEndLeft, response.militaryEndLeft) + assertEquals(expectedResponse.sinceLove, response.sinceLove) } @DisplayName("nickName-성공") diff --git a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt index 6b643fb6..92a6719f 100644 --- a/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/couple/facade/CoupleFacadeTest.kt @@ -113,20 +113,22 @@ class CoupleFacadeTest { @DisplayName("디데이 조회 - 정상응답") @Test fun getDday(){ + val expectedResponse = DdayResponse.of(100, 200,-345) `when`(customUserDetails.getId()).thenReturn(1L) - `when`(coupleInfoService.getDday(customUserDetails.getId())).thenReturn(DdayResponse(100, 200, -345)) + `when`(coupleInfoService.getDday(customUserDetails.getId())).thenReturn(expectedResponse) val result = coupleFacade.getDday(customUserDetails) verify(coupleInfoService).getDday(1L) - assertEquals(100, result.sinceLove) - assertEquals(200, result.sinceMilitaryStart) - assertEquals(-345, result.militaryEndLeft) + assertEquals(expectedResponse.sinceLove, result.sinceLove) + assertEquals(expectedResponse.sinceMilitaryStart, result.sinceMilitaryStart) + assertEquals(expectedResponse.militaryEndLeft, result.militaryEndLeft) } @DisplayName("디데이 조회 - 날짜 정보 안 들어갔을 때") @Test fun getDdayNull(){ + val expectedResponse = DdayResponse.of(null, null,null) `when`(customUserDetails.getId()).thenReturn(1L) - `when`(coupleInfoService.getDday(customUserDetails.getId())).thenReturn(DdayResponse(null, null, null)) + `when`(coupleInfoService.getDday(customUserDetails.getId())).thenReturn(expectedResponse) val result = coupleFacade.getDday(customUserDetails) verify(coupleInfoService).getDday(1L) assertEquals(null, result.sinceLove) From 45a7aefdf30ce3dd07bb6d3bae6f3d50e1a161d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 26 May 2025 02:44:35 +0900 Subject: [PATCH 320/357] =?UTF-8?q?refactor=20:=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9D=98=20=ED=8E=B8=EB=A6=AC=ED=95=A8=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=EC=84=9C(=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=8B=9C=EA=B0=81=EB=B0=94=EA=BF=80=20?= =?UTF-8?q?=EB=95=8C=20yml=ED=8C=8C=EC=9D=BC=EB=A7=8C=20=EC=A1=B0=EC=A0=95?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20reaction=EC=8B=9C=ED=82=A4=EB=A9=B4=20?= =?UTF-8?q?=EB=90=98=EA=B2=8C=EB=81=94)=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=EB=A7=81=20=ED=81=AC=EB=A1=A0=EC=8B=9D=20yml=EB=A1=9C=20?= =?UTF-8?q?=EB=B9=BC=EB=82=B4=EA=B8=B0=20#142?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt | 2 +- .../kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt index dd77de2d..ce0431be 100644 --- a/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt @@ -23,7 +23,7 @@ class DdayAlarmFacade( private val log: Logger = LoggerFactory.getLogger(QuestionAlarmFacade::class.java) private val alarmTitle = "오늘의 디데이가 도착했어요" - @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") + @Scheduled(cron = "\${scheduling.cron.d-day}", zone = "\${scheduling.zone.seoul}") fun sendDdayAlarms() { val sendDdayAlarmContents = anniversaryService.getTodayAnniversaryMemberFcmTokens(LocalDate.now()) log.info("전송 크기 : ${sendDdayAlarmContents.size}") diff --git a/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt index d964d4ab..852bf841 100644 --- a/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt @@ -28,7 +28,7 @@ class QuestionAlarmFacade ( "매일 사랑이 자라요+가벼운 전화통화 어때요?" ) - @Scheduled(cron = "0 0 19 * * *", zone = "Asia/Seoul") + @Scheduled(cron = "\${scheduling.cron.question}", zone = "\${scheduling.zone.seoul}") fun sendQuestionAlarms() { val coupleMembers = memberService.getAllCoupledMemberWithEnabledNotification() runBlocking { From 92814e1339353f1ac7fb4ae292c4bb10ac49a772 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 26 May 2025 20:25:17 +0900 Subject: [PATCH 321/357] =?UTF-8?q?feat:=20=ED=8E=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20=EC=82=AC=EC=A7=84=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/domain/service/PictureService.kt | 12 +- .../dto/request/UpsertLetterRequest.kt | 2 + .../facade/UpsertAndDeleteLetterFacade.kt | 24 ++- .../facade/UpsertAndDeleteLetterFacadeTest.kt | 138 ------------------ 4 files changed, 31 insertions(+), 145 deletions(-) delete mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt index 16da3566..cd177bd5 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/PictureService.kt @@ -22,6 +22,11 @@ class PictureService( return pictureRepository.findAllByPictureUrlIn(urls) } + @Transactional(readOnly = true) + fun findAllByLetterId(letterId: Long): List { + return pictureRepository.findAllByLetterId(letterId) + } + @Transactional(readOnly = true) fun findFirstByLetterId(letterId: Long): Picture? { return pictureRepository.findFirstByLetterIdOrderByIdAsc(letterId) @@ -43,7 +48,7 @@ class PictureService( } @Transactional(readOnly = true) - fun findAllByLetterIds(letterIds : List) : List { + fun findAllByLetterIds(letterIds: List): List { return pictureRepository.findAllByLetterIdIn(letterIds) } @@ -51,4 +56,9 @@ class PictureService( fun deleteAllByLetterIds(letterIds: List) { pictureRepository.deleteAllByLetterIdIn(letterIds) } + + @Transactional + fun deleteAll(pictures: List) { + pictureRepository.deleteAll(pictures) + } } diff --git a/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertLetterRequest.kt b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertLetterRequest.kt index de50355a..f7f9b68a 100644 --- a/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertLetterRequest.kt +++ b/src/main/kotlin/gomushin/backend/schedule/dto/request/UpsertLetterRequest.kt @@ -11,4 +11,6 @@ data class UpsertLetterRequest( val title: String, @Schema(description = "편지 내용", example = "화이팅") val content: String, + @Schema(description = "편지 사진 URL 목록", example = "[\"https://example.com/picture1.jpg\", \"https://example.com/picture2.jpg\"]") + val pictureUrls : List = emptyList(), ) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt index 4260f461..16f7859c 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/UpsertAndDeleteLetterFacade.kt @@ -1,12 +1,15 @@ package gomushin.backend.schedule.facade import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.event.dto.S3DeleteEvent import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.core.service.S3Service +import gomushin.backend.schedule.domain.entity.Picture import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.domain.service.PictureService import gomushin.backend.schedule.domain.service.ScheduleService import gomushin.backend.schedule.dto.request.UpsertLetterRequest +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile @@ -17,6 +20,7 @@ class UpsertAndDeleteLetterFacade( private val s3Service: S3Service, private val pictureService: PictureService, private val scheduleService: ScheduleService, + private val applicationEventPublisher: ApplicationEventPublisher, ) { // TODO: 이벤트 드리븐하게 수정 @Transactional @@ -39,14 +43,22 @@ class UpsertAndDeleteLetterFacade( upsertLetterRequest ) - val pictureList = mutableListOf() + val existingPictures = pictureService.findAllByLetterId(letter.id) + val existingUrls = existingPictures.map { it.pictureUrl } - pictures?.let { - it.forEach { picture -> - val fileUrl = s3Service.uploadFile(picture) - pictureList.add(fileUrl) + val toDelete = existingPictures.filter { it.pictureUrl !in upsertLetterRequest.pictureUrls } + val pictureUrlsToDelete = toDelete.map { it.pictureUrl } + pictureService.deleteAll(toDelete) + + val uploadedUrls = pictures?.map { s3Service.uploadFile(it) } ?: emptyList() + uploadedUrls + .filter { it !in existingUrls } + .forEach { url -> + pictureService.saveAll(listOf(Picture(letterId = letter.id, pictureUrl = url))) } - pictureService.upsert(letter.id, pictureList) + + if (pictureUrlsToDelete.isNotEmpty()) { + applicationEventPublisher.publishEvent(S3DeleteEvent(pictureUrlsToDelete)) } } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt deleted file mode 100644 index 23606431..00000000 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteLetterFacadeTest.kt +++ /dev/null @@ -1,138 +0,0 @@ -package gomushin.backend.schedule.domain.facade - -import gomushin.backend.core.CustomUserDetails -import gomushin.backend.core.service.S3Service -import gomushin.backend.couple.domain.entity.Couple -import gomushin.backend.schedule.domain.entity.Letter -import gomushin.backend.schedule.domain.entity.Schedule -import gomushin.backend.schedule.domain.service.LetterService -import gomushin.backend.schedule.domain.service.PictureService -import gomushin.backend.schedule.domain.service.ScheduleService -import gomushin.backend.schedule.dto.request.UpsertLetterRequest -import gomushin.backend.schedule.facade.UpsertAndDeleteLetterFacade -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.InjectMocks -import org.mockito.Mock -import org.mockito.Mockito.* -import org.mockito.junit.jupiter.MockitoExtension -import org.springframework.web.multipart.MultipartFile - -@ExtendWith(MockitoExtension::class) -class UpsertAndDeleteLetterFacadeTest { - - @Mock - private lateinit var letterService: LetterService - - @Mock - private lateinit var s3Service: S3Service - - @Mock - private lateinit var pictureService: PictureService - - @Mock - private lateinit var scheduleService: ScheduleService - - @InjectMocks - private lateinit var upsertAndDeleteLetterFacade: UpsertAndDeleteLetterFacade - - - @Nested - inner class Upsert { - - @DisplayName("업로드 성공 - 업로드된 사진이 있을 때") - @Test - fun upsertWithPictures_success() { - // given - val customUserDetails = mock(CustomUserDetails::class.java) - val upsertLetterRequest = UpsertLetterRequest( - title = "제목", - content = "내용", - scheduleId = 1L - ) - val pictures = listOf(mock(MultipartFile::class.java)) - val schedule = mock(Schedule::class.java) - val letter = mock(Letter::class.java) - val couple = mock(Couple::class.java) - - // when - `when`(letter.id).thenReturn(1L) - `when`(customUserDetails.getId()).thenReturn(1L) - `when`(scheduleService.getById(upsertLetterRequest.scheduleId)).thenReturn(schedule) - `when`(customUserDetails.getCouple()).thenReturn(couple) - `when`(customUserDetails.getCouple().id).thenReturn(1L) - `when`(customUserDetails.username).thenReturn("테스트 이름") - `when`(schedule.coupleId).thenReturn(1L) - `when`(letterService.upsert(1L, customUserDetails.username, couple, upsertLetterRequest)).thenReturn(letter) - `when`(s3Service.uploadFile(pictures[0])).thenReturn("http://example.com/test.jpg") - doNothing().`when`(pictureService).upsert(1L, listOf("http://example.com/test.jpg")) - upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) - - // then - verify( - letterService, - times(1) - ).upsert(1L, "테스트 이름", couple, upsertLetterRequest) - } - - @DisplayName("업로드 성공 - 업로드된 사진이 없을 때") - @Test - fun upsertWithoutPictures_success() { - // given - val customUserDetails = mock(CustomUserDetails::class.java) - val upsertLetterRequest = UpsertLetterRequest( - title = "제목", - content = "내용", - scheduleId = 1L - ) - val pictures: List? = null - val letter = mock(Letter::class.java) - val couple = mock(Couple::class.java) - val schedule = mock(Schedule::class.java) - - // when - `when`(customUserDetails.getId()).thenReturn(1L) - `when`(scheduleService.getById(upsertLetterRequest.scheduleId)).thenReturn(schedule) - `when`(customUserDetails.getCouple()).thenReturn(couple) - `when`(customUserDetails.getCouple().id).thenReturn(1L) - `when`(customUserDetails.username).thenReturn("테스트 이름") - `when`(schedule.coupleId).thenReturn(1L) - `when`(letterService.upsert(1L, customUserDetails.username, couple, upsertLetterRequest)).thenReturn(letter) - upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) - - // then - verify(letterService, times(1)).upsert(1L, "테스트 이름", couple, upsertLetterRequest) - verify(s3Service, never()).uploadFile(org.mockito.kotlin.any()) - verify(pictureService, never()).upsert(org.mockito.kotlin.any(), org.mockito.kotlin.any()) - } - } - - @DisplayName("삭제 성공") - @Test - fun delete_success() { - // given - val customUserDetails = mock(CustomUserDetails::class.java) - val scheduleId = 1L - val letterId = 1L - val mockLetter = mock(Letter::class.java) - - // when - `when`(letterService.getById(letterId)).thenReturn(mockLetter) - `when`(customUserDetails.getId()).thenReturn(1L) - `when`(mockLetter.authorId).thenReturn(1L) - `when`(mockLetter.scheduleId).thenReturn(scheduleId) - upsertAndDeleteLetterFacade.delete( - customUserDetails, - scheduleId, - letterId - ) - - // then - verify(letterService, times(1)).getById(letterId) - verify(letterService, times(1)).delete(letterId) - verify(pictureService, times(1)).deleteAllByLetterId(letterId) - - } -} From ce5d58bddc7111c5e945fc9f609b21892beb8ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 28 May 2025 04:59:36 +0900 Subject: [PATCH 322/357] =?UTF-8?q?feat=20:=20=EB=94=94=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=EC=97=90=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=20url=EC=B6=94=EA=B0=80=20#149?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/alarm/dto/FCMMessage.kt | 11 ++++++++++- .../gomushin/backend/alarm/facade/DdayAlarmFacade.kt | 3 ++- .../backend/alarm/facade/QuestionAlarmFacade.kt | 3 ++- .../gomushin/backend/alarm/service/FCMService.kt | 9 ++++++--- .../backend/alarm/service/StatusAlarmService.kt | 4 +++- .../gomushin/backend/alarm/value/RedirectURL.kt | 6 ++++++ 6 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/gomushin/backend/alarm/value/RedirectURL.kt diff --git a/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt b/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt index 9d359f9b..fc4d7094 100644 --- a/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt +++ b/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt @@ -6,7 +6,8 @@ data class FCMMessage( ) { data class Message( val notification: Notification, - val token: String + val token: String, + val webpush : Webpush? ) data class Notification( @@ -14,4 +15,12 @@ data class FCMMessage( val body: String, val image: String? ) + + data class Webpush( + val fcm_options : Fcm_options? + ) + + data class Fcm_options( + val link : String? + ) } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt index ce0431be..a4709d35 100644 --- a/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt @@ -2,6 +2,7 @@ package gomushin.backend.alarm.facade import gomushin.backend.alarm.service.FCMService import gomushin.backend.alarm.service.NotificationRedisService +import gomushin.backend.alarm.value.RedirectURL import gomushin.backend.couple.domain.service.AnniversaryService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -33,7 +34,7 @@ class DdayAlarmFacade( try { log.info("디데이 메시지 전송 : 수신자 {${content.memberId}}, 제목 {${alarmTitle}}, 내용{${content.title}}, 전송시각{${LocalDateTime.now()}}\n") notificationRedisService.saveAlarm(alarmTitle, content.title, content.memberId) - fcmService.sendMessageTo(content.fcmToken, alarmTitle, content.title) + fcmService.sendMessageTo(content.fcmToken, alarmTitle, content.title, RedirectURL.DDAY) } catch (e: Exception) { log.error("디데이 메시지 전송오류 : 수신자 {${content.memberId}}, 전송시각{${LocalDateTime.now()}}\n") } diff --git a/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt index 852bf841..ebe8ebcb 100644 --- a/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt @@ -3,6 +3,7 @@ package gomushin.backend.alarm.facade import gomushin.backend.alarm.service.FCMService import gomushin.backend.alarm.util.MessageParsingUtil import gomushin.backend.alarm.service.NotificationRedisService +import gomushin.backend.alarm.value.RedirectURL import gomushin.backend.member.domain.service.MemberService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -39,7 +40,7 @@ class QuestionAlarmFacade ( val (title, sendContent) = MessageParsingUtil.parse(notificationContent) log.info("질문형 메시지 전송 : 수신자 {${member.id}}, 제목 {${title}}, 내용{${sendContent}}, 전송시각{${LocalDateTime.now()}}\n") notificationRedisService.saveAlarm(title, sendContent, member.id) - fcmService.sendMessageTo(member.fcmToken, title, sendContent) + fcmService.sendMessageTo(member.fcmToken, title, sendContent, RedirectURL.MAIN) } catch (e: Exception) { log.error("질문형 메시지 전송오류 : 수신자 {${member.name}}, 전송시각{${LocalDateTime.now()}}\n") } diff --git a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt index 30d295b1..712427a0 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt @@ -33,8 +33,8 @@ class FCMService( private val client: OkHttpClient = OkHttpClient() } @Throws(IOException::class) - fun sendMessageTo(targetToken: String, title: String, body: String) { - val message = makeMessage(targetToken, title, body) + fun sendMessageTo(targetToken: String, title: String, body: String, link: String?) { + val message = makeMessage(targetToken, title, body, link) val requestBody = message .toRequestBody("application/json; charset=utf-8".toMediaType()) @@ -52,7 +52,7 @@ class FCMService( } @Throws(JsonProcessingException::class) - private fun makeMessage(targetToken: String, title: String, body: String): String { + private fun makeMessage(targetToken: String, title: String, body: String, link:String?): String { val fcmMessage = FCMMessage( validateOnly = false, message = FCMMessage.Message( @@ -61,6 +61,9 @@ class FCMService( title = title, body = body, image = null + ), + webpush = FCMMessage.Webpush( + FCMMessage.Fcm_options(link) ) ) ) diff --git a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt index d9aa0949..f7ae0c0d 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt @@ -1,9 +1,11 @@ package gomushin.backend.alarm.service import gomushin.backend.alarm.util.MessageParsingUtil +import gomushin.backend.alarm.value.RedirectURL import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.value.Emotion +import org.springframework.data.redis.core.script.RedisScript import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @@ -56,6 +58,6 @@ class StatusAlarmService ( } val token = receiver.fcmToken notificationRedisService.saveAlarm(title, sendContent, receiver.id) - fcmService.sendMessageTo(token, title, sendContent) + fcmService.sendMessageTo(token, title, sendContent, RedirectURL.MAIN) } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/alarm/value/RedirectURL.kt b/src/main/kotlin/gomushin/backend/alarm/value/RedirectURL.kt new file mode 100644 index 00000000..6e336a34 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/alarm/value/RedirectURL.kt @@ -0,0 +1,6 @@ +package gomushin.backend.alarm.value + +object RedirectURL { + const val MAIN = "https://www.sarangkkun.site" + const val DDAY = "https://www.sarangkkun.site/calendar/dday" +} \ No newline at end of file From 25ccbc56fb81b01cecfaf8f4ba178347d9c1b8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 28 May 2025 12:17:25 +0900 Subject: [PATCH 323/357] =?UTF-8?q?refactor=20:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20CamelCase=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD(=EC=BD=94=EB=93=9C=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=20=EB=A7=9E=EC=B6=94=EA=B8=B0=20=EC=9C=84=ED=95=A8)?= =?UTF-8?q?=20#149?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 4 ++++ .../kotlin/gomushin/backend/alarm/dto/FCMMessage.kt | 11 ++++++++--- .../gomushin/backend/alarm/service/FCMService.kt | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 302a0ef5..cc48c209 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { kotlin("plugin.spring") version "2.1.0" kotlin("plugin.jpa") version "2.1.0" kotlin("plugin.allopen") version "2.1.0" + kotlin("plugin.serialization") version "2.1.0" id("org.springframework.boot") version "3.4.3" id("io.spring.dependency-management") version "1.1.7" } @@ -81,6 +82,9 @@ dependencies { //coroutine implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + //serializable + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") + implementation("org.springframework.boot:spring-boot-starter-validation") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") diff --git a/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt b/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt index fc4d7094..61bded41 100644 --- a/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt +++ b/src/main/kotlin/gomushin/backend/alarm/dto/FCMMessage.kt @@ -1,5 +1,8 @@ package gomushin.backend.alarm.dto +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + data class FCMMessage( val validateOnly: Boolean, val message: Message @@ -15,12 +18,14 @@ data class FCMMessage( val body: String, val image: String? ) - + @Serializable data class Webpush( - val fcm_options : Fcm_options? + @SerialName("fcm_options") + val fcmOptions : FcmOptions? ) - data class Fcm_options( + @Serializable + data class FcmOptions( val link : String? ) } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt index 712427a0..92769955 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/FCMService.kt @@ -63,7 +63,7 @@ class FCMService( image = null ), webpush = FCMMessage.Webpush( - FCMMessage.Fcm_options(link) + FCMMessage.FcmOptions(link) ) ) ) From 52bdf38543e353671b19eb994869ea84cc1438eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 28 May 2025 12:36:06 +0900 Subject: [PATCH 324/357] =?UTF-8?q?refactor=20:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89?= =?UTF-8?q?=ED=8A=B8=20url=20=EA=B2=BD=EB=A1=9C=20=EC=A0=95=EB=B3=B4=20yml?= =?UTF-8?q?=EC=97=90=20=EC=A3=BC=EC=9E=85=20#149?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/alarm/facade/DdayAlarmFacade.kt | 5 +++-- .../backend/alarm/facade/QuestionAlarmFacade.kt | 5 +++-- .../backend/alarm/service/StatusAlarmService.kt | 6 +++--- .../gomushin/backend/alarm/value/RedirectURL.kt | 13 +++++++++---- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt index a4709d35..0b797a98 100644 --- a/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/DdayAlarmFacade.kt @@ -19,7 +19,8 @@ import java.time.LocalDateTime class DdayAlarmFacade( private val fcmService: FCMService, private val anniversaryService: AnniversaryService, - private val notificationRedisService: NotificationRedisService + private val notificationRedisService: NotificationRedisService, + private val redirectURL: RedirectURL ) { private val log: Logger = LoggerFactory.getLogger(QuestionAlarmFacade::class.java) private val alarmTitle = "오늘의 디데이가 도착했어요" @@ -34,7 +35,7 @@ class DdayAlarmFacade( try { log.info("디데이 메시지 전송 : 수신자 {${content.memberId}}, 제목 {${alarmTitle}}, 내용{${content.title}}, 전송시각{${LocalDateTime.now()}}\n") notificationRedisService.saveAlarm(alarmTitle, content.title, content.memberId) - fcmService.sendMessageTo(content.fcmToken, alarmTitle, content.title, RedirectURL.DDAY) + fcmService.sendMessageTo(content.fcmToken, alarmTitle, content.title, redirectURL.dday) } catch (e: Exception) { log.error("디데이 메시지 전송오류 : 수신자 {${content.memberId}}, 전송시각{${LocalDateTime.now()}}\n") } diff --git a/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt index ebe8ebcb..0bbb92e7 100644 --- a/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt +++ b/src/main/kotlin/gomushin/backend/alarm/facade/QuestionAlarmFacade.kt @@ -19,7 +19,8 @@ import java.time.LocalDateTime class QuestionAlarmFacade ( private val fcmService: FCMService, private val memberService: MemberService, - private val notificationRedisService: NotificationRedisService + private val notificationRedisService: NotificationRedisService, + private val redirectURL: RedirectURL ) { private val log: Logger = LoggerFactory.getLogger(QuestionAlarmFacade::class.java) private val questionMessages = listOf( @@ -40,7 +41,7 @@ class QuestionAlarmFacade ( val (title, sendContent) = MessageParsingUtil.parse(notificationContent) log.info("질문형 메시지 전송 : 수신자 {${member.id}}, 제목 {${title}}, 내용{${sendContent}}, 전송시각{${LocalDateTime.now()}}\n") notificationRedisService.saveAlarm(title, sendContent, member.id) - fcmService.sendMessageTo(member.fcmToken, title, sendContent, RedirectURL.MAIN) + fcmService.sendMessageTo(member.fcmToken, title, sendContent, redirectURL.main) } catch (e: Exception) { log.error("질문형 메시지 전송오류 : 수신자 {${member.name}}, 전송시각{${LocalDateTime.now()}}\n") } diff --git a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt index f7ae0c0d..105025a9 100644 --- a/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt +++ b/src/main/kotlin/gomushin/backend/alarm/service/StatusAlarmService.kt @@ -5,14 +5,14 @@ import gomushin.backend.alarm.value.RedirectURL import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.value.Emotion -import org.springframework.data.redis.core.script.RedisScript import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @Service class StatusAlarmService ( private val fcmService: FCMService, - private val notificationRedisService: NotificationRedisService + private val notificationRedisService: NotificationRedisService, + private val redirectURL: RedirectURL ) { private val statusMessage: Map> = mapOf( Emotion.MISS to listOf( @@ -58,6 +58,6 @@ class StatusAlarmService ( } val token = receiver.fcmToken notificationRedisService.saveAlarm(title, sendContent, receiver.id) - fcmService.sendMessageTo(token, title, sendContent, RedirectURL.MAIN) + fcmService.sendMessageTo(token, title, sendContent, redirectURL.main) } } \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/alarm/value/RedirectURL.kt b/src/main/kotlin/gomushin/backend/alarm/value/RedirectURL.kt index 6e336a34..0b056bc7 100644 --- a/src/main/kotlin/gomushin/backend/alarm/value/RedirectURL.kt +++ b/src/main/kotlin/gomushin/backend/alarm/value/RedirectURL.kt @@ -1,6 +1,11 @@ package gomushin.backend.alarm.value -object RedirectURL { - const val MAIN = "https://www.sarangkkun.site" - const val DDAY = "https://www.sarangkkun.site/calendar/dday" -} \ No newline at end of file +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Component + +@Component +@ConfigurationProperties(prefix = "fcm.redirect") +data class RedirectURL ( + var main: String = "", + var dday: String = "" +) \ No newline at end of file From 0f740de9d534dc857497fee86d67f0c91a27bf61 Mon Sep 17 00:00:00 2001 From: kimyeoungrok <127182406+kimyeoungrok@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:37:46 +0900 Subject: [PATCH 325/357] Update deploy.yml --- .github/workflows/deploy.yml | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 60e5b4cd..39586539 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,7 @@ jobs: - name: Set up SSH key uses: webfactory/ssh-agent@v0.5.3 with: - ssh-private-key: ${{ secrets.NCP_SSH_PRIVATE_KEY }} + ssh-private-key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} - name: Gradle 캐시 적용 uses: actions/cache@v3 @@ -69,12 +69,11 @@ jobs: - name: Docker Buildx 세팅 uses: docker/setup-buildx-action@v3 - - name: NCP 레지스트리 로그인 - uses: docker/login-action@v3 + - name: 도커 로그 + uses: docker/login-action@v2 with: - registry: ${{ secrets.NCP_CONTAINER_REGISTRY }} - username: ${{ secrets.NCP_ACCESS_KEY }} - password: ${{ secrets.NCP_SECRET_KEY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: 도커 이미지 빌드 후 푸시 if: success() @@ -83,22 +82,22 @@ jobs: context: . file: ./Dockerfile push: true - tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/sarang-backend:${{ github.sha }} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/sarang-backend:${{ github.sha }} platforms: linux/amd64,linux/arm64 - - name: Docker Compose 파일 NCP 서버로 전송 - run: scp -o StrictHostKeyChecking=no -P ${{ secrets.NCP_PORT }} docker-compose.yml ${{ secrets.NCP_USERNAME }}@${{ secrets.NCP_HOST }}:./ + - name: Docker Compose 파일 EC2 서버로 전송 + run: scp -o StrictHostKeyChecking=no -P ${{ secrets.EC2_PORT }} docker-compose.yml ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}:./ - - name: NCP 접속 후 이미지 다운로드 및 배포 + - name: EC2 접속 후 이미지 다운로드 및 배포 if: success() uses: appleboy/ssh-action@master with: - host: ${{ secrets.NCP_HOST }} - username: ${{ secrets.NCP_USERNAME }} - password: ${{ secrets.NCP_PASSWORD }} - port: ${{ secrets.NCP_PORT }} + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} + port: ${{ secrets.EC2_PORT }} script: | - export NCP_CONTAINER_REGISTRY=${{ secrets.NCP_CONTAINER_REGISTRY }} + export DOCKER_CONTAINER_REGISTRY=${{ secrets.DOCKERHUB_USERNAME }} export GITHUB_SHA=${{ github.sha }} sudo chmod +x ./deploy.sh ./deploy.sh From f466deb0959008040f4bd33b4b071ef4f1d2da7c Mon Sep 17 00:00:00 2001 From: kimyeoungrok <127182406+kimyeoungrok@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:42:35 +0900 Subject: [PATCH 326/357] Update docker-compose.yml --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0cc31548..d066f87a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: blue: - image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" + image: "${DOCKER_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" container_name: sarang-backend-blue environment: TZ: Asia/Seoul @@ -15,7 +15,7 @@ services: - sarang-backend-network green: - image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" + image: "${DOCKER_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" container_name: sarang-backend-green environment: TZ: Asia/Seoul From d5bc136a4614eb1f5f80fa725a4875e17beb7b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 9 Jul 2025 18:50:26 +0900 Subject: [PATCH 327/357] =?UTF-8?q?fix=20:=20compose=EC=88=98=EC=A0=95(?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=EC=84=9C=EB=B2=84=EB=9D=BC=20blue/green=20x)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d066f87a..36c72d00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.8" services: - blue: + server: image: "${DOCKER_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" container_name: sarang-backend-blue environment: @@ -13,20 +13,6 @@ services: - minio networks: - sarang-backend-network - - green: - image: "${DOCKER_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" - container_name: sarang-backend-green - environment: - TZ: Asia/Seoul - ports: - - '8081:8080' - depends_on: - - redis - - minio - networks: - - sarang-backend-network - redis: image: redis:6.0.9 container_name: redis From 9ff149895f424b4d20407e14d065d681ba86b534 Mon Sep 17 00:00:00 2001 From: kimyeoungrok <127182406+kimyeoungrok@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:55:13 +0900 Subject: [PATCH 328/357] Update deploy.yml --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 39586539..95d7fb6e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -98,6 +98,7 @@ jobs: port: ${{ secrets.EC2_PORT }} script: | export DOCKER_CONTAINER_REGISTRY=${{ secrets.DOCKERHUB_USERNAME }} + export DOKCER_HUB_PASSWORD=${{secrets.DOCKERHUB_PASSWORD}} export GITHUB_SHA=${{ github.sha }} sudo chmod +x ./deploy.sh ./deploy.sh From 8dddbf25b768260d6296b107b57c9998cce89a7a Mon Sep 17 00:00:00 2001 From: kimyeoungrok <127182406+kimyeoungrok@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:15:48 +0900 Subject: [PATCH 329/357] Update deploy.yml --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 95d7fb6e..5fcb12c3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -98,7 +98,7 @@ jobs: port: ${{ secrets.EC2_PORT }} script: | export DOCKER_CONTAINER_REGISTRY=${{ secrets.DOCKERHUB_USERNAME }} - export DOKCER_HUB_PASSWORD=${{secrets.DOCKERHUB_PASSWORD}} + export DOCKERHUB_PASSWORD=${{secrets.DOCKERHUB_PASSWORD}} export GITHUB_SHA=${{ github.sha }} sudo chmod +x ./deploy.sh ./deploy.sh From 96095f3057a82df3c807c3eb72ad00ef55361937 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 9 Jun 2025 23:21:20 +0900 Subject: [PATCH 330/357] =?UTF-8?q?chore:=20kotest=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=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 --- build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index cc48c209..fc41345c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ group = "gomushin" version = "0.0.1-SNAPSHOT" val mockkVersion = "1.13.10" +val kotestVersion = "5.5.4" java { toolchain { @@ -90,6 +91,8 @@ dependencies { testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("io.mockk:mockk:${mockkVersion}") + testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") + testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } From b031e77106ec215c484a59bedac05aecf4cbed9a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 9 Jun 2025 23:22:02 +0900 Subject: [PATCH 331/357] =?UTF-8?q?refactor:=20CommentService=20=EC=99=80?= =?UTF-8?q?=20ReadLetterFacade=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/schedule/domain/service/CommentService.kt | 4 ++-- .../gomushin/backend/schedule/facade/ReadLetterFacade.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt index 5465ec5e..dacb6bc4 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/service/CommentService.kt @@ -39,8 +39,8 @@ class CommentService( } @Transactional(readOnly = true) - fun findAllByLetter(letter: Letter): List { - return commentRepository.findAllByLetterId(letter.id) + fun findAllByLetterId(id: Long): List { + return commentRepository.findAllByLetterId(id) } @Transactional(readOnly = true) diff --git a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt index 8188ebcc..8af03208 100644 --- a/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt +++ b/src/main/kotlin/gomushin/backend/schedule/facade/ReadLetterFacade.kt @@ -60,7 +60,7 @@ class ReadLetterFacade( PictureResponse.of(picture) } - val comments = commentService.findAllByLetter(letter) + val comments = commentService.findAllByLetterId(letter.id) val commentResponses = comments.map { comment -> CommentResponse.of(comment) } From dc6b723d51c8384e708d071e63afa9c00f38008e Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 9 Jun 2025 23:22:21 +0900 Subject: [PATCH 332/357] =?UTF-8?q?refactor:=20test=20=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20context=20=EB=A5=BC=20public=20=EC=9C=BC?= =?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 --- .../gomushin/backend/core/common/support/SpringContextHolder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/common/support/SpringContextHolder.kt b/src/main/kotlin/gomushin/backend/core/common/support/SpringContextHolder.kt index 5ac49f39..d63d9f63 100644 --- a/src/main/kotlin/gomushin/backend/core/common/support/SpringContextHolder.kt +++ b/src/main/kotlin/gomushin/backend/core/common/support/SpringContextHolder.kt @@ -6,7 +6,7 @@ import org.springframework.stereotype.Component @Component object SpringContextHolder : ApplicationContextAware { - private lateinit var context: ApplicationContext + lateinit var context: ApplicationContext override fun setApplicationContext(applicationContext: ApplicationContext) { context = applicationContext From 6e480e9c6348514c58b30b97a47fa8c95277adbf Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Mon, 9 Jun 2025 23:22:40 +0900 Subject: [PATCH 333/357] =?UTF-8?q?test:=20Comment=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=85=8C=EC=8A=A4=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 --- .../UpsertAndDeleteCommentFacadeTest.kt | 190 +++++++++++----- .../domain/service/CommentServiceTest.kt | 211 +++++++++++++++--- .../domain/service/ScheduleServiceTest.kt | 59 ++--- 3 files changed, 342 insertions(+), 118 deletions(-) diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt index 14334823..f4b5bb96 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/facade/UpsertAndDeleteCommentFacadeTest.kt @@ -1,6 +1,9 @@ package gomushin.backend.schedule.domain.facade import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.common.support.SpringContextHolder +import gomushin.backend.core.configuration.env.AppEnv +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.member.domain.entity.Member import gomushin.backend.member.domain.service.MemberService import gomushin.backend.schedule.domain.entity.Comment @@ -9,90 +12,165 @@ import gomushin.backend.schedule.domain.service.CommentService import gomushin.backend.schedule.domain.service.LetterService import gomushin.backend.schedule.dto.request.UpsertCommentRequest import gomushin.backend.schedule.facade.UpsertAndDeleteCommentFacade +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.InjectMocks -import org.mockito.Mock -import org.mockito.Mockito.* -import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.context.ApplicationContext import kotlin.test.Test -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockKExtension::class) class UpsertAndDeleteCommentFacadeTest { - @Mock + @MockK private lateinit var commentService: CommentService - @Mock + @MockK private lateinit var letterService: LetterService - @Mock + @MockK private lateinit var memberService: MemberService - @InjectMocks + @MockK(relaxed = true) + private lateinit var mockAppEnv: AppEnv + + @MockK + private lateinit var mockApplicationContext: ApplicationContext + + @InjectMockKs private lateinit var upsertAndDeleteCommentFacade: UpsertAndDeleteCommentFacade + @BeforeEach + fun setup() { + SpringContextHolder.context = mockApplicationContext + every { mockApplicationContext.getBean(AppEnv::class.java) } returns mockAppEnv + every { mockAppEnv.getId() } returns "test-env" + } + + @DisplayName("댓글 생성 또는 수정 성공") @Test fun upsert_success() { // given - val customUserDetails = mock(CustomUserDetails::class.java) + val customUserDetails = mockk() val letterId = 1L - val memberId = 1L - val upsertCommentRequest = UpsertCommentRequest( - commentId = null, - content = "댓글 내용" - ) - - val mockMember = mock(Member::class.java).apply { - `when`(id).thenReturn(memberId) - `when`(nickname).thenReturn("테스트유저") - } - val mockLetter = mock(Letter::class.java).apply { - `when`(id).thenReturn(letterId) - } - - `when`(customUserDetails.getId()).thenReturn(memberId) - `when`(memberService.getById(memberId)).thenReturn(mockMember) - `when`(letterService.getById(letterId)).thenReturn(mockLetter) + val upsertCommentRequest = mockk() + val member = mockk() + val letter = mockk() + every { customUserDetails.getId() } returns 1L + every { memberService.getById(any()) } returns member + every { letterService.getById(any()) } returns letter + every { upsertCommentRequest.commentId } returns 1L + every { letter.id } returns letterId + every { member.id } returns 1L + every { member.nickname } returns "닉네임" + every { commentService.upsert(any(), any(), any(), any(), any()) } returns Unit // when upsertAndDeleteCommentFacade.upsert(customUserDetails, letterId, upsertCommentRequest) // then - verify(commentService, times(1)).upsert( - id = upsertCommentRequest.commentId, - letterId = mockLetter.id, - authorId = mockMember.id, - nickname = "테스트유저", - upsertCommentRequest = upsertCommentRequest - ) + verify { memberService.getById(1L) } + verify { letterService.getById(1L) } + verify { commentService.upsert(1L, 1L, 1L, "닉네임", upsertCommentRequest) } } - @DisplayName("댓글 삭제 성공") - @Test - fun delete_success() { - // given - val customUserDetails = mock(CustomUserDetails::class.java) - val letterId = 1L - val commentId = 1L - val mockMember = mock(Member::class.java) - val mockComment = mock(Comment::class.java) - // when - `when`(customUserDetails.getId()).thenReturn(1L) - `when`(memberService.getById(customUserDetails.getId())).thenReturn(mockMember) - `when`(commentService.getById(commentId)).thenReturn(mockComment) - `when`(mockMember.id).thenReturn(1L) - `when`(mockComment.authorId).thenReturn(1L) - `when`(mockComment.letterId).thenReturn(letterId) - `when`(mockComment.letterId).thenReturn(letterId) + @Nested + inner class DeleteTest { + @DisplayName("댓글 삭제 성공") + @Test + fun delete_success() { + // given + val customUserDetails = mockk() + val letterId = 1L + val memberId = 1L + val commentId = 1L + val authorId = 1L + val member = mockk() + val comment = mockk() - upsertAndDeleteCommentFacade.delete(customUserDetails, letterId, commentId) + every { customUserDetails.getId() } returns 1L + every { memberService.getById(any()) } returns member + every { commentService.getById(commentId) } returns comment + every { comment.authorId } returns authorId + every { member.id } returns memberId + every { comment.letterId } returns letterId + every { commentService.delete(commentId) } returns Unit - // then - verify(memberService, times(1)).getById(customUserDetails.getId()) - verify(commentService, times(1)).getById(commentId) - verify(commentService, times(1)).delete(commentId) + // when + upsertAndDeleteCommentFacade.delete(customUserDetails, letterId, commentId) + + // then + verify { memberService.getById(1L) } + verify { commentService.getById(commentId) } + verify { commentService.delete(commentId) } + } + + @DisplayName("댓글 삭제 시 comment 작성자 ID와 Member ID가 다를 경우, 에러 발생") + @Test + fun delete_shouldThrowBadRequestException_When_Comment_AuthorId_is_Not_MemberId() { + // given + val customUserDetails = mockk() + val letterId = 1L + val memberId = 2L + val commentId = 1L + val authorId = 1L + val member = mockk() + val comment = mockk() + + every { customUserDetails.getId() } returns 1L + every { memberService.getById(any()) } returns member + every { commentService.getById(commentId) } returns comment + every { comment.authorId } returns authorId + every { member.id } returns memberId + every { comment.letterId } returns letterId + every { commentService.delete(commentId) } returns Unit + + // when + val exception = shouldThrow { + upsertAndDeleteCommentFacade.delete(customUserDetails, letterId, commentId) + } + + // then + exception.error.element.message.resolved shouldBe "댓글은 작성자만 삭제하거나 업데이트 할 수 있어요." + } + + @DisplayName("댓글의 letterId와 입력으로 받은 letterId가 다른 경우, 예외를 발생시킨다.") + @Test + fun delete_shouldThrowBadRequestException_when_commentLetterId_and_letterId_not_match() { + // given + val customUserDetails = mockk() + val letterId = 1L + val memberId = 1L + val commentId = 1L + val authorId = 1L + val member = mockk() + val comment = mockk() + + every { customUserDetails.getId() } returns 1L + every { memberService.getById(any()) } returns member + every { commentService.getById(commentId) } returns comment + every { comment.authorId } returns authorId + every { member.id } returns memberId + every { comment.letterId } returns 2L + every { commentService.delete(commentId) } returns Unit + + // when + val exception = shouldThrow { + upsertAndDeleteCommentFacade.delete(customUserDetails, letterId, commentId) + } + + // then + exception.error.element.message.resolved shouldBe "편지에 해당하는 댓글이 아니에요." + } } } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt index b10d7b86..650f080b 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt @@ -1,74 +1,217 @@ package gomushin.backend.schedule.domain.service +import gomushin.backend.core.common.support.SpringContextHolder +import gomushin.backend.core.configuration.env.AppEnv +import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.schedule.domain.entity.Comment import gomushin.backend.schedule.domain.repository.CommentRepository import gomushin.backend.schedule.dto.request.UpsertCommentRequest +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.InjectMocks -import org.mockito.Mock -import org.mockito.Mockito.* -import org.mockito.junit.jupiter.MockitoExtension -import java.util.* +import org.springframework.context.ApplicationContext +import org.springframework.data.repository.findByIdOrNull import kotlin.test.Test -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockKExtension::class) class CommentServiceTest { - @Mock - lateinit var commentRepository: CommentRepository + @MockK + private lateinit var commentRepository: CommentRepository - @InjectMocks - lateinit var commentService: CommentService + @MockK(relaxed = true) + private lateinit var mockAppEnv: AppEnv + + @MockK + private lateinit var mockApplicationContext: ApplicationContext + + @InjectMockKs + private lateinit var commentService: CommentService + + @BeforeEach + fun setup() { + SpringContextHolder.context = mockApplicationContext + every { mockApplicationContext.getBean(AppEnv::class.java) } returns mockAppEnv + every { mockAppEnv.getId() } returns "test-env" + } @Nested - inner class Upsert { + inner class UpsertTest { + @DisplayName("id가 입력으로 들어오지 않는 경우, 댓글을 생성한다.") + @Test + fun upsert_shouldCreateComment_When_ParameterId_NotExists() { + // given + val letterId = 1L + val authorId = 1L + val nickname = "닉네임" + val upsertCommentRequest = mockk() + val comment = mockk() + every { upsertCommentRequest.content } returns "훈련 힘내" + every { upsertCommentRequest.commentId } returns 1L + every { commentRepository.save(any()) } returns comment - @DisplayName("업데이트 성공 - 댓글이 존재할 때") + // when + commentService.upsert(null, letterId, authorId, nickname, upsertCommentRequest) + + // then + verify { commentService.save(any()) } + } + + @DisplayName("id로 찾은 댓글의 작성자와 수정하려는 authorId가 다른 경우, 에러를 반환한다.") @Test - fun upload_success() { + fun upsert_shouldThrowException_When_CommentAuthorId_And_AuthorId_Not_Matched() { // given val id = 1L val letterId = 1L val authorId = 1L val nickname = "닉네임" - val upsertCommentRequest = UpsertCommentRequest( - commentId = 1L, - content = "내용" - ) - val mockComment = mock(Comment::class.java).apply { - `when`(this.authorId).thenReturn(authorId) + val upsertCommentRequest = mockk() + val comment = mockk() + every { commentRepository.findByIdOrNull(1L) } returns comment + every { upsertCommentRequest.content } returns "훈련 힘내" + every { upsertCommentRequest.commentId } returns 1L + every { comment.authorId } returns 2L + + // when + val exception = shouldThrow { + commentService.upsert(id, letterId, authorId, nickname, upsertCommentRequest) } + // then + exception.error.element.message.resolved shouldBe "댓글은 작성자만 삭제하거나 업데이트 할 수 있어요." + } + } + + @Nested + inner class ReadTest { + @DisplayName("존재하지 않는 댓글 ID로 조회 시 BadRequestException 발생") + @Test + fun getById_shouldThrowBadRequestException_When_NotExistId() { + every { commentRepository.findByIdOrNull(any()) } returns null + + val exception = shouldThrow { + commentService.getById(999L) + } + + exception.error.element.message.resolved shouldBe "댓글을 찾을 수 없어요." + } + + @DisplayName("존재하는 댓글 ID로 조회 시 댓글 객체 반환") + @Test + fun getById_success() { + // given + val id = 1L + val comment = mockk() + every { commentRepository.findByIdOrNull(id) } returns comment + // when + val result = commentService.getById(id) + + // then + result shouldBe comment + verify { commentRepository.findByIdOrNull(id) } + verify { commentService.findById(id) } + } + + @DisplayName("findById 호출 시 존재하지 않는 ID로 조회 시 null 반환") + @Test + fun findById_shouldReturnNull_When_NotExistId() { + // given + val id = 999L + every { commentRepository.findByIdOrNull(id) } returns null + // when - `when`(commentRepository.findById(id)).thenReturn(Optional.of(mockComment)) - commentService.upsert(id, letterId, authorId, nickname, upsertCommentRequest) + val result = commentService.findById(id) // then - verify(mockComment).content = upsertCommentRequest.content + result shouldBe null + verify { commentRepository.findByIdOrNull(id) } } - @DisplayName("댓글 생성 성공") + @DisplayName("findById 호출 시 존재하는 ID로 조회 시 댓글 객체 반환") @Test - fun insert_success() { + fun findById_shouldReturnComment_When_ExistId() { + // given + val id = 1L + val comment = mockk() + every { commentRepository.findByIdOrNull(id) } returns comment + + // when + val result = commentService.findById(id) + + // then + result shouldBe comment + verify { commentRepository.findByIdOrNull(id) } + } + + @DisplayName("findAllByLetterId 호출 시 댓글 리스트 반환") + @Test + fun findAllByLetterId_success() { // given val letterId = 1L - val authorId = 1L - val nickname = "닉네임" - val upsertCommentRequest = UpsertCommentRequest( - commentId = null, - content = "내용" - ) - val mockComment = mock(Comment::class.java) + val comments = listOf(mockk(), mockk()) + every { commentRepository.findAllByLetterId(letterId) } returns comments // when - `when`(commentRepository.save(any())).thenReturn(mockComment) - commentService.upsert(null, letterId, authorId, nickname, upsertCommentRequest) + val result = commentService.findAllByLetterId(letterId) // then - verify(commentRepository, times(1)).save(org.mockito.kotlin.any()) + result shouldBe comments + verify { commentRepository.findAllByLetterId(letterId) } } + } + @DisplayName("save 호출 시 댓글 저장 후 반환") + @Test + fun save_shouldReturnSavedComment() { + // given + val comment = mockk() + every { commentRepository.save(comment) } returns comment + // when + val result = commentService.save(comment) + + // then + result shouldBe comment + verify { commentRepository.save(comment) } } + + @Nested + inner class DeleteTest { + @DisplayName("delete 호출 시 댓글 삭제") + @Test + fun delete_shouldDeleteComment() { + // given + val id = 1L + every { commentRepository.deleteById(id) } returns Unit + + // when + commentService.delete(id) + + // then + verify { commentRepository.deleteById(id) } + } + + @DisplayName("deleteAllByLetterId 호출 시 commentRepository.deleteAllByLetterId 가 1회 호출된다.") + @Test + fun deleteAllByLetterId_shouldCallDeleteAllByLetterId() { + // given + val letterId = 1L + every { commentRepository.deleteAllByLetterId(any()) } returns Unit + + // when + commentService.deleteAllByLetterId(letterId) + + // then + verify { commentRepository.deleteAllByLetterId(letterId) } + } + } + } diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt index 3bb353bc..82c80c5f 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/ScheduleServiceTest.kt @@ -6,14 +6,24 @@ import gomushin.backend.core.infrastructure.exception.BadRequestException import gomushin.backend.schedule.domain.entity.Schedule import gomushin.backend.schedule.domain.repository.ScheduleRepository import gomushin.backend.schedule.dto.request.UpsertScheduleRequest -import io.mockk.* +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.junit5.MockKExtension -import org.junit.jupiter.api.* +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.context.ApplicationContext import org.springframework.data.repository.findByIdOrNull import java.time.LocalDateTime -import kotlin.test.assertEquals + @ExtendWith(MockKExtension::class) class ScheduleServiceTest { @@ -21,22 +31,21 @@ class ScheduleServiceTest { @MockK lateinit var scheduleRepository: ScheduleRepository - private lateinit var scheduleService: ScheduleService + @MockK(relaxed = true) + private lateinit var mockAppEnv: AppEnv + + @MockK + private lateinit var mockApplicationContext: ApplicationContext + + + @InjectMockKs + lateinit var scheduleService: ScheduleService @BeforeEach - fun setUp() { - // 확장함수 모킹 - mockkStatic("org.springframework.data.repository.CrudRepositoryExtensionsKt") - - // 서비스 인스턴스 직접 생성 - // 확장함수를 사용하는 경우, @InjectMockks가 아닌 직접 생성해야 함 - scheduleService = ScheduleService(scheduleRepository) - - // SpringContextHolder 모킹 - mockkObject(SpringContextHolder) - val mockAppEnv = mockk() - every { SpringContextHolder.getBean(AppEnv::class.java) } returns mockAppEnv - every { mockAppEnv.getId() } returns "test" + fun setup() { + SpringContextHolder.context = mockApplicationContext + every { mockApplicationContext.getBean(AppEnv::class.java) } returns mockAppEnv + every { mockAppEnv.getId() } returns "test-env" } @Nested @@ -102,22 +111,17 @@ class ScheduleServiceTest { @Nested inner class ReadTest { - @DisplayName("getById() 경우, 존재하지 않는 id로 조회 시 BadRequestException 발생") + @DisplayName("존재하지 않는 ID로 검색 시 예외 발생") @Test fun getById_notExistId_throwBadRequestException() { // given - val id = 1L - every { - scheduleRepository.findByIdOrNull(id) - } returns null + every { scheduleRepository.findByIdOrNull(any()) } returns null // when & then - val exception = assertThrows { - scheduleService.getById(id) + val exception = shouldThrow { + scheduleService.getById(1L) } - val errorMessage = exception.error.element.message.resolved - - assertEquals("해당 일정이 존재하지 않아요.", errorMessage) + exception.error.element.message.resolved shouldBe "해당 일정이 존재하지 않아요." } } @@ -150,6 +154,5 @@ class ScheduleServiceTest { // then verify { scheduleRepository.deleteById(scheduleId) } } - } } From 7fa6b5596d6bf08e93d4a6242526bad062d0670a Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sun, 13 Jul 2025 01:16:53 +0900 Subject: [PATCH 334/357] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AC=EC=9A=A9=20=EC=8B=9C=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20verify=20=EC=82=AC=EC=9A=A9=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 --- .../backend/schedule/domain/service/CommentServiceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt index 650f080b..86fc2537 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/service/CommentServiceTest.kt @@ -63,7 +63,7 @@ class CommentServiceTest { commentService.upsert(null, letterId, authorId, nickname, upsertCommentRequest) // then - verify { commentService.save(any()) } + verify { commentRepository.save(any()) } } @DisplayName("id로 찾은 댓글의 작성자와 수정하려는 authorId가 다른 경우, 에러를 반환한다.") From 844d02cab71523e99677f3ae9e8ffd36a1c048b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 17 Jul 2025 02:57:53 +0900 Subject: [PATCH 335/357] =?UTF-8?q?feat:=20blue-green=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=20=EC=9E=AC=EA=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 36c72d00..0cc31548 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ version: "3.8" services: - server: - image: "${DOCKER_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" + blue: + image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" container_name: sarang-backend-blue environment: TZ: Asia/Seoul @@ -13,6 +13,20 @@ services: - minio networks: - sarang-backend-network + + green: + image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" + container_name: sarang-backend-green + environment: + TZ: Asia/Seoul + ports: + - '8081:8080' + depends_on: + - redis + - minio + networks: + - sarang-backend-network + redis: image: redis:6.0.9 container_name: redis From 13bdd5120738fd99ddc826938194165b2d5bd276 Mon Sep 17 00:00:00 2001 From: kimyeoungrok <127182406+kimyeoungrok@users.noreply.github.com> Date: Thu, 17 Jul 2025 03:21:59 +0900 Subject: [PATCH 336/357] =?UTF-8?q?docker-compose=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 환경변수 값 잘못된거 수정 --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0cc31548..d066f87a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: blue: - image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" + image: "${DOCKER_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" container_name: sarang-backend-blue environment: TZ: Asia/Seoul @@ -15,7 +15,7 @@ services: - sarang-backend-network green: - image: "${NCP_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" + image: "${DOCKER_CONTAINER_REGISTRY}/sarang-backend:${GITHUB_SHA}" container_name: sarang-backend-green environment: TZ: Asia/Seoul From c157f54f61efe6939b6c607d3997251555ee2a42 Mon Sep 17 00:00:00 2001 From: kimyeoungrok <127182406+kimyeoungrok@users.noreply.github.com> Date: Thu, 17 Jul 2025 03:36:51 +0900 Subject: [PATCH 337/357] Update deploy.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker compose파일이 변수를 읽을 수 있도록 .env파일에 저장 --- .github/workflows/deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5fcb12c3..1005f966 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -97,8 +97,8 @@ jobs: key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} port: ${{ secrets.EC2_PORT }} script: | - export DOCKER_CONTAINER_REGISTRY=${{ secrets.DOCKERHUB_USERNAME }} - export DOCKERHUB_PASSWORD=${{secrets.DOCKERHUB_PASSWORD}} - export GITHUB_SHA=${{ github.sha }} + export DOCKER_CONTAINER_REGISTRY=${{ secrets.DOCKERHUB_USERNAME }} >> .env + export DOCKERHUB_PASSWORD=${{secrets.DOCKERHUB_PASSWORD}} >> .env + export GITHUB_SHA=${{ github.sha }} >> .env sudo chmod +x ./deploy.sh ./deploy.sh From 46a2984a9043954b42f71f255f023f4c83df7885 Mon Sep 17 00:00:00 2001 From: kimyeoungrok <127182406+kimyeoungrok@users.noreply.github.com> Date: Thu, 17 Jul 2025 04:00:12 +0900 Subject: [PATCH 338/357] Update deploy.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compose버전 업데이트에 따라 환경변수 파일 세팅 작업 추가 --- .github/workflows/deploy.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1005f966..e85a031c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -97,8 +97,12 @@ jobs: key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} port: ${{ secrets.EC2_PORT }} script: | - export DOCKER_CONTAINER_REGISTRY=${{ secrets.DOCKERHUB_USERNAME }} >> .env - export DOCKERHUB_PASSWORD=${{secrets.DOCKERHUB_PASSWORD}} >> .env - export GITHUB_SHA=${{ github.sha }} >> .env + echo "DOCKER_CONTAINER_REGISTRY=${{ secrets.DOCKERHUB_USERNAME }}" > .env + echo "DOCKERHUB_PASSWORD=${{secrets.DOCKERHUB_PASSWORD}}" >> .env + echo "GITHUB_SHA=${{ github.sha }}" >> .env + echo "MINIO_ROOT_USER=${{secrets.MINIO_ROOT_USER}}" >> .env + echo "MINIO_ROOT_PASSWORD=${{secrets.MINIO_ROOT_PASSWORD}}" >> .env + echo "MINIO_SERVER_URL=${{secrets.MINIO_SERVER_URL}}" >> .env + echo "MINIO_BROWSER_REDIRECT_URL=${{secrets.MINIO_BROWSER_REDIRECT_URL}}" >> .env sudo chmod +x ./deploy.sh ./deploy.sh From 982290b18def614f3032fb664b71c1de42c200c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 16 Jul 2025 01:52:11 +0900 Subject: [PATCH 339/357] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=A1=9C=EA=B9=85=20AOP=20=EA=B5=AC=ED=98=84=20#15?= =?UTF-8?q?3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logging/CachedBodyHttpServletRequest.kt | 27 +++++++++++ .../filter/logging/LoggingFilter.kt | 45 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/CachedBodyHttpServletRequest.kt create mode 100644 src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/CachedBodyHttpServletRequest.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/CachedBodyHttpServletRequest.kt new file mode 100644 index 00000000..dd253e6d --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/CachedBodyHttpServletRequest.kt @@ -0,0 +1,27 @@ +package gomushin.backend.core.infrastructure.filter.logging + +import jakarta.servlet.ReadListener +import jakarta.servlet.ServletInputStream +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletRequestWrapper +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.InputStreamReader + +class CachedBodyHttpServletRequest(request: HttpServletRequest) : HttpServletRequestWrapper(request) { + private val cachedBody: ByteArray = request.inputStream.readBytes() + + override fun getInputStream(): ServletInputStream { + val byteArrayInputStream = ByteArrayInputStream(cachedBody) + return object : ServletInputStream() { + override fun isFinished() = byteArrayInputStream.available() == 0 + override fun isReady() = true + override fun setReadListener(readListener: ReadListener?) {} + override fun read(): Int = byteArrayInputStream.read() + } + } + + override fun getReader(): BufferedReader { + return BufferedReader(InputStreamReader(inputStream)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt new file mode 100644 index 00000000..e42a4772 --- /dev/null +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt @@ -0,0 +1,45 @@ +package gomushin.backend.core.infrastructure.filter.logging + +import gomushin.backend.core.CustomUserDetails +import jakarta.servlet.Filter +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import jakarta.servlet.http.HttpServletRequest +import org.slf4j.LoggerFactory +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component + +@Component +class LoggingFilter : Filter { + + private val log = LoggerFactory.getLogger(LoggingFilter::class.java) + + override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + if (request is HttpServletRequest) { + val wrappedRequest = CachedBodyHttpServletRequest(request) + + val url = wrappedRequest.requestURI + val method = wrappedRequest.method + val body = runCatching { wrappedRequest.reader.readLines().joinToString("") }.getOrNull() + + val authentication = SecurityContextHolder.getContext().authentication + + val userId: Long? = when { + authentication == null || !authentication.isAuthenticated || + authentication is AnonymousAuthenticationToken -> null + authentication.principal is CustomUserDetails -> { + (authentication.principal as CustomUserDetails).getId() + } + else -> null + } + + log.info("[REQUEST LOG] userId={}, URL={}, Method={}, Body={}", userId, url, method, body) + + chain.doFilter(wrappedRequest, response) + } else { + chain.doFilter(request, response) + } + } +} \ No newline at end of file From 385300900e12eb4cff99c9e40514a40784c2f4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 16 Jul 2025 01:52:27 +0900 Subject: [PATCH 340/357] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC=20=EB=A1=9C=EA=B9=85=20AOP=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#153?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/web/response/exception/ApiExceptionHandler.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/core/common/web/response/exception/ApiExceptionHandler.kt b/src/main/kotlin/gomushin/backend/core/common/web/response/exception/ApiExceptionHandler.kt index bf0d0250..1025a2f4 100644 --- a/src/main/kotlin/gomushin/backend/core/common/web/response/exception/ApiExceptionHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/common/web/response/exception/ApiExceptionHandler.kt @@ -1,16 +1,19 @@ package gomushin.backend.core.common.web.response.exception import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.core.infrastructure.filter.logging.LoggingFilter +import org.slf4j.LoggerFactory import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice @RestControllerAdvice class ApiExceptionHandler { - + private val log = LoggerFactory.getLogger(LoggingFilter::class.java) @ExceptionHandler(ApiErrorException::class) fun handleApiErrorExtention(ex: ApiErrorException): ResponseEntity> { val status = ex.error.element.status + log.warn("[Error] errorStatus : {}, errorMessage : {}",ex.error.element.code.value, ex.error.element.message.resolved) return ResponseEntity.status(status.code).body(ApiResponse.error(ex.error)) } } From d4915edb9c3dd892b07075992b834d260e15e4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Wed, 16 Jul 2025 01:52:37 +0900 Subject: [PATCH 341/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=B6=80=EB=B6=84=EC=97=90=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#153?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../couple/domain/service/CoupleConnectService.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt index 5079b6a3..5e5331b3 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -1,9 +1,11 @@ package gomushin.backend.couple.domain.service import gomushin.backend.core.infrastructure.exception.BadRequestException +import gomushin.backend.core.infrastructure.filter.logging.LoggingFilter import gomushin.backend.couple.domain.entity.Couple import gomushin.backend.member.domain.service.MemberService import gomushin.backend.member.util.CoupleCodeGeneratorUtil +import org.slf4j.LoggerFactory import org.springframework.data.redis.core.StringRedisTemplate import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -15,7 +17,7 @@ class CoupleConnectService( private val coupleService: CoupleService, private val memberService: MemberService, ) { - + private val log = LoggerFactory.getLogger(LoggingFilter::class.java) companion object { private val COUPLE_CODE_DURATION = Duration.ofMinutes(60) private const val COUPLE_CODE_PREFIX = "COUPLE_CODE:" @@ -25,6 +27,7 @@ class CoupleConnectService( val coupleCode = CoupleCodeGeneratorUtil.generateCoupleCode() val key = getCoupleCodeKey(coupleCode) redisTemplate.opsForValue().set(key, userId.toString(), COUPLE_CODE_DURATION) + log.info("[GenerateCoupleCode] generator_userId : {}, code : {}", userId, coupleCode) return coupleCode } @@ -36,7 +39,7 @@ class CoupleConnectService( if (invitorId == inviteeId) { throw BadRequestException("sarangggun.couple.couple-code-same") } - + log.info("[ConnectCouple] invitorId : {}, inviteeId : {}", invitorId, inviteeId) val couple = Couple.of( invitorId, inviteeId, @@ -48,7 +51,7 @@ class CoupleConnectService( val savedCouple = save(couple) delete(key) - + log.info("[ConnectCouple] invitorId : {}, inviteeId : {} - connect Succeed!", invitorId, inviteeId) return savedCouple } From e4a66556fc12594c834a063ecf786dafc49c7a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Thu, 17 Jul 2025 20:26:57 +0900 Subject: [PATCH 342/357] =?UTF-8?q?fix=20:=20[=EB=A6=AC=EB=B7=B0=EB=B0=98?= =?UTF-8?q?=EC=98=81]=20=EB=A1=9C=EA=B7=B8=EB=A0=88=EB=B2=A8=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=20=EC=BB=A8=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=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 --- .../common/web/response/exception/ApiExceptionHandler.kt | 2 +- .../backend/couple/domain/service/CoupleConnectService.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/common/web/response/exception/ApiExceptionHandler.kt b/src/main/kotlin/gomushin/backend/core/common/web/response/exception/ApiExceptionHandler.kt index 1025a2f4..f9d8c321 100644 --- a/src/main/kotlin/gomushin/backend/core/common/web/response/exception/ApiExceptionHandler.kt +++ b/src/main/kotlin/gomushin/backend/core/common/web/response/exception/ApiExceptionHandler.kt @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice @RestControllerAdvice class ApiExceptionHandler { - private val log = LoggerFactory.getLogger(LoggingFilter::class.java) + private val log = LoggerFactory.getLogger(ApiExceptionHandler::class.java) @ExceptionHandler(ApiErrorException::class) fun handleApiErrorExtention(ex: ApiErrorException): ResponseEntity> { val status = ex.error.element.status diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt index 5e5331b3..05626185 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -17,7 +17,7 @@ class CoupleConnectService( private val coupleService: CoupleService, private val memberService: MemberService, ) { - private val log = LoggerFactory.getLogger(LoggingFilter::class.java) + private val log = LoggerFactory.getLogger(CoupleConnectService::class.java) companion object { private val COUPLE_CODE_DURATION = Duration.ofMinutes(60) private const val COUPLE_CODE_PREFIX = "COUPLE_CODE:" @@ -27,7 +27,7 @@ class CoupleConnectService( val coupleCode = CoupleCodeGeneratorUtil.generateCoupleCode() val key = getCoupleCodeKey(coupleCode) redisTemplate.opsForValue().set(key, userId.toString(), COUPLE_CODE_DURATION) - log.info("[GenerateCoupleCode] generator_userId : {}, code : {}", userId, coupleCode) + log.debug("[GenerateCoupleCode] generator_userId : {}, code : {}", userId, coupleCode) return coupleCode } @@ -39,7 +39,7 @@ class CoupleConnectService( if (invitorId == inviteeId) { throw BadRequestException("sarangggun.couple.couple-code-same") } - log.info("[ConnectCouple] invitorId : {}, inviteeId : {}", invitorId, inviteeId) + log.debug("[ConnectCouple] invitorId : {}, inviteeId : {}", invitorId, inviteeId) val couple = Couple.of( invitorId, inviteeId, @@ -51,7 +51,7 @@ class CoupleConnectService( val savedCouple = save(couple) delete(key) - log.info("[ConnectCouple] invitorId : {}, inviteeId : {} - connect Succeed!", invitorId, inviteeId) + log.debug("[ConnectCouple] invitorId : {}, inviteeId : {} - connect Succeed!", invitorId, inviteeId) return savedCouple } From b01087d13877c244f574ab86ca395f03c5fad453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 19 Jul 2025 11:34:45 +0900 Subject: [PATCH 343/357] =?UTF-8?q?hotfix=20:=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=ED=95=A0=20=EC=BF=A0=ED=82=A4=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C,=20secure=EC=84=A4=EC=A0=95=20true=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20#157?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/auth/domain/service/AuthService.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/auth/domain/service/AuthService.kt b/src/main/kotlin/gomushin/backend/auth/domain/service/AuthService.kt index ad8b8e0a..fd011682 100644 --- a/src/main/kotlin/gomushin/backend/auth/domain/service/AuthService.kt +++ b/src/main/kotlin/gomushin/backend/auth/domain/service/AuthService.kt @@ -2,10 +2,14 @@ package gomushin.backend.auth.domain.service import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServletResponse +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service @Service -class AuthService { +class AuthService( + @Value("\${cookie.domain}") + private val cookieDomain: String +) { companion object { private const val AT_PREFIX = "access_token" private const val RT_PREFIX = "refresh_token" @@ -14,13 +18,17 @@ class AuthService { fun logout(response: HttpServletResponse) { val expiredAccess = Cookie(AT_PREFIX, "").apply { path = "/" + domain = cookieDomain isHttpOnly = true + secure = true maxAge = 0 } val expiredRefresh = Cookie(RT_PREFIX, "").apply { path = "/" + domain = cookieDomain isHttpOnly = true + secure = true maxAge = 0 } From 17f742bdcc3afeabbca45d96c6c00dbf99563ff3 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Jul 2025 11:57:20 +0900 Subject: [PATCH 344/357] =?UTF-8?q?feat:=20=EC=BB=A4=ED=94=8C=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=BB=A4=ED=94=8C=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=98=EB=8A=94=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=EC=99=80=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/gomushin/backend/member/domain/entity/Member.kt | 6 ++++++ .../gomushin/backend/member/domain/service/MemberService.kt | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt index 0a0d2ce9..14bb7df8 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt @@ -52,6 +52,8 @@ class Member( ) : BaseEntity() { companion object { + private const val EMPTY_STATUS_MESSAGE = "" + fun create( name: String, nickname: String?, @@ -100,4 +102,8 @@ class Member( fun updateIsCouple(isCouple: Boolean) { this.isCouple = isCouple } + + fun clearStatusMessage() { + this.statusMessage = EMPTY_STATUS_MESSAGE + } } diff --git a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt index 4704bf96..911f1c79 100644 --- a/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt +++ b/src/main/kotlin/gomushin/backend/member/domain/service/MemberService.kt @@ -53,4 +53,10 @@ class MemberService( fun getAllCoupledMemberWithEnabledNotification() : List{ return memberRepository.findCoupleMembersWithEnabledNotification() } + + @Transactional + fun clearMemberStatusMessage(id: Long) { + val member = getById(id) + member.clearStatusMessage() + } } From 48c9966209c06cdbb1eeafefc82c56d68f3cf569 Mon Sep 17 00:00:00 2001 From: HoyeongJeon Date: Sat, 19 Jul 2025 11:57:32 +0900 Subject: [PATCH 345/357] =?UTF-8?q?fix:=20=ED=83=88=ED=87=B4=20=EC=8B=9C?= =?UTF-8?q?=20=EC=BB=A4=ED=94=8C=20=EB=A9=94=EC=8B=9C=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/member/facade/LeaveFacade.kt | 48 ++++-- .../backend/member/facade/LeaveFacadeTest.kt | 162 ++++++++++++++++++ 2 files changed, 199 insertions(+), 11 deletions(-) create mode 100644 src/test/kotlin/gomushin/backend/member/facade/LeaveFacadeTest.kt diff --git a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt index aae35066..a7b0834b 100644 --- a/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt +++ b/src/main/kotlin/gomushin/backend/member/facade/LeaveFacade.kt @@ -34,17 +34,13 @@ class LeaveFacade( val coupleId = customUserDetails.getCouple().id val partner = coupleInfoService.findCoupleMember(memberId) anniversaryService.deleteAllByCoupleId(coupleId) - commentService.deleteAllByMemberId(memberId) - commentService.deleteAllByMemberId(partner.id) + deleteComments(memberId, partner.id) coupleService.deleteByMemberId(memberId) - notificationService.deleteAllByMember(memberId) - notificationService.deleteAllByMember(partner.id) - scheduleService.deleteAllByMemberId(memberId) - scheduleService.deleteAllByMemberId(partner.id) + deleteNotifications(memberId, partner.id) + deleteSchedules(memberId, partner.id) val memberLetters = letterService.findAllByAuthorId(memberId) val partnerLetters = letterService.findAllByAuthorId(partner.id) - val pictureUrlsToDelete = mutableListOf() pictureService.findAllByLetterIds(memberLetters) @@ -54,15 +50,15 @@ class LeaveFacade( .takeIf { it.isNotEmpty() } ?.forEach { picture -> pictureUrlsToDelete.add(picture.pictureUrl) } - pictureService.deleteAllByLetterIds(memberLetters) - pictureService.deleteAllByLetterIds(partnerLetters) + deleteAllPictures(memberLetters, partnerLetters) + deleteAllLetters(memberId, partner.id) - letterService.deleteAllByMemberId(memberId) - letterService.deleteAllByMemberId(partner.id) + clearCoupleStatusMessages(memberId, partner.id) partner.updateIsCouple(false) memberService.deleteMember(memberId) + if (pictureUrlsToDelete.isNotEmpty()) { applicationEventPublisher.publishEvent( S3DeleteEvent( @@ -71,5 +67,35 @@ class LeaveFacade( ) } } + + private fun deleteComments(memberId: Long, partnerId: Long) { + commentService.deleteAllByMemberId(memberId) + commentService.deleteAllByMemberId(partnerId) + } + + private fun deleteNotifications(memberId: Long, partnerId: Long) { + notificationService.deleteAllByMember(memberId) + notificationService.deleteAllByMember(partnerId) + } + + private fun deleteSchedules(memberId: Long, partnerId: Long) { + scheduleService.deleteAllByMemberId(memberId) + scheduleService.deleteAllByMemberId(partnerId) + } + + private fun deleteAllPictures(memberLetterIds: List, partnerLetterIds: List) { + pictureService.deleteAllByLetterIds(memberLetterIds) + pictureService.deleteAllByLetterIds(partnerLetterIds) + } + + private fun deleteAllLetters(memberId: Long, partnerId: Long) { + letterService.deleteAllByMemberId(memberId) + letterService.deleteAllByMemberId(partnerId) + } + + private fun clearCoupleStatusMessages(memberId: Long, partnerId: Long) { + memberService.clearMemberStatusMessage(memberId) + memberService.clearMemberStatusMessage(partnerId) + } } diff --git a/src/test/kotlin/gomushin/backend/member/facade/LeaveFacadeTest.kt b/src/test/kotlin/gomushin/backend/member/facade/LeaveFacadeTest.kt new file mode 100644 index 00000000..9a5ba573 --- /dev/null +++ b/src/test/kotlin/gomushin/backend/member/facade/LeaveFacadeTest.kt @@ -0,0 +1,162 @@ +package gomushin.backend.member.facade + +import gomushin.backend.core.CustomUserDetails +import gomushin.backend.core.event.dto.S3DeleteEvent +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.couple.domain.service.AnniversaryService +import gomushin.backend.couple.domain.service.CoupleInfoService +import gomushin.backend.couple.domain.service.CoupleService +import gomushin.backend.member.domain.entity.Member +import gomushin.backend.member.domain.service.MemberService +import gomushin.backend.member.domain.service.NotificationService +import gomushin.backend.schedule.domain.entity.Picture +import gomushin.backend.schedule.domain.service.CommentService +import gomushin.backend.schedule.domain.service.LetterService +import gomushin.backend.schedule.domain.service.PictureService +import gomushin.backend.schedule.domain.service.ScheduleService +import io.mockk.* +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.context.ApplicationEventPublisher + +@ExtendWith(MockKExtension::class) +class LeaveFacadeTest { + @MockK + lateinit var anniversaryService: AnniversaryService + @MockK + lateinit var commentService: CommentService + @MockK + lateinit var coupleService: CoupleService + @MockK + lateinit var letterService: LetterService + @MockK + lateinit var memberService: MemberService + @MockK + lateinit var notificationService: NotificationService + @MockK + lateinit var pictureService: PictureService + @MockK + lateinit var scheduleService: ScheduleService + @MockK + lateinit var coupleInfoService: CoupleInfoService + @MockK + lateinit var applicationEventPublisher: ApplicationEventPublisher + + @InjectMockKs + lateinit var leaveFacade: LeaveFacade + + private val customUserDetails = mockk() + private val couple = mockk() + private val partner = mockk() + + @BeforeEach + fun setUp() { + every { customUserDetails.getId() } returns 1L + every { customUserDetails.getCouple() } returns couple + every { couple.id } returns 100L + every { partner.id } returns 2L + every { partner.updateIsCouple(false) } just Runs + } + + @DisplayName("leave 메서드는 회원 탈퇴를 성공적으로 처리한다.") + @Test + fun leave_success() { + // given + val memberId = 1L + val partnerId = 2L + val coupleId = 100L + val memberLetterIds = listOf(10L, 11L) + val partnerLetterIds = listOf(20L, 21L) + val pictures = listOf( + mockk { every { pictureUrl } returns "url1" }, + mockk { every { pictureUrl } returns "url2" } + ) + + every { coupleInfoService.findCoupleMember(memberId) } returns partner + every { anniversaryService.deleteAllByCoupleId(coupleId) } just Runs + every { commentService.deleteAllByMemberId(memberId) } just Runs + every { commentService.deleteAllByMemberId(partnerId) } just Runs + every { coupleService.deleteByMemberId(memberId) } just Runs + every { notificationService.deleteAllByMember(memberId) } just Runs + every { notificationService.deleteAllByMember(partnerId) } just Runs + every { scheduleService.deleteAllByMemberId(memberId) } just Runs + every { scheduleService.deleteAllByMemberId(partnerId) } just Runs + every { letterService.findAllByAuthorId(memberId) } returns memberLetterIds + every { letterService.findAllByAuthorId(partnerId) } returns partnerLetterIds + every { pictureService.findAllByLetterIds(memberLetterIds) } returns pictures + every { pictureService.findAllByLetterIds(partnerLetterIds) } returns emptyList() + every { pictureService.deleteAllByLetterIds(memberLetterIds) } just Runs + every { pictureService.deleteAllByLetterIds(partnerLetterIds) } just Runs + every { letterService.deleteAllByMemberId(memberId) } just Runs + every { letterService.deleteAllByMemberId(partnerId) } just Runs + every { memberService.clearMemberStatusMessage(memberId) } just Runs + every { memberService.clearMemberStatusMessage(partnerId) } just Runs + every { memberService.deleteMember(memberId) } just Runs + every { applicationEventPublisher.publishEvent(any()) } just Runs + + // when + leaveFacade.leave(customUserDetails) + + // then + verify(exactly = 1) { coupleInfoService.findCoupleMember(memberId) } + verify(exactly = 1) { anniversaryService.deleteAllByCoupleId(coupleId) } + verify(exactly = 1) { commentService.deleteAllByMemberId(memberId) } + verify(exactly = 1) { commentService.deleteAllByMemberId(partnerId) } + verify(exactly = 1) { coupleService.deleteByMemberId(memberId) } + verify(exactly = 1) { notificationService.deleteAllByMember(memberId) } + verify(exactly = 1) { notificationService.deleteAllByMember(partnerId) } + verify(exactly = 1) { scheduleService.deleteAllByMemberId(memberId) } + verify(exactly = 1) { scheduleService.deleteAllByMemberId(partnerId) } + verify(exactly = 1) { letterService.findAllByAuthorId(memberId) } + verify(exactly = 1) { letterService.findAllByAuthorId(partnerId) } + verify(exactly = 1) { pictureService.findAllByLetterIds(memberLetterIds) } + verify(exactly = 1) { pictureService.findAllByLetterIds(partnerLetterIds) } + verify(exactly = 1) { pictureService.deleteAllByLetterIds(memberLetterIds) } + verify(exactly = 1) { pictureService.deleteAllByLetterIds(partnerLetterIds) } + verify(exactly = 1) { letterService.deleteAllByMemberId(memberId) } + verify(exactly = 1) { letterService.deleteAllByMemberId(partnerId) } + verify(exactly = 1) { memberService.clearMemberStatusMessage(memberId) } + verify(exactly = 1) { memberService.clearMemberStatusMessage(partnerId) } + verify(exactly = 1) { partner.updateIsCouple(false) } + verify(exactly = 1) { memberService.deleteMember(memberId) } + verify(exactly = 1) { applicationEventPublisher.publishEvent(any()) } + } + + @DisplayName("사진이 없는 경우 S3DeleteEvent가 발생하지 않는다.") + @Test + fun leave_success_without_pictures() { + // given + val memberId = 1L + val partnerId = 2L + val coupleId = 100L + val memberLetterIds = listOf(10L, 11L) + val partnerLetterIds = listOf(20L, 21L) + + every { coupleInfoService.findCoupleMember(memberId) } returns partner + every { anniversaryService.deleteAllByCoupleId(coupleId) } just Runs + every { commentService.deleteAllByMemberId(any()) } just Runs + every { coupleService.deleteByMemberId(memberId) } just Runs + every { notificationService.deleteAllByMember(any()) } just Runs + every { scheduleService.deleteAllByMemberId(any()) } just Runs + every { letterService.findAllByAuthorId(memberId) } returns memberLetterIds + every { letterService.findAllByAuthorId(partnerId) } returns partnerLetterIds + every { pictureService.findAllByLetterIds(memberLetterIds) } returns emptyList() + every { pictureService.findAllByLetterIds(partnerLetterIds) } returns emptyList() + every { pictureService.deleteAllByLetterIds(any()) } just Runs + every { letterService.deleteAllByMemberId(any()) } just Runs + every { memberService.clearMemberStatusMessage(any()) } just Runs + every { memberService.deleteMember(memberId) } just Runs + + // when + leaveFacade.leave(customUserDetails) + + // then + verify(exactly = 0) { applicationEventPublisher.publishEvent(any()) } + verify(exactly = 1) { memberService.deleteMember(memberId) } + } +} From e1cf20dce1610e4432770d6affc2878c9e1bd98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 21 Jul 2025 20:29:25 +0900 Subject: [PATCH 346/357] =?UTF-8?q?feat=20:=20findByCoupleIdAndYearAndMont?= =?UTF-8?q?h=EC=97=90=20endDate=ED=95=84=EB=93=9C=20=EC=83=81=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=EB=8F=84=20=EA=B2=80=EC=83=89=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20#161?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/domain/repository/ScheduleRepository.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt index 0629c7bd..61cebd3e 100644 --- a/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt +++ b/src/main/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepository.kt @@ -27,8 +27,12 @@ interface ScheduleRepository : JpaRepository { ) FROM Schedule s WHERE s.coupleId = :coupleId - AND function('YEAR', s.startDate) = :year - AND function('MONTH', s.startDate) = :month + AND ( + (function('YEAR', s.startDate) = :year AND function('MONTH', s.startDate) = :month) + OR + (function('YEAR', s.endDate) = :year AND function('MONTH', s.endDate) = :month) + ) + """ ) fun findByCoupleIdAndYearAndMonth(coupleId: Long, year: Int, month: Int): List From e97a464aba4d36a06e966126e2a507ee7d5afdeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 21 Jul 2025 20:29:44 +0900 Subject: [PATCH 347/357] =?UTF-8?q?test=20:=20findByCoupleIdAndYearAndMont?= =?UTF-8?q?h=EC=97=90=20endDate=ED=95=84=EB=93=9C=20=EC=83=81=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=EB=8F=84=20=EA=B2=80=EC=83=89=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20->=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EB=A0=88=ED=8C=8C=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20#161?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ScheduleRepositoryTest.kt | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/test/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepositoryTest.kt diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepositoryTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepositoryTest.kt new file mode 100644 index 00000000..46bef48d --- /dev/null +++ b/src/test/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepositoryTest.kt @@ -0,0 +1,87 @@ +package gomushin.backend.schedule.domain.repository + +import gomushin.backend.couple.domain.entity.Couple +import gomushin.backend.schedule.domain.entity.Schedule +import gomushin.backend.schedule.dto.response.MonthlySchedulesResponse +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.Matchers.containsInAnyOrder +import org.junit.jupiter.api.Assertions.assertEquals +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.junit.jupiter.api.extension.ExtendWith +import org.mockito.junit.jupiter.MockitoExtension +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.time.Month + +@DataJpaTest +@Transactional +@ExtendWith(MockitoExtension::class) +class ScheduleRepositoryTest @Autowired constructor( + val scheduleRepository : ScheduleRepository +){ + private lateinit var couple : Couple + @BeforeEach + fun setup() { + couple = Couple.of(1L, 2L); + val scheduleList = listOf( + Schedule.of( + couple.id, + 1L, + "시작월과 끝월이 같음", + LocalDateTime.of(2025, 7, 21, 0, 0, 0, 0), + LocalDateTime.of(2025, 7,22,0,0,0,0), + "VERY_TIRED", + true), + Schedule.of( + couple.id, + 1L, + "시작월과 끝월이 다름", + LocalDateTime.of(2025, 7, 21, 0, 0, 0, 0), + LocalDateTime.of(2025, 8,1,0,0,0,0), + "VERY_TIRED", + true) + ) + scheduleRepository.saveAll(scheduleList); + } + @DisplayName("startDate 기준으로 검색이 되는지 테스트") + @Test + fun filters_schedules_by_startDate_within_given_year_and_month() { + //when + val responseList = scheduleRepository.findByCoupleIdAndYearAndMonth(couple.id, 2025, 7) + //then + assertEquals(2, responseList.size) + assertTrue( + responseList.stream().allMatch { i: MonthlySchedulesResponse -> + (i.startDate.year == 2025 && i.endDate.year == 2025) + && + (i.startDate.month == Month.JULY || i.endDate.month == Month.JULY) + } + ) + val actualContents: List = responseList.map { it.title } + MatcherAssert.assertThat(actualContents, containsInAnyOrder("시작월과 끝월이 같음", "시작월과 끝월이 다름")) + } + + @DisplayName("endDate 기준으로 검색이 되는지 테스트") + @Test + fun filters_schedules_by_endDate_within_given_year_and_month() { + //when + val responseList = scheduleRepository.findByCoupleIdAndYearAndMonth(couple.id, 2025, 8) + //then + assertEquals(1, responseList.size) + assertTrue( + responseList.stream().allMatch { i: MonthlySchedulesResponse -> + (i.startDate.year == 2025 && i.endDate.year == 2025) + && + (i.startDate.month == Month.JULY || i.endDate.month == Month.JULY) + } + ) + val actualContents: List = responseList.map { it.title } + MatcherAssert.assertThat(actualContents, containsInAnyOrder("시작월과 끝월이 다름")) + } +} \ No newline at end of file From cc42484f8f9452c6540f65926837b6fff91a9253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 21 Jul 2025 20:35:25 +0900 Subject: [PATCH 348/357] =?UTF-8?q?test=20:=20endDate=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B2=80=EC=83=89=EC=9D=B4=20=EB=90=98?= =?UTF-8?q?=EB=8A=94=EC=A7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=EC=97=90=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EA=B5=AC=EB=AC=B8=20=EC=88=98=EC=A0=95(startDate=EB=98=90?= =?UTF-8?q?=EB=8A=94=20endDate=EA=B0=80=208=EC=9B=94=EC=97=90=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=ED=95=98=EB=8A=94=EC=A7=80=20=EA=B2=80=EC=82=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EB=80=8C=EC=96=B4=EC=95=BC=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BF=88)=20#161?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/domain/repository/ScheduleRepositoryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepositoryTest.kt b/src/test/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepositoryTest.kt index 46bef48d..51361427 100644 --- a/src/test/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepositoryTest.kt +++ b/src/test/kotlin/gomushin/backend/schedule/domain/repository/ScheduleRepositoryTest.kt @@ -78,7 +78,7 @@ class ScheduleRepositoryTest @Autowired constructor( responseList.stream().allMatch { i: MonthlySchedulesResponse -> (i.startDate.year == 2025 && i.endDate.year == 2025) && - (i.startDate.month == Month.JULY || i.endDate.month == Month.JULY) + (i.startDate.month == Month.AUGUST || i.endDate.month == Month.AUGUST) } ) val actualContents: List = responseList.map { it.title } From aa1e3675c67debb2451eb0dc973f601975504cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 21 Jul 2025 22:54:11 +0900 Subject: [PATCH 349/357] =?UTF-8?q?fix=20:=20requestpart=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=EC=A0=81=20=EB=B0=94=EC=9D=B8=EB=94=A9=20=EC=88=98?= =?UTF-8?q?=ED=96=89=20#163?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/presentation/UpsertAndDeleteLetterController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt index b271c59e..09d23fe2 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt @@ -24,7 +24,7 @@ class UpsertAndDeleteLetterController( @Operation(summary = "편지 수정하거나 추가하기", description = "upsertLetter") fun upsertLetter( @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @RequestPart upsertLetterRequest: UpsertLetterRequest, + @RequestPart("upsertLetterRequest") upsertLetterRequest: UpsertLetterRequest, @RequestPart("pictures", required = false) pictures: List?, ): ApiResponse { upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) From 13bcbbf040240ea67a50c9100ba3d78192e8e7e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 22 Jul 2025 00:59:39 +0900 Subject: [PATCH 350/357] =?UTF-8?q?fix=20:=20multipart=EC=9D=BC=EB=95=8C?= =?UTF-8?q?=EB=8A=94=20controller=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=ED=96=89=20#163?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/infrastructure/filter/logging/LoggingFilter.kt | 3 +++ .../presentation/UpsertAndDeleteLetterController.kt | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt index e42a4772..68695b3b 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt @@ -18,6 +18,9 @@ class LoggingFilter : Filter { override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { if (request is HttpServletRequest) { + if(request.contentType?.startsWith("multipart/") == true) { + chain.doFilter(request, response) + } val wrappedRequest = CachedBodyHttpServletRequest(request) val url = wrappedRequest.requestURI diff --git a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt index 09d23fe2..46b5d69d 100644 --- a/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt +++ b/src/main/kotlin/gomushin/backend/schedule/presentation/UpsertAndDeleteLetterController.kt @@ -2,12 +2,13 @@ package gomushin.backend.schedule.presentation import gomushin.backend.core.CustomUserDetails import gomushin.backend.core.common.web.response.ApiResponse +import gomushin.backend.core.infrastructure.filter.logging.LoggingFilter import gomushin.backend.schedule.dto.request.UpsertLetterRequest import gomushin.backend.schedule.facade.UpsertAndDeleteLetterFacade import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus -import org.springframework.http.MediaType import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile @@ -17,6 +18,7 @@ import org.springframework.web.multipart.MultipartFile class UpsertAndDeleteLetterController( private val upsertAndDeleteLetterFacade: UpsertAndDeleteLetterFacade, ) { + private val log = LoggerFactory.getLogger(UpsertAndDeleteLetterController::class.java) @ResponseStatus(HttpStatus.CREATED) @PostMapping( ApiPath.LETTERS @@ -24,9 +26,10 @@ class UpsertAndDeleteLetterController( @Operation(summary = "편지 수정하거나 추가하기", description = "upsertLetter") fun upsertLetter( @AuthenticationPrincipal customUserDetails: CustomUserDetails, - @RequestPart("upsertLetterRequest") upsertLetterRequest: UpsertLetterRequest, + @RequestPart upsertLetterRequest: UpsertLetterRequest, @RequestPart("pictures", required = false) pictures: List?, ): ApiResponse { + log.info("[REQUEST LOG] userId={}, URL={}, Method={}, Body={}", customUserDetails.getId(), ApiPath.LETTERS, "POST", upsertLetterRequest) upsertAndDeleteLetterFacade.upsert(customUserDetails, upsertLetterRequest, pictures) return ApiResponse.success(true) } From aadb38b3fa2a6903d438dd8f34b04e870a3034f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Tue, 22 Jul 2025 01:15:12 +0900 Subject: [PATCH 351/357] =?UTF-8?q?chore=20:=20[=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=B0=98=EC=98=81]=20return=EA=B5=AC=EB=AC=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#163?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/core/infrastructure/filter/logging/LoggingFilter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt index 68695b3b..3cd03e67 100644 --- a/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt +++ b/src/main/kotlin/gomushin/backend/core/infrastructure/filter/logging/LoggingFilter.kt @@ -20,6 +20,7 @@ class LoggingFilter : Filter { if (request is HttpServletRequest) { if(request.contentType?.startsWith("multipart/") == true) { chain.doFilter(request, response) + return } val wrappedRequest = CachedBodyHttpServletRequest(request) From fa71ac021ac16aca699d251c75d37e731c2ff400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Sat, 19 Jul 2025 17:44:47 +0900 Subject: [PATCH 352/357] =?UTF-8?q?feat=20:=20Loki,=20promtail,=20grafana?= =?UTF-8?q?=20=EA=B5=AC=EC=B6=95=20#155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 + build.gradle.kts | 3 + docker-compose.yml | 4 ++ observability/docker-compose.monitoring.yml | 42 ++++++++++++ observability/loki/local-config.yaml | 65 +++++++++++++++++++ observability/promtail/promtail-config.yaml | 14 ++++ .../domain/service/CoupleConnectService.kt | 6 +- src/main/resources/logback-spring.xml | 27 ++++++++ 8 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 observability/docker-compose.monitoring.yml create mode 100644 observability/loki/local-config.yaml create mode 100644 observability/promtail/promtail-config.yaml create mode 100644 src/main/resources/logback-spring.xml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e85a031c..9d9762d6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -88,6 +88,9 @@ jobs: - name: Docker Compose 파일 EC2 서버로 전송 run: scp -o StrictHostKeyChecking=no -P ${{ secrets.EC2_PORT }} docker-compose.yml ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}:./ + - name: Docker Compose Mornitoring 파일 EC2 서버로 전송 + run: scp -o StrictHostKeyChecking=no -P ${{ secrets.EC2_PORT }} ./observability/docker-compose.monitoring.yml ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}:~/observability/ + - name: EC2 접속 후 이미지 다운로드 및 배포 if: success() uses: appleboy/ssh-action@master diff --git a/build.gradle.kts b/build.gradle.kts index fc41345c..a61c09b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -86,6 +86,9 @@ dependencies { //serializable implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") + //logback-encoder + implementation ("net.logstash.logback:logstash-logback-encoder:7.4") + implementation("org.springframework.boot:spring-boot-starter-validation") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") diff --git a/docker-compose.yml b/docker-compose.yml index d066f87a..ada0f93d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: TZ: Asia/Seoul ports: - '8080:8080' + volumes: + - /home/ubuntu/my/logs:/my/logs depends_on: - redis - minio @@ -21,6 +23,8 @@ services: TZ: Asia/Seoul ports: - '8081:8080' + volumes: + - /home/ubuntu/my/logs:/my/logs depends_on: - redis - minio diff --git a/observability/docker-compose.monitoring.yml b/observability/docker-compose.monitoring.yml new file mode 100644 index 00000000..5e11568f --- /dev/null +++ b/observability/docker-compose.monitoring.yml @@ -0,0 +1,42 @@ +version: '3.8' + +services: + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + - "9096:9096" + volumes: + - ./loki:/etc/loki + command: -config.file=/etc/loki/local-config.yaml + restart: unless-stopped + networks: + - sarang-backend-network + + promtail: + image: grafana/promtail:2.9.0 + volumes: + - ./promtail/config.yaml:/etc/promtail/config.yaml:ro + - /home/ubuntu/my/logs:/var/my/logs:ro # 호스트 로그 → 컨테이너 안 경로 + restart: unless-stopped + networks: + - sarang-backend-network + depends_on: + - loki + + grafana: + image: grafana/grafana:10.2.0 + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + restart: unless-stopped + networks: + - sarang-backend-network + depends_on: + - loki + + +networks: + sarang-backend-network: + driver: bridge \ No newline at end of file diff --git a/observability/loki/local-config.yaml b/observability/loki/local-config.yaml new file mode 100644 index 00000000..2ab8d8ac --- /dev/null +++ b/observability/loki/local-config.yaml @@ -0,0 +1,65 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + log_level: info + grpc_server_max_concurrent_streams: 100 + +common: + instance_addr: 127.0.0.1 + path_prefix: /tmp/loki + storage: + filesystem: + chunks_directory: /tmp/loki/chunks + rules_directory: /tmp/loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + +limits_config: + metric_aggregation_enabled: true + enable_multi_variant_queries: true + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +pattern_ingester: + enabled: true + metric_aggregation: + loki_address: localhost:3100 + +ruler: + alertmanager_url: http://localhost:9093 + +frontend: + encoding: protobuf + + +# By default, Loki will send anonymous, but uniquely-identifiable usage and configuration +# analytics to Grafana Labs. These statistics are sent to https://stats.grafana.org/ +# +# Statistics help us better understand how Loki is used, and they show us performance +# levels for most users. This helps us prioritize features and documentation. +# For more information on what's sent, look at +# https://github.com/grafana/loki/blob/main/pkg/analytics/stats.go +# Refer to the buildReport method to see what goes into a report. +# +# If you would like to disable reporting, uncomment the following lines: +#analytics: +# reporting_enabled: false \ No newline at end of file diff --git a/observability/promtail/promtail-config.yaml b/observability/promtail/promtail-config.yaml new file mode 100644 index 00000000..41665294 --- /dev/null +++ b/observability/promtail/promtail-config.yaml @@ -0,0 +1,14 @@ +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://localhost:3100/loki/api/v1/push + +scrape_configs: + - job_name: application-log + static_configs: + - targets: + - localhost + labels: + job: logs + __path__: /var/my/logs/*.log \ No newline at end of file diff --git a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt index 05626185..f22e6b3f 100644 --- a/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt +++ b/src/main/kotlin/gomushin/backend/couple/domain/service/CoupleConnectService.kt @@ -27,7 +27,7 @@ class CoupleConnectService( val coupleCode = CoupleCodeGeneratorUtil.generateCoupleCode() val key = getCoupleCodeKey(coupleCode) redisTemplate.opsForValue().set(key, userId.toString(), COUPLE_CODE_DURATION) - log.debug("[GenerateCoupleCode] generator_userId : {}, code : {}", userId, coupleCode) + log.info("[GenerateCoupleCode] generator_userId : {}, code : {}", userId, coupleCode) return coupleCode } @@ -39,7 +39,7 @@ class CoupleConnectService( if (invitorId == inviteeId) { throw BadRequestException("sarangggun.couple.couple-code-same") } - log.debug("[ConnectCouple] invitorId : {}, inviteeId : {}", invitorId, inviteeId) + log.info("[ConnectCouple] invitorId : {}, inviteeId : {}", invitorId, inviteeId) val couple = Couple.of( invitorId, inviteeId, @@ -51,7 +51,7 @@ class CoupleConnectService( val savedCouple = save(couple) delete(key) - log.debug("[ConnectCouple] invitorId : {}, inviteeId : {} - connect Succeed!", invitorId, inviteeId) + log.info("[ConnectCouple] invitorId : {}, inviteeId : {} - connect Succeed!", invitorId, inviteeId) return savedCouple } diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..624f2c4f --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,27 @@ + + + + + ${LOG_PATH}/application-%d{yyyy-MM-dd}.%i.log + 2 + 100MB + 1GB + + + %d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n + + + + + + + %d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n + + + + + + + + + \ No newline at end of file From 8cf466a3abbe352f317155bb225687e81427a78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 4 Aug 2025 19:39:40 +0900 Subject: [PATCH 353/357] =?UTF-8?q?chore=20:=20[=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=B0=98=EC=98=81]=20loki=EB=B2=84=EC=A0=84=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=20#155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- observability/docker-compose.monitoring.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/observability/docker-compose.monitoring.yml b/observability/docker-compose.monitoring.yml index 5e11568f..5537e4a3 100644 --- a/observability/docker-compose.monitoring.yml +++ b/observability/docker-compose.monitoring.yml @@ -2,7 +2,7 @@ version: '3.8' services: loki: - image: grafana/loki:latest + image: grafana/loki:main-1063089 ports: - "3100:3100" - "9096:9096" From 247da8e136bb3d11c16042c57a26c02afb82341b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 4 Aug 2025 19:54:08 +0900 Subject: [PATCH 354/357] =?UTF-8?q?chore=20:=20[=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=B0=98=EC=98=81]=20grafana=20=EC=95=94=ED=98=B8=ED=99=94=20#?= =?UTF-8?q?155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 1 + observability/docker-compose.monitoring.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9d9762d6..818864fe 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -107,5 +107,6 @@ jobs: echo "MINIO_ROOT_PASSWORD=${{secrets.MINIO_ROOT_PASSWORD}}" >> .env echo "MINIO_SERVER_URL=${{secrets.MINIO_SERVER_URL}}" >> .env echo "MINIO_BROWSER_REDIRECT_URL=${{secrets.MINIO_BROWSER_REDIRECT_URL}}" >> .env + echo "GF_SECURITY_ADMIN_PASSWORD=${{secrets.GF_SECURITY_ADMIN_PASSWORD}}" >> .env sudo chmod +x ./deploy.sh ./deploy.sh diff --git a/observability/docker-compose.monitoring.yml b/observability/docker-compose.monitoring.yml index 5537e4a3..3f747977 100644 --- a/observability/docker-compose.monitoring.yml +++ b/observability/docker-compose.monitoring.yml @@ -29,7 +29,7 @@ services: ports: - "3000:3000" environment: - - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD} restart: unless-stopped networks: - sarang-backend-network From b1c6d187f557e17b8e4dfc89c28f343d4a09dc25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 4 Aug 2025 19:59:27 +0900 Subject: [PATCH 355/357] =?UTF-8?q?chore=20:=20[=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=B0=98=EC=98=81]=20grafana=20=EC=95=94=ED=98=B8=ED=99=94=20#?= =?UTF-8?q?155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- observability/docker-compose.monitoring.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/observability/docker-compose.monitoring.yml b/observability/docker-compose.monitoring.yml index 3f747977..21da7fad 100644 --- a/observability/docker-compose.monitoring.yml +++ b/observability/docker-compose.monitoring.yml @@ -30,6 +30,7 @@ services: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD} + - GF_SECURITY_ADMIN_PWD__IS__SET=true restart: unless-stopped networks: - sarang-backend-network From c128a7ea7d54e35f39db5424b7963fc45cb87d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 4 Aug 2025 20:21:38 +0900 Subject: [PATCH 356/357] =?UTF-8?q?chore=20:=20[=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=B0=98=EC=98=81]=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=ED=99=94=20#155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 ++ docker-compose.yml | 4 ++-- observability/docker-compose.monitoring.yml | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 818864fe..cd2447bb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -108,5 +108,7 @@ jobs: echo "MINIO_SERVER_URL=${{secrets.MINIO_SERVER_URL}}" >> .env echo "MINIO_BROWSER_REDIRECT_URL=${{secrets.MINIO_BROWSER_REDIRECT_URL}}" >> .env echo "GF_SECURITY_ADMIN_PASSWORD=${{secrets.GF_SECURITY_ADMIN_PASSWORD}}" >> .env + echo "HOST_LOG_DIR=${{secrets.HOST_LOG_DIR}}" >> .env + chmod 600 .env sudo chmod +x ./deploy.sh ./deploy.sh diff --git a/docker-compose.yml b/docker-compose.yml index ada0f93d..6925cea3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: ports: - '8080:8080' volumes: - - /home/ubuntu/my/logs:/my/logs + - ${HOST_LOG_DIR:-./logs}:/my/logs depends_on: - redis - minio @@ -24,7 +24,7 @@ services: ports: - '8081:8080' volumes: - - /home/ubuntu/my/logs:/my/logs + - ${HOST_LOG_DIR:-./logs}:/my/logs depends_on: - redis - minio diff --git a/observability/docker-compose.monitoring.yml b/observability/docker-compose.monitoring.yml index 21da7fad..945f5ec5 100644 --- a/observability/docker-compose.monitoring.yml +++ b/observability/docker-compose.monitoring.yml @@ -16,8 +16,8 @@ services: promtail: image: grafana/promtail:2.9.0 volumes: - - ./promtail/config.yaml:/etc/promtail/config.yaml:ro - - /home/ubuntu/my/logs:/var/my/logs:ro # 호스트 로그 → 컨테이너 안 경로 + - ./promtail/promtail-config.yaml:/etc/promtail/config.yaml:ro + - ${HOST_LOG_DIR:-./logs}:/var/my/logs:ro # 호스트 로그 → 컨테이너 안 경로 restart: unless-stopped networks: - sarang-backend-network From fb2f06ca3fe577b3df1bcf63c52f34268ba88362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=98=81=EB=A1=9D?= Date: Mon, 4 Aug 2025 20:24:46 +0900 Subject: [PATCH 357/357] =?UTF-8?q?chore=20:=20[=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=B0=98=EC=98=81]=20loki=EB=B2=84=EC=A0=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20#155?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- observability/docker-compose.monitoring.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/observability/docker-compose.monitoring.yml b/observability/docker-compose.monitoring.yml index 945f5ec5..2b0a9e89 100644 --- a/observability/docker-compose.monitoring.yml +++ b/observability/docker-compose.monitoring.yml @@ -2,7 +2,7 @@ version: '3.8' services: loki: - image: grafana/loki:main-1063089 + image: grafana/loki:2.9.0 ports: - "3100:3100" - "9096:9096"