Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature/12] feat: 온보딩 선택을 등록하는 API 구현 #14

Merged
merged 10 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#

# 문서
swagger: http://localhost:8080/swagger-ui

# 설정
vm options: `-Duser.timezone=Asia/Seoul`
22 changes: 21 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ plugins {
kotlin("plugin.jpa") version "1.9.24"
kotlin("jvm") version "1.9.24"
kotlin("plugin.spring") version "1.9.24"
kotlin("plugin.allopen") version "1.5.31"
kotlin("plugin.noarg") version "1.5.31"
}

allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.Convert")
}

noArg {
annotation("javax.persistence.Entity")
annotation("javax.persistence.Convert")
Comment on lines +7 to +18
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코틀린은 디폴트가 final인데, jpa는 프록시를 생성해야 하기 때문에 상속이 가능해야하잖아요. 그래서 open으로 상속이 가능하게 바꿔주는 설정입니다

}

group = "com.nexters"
Expand All @@ -23,13 +35,21 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")

implementation("io.springfox:springfox-boot-starter:3.0.0")
implementation("io.springfox:springfox-swagger-ui:3.0.0")
Comment on lines +39 to +40
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swagger 관련 설정입니다!


implementation("com.fasterxml.jackson.core:jackson-databind:2.15.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

implementation("io.github.microutils:kotlin-logging:3.0.5")
implementation("org.slf4j:slf4j-simple:2.0.7")
Comment on lines +45 to +46
Copy link
Collaborator Author

@injoon2019 injoon2019 Jul 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코틀린 로그 설정이에요. 로그 남기고 싶은 클래스에서

    private val log = KotlinLogging.logger {  }

    log.info { "로그입니다" }

이렇게 쓰시면 돼요


implementation("org.jetbrains.kotlin:kotlin-reflect")

runtimeOnly("com.h2database:h2")
runtimeOnly("mysql:mysql-connector-java")


testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/nexters/bottles/BottlesApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import org.springframework.boot.runApplication
class BottlesApplication

fun main(args: Array<String>) {
runApplication<BottlesApplication>(*args)
runApplication<BottlesApplication>(*args)
}
28 changes: 28 additions & 0 deletions src/main/kotlin/com/nexters/bottles/config/JacksonConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.nexters.bottles.config

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinFeature
import com.fasterxml.jackson.module.kotlin.KotlinModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class JacksonConfig {

companion object {
val kotlinModule = KotlinModule.Builder()
.withReflectionCacheSize(512)
.configure(KotlinFeature.NullToEmptyCollection, false)
.configure(KotlinFeature.NullToEmptyMap, false)
.configure(KotlinFeature.NullIsSameAsDefault, false)
.configure(KotlinFeature.StrictNullChecks, false)
.build()
}

@Bean
fun objectMapper(): ObjectMapper {
return ObjectMapper().registerModule(
kotlinModule
)
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/com/nexters/bottles/config/SwaggerConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.nexters.bottles.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import springfox.documentation.builders.PathSelectors
import springfox.documentation.builders.RequestHandlerSelectors
import springfox.documentation.spi.DocumentationType
import springfox.documentation.spring.web.plugins.Docket

@Configuration
class SwaggerConfig {

@Bean
fun api(): Docket {
return Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.nexters.bottles"))
.paths(PathSelectors.any())
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.nexters.bottles.user.controller

import com.nexters.bottles.user.controller.dto.ProfileChoiceResponseDto
import com.nexters.bottles.user.controller.dto.RegisterProfileRequestDto
import com.nexters.bottles.user.facade.UserProfileFacade
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.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/v1")
class UserProfileController(
private val profileFacade: UserProfileFacade,
) {

@PostMapping("/profile/choice")
fun registerProfile(@RequestBody registerProfileRequestDto: RegisterProfileRequestDto) {
profileFacade.saveProfile(registerProfileRequestDto)
}

@GetMapping("/profile/choice")
fun getProfileChoiceList() : ProfileChoiceResponseDto {
return profileFacade.getProfileChoice()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.nexters.bottles.user.controller.dto

data class InterestDto(
val culture: List<String> = arrayListOf(),
val sports: List<String> = arrayListOf(),
val entertainment: List<String> = arrayListOf(),
val etc: List<String> = arrayListOf(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.nexters.bottles.user.controller.dto

data class ProfileChoiceResponseDto(
val regions: List<Map<String, Any>>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.nexters.bottles.user.controller.dto

data class RegionDto(
val city: String,
val state: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.nexters.bottles.user.controller.dto

data class RegisterProfileRequestDto(
val mbti: String,
val keyword: List<String>,
val interest: InterestDto,
val job: String,
var smoking: String,
var alcohol: String,
val religion: String,
val region: RegionDto
) {
}
15 changes: 15 additions & 0 deletions src/main/kotlin/com/nexters/bottles/user/domain/BaseEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.nexters.bottles.user.domain

import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp
import java.time.LocalDateTime
import javax.persistence.MappedSuperclass

@MappedSuperclass
open class BaseEntity(
@CreationTimestamp
open val createdAt: LocalDateTime = LocalDateTime.now(),

@UpdateTimestamp
open var updatedAt: LocalDateTime = LocalDateTime.now()
)
25 changes: 25 additions & 0 deletions src/main/kotlin/com/nexters/bottles/user/domain/User.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.nexters.bottles.user.domain

import com.nexters.bottles.user.domain.enum.Gender
import javax.persistence.*

@Entity
class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

var name: String? = null,

var kakaoId: String? = null,

var phoneNumber: String? = null,

@OneToOne(mappedBy = "user", cascade = [CascadeType.ALL])
var userProfile: UserProfile? = null,
Comment on lines +18 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 @OneToOne을 걸면 User와 UserProfile이 양방향 연관관계가 되는데, 굳이 양방향 관계를 가질 필요가 없을 것 같아요!
그래서 제거해도 될 것 같은데 어떠신가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 없는게 좋습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.MappingException: Could not determine type for: com.nexters.bottles.user.domain.User, at table: user_profile, for columns: [org.hibernate.mapping.Column(user_id)]

요거 없앴더니 이렇게 에러가 나요!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 혹시 어떤부분을 없앤건가요? User에서 24,25 line 삭제했는데 저렇게 나오나요?!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네! 제가 이해를 잘 못했나 일단 요것도 뒤에서 챙겨볼게요!


@Enumerated(EnumType.STRING)
var gender: Gender = Gender.MALE,

) : BaseEntity() {
}
32 changes: 32 additions & 0 deletions src/main/kotlin/com/nexters/bottles/user/domain/UserProfile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.nexters.bottles.user.domain

import com.nexters.bottles.user.controller.dto.InterestDto
import com.nexters.bottles.user.controller.dto.RegionDto
import com.nexters.bottles.user.repository.converter.UserProfileSelectConverter
import javax.persistence.*

@Entity
class UserProfile(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,

@OneToOne
@JoinColumn(name = "user_id")
var user: User? = null,

@Convert(converter = UserProfileSelectConverter::class)
var profileSelect: UserProfileSelect,
) : BaseEntity()

data class UserProfileSelect(
val mbti: String,
val keyword: List<String> = arrayListOf(),
val interest: InterestDto,
val job: String,
val smoking: String,
val alcohol: String,
val religion: String,
val region: RegionDto,
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.nexters.bottles.user.domain.enum

enum class Gender(
val displayName: String,
) {
MALE("남자"),
FEMALE("여자"),
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.nexters.bottles.user.facade

import com.nexters.bottles.user.controller.dto.ProfileChoiceResponseDto
import com.nexters.bottles.user.controller.dto.RegisterProfileRequestDto
import com.nexters.bottles.user.domain.UserProfile
import com.nexters.bottles.user.domain.UserProfileSelect
import com.nexters.bottles.user.service.UserProfileService
import org.springframework.stereotype.Component
import regions

@Component
class UserProfileFacade(
private val profileService: UserProfileService,
) {

fun saveProfile(profileDto: RegisterProfileRequestDto) {
validateProfile(profileDto)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Facade 클래스에서 하는 일은 dto를 검증하는 일이 되겠군요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아요 그리고 여러 서비스에서 조회한 것들을 합치고, 로직이 들어갑니다. 또 외부 api를 호출하는 클래스를 호출하기도하고요

val convertedProfileDto = convertProfileDto(profileDto)

profileService.saveProfile(
UserProfile(
profileSelect = UserProfileSelect(
mbti = convertedProfileDto.mbti,
keyword = convertedProfileDto.keyword,
interest = convertedProfileDto.interest,
job = convertedProfileDto.job,
smoking = convertedProfileDto.smoking,
alcohol = convertedProfileDto.alcohol,
religion = convertedProfileDto.religion,
region = convertedProfileDto.region,
)
)
)
}

fun getProfileChoice(): ProfileChoiceResponseDto {
return ProfileChoiceResponseDto(
regions = regions
)
}

private fun validateProfile(profileDto: RegisterProfileRequestDto) {
require(profileDto.keyword.size <= 5) {
"키워드는 5개 이하여야 해요"
}
val interestCount = profileDto.interest.culture.size + profileDto.interest.sports.size
+profileDto.interest.entertainment.size + profileDto.interest.etc.size
require(interestCount <= 5) {
"취미는 5개 이하여야 해요"
Comment on lines +46 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스크린샷 2024-07-22 오후 1 30 10

38 line에서 인텔리제이에서 경고가 뜨는데 연산이 제대로 동작하는지 확인해주세요!

Copy link
Collaborator Author

@injoon2019 injoon2019 Jul 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

엇 저는 안뜨는데 공백이 없어서 그런가 싶어서 공백 추가할게요!

}
}

private fun convertProfileDto(profileDto: RegisterProfileRequestDto): RegisterProfileRequestDto {
when(profileDto.smoking) {
"전혀 피우지 않아요" -> profileDto.smoking = "흡연 안해요"
"가끔 피워요" -> profileDto.smoking = "흡연은 가끔"
"자주 피워요" -> profileDto.smoking = "흡연해요"
}
when(profileDto.alcohol) {
"한 방울도 마시지 않아요" -> profileDto.smoking = "술은 안해요"
"때에 따라 적당히 즐겨요" -> profileDto.smoking = "술은 적당히"
"자주 찾는 편이에요" -> profileDto.smoking = "술을 즐겨요"
}
return profileDto
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.nexters.bottles.user.repository

import com.nexters.bottles.user.domain.UserProfile
import org.springframework.data.jpa.repository.JpaRepository

interface UserProfileRepository : JpaRepository<UserProfile, Long>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.nexters.bottles.user.repository

import com.nexters.bottles.user.domain.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository : JpaRepository<User, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.nexters.bottles.user.repository.converter

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinFeature
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.nexters.bottles.config.JacksonConfig.Companion.kotlinModule
import com.nexters.bottles.user.domain.UserProfileSelect
import javax.persistence.AttributeConverter
import javax.persistence.Converter

@Converter(autoApply = true)
class UserProfileSelectConverter : AttributeConverter<UserProfileSelect, String> {

private val objectMapper = ObjectMapper().registerModule(kotlinModule)

override fun convertToDatabaseColumn(attribute: UserProfileSelect?): String {
return try {
objectMapper.writeValueAsString(attribute)
} catch (e: Exception) {
throw RuntimeException("Error converting JSON to String", e)
}
}

override fun convertToEntityAttribute(dbData: String?): UserProfileSelect? {
return try {
dbData?.let { objectMapper.readValue(it, UserProfileSelect::class.java) }
} catch (e: Exception) {
throw RuntimeException("Error converting String to JSON", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.nexters.bottles.user.service

import com.nexters.bottles.user.domain.UserProfile
import com.nexters.bottles.user.repository.UserProfileRepository
import com.nexters.bottles.user.repository.UserRepository
import mu.KotlinLogging
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import javax.transaction.Transactional

@Service
class UserProfileService(
private val profileRepository: UserProfileRepository,
private val userRepository: UserRepository,
) {

private val log = KotlinLogging.logger { }

@Transactional
fun saveProfile(userProfile: UserProfile): UserProfile {
val user = userRepository.findByIdOrNull(1L) // TODO User 회원 가입 기능 구현후 수정

userProfile.user = user
return profileRepository.save(userProfile)
}
Comment on lines +19 to +25
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jpa 객체 매핑이 잘 생각나지 않는데..

image
이렇게 1차 캐시에 없으면 null이 나더라고요. 원래 이랬던가요?? 사실 user는 id만 있어도 되는건데, 불필요하게 조회하는 느낌이 들었어요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오류가 저 api를 호출할 때 나는건가요?
우선 user_profile 테이블에서 user_id가 not null로 설정되어 있어서 save 할 때 null 이면 무조건 오류가 나는거 아닐까요!
그리고 어짜피 프로필을 등록하려는 user를 검증하려면 결국 user를 한 번은 조회해야 하지 않나요?!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 api 호출때요! 아하 넵넵 그럼 문제가 없는걸로요!

}
Loading
Loading