diff --git a/setup-git-hooks.sh b/setup-git-hooks.sh old mode 100644 new mode 100755 diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/controller/GuideController.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/controller/GuideController.kt new file mode 100644 index 0000000..5944fb7 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/controller/GuideController.kt @@ -0,0 +1,51 @@ +package com.back.koreaTravelGuide.domain.guide.controller + +import com.back.koreaTravelGuide.common.ApiResponse +import com.back.koreaTravelGuide.domain.guide.service.GuideService +import com.back.koreaTravelGuide.domain.user.dto.request.GuideUpdateRequest +import com.back.koreaTravelGuide.domain.user.dto.response.GuideResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "가이드 API", description = "가이드 조회 및 프로필 관리에 대한 API") +@RestController +@RequestMapping("/api/guides") +class GuideController( + private val guideService: GuideService, +) { + @Operation(summary = "가이드 목록 조회") + @GetMapping + fun getAllGuides(): ResponseEntity>> { + val guides = guideService.getAllGuides() + return ResponseEntity.ok(ApiResponse("전체 가이드 목록을 조회했습니다.", guides)) + } + + @Operation(summary = "가이드 단건 조회") + @GetMapping("/{guideId}") + fun getGuideById( + @PathVariable guideId: Long, + ): ResponseEntity> { + val guide = guideService.getGuideById(guideId) + return ResponseEntity.ok(ApiResponse("가이드 정보를 성공적으로 조회했습니다.", guide)) + } + + @Operation(summary = "가이드 프로필 수정") + @PreAuthorize("hasRole('GUIDE')") + @PatchMapping("/me") + fun updateMyGuideProfile( + @AuthenticationPrincipal guideId: Long, + @RequestBody request: GuideUpdateRequest, + ): ResponseEntity> { + val updatedGuideProfile = guideService.updateGuideProfile(guideId, request) + return ResponseEntity.ok(ApiResponse("가이드 정보가 성공적으로 수정되었습니다.", updatedGuideProfile)) + } +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/controller/UserController.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/controller/UserController.kt index 851fc2e..32d1fab 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/controller/UserController.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/controller/UserController.kt @@ -1,4 +1,49 @@ package com.back.koreaTravelGuide.domain.user.controller -// TODO: 사용자 컨트롤러 - 사용자 관련 API 엔드포인트 (향후 구현) -class UserController +import com.back.koreaTravelGuide.common.ApiResponse +import com.back.koreaTravelGuide.domain.user.dto.request.UserUpdateRequest +import com.back.koreaTravelGuide.domain.user.dto.response.UserResponse +import com.back.koreaTravelGuide.domain.user.service.UserService +import io.swagger.v3.oas.annotations.Operation +import org.springframework.http.ResponseEntity +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.PatchMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/users") +class UserController( + private val userService: UserService, +) { + @Operation(summary = "내 정보 조회") + @GetMapping("/me") + fun getMyProfile( + @AuthenticationPrincipal userId: Long, + ): ResponseEntity> { + val userProfile = userService.getUserProfileById(userId) + return ResponseEntity.ok(ApiResponse("내 정보를 성공적으로 조회했습니다.", userProfile)) + } + + @Operation(summary = "내 프로필 수정") + @PatchMapping("/me") + fun updateMyProfile( + @AuthenticationPrincipal userId: Long, + @RequestBody request: UserUpdateRequest, + ): ResponseEntity> { + val updatedProfile = userService.updateUserProfile(userId, request) + return ResponseEntity.ok(ApiResponse("정보가 성공적으로 수정되었습니다.", updatedProfile)) + } + + @Operation(summary = "회원 탈퇴") + @DeleteMapping("/me") + fun deleteMe( + @AuthenticationPrincipal userId: Long, + ): ResponseEntity> { + userService.deleteUser(userId) + return ResponseEntity.ok(ApiResponse("회원 탈퇴가 완료되었습니다.")) + } +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/UserRequest.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/UserRequest.kt deleted file mode 100644 index 5bbc2a3..0000000 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/UserRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.back.koreaTravelGuide.domain.user.dto - -// TODO: 사용자 요청 DTO - 사용자 등록/수정 요청 (향후 구현) -data class UserRequest( - val username: String, - val email: String, -) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/UserResponse.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/UserResponse.kt deleted file mode 100644 index 5011cbc..0000000 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/UserResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.back.koreaTravelGuide.domain.user.dto - -// TODO: 사용자 응답 DTO - 사용자 정보 반환 (향후 구현) -data class UserResponse( - val id: Long, - val username: String, - val email: String, -) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/request/GuideUpdateRequest.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/request/GuideUpdateRequest.kt new file mode 100644 index 0000000..11c5200 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/request/GuideUpdateRequest.kt @@ -0,0 +1,8 @@ +package com.back.koreaTravelGuide.domain.user.dto.request + +data class GuideUpdateRequest( + val nickname: String?, + val profileImageUrl: String?, + val location: String?, + val description: String?, +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/request/UserUpdateRequest.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/request/UserUpdateRequest.kt new file mode 100644 index 0000000..ba4e046 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/request/UserUpdateRequest.kt @@ -0,0 +1,6 @@ +package com.back.koreaTravelGuide.domain.user.dto.request + +data class UserUpdateRequest( + val nickname: String?, + val profileImageUrl: String?, +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/response/GuideResponse.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/response/GuideResponse.kt new file mode 100644 index 0000000..c6e9b2e --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/response/GuideResponse.kt @@ -0,0 +1,28 @@ +package com.back.koreaTravelGuide.domain.user.dto.response + +import com.back.koreaTravelGuide.domain.user.entity.User +import com.back.koreaTravelGuide.domain.user.enums.UserRole + +data class GuideResponse( + val id: Long, + val email: String, + val nickname: String, + val profileImageUrl: String?, + val role: UserRole, + val location: String?, + val description: String?, +) { + companion object { + fun from(user: User): GuideResponse { + return GuideResponse( + id = user.id!!, + email = user.email, + nickname = user.nickname, + profileImageUrl = user.profileImageUrl, + role = user.role, + location = user.location, + description = user.description, + ) + } + } +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/response/UserResponse.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/response/UserResponse.kt new file mode 100644 index 0000000..925cf20 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/dto/response/UserResponse.kt @@ -0,0 +1,24 @@ +package com.back.koreaTravelGuide.domain.user.dto.response + +import com.back.koreaTravelGuide.domain.user.entity.User +import com.back.koreaTravelGuide.domain.user.enums.UserRole + +data class UserResponse( + val id: Long, + val email: String, + val nickname: String, + val profileImageUrl: String?, + val role: UserRole, +) { + companion object { + fun from(user: User): UserResponse { + return UserResponse( + id = user.id!!, + email = user.email, + nickname = user.nickname, + profileImageUrl = user.profileImageUrl, + role = user.role, + ) + } + } +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt index bff2382..7124b57 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt @@ -1,4 +1,48 @@ package com.back.koreaTravelGuide.domain.user.entity -// TODO: 사용자 엔티티 - 사용자 정보 저장 (향후 구현) -class User +import com.back.koreaTravelGuide.domain.user.enums.UserRole +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EntityListeners +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.LocalDateTime + +@Entity +@Table(name = "users") +@EntityListeners(AuditingEntityListener::class) +class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + @Column(name = "oauth_provider", nullable = false) + val oauthProvider: String, + @Column(name = "oauth_id", nullable = false) + val oauthId: String, + @Column(unique = true, nullable = false) + val email: String, + @Column(nullable = false) + var nickname: String, + @Column(name = "profile_image_url") + var profileImageUrl: String? = null, + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var role: UserRole = UserRole.USER, + @Column + var location: String? = null, + @Column(columnDefinition = "TEXT") + var description: String? = null, + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + val createdAt: LocalDateTime = LocalDateTime.now(), + @LastModifiedDate + @Column(name = "last_login_at") + var lastLoginAt: LocalDateTime = LocalDateTime.now(), +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/enums/UserRole.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/enums/UserRole.kt new file mode 100644 index 0000000..a77bba5 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/enums/UserRole.kt @@ -0,0 +1,7 @@ +package com.back.koreaTravelGuide.domain.user.enums + +enum class UserRole { + USER, + ADMIN, + GUIDE, +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/repository/UserRepository.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/repository/UserRepository.kt index f3a9f9a..35e6d6e 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/repository/UserRepository.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/repository/UserRepository.kt @@ -1,4 +1,11 @@ package com.back.koreaTravelGuide.domain.user.repository -// TODO: 사용자 리포지토리 - 사용자 데이터 저장/조회 (향후 구현) -interface UserRepository +import com.back.koreaTravelGuide.domain.user.entity.User +import com.back.koreaTravelGuide.domain.user.enums.UserRole +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface UserRepository : JpaRepository { + fun findByRole(role: UserRole): List +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/service/GuideService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/service/GuideService.kt new file mode 100644 index 0000000..4f254d7 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/service/GuideService.kt @@ -0,0 +1,52 @@ +package com.back.koreaTravelGuide.domain.guide.service + +import com.back.koreaTravelGuide.domain.user.dto.request.GuideUpdateRequest +import com.back.koreaTravelGuide.domain.user.dto.response.GuideResponse +import com.back.koreaTravelGuide.domain.user.enums.UserRole +import com.back.koreaTravelGuide.domain.user.repository.UserRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class GuideService( + private val userRepository: UserRepository, +) { + @Transactional(readOnly = true) + fun getAllGuides(): List { + return userRepository.findByRole(UserRole.GUIDE) + .map { GuideResponse.from(it) } + } + + @Transactional(readOnly = true) + fun getGuideById(guideId: Long): GuideResponse { + val user = + userRepository.findById(guideId) + .orElseThrow { NoSuchElementException() } + + if (user.role != UserRole.GUIDE) { + throw IllegalArgumentException() + } + return GuideResponse.from(user) + } + + fun updateGuideProfile( + guideId: Long, + request: GuideUpdateRequest, + ): GuideResponse { + val user = + userRepository.findById(guideId) + .orElseThrow { NoSuchElementException() } + + if (user.role != UserRole.GUIDE) { + throw IllegalAccessException() + } + + user.nickname = request.nickname ?: user.nickname + user.profileImageUrl = request.profileImageUrl ?: user.profileImageUrl + user.location = request.location ?: user.location + user.description = request.description ?: user.description + + return GuideResponse.from(userRepository.save(user)) + } +} diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/service/UserService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/service/UserService.kt index 4b029d0..027eb45 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/service/UserService.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/service/UserService.kt @@ -1,4 +1,42 @@ package com.back.koreaTravelGuide.domain.user.service -// TODO: 사용자 서비스 - 사용자 관리 비즈니스 로직 (향후 구현) -class UserService +import com.back.koreaTravelGuide.domain.user.dto.request.UserUpdateRequest +import com.back.koreaTravelGuide.domain.user.dto.response.UserResponse +import com.back.koreaTravelGuide.domain.user.repository.UserRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class UserService( + private val userRepository: UserRepository, +) { + @Transactional(readOnly = true) + fun getUserProfileById(id: Long): UserResponse { + val user = + userRepository.findById(id) + .orElseThrow { NoSuchElementException() } + return UserResponse.from(user) + } + + fun updateUserProfile( + id: Long, + request: UserUpdateRequest, + ): UserResponse { + val user = + userRepository.findById(id) + .orElseThrow { NoSuchElementException() } + + user.nickname = request.nickname ?: user.nickname + user.profileImageUrl = request.profileImageUrl ?: user.profileImageUrl + + return UserResponse.from(userRepository.save(user)) + } + + fun deleteUser(id: Long) { + if (!userRepository.existsById(id)) { + throw NoSuchElementException() + } + userRepository.deleteById(id) + } +}