-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 회원 설문 도메인 및 API 구현 #32
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
Changes from 6 commits
c33a9c8
dfebb43
7a189be
e0e645c
f5c6781
ae57e12
c64d3cd
9944d61
b1dc23e
7b73de2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.doki.user.user.application | ||
|
|
||
| import com.doki.user.user.application.dto.CreateUserSurveyCommand | ||
| import com.doki.user.user.domain.UserRepository | ||
| import com.doki.user.user.domain.UserSurvey | ||
| import com.doki.user.user.domain.dto.UserSurveyCreatedResponse | ||
| import com.doki.user.user.domain.dto.UserSurveyResponse | ||
| import com.doki.user.user.domain.UserSurveyRepository | ||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
|
|
||
| @Transactional | ||
| @Service | ||
| class UserSurveyCommandService( | ||
| private val userRepository: UserRepository, | ||
| private val userSurveyRepository: UserSurveyRepository, | ||
| ) { | ||
|
|
||
| fun findUserSurvey(userId: Long): UserSurveyResponse { | ||
| val survey = userSurveyRepository.findByUserId(userId) | ||
| ?: throw IllegalArgumentException("UserSurvey does not exist.") | ||
|
|
||
| return UserSurveyResponse.from(survey) | ||
| } | ||
|
|
||
| fun saveUserSurvey(userId: Long, command: CreateUserSurveyCommand): UserSurveyCreatedResponse { | ||
| val user = userRepository.findById(userId) | ||
| ?: throw IllegalArgumentException("User with id $userId does not exist.") | ||
|
|
||
| if (userSurveyRepository.existsByUserId(userId)) { | ||
| throw IllegalArgumentException("UserSurvey already exists.") | ||
| } | ||
|
|
||
| val survey = UserSurvey.of( | ||
| user = user, | ||
| concept = command.concept, | ||
| idolName = command.idolName, | ||
| visitStartDate = command.visitStartDate, | ||
| visitEndDate = command.visitEndDate, | ||
| places = command.places | ||
| ) | ||
|
|
||
| val saved = userSurveyRepository.save(survey) | ||
| return UserSurveyCreatedResponse(saved.id) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.doki.user.user.application.dto | ||
|
|
||
| import com.doki.user.user.domain.vo.ConceptType | ||
| import java.time.LocalDate | ||
|
|
||
| data class CreateUserSurveyCommand ( | ||
| val concept: ConceptType, | ||
| val idolName: String, | ||
| val visitStartDate: LocalDate, | ||
| val visitEndDate: LocalDate, | ||
| val places: List<String> | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.doki.user.user.domain | ||
|
|
||
| interface UserRepository { | ||
|
|
||
| fun findById(userId: Long): User? | ||
|
|
||
| fun findByEmail(email: String): User? | ||
|
|
||
| fun save(user: User): User | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package com.doki.user.user.domain | ||
|
|
||
| import com.doki.global.BaseEntity | ||
| import com.doki.user.user.domain.vo.ConceptType | ||
| import jakarta.persistence.* | ||
| import java.time.LocalDate | ||
|
|
||
| @Entity | ||
| @Table(name = "user_survey") | ||
| class UserSurvey( | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| val id: Long = 0L, | ||
|
|
||
| @OneToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "user_id", unique = true) | ||
| val user: User, | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(nullable = false, length = 30) | ||
| val concept: ConceptType, | ||
|
|
||
| @Column(name = "idol_name", nullable = false) | ||
| val idolName: String, | ||
|
|
||
| @Column(name = "visit_start_date", nullable = false) | ||
| val visitStartDate: LocalDate, | ||
|
|
||
| @Column(name = "visit_end_date", nullable = false) | ||
| val visitEndDate: LocalDate, | ||
|
|
||
| @ElementCollection | ||
| @CollectionTable( | ||
| name = "user_survey_places", | ||
| joinColumns = [JoinColumn(name = "user_survey_id")] | ||
| ) | ||
| @Column(name = "place", nullable = false, length = 100) | ||
| val places: List<String> | ||
|
Comment on lines
+32
to
+38
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사전 위치 기반으로 데이터를 제공해준다면, 해당 정보가 필요할 거 같은데 조회할 때 복잡한 요구사항은 없을 것 같아서 지금 구조도 괜찮아 보입니다! 나중에 위치 데이터를 복잡하게 다룬다면 그때 개선하는 걸로 하시죠!! |
||
| ) : BaseEntity() { | ||
|
|
||
| init { | ||
| require(places.isNotEmpty()) { "place must not be blank" } | ||
| require(!visitEndDate.isBefore(visitStartDate)) { "visitEndDate must be on or after visitStartDate" } | ||
| } | ||
|
|
||
| companion object { | ||
| private fun normalizeplaces(places: List<String>): List<String> = | ||
| places.map { it.trim() } | ||
| .filter { it.isNotBlank() } | ||
| .distinct() | ||
|
|
||
| fun of( | ||
| user: User, | ||
| concept: ConceptType, | ||
| idolName: String, | ||
| visitStartDate: LocalDate, | ||
| visitEndDate: LocalDate, | ||
| places: List<String> | ||
| ): UserSurvey { | ||
| return UserSurvey( | ||
| user = user, | ||
| concept = concept, | ||
| idolName = idolName.trim(), | ||
| visitStartDate = visitStartDate, | ||
| visitEndDate = visitEndDate, | ||
| places = normalizeplaces(places) | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.doki.user.user.domain | ||
|
|
||
| interface UserSurveyRepository { | ||
|
|
||
| fun findByUserId(userId: Long): UserSurvey? | ||
|
|
||
| fun existsByUserId(userId: Long): Boolean | ||
|
|
||
| fun save(userSurvey: UserSurvey): UserSurvey | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.doki.user.user.domain.dto | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema | ||
|
|
||
| data class UserSurveyCreatedResponse( | ||
| @field:Schema(description = "생성된 설문조사 ID", example = "1") | ||
| val id: Long | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package com.doki.user.user.domain.dto | ||
|
|
||
| import com.doki.user.user.domain.UserSurvey | ||
| import com.doki.user.user.domain.vo.ConceptType | ||
| import io.swagger.v3.oas.annotations.media.Schema | ||
| import java.time.LocalDate | ||
|
|
||
| data class UserSurveyResponse( | ||
| @field:Schema(description = "설문조사 ID", example = "1") | ||
| val id: Long, | ||
|
|
||
| @field:Schema(description = "선호 컨셉", example = "GIRL_CRUSH") | ||
| val concept: ConceptType, | ||
|
|
||
| @field:Schema(description = "선호 아이돌 또는 그룹", example = "에스파") | ||
| val idolName: String, | ||
|
|
||
| @field:Schema(description = "여행 시작일", example = "2025-07-20") | ||
| val visitStartDate: LocalDate, | ||
|
|
||
| @field:Schema(description = "여행 종료일", example = "2025-07-22") | ||
| val visitEndDate: LocalDate, | ||
|
|
||
| @field:Schema(description = "여행 장소", example = "['용산구', '마포구']") | ||
| val places: List<String> | ||
| ) { | ||
| companion object { | ||
| fun from(userSurvey: UserSurvey): UserSurveyResponse { | ||
| return UserSurveyResponse( | ||
| id = userSurvey.id, | ||
| concept = userSurvey.concept, | ||
| idolName = userSurvey.idolName, | ||
| visitStartDate = userSurvey.visitStartDate, | ||
| visitEndDate = userSurvey.visitEndDate, | ||
| places = userSurvey.places | ||
| ) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.doki.user.user.domain.vo | ||
|
|
||
| enum class ConceptType { | ||
| GIRL_CRUSH, | ||
| LOVELY_FRESH, | ||
| ELEGANT_GLAM, | ||
| DREAMY, | ||
| HIGHTEEN, | ||
| ETC | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| package com.doki.user.domain.vo | ||
| package com.doki.user.user.domain.vo | ||
|
|
||
| enum class UserRole { | ||
| USER // 회원 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.doki.user.user.infrastructure | ||
|
|
||
| import com.doki.user.user.domain.User | ||
| import org.springframework.data.jpa.repository.JpaRepository | ||
|
|
||
| interface UserJpaRepository : JpaRepository<User, Long> { | ||
| fun findByEmail(email: String): User? | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.doki.user.user.infrastructure | ||
|
|
||
| import com.doki.user.user.domain.User | ||
| import com.doki.user.user.domain.UserRepository | ||
| import org.springframework.data.repository.findByIdOrNull | ||
| import org.springframework.stereotype.Repository | ||
|
|
||
| @Repository | ||
| class UserRepositoryImpl( | ||
| private val userJpaRepository: UserJpaRepository | ||
| ): UserRepository { | ||
|
|
||
| override fun findById(userId: Long): User? = | ||
| userJpaRepository.findByIdOrNull(userId) | ||
|
|
||
| override fun findByEmail(email: String): User? = | ||
| userJpaRepository.findByEmail(email) | ||
|
|
||
| override fun save(user: User): User = | ||
| userJpaRepository.save(user) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.doki.user.user.infrastructure | ||
|
|
||
| import com.doki.user.user.domain.UserSurvey | ||
| import org.springframework.data.jpa.repository.JpaRepository | ||
|
|
||
| interface UserSurveyJpaRepository: JpaRepository<UserSurvey, Long> { | ||
| fun findByUserId(userId: Long): UserSurvey? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코틀린 jpa에서는 findByIdOrNull 이라는 확장함수도 제공하는데 한 번 봐보시면 도움 되실 것 같아요~
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 설문은 회원당 1개만 존재하고,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아아 이해했습니다~ 어차피 user랑 survey는 1:1이니 그대로 두셔도 될 거 같아요 (인덱스만 잘 걸어주세요!) |
||
| fun existsByUserId(userId: Long): Boolean | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이건 조회용으로 보여서 QueryService로 가는게 좋아보입니다!
현재 커맨드 서비스에서 사용한다면, private fun으로 만들어주세요