diff --git a/src/main/kotlin/org/tenten/bittakotlin/chat/controller/ChatController.kt b/src/main/kotlin/org/tenten/bittakotlin/chat/controller/ChatController.kt index 338e263..fd61afc 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/chat/controller/ChatController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/chat/controller/ChatController.kt @@ -1,5 +1,7 @@ package org.tenten.bittakotlin.chat.controller +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable import org.springframework.http.ResponseEntity import org.springframework.messaging.handler.annotation.MessageMapping import org.springframework.messaging.simp.SimpMessagingTemplate @@ -9,6 +11,7 @@ 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.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.tenten.bittakotlin.chat.dto.ChatRequestDto import org.tenten.bittakotlin.chat.dto.ChatResponseDto @@ -28,17 +31,20 @@ class ChatController( private val chatRoomService: ChatRoomService ) { @MessageMapping("/send") - fun send(@RequestBody requestDto: ChatRequestDto.Send): Unit { + fun send(requestDto: ChatRequestDto.Send): Unit { val responseDto: ChatResponseDto.Send = chatService.save(requestDto) simpMessagingTemplate.convertAndSend("/room/${responseDto.chatRoomId}", responseDto) } @GetMapping("/room") - fun read(@RequestBody requestDto: ChatRequestDto.Read): ResponseEntity> { + fun read(@RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "20") size: Int + , @RequestBody requestDto: ChatRequestDto.Read): ResponseEntity> { + val pageable: Pageable = PageRequest.of(page, size) + return ResponseEntity.ok(mapOf( - "message" to "파일 조회 링크를 성공적으로 생성했습니다.", - "result" to chatService.get(requestDto) + "message" to "채팅 목록을 성공적으로 생성했습니다.", + "result" to chatService.get(pageable, requestDto) )) } diff --git a/src/main/kotlin/org/tenten/bittakotlin/chat/repository/ChatRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/chat/repository/ChatRepository.kt index 23bf0f9..b1bd100 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/chat/repository/ChatRepository.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/chat/repository/ChatRepository.kt @@ -1,5 +1,7 @@ package org.tenten.bittakotlin.chat.repository +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param @@ -8,6 +10,6 @@ import org.tenten.bittakotlin.chat.entity.Chat @Repository interface ChatRepository : JpaRepository { - @Query("SELECT c FROM Chat c WHERE c.chatRoom.id = :chatRoomId") - fun findAllByChatRoomId(@Param("chatRoomId") chatRoomId: Long): List + @Query("SELECT c FROM Chat c WHERE c.chatRoom.id = :chatRoomId ORDER BY c.id DESC") + fun findAllByChatRoomIdOrderByIdDesc(@Param("chatRoomId") chatRoomId: Long, pageable: Pageable): Slice } \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/chat/service/ChatService.kt b/src/main/kotlin/org/tenten/bittakotlin/chat/service/ChatService.kt index e4b28e6..3532649 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/chat/service/ChatService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/chat/service/ChatService.kt @@ -1,10 +1,11 @@ package org.tenten.bittakotlin.chat.service +import org.springframework.data.domain.Pageable import org.tenten.bittakotlin.chat.dto.ChatRequestDto import org.tenten.bittakotlin.chat.dto.ChatResponseDto interface ChatService { - fun get(requestDto: ChatRequestDto.Read): List + fun get(pageable: Pageable, requestDto: ChatRequestDto.Read): List fun save(requestDto: ChatRequestDto.Send): ChatResponseDto.Send diff --git a/src/main/kotlin/org/tenten/bittakotlin/chat/service/ChatServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/chat/service/ChatServiceImpl.kt index d6737f8..84970ec 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/chat/service/ChatServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/chat/service/ChatServiceImpl.kt @@ -2,6 +2,8 @@ package org.tenten.bittakotlin.chat.service import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice import org.springframework.stereotype.Service import org.tenten.bittakotlin.chat.constant.ChatError import org.tenten.bittakotlin.chat.dto.ChatRequestDto @@ -25,20 +27,20 @@ class ChatServiceImpl( private val logger: Logger = LoggerFactory.getLogger(ChatServiceImpl::class.java) } - override fun get(requestDto: ChatRequestDto.Read): List { + override fun get(pageable: Pageable, requestDto: ChatRequestDto.Read): List { var result: MutableList = mutableListOf() try { val chatRoom: ChatRoom = chatRoomService.getChatRoomByNicknames(requestDto.sender, requestDto.receiver) - val chats: List = chatRepository.findAllByChatRoomId(chatRoom.id!!) + val chats: Slice = chatRepository.findAllByChatRoomIdOrderByIdDesc(chatRoom.id!!, pageable) - chats.forEach { c -> result.add( + chats.forEach { chat -> result.add( ChatResponseDto.Read( - chatId = c.id!!, - sender = c.profile.nickname, - message = c.message, - deleted = c.deleted, - chatAt = c.createdAt!! + chatId = chat.id!!, + sender = chat.profile.nickname, + message = chat.message, + deleted = chat.deleted, + chatAt = chat.createdAt!! )) } } catch (e: NoSuchElementException) { diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/constant/FeedError.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/constant/FeedError.kt new file mode 100644 index 0000000..1c40103 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/constant/FeedError.kt @@ -0,0 +1,8 @@ +package org.tenten.bittakotlin.feed.constant + +enum class FeedError(val code: Int, val message: String) { + NOT_FOUND(404, "피드가 존재하지 않습니다."), + CANNOT_FOUND(404, "피드가 존재하지 않습니다."), + CANNOT_MODIFY_BAD_AUTHORITY(403, "피드를 수정할 권한이 없습니다."), + CANNOT_DELETE_BAD_AUTHORITY(403, "피드를 삭제할 권한이 없습니다."), +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/controller/FeedController.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/controller/FeedController.kt new file mode 100644 index 0000000..064d109 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/controller/FeedController.kt @@ -0,0 +1,74 @@ +package org.tenten.bittakotlin.feed.controller + +import org.tenten.bittakotlin.feed.service.FeedService +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.tenten.bittakotlin.feed.dto.FeedRequestDto +import org.tenten.bittakotlin.security.service.PrincipalProvider + +@RestController +@RequestMapping("/api/v1/feed") +class FeedController ( + private val feedService: FeedService +) { + @GetMapping + fun readAll( + @RequestParam(required = false, defaultValue = "0", value = "page") page: Int, + @RequestParam(required = false, defaultValue = "10", value = "size") size: Int, + @RequestParam(required = false, value = "nickname") nickname: String?, + @RequestParam(required = false, value = "title") title: String? + ): ResponseEntity> { + val pageable: Pageable = PageRequest.of(page, size) + + return ResponseEntity.ok(mapOf( + "message" to "피드 목록을 성공적으로 조회했습니다.", + "result" to feedService.getAll(pageable, nickname, title) + )) + } + + @GetMapping("/random") + fun readRandom(@RequestParam(required = false, defaultValue = "0", value = "page") size: Int + ): ResponseEntity> { + return ResponseEntity.ok(mapOf( + "message" to "피드 목록을 무작위로 조회했습니다.", + "result" to feedService.getRandom(size) + )) + } + + @GetMapping("/{id}") + fun read(@PathVariable("id") id: Long): ResponseEntity> { + return ResponseEntity.ok(mapOf( + "message" to "피드를 성공적으로 조회했습니다.", + "result" to feedService.get(id) + )) + } + + @PostMapping + fun create(requestDto: FeedRequestDto.Create): ResponseEntity> { + return ResponseEntity.ok(mapOf( + "message" to "피드를 성공적으로 등록했습니다.", + "result" to feedService.save(requestDto) + )) + } + + @PutMapping + fun modifyFeed( + @PathVariable("id") id: Long, @RequestBody requestDto: FeedRequestDto.Modify): + ResponseEntity> { + return ResponseEntity.ok(mapOf( + "message" to "피드를 성공적으로 수정했습니다.", + "result" to feedService.update(id, requestDto) + )) + } + + @DeleteMapping("/{id}") + fun deleteFeed(@PathVariable id: Long): ResponseEntity> { + feedService.delete(id) + + return ResponseEntity.ok(mapOf( + "message" to "피드를 성공적으로 삭제했습니다." + )) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedRequestDto.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedRequestDto.kt new file mode 100644 index 0000000..eb3f879 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedRequestDto.kt @@ -0,0 +1,23 @@ +package org.tenten.bittakotlin.feed.dto + +import org.tenten.bittakotlin.media.dto.MediaRequestDto + +class FeedRequestDto { + data class Create ( + val title: String, + + val content: String, + + val medias: List? + ) + + data class Modify ( + val title: String, + + val content: String, + + val uploads: List?, + + val deletes: List? + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedResponseDto.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedResponseDto.kt new file mode 100644 index 0000000..0711e46 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedResponseDto.kt @@ -0,0 +1,28 @@ +package org.tenten.bittakotlin.feed.dto + +import org.tenten.bittakotlin.media.dto.MediaResponseDto +import java.time.LocalDateTime + +class FeedResponseDto { + data class Read ( + val id: Long, + + val title: String, + + val content: String, + + val author: String, + + val createdAt: LocalDateTime, + + val medias: List + ) + + data class Create ( + val medias: List + ) + + data class Modify ( + val medias: List + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/entity/Feed.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/entity/Feed.kt new file mode 100644 index 0000000..1b86fe8 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/entity/Feed.kt @@ -0,0 +1,38 @@ +package org.tenten.bittakotlin.feed.entity + +import jakarta.persistence.* +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.tenten.bittakotlin.profile.entity.Profile +import java.time.LocalDateTime +import java.util.* + +@Entity +@EntityListeners(AuditingEntityListener::class) +data class Feed( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @Column(nullable = false, length = 50) + var title: String, + + @Column(nullable = false, length = 1000) + var content: String, + + @ManyToOne + @JoinColumn(name = "profile_id", nullable = false) + val profile: Profile, + + @OneToMany(mappedBy = "feed", cascade = [CascadeType.ALL]) + val feedMedias: List = mutableListOf(), + + @CreatedDate + @Column(updatable = false, nullable = false) + val createdAt: LocalDateTime? = null, + + @LastModifiedDate + @Column(updatable = true, nullable = false) + var updatedAt: LocalDateTime? = null +) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/entity/FeedMedia.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/entity/FeedMedia.kt new file mode 100644 index 0000000..d2fbe1f --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/entity/FeedMedia.kt @@ -0,0 +1,22 @@ +package org.tenten.bittakotlin.feed.entity + +import jakarta.persistence.EmbeddedId +import jakarta.persistence.Entity +import jakarta.persistence.ManyToOne +import jakarta.persistence.MapsId +import org.tenten.bittakotlin.feed.entity.key.FeedMediaId +import org.tenten.bittakotlin.media.entity.Media + +@Entity +data class FeedMedia ( + @EmbeddedId + val id: FeedMediaId? = null, + + @ManyToOne + @MapsId("feedId") + val feed: Feed, + + @ManyToOne + @MapsId("mediaId") + val media: Media +) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/entity/key/FeedMediaId.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/entity/key/FeedMediaId.kt new file mode 100644 index 0000000..7790a5e --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/entity/key/FeedMediaId.kt @@ -0,0 +1,11 @@ +package org.tenten.bittakotlin.feed.entity.key + +import jakarta.persistence.Embeddable +import java.io.Serializable + +@Embeddable +data class FeedMediaId( + val feedId: Long, + + val mediaId: Long +) : Serializable \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedException.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedException.kt new file mode 100644 index 0000000..9c4c10e --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedException.kt @@ -0,0 +1,14 @@ +package com.prgrms2.java.bitta.feed.exception + +import org.tenten.bittakotlin.feed.constant.FeedError + +class FeedException( + val code: Int, + + override val message: String +) : RuntimeException(message) { + constructor(feedError: FeedError): this( + code = feedError.code, + message = feedError.message + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedMediaRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedMediaRepository.kt new file mode 100644 index 0000000..c4d6e52 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedMediaRepository.kt @@ -0,0 +1,16 @@ +package org.tenten.bittakotlin.feed.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository +import org.tenten.bittakotlin.feed.entity.FeedMedia + +@Repository +interface FeedMediaRepository : JpaRepository { + @Query("SELECT fm.media.filename FROM FeedMedia fm WHERE fm.feed.id = :feedId") + fun findFilenamesByFeedId(@Param("feedId") feedId: Long): List + + @Query("DELETE FROM FeedMedia fm WHERE fm.media.id = :mediaId") + fun deleteByMediaId(@Param("mediaId") mediaId: Long) +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt new file mode 100644 index 0000000..ee1bd49 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt @@ -0,0 +1,25 @@ +package org.tenten.bittakotlin.feed.repository + +import org.tenten.bittakotlin.feed.entity.Feed +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.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository + +@Repository +interface FeedRepository : JpaRepository { + + @Query("SELECT f FROM Feed f WHERE f.profile.nickname LIKE %:nickname% ORDER BY f.id DESC") + fun findAllLikeNicknameOrderByIdDesc(@Param("nickname") nickname: String, pageable: Pageable): Page + + @Query("SELECT f FROM Feed f WHERE f.title LIKE %:title% ORDER BY f.id DESC") + fun findAllLikeTitleOrderByIdDesc(@Param("title") title: String, pageable: Pageable): Page + + @Query("SELECT f FROM Feed f WHERE f.profile.nickname LIKE %:nickname% AND f.title LIKE %:title% ORDER BY f.id DESC") + fun findAllLikeNicknameAndTitleOrderByIdDesc(@Param("nickname") nickname: String, @Param("title") title: String, + pageable: Pageable): Page + + fun findAllByOrderByIdDesc(pageable: Pageable): Page +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedMediaService.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedMediaService.kt new file mode 100644 index 0000000..3cd196e --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedMediaService.kt @@ -0,0 +1,16 @@ +package org.tenten.bittakotlin.feed.service + +import org.tenten.bittakotlin.feed.entity.Feed +import org.tenten.bittakotlin.media.dto.MediaRequestDto +import org.tenten.bittakotlin.media.dto.MediaResponseDto +import org.tenten.bittakotlin.profile.entity.Profile + +interface FeedMediaService { + fun save(feed: Feed, profile: Profile, uploadRequestDto: List): List + + fun delete(deleteRequestDto: List) + + fun delete(feedId: Long) + + fun getMedias(feedId: Long): List +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedMediaServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedMediaServiceImpl.kt new file mode 100644 index 0000000..658428e --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedMediaServiceImpl.kt @@ -0,0 +1,76 @@ +package org.tenten.bittakotlin.feed.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.tenten.bittakotlin.feed.entity.Feed +import org.tenten.bittakotlin.feed.entity.FeedMedia +import org.tenten.bittakotlin.feed.repository.FeedMediaRepository +import org.tenten.bittakotlin.media.dto.MediaRequestDto +import org.tenten.bittakotlin.media.dto.MediaResponseDto +import org.tenten.bittakotlin.media.service.MediaService +import org.tenten.bittakotlin.profile.entity.Profile + +@Service +class FeedMediaServiceImpl( + private val feedMediaRepository: FeedMediaRepository, + + private val mediaService: MediaService +) : FeedMediaService { + @Transactional + override fun save(feed: Feed, profile: Profile, uploadRequestDtos: List): List { + val responseDto: MutableList = mutableListOf() + + uploadRequestDtos.forEach { uploadRequestDto -> + val mediaResponseDto: MediaResponseDto.Upload = mediaService.upload(uploadRequestDto, profile) + + feedMediaRepository.save(FeedMedia( + feed = feed, + media = mediaResponseDto.media + )) + + responseDto.add(MediaResponseDto.Read( + filename = mediaResponseDto.media.filename, + url = mediaResponseDto.url + )) + } + + return responseDto + } + + @Transactional + override fun delete(deleteRequestDto: List) { + deleteRequestDto.forEach { deleteRequestDto -> + val id: Long = mediaService.delete(deleteRequestDto) + + feedMediaRepository.deleteByMediaId(id) + } + } + + @Transactional + override fun delete(feedId: Long) { + val filenames: List = feedMediaRepository.findFilenamesByFeedId(feedId) + + filenames.forEach { filename -> + val id: Long = mediaService.delete(MediaRequestDto.Delete( + filename = filename + )) + + feedMediaRepository.deleteByMediaId(id) + } + } + + @Transactional(readOnly = true) + override fun getMedias(feedId: Long): List { + val filenames: List = feedMediaRepository.findFilenamesByFeedId(feedId) + + val medias: MutableList = mutableListOf() + + filenames.forEach { filename -> + medias.add(mediaService.read(MediaRequestDto.Read( + filename = filename + ))) + } + + return medias + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedService.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedService.kt new file mode 100644 index 0000000..aca1356 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedService.kt @@ -0,0 +1,20 @@ +package org.tenten.bittakotlin.feed.service + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.tenten.bittakotlin.feed.dto.FeedRequestDto +import org.tenten.bittakotlin.feed.dto.FeedResponseDto + +interface FeedService { + fun get(id: Long): FeedResponseDto.Read + + fun getAll(pageable: Pageable, username: String?, title: String?): Page + + fun getRandom(pageSize: Int): Page + + fun save(requestDto: FeedRequestDto.Create): FeedResponseDto.Create + + fun update(feedId: Long, requestDto: FeedRequestDto.Modify): FeedResponseDto.Modify + + fun delete(id: Long) +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedServiceImpl.kt new file mode 100644 index 0000000..e63d104 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedServiceImpl.kt @@ -0,0 +1,158 @@ +package org.tenten.bittakotlin.feed.service + +import com.prgrms2.java.bitta.feed.exception.FeedException +import org.tenten.bittakotlin.feed.entity.Feed +import org.tenten.bittakotlin.feed.constant.FeedError +import org.tenten.bittakotlin.feed.repository.FeedRepository +import lombok.RequiredArgsConstructor +import lombok.extern.slf4j.Slf4j +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.tenten.bittakotlin.feed.dto.FeedRequestDto +import org.tenten.bittakotlin.feed.dto.FeedResponseDto +import org.tenten.bittakotlin.profile.entity.Profile +import org.tenten.bittakotlin.profile.service.ProfileService + +@Service +@RequiredArgsConstructor +@Slf4j +class FeedServiceImpl( + private val feedRepository: FeedRepository, + + private val profileService: ProfileService, + + private val feedMediaService: FeedMediaService, +) : FeedService { + @Transactional(readOnly = true) + override fun get(id: Long): FeedResponseDto.Read { + val feed = feedRepository.findById(id) + .orElseThrow { FeedException(FeedError.CANNOT_FOUND) } + + return toReadResponseDto(feed) + } + + @Transactional(readOnly = true) + override fun getAll(pageable: Pageable, username: String?, title: String?): Page { + val feeds: Page = if (!username.isNullOrBlank() && !title.isNullOrBlank()) { + feedRepository.findAllLikeNicknameAndTitleOrderByIdDesc(username, title, pageable) + } else if (!username.isNullOrBlank()) { + feedRepository.findAllLikeNicknameOrderByIdDesc(username, pageable) + } else if (!title.isNullOrBlank()) { + feedRepository.findAllLikeTitleOrderByIdDesc(title, pageable) + } else { + feedRepository.findAllByOrderByIdDesc(pageable) + } + + if (feeds.isEmpty) { + throw FeedException(FeedError.CANNOT_FOUND) + } + + return feeds.map { feed -> toReadResponseDto(feed) } + } + + override fun getRandom(pageSize: Int): Page { + val totalElements = feedRepository.count() + + if (totalElements == 0L) { + throw FeedException(FeedError.CANNOT_FOUND) + } + + val totalPages: Int = ((totalElements - 1) / pageSize).toInt() + + val randomPage: Int = (0 until totalPages).random() + + val pageable: Pageable = PageRequest.of(randomPage, pageSize) + + val feeds: Page = feedRepository.findAllByOrderByIdDesc(pageable) + + return PageImpl(feeds.content.shuffled(), pageable, feeds.totalElements) + .map { feed -> toReadResponseDto(feed) } + } + + @Transactional + override fun save(requestDto: FeedRequestDto.Create): FeedResponseDto.Create { + val profile: Profile = profileService.getByPrincipal() + + val feed: Feed = feedRepository.save(Feed( + title = requestDto.title, + content = requestDto.content, + profile = profile + )) + + return if (requestDto.medias != null) { + FeedResponseDto.Create( + medias = feedMediaService.save(feed, profile, requestDto.medias)) + } else { + FeedResponseDto.Create(emptyList()) + } + } + + + + @Transactional + override fun update(feedId: Long, requestDto: FeedRequestDto.Modify): FeedResponseDto.Modify { + val profile: Profile = profileService.getByPrincipal() + + val feed: Feed = feedRepository.findById(feedId) + .orElseThrow { FeedException(FeedError.CANNOT_FOUND) } + + if (profile == feed.profile) { + throw FeedException(FeedError.CANNOT_MODIFY_BAD_AUTHORITY) + } + + feed.title = requestDto.title + feed.content = requestDto.content + + feedRepository.save(feed) + + val uploads = if (requestDto.uploads != null) { + feedMediaService.save(feed, profile, requestDto.uploads) + } else { + null + } + + if (requestDto.deletes != null) { + feedMediaService.delete(requestDto.deletes) + } + + return if (uploads != null) { + FeedResponseDto.Modify( + medias = uploads + ) + } else { + FeedResponseDto.Modify(emptyList()) + } + } + + + @Transactional + override fun delete(id: Long) { + val profile: Profile = profileService.getByPrincipal() + + val feed: Feed = feedRepository.findById(id) + .orElseThrow { FeedException(FeedError.CANNOT_FOUND) } + + if (profile == feed.profile) { + throw FeedException(FeedError.CANNOT_DELETE_BAD_AUTHORITY) + } + + feedMediaService.delete(id) + + feedRepository.delete(feed) + } + + private fun toReadResponseDto(feed: Feed): FeedResponseDto.Read { + return FeedResponseDto.Read( + id = feed.id!!, + title = feed.title, + content = feed.content, + author = feed.profile.nickname, + createdAt = feed.createdAt!!, + medias = feedMediaService.getMedias(feed.id!!) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt index 0557268..1699587 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/jobpost/controller/JobPostViewController.kt @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping @Controller -@RequestMapping("/jobpost") +@RequestMapping("/job-post") class JobPostViewController { @GetMapping fun showJobpostPage(model: Model?): String { diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaController.kt b/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaController.kt deleted file mode 100644 index bdd4a0f..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaController.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.tenten.bittakotlin.media.controller - -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import org.tenten.bittakotlin.media.dto.MediaRequestDto -import org.tenten.bittakotlin.media.service.MediaService - -@RestController -@RequestMapping("/api/v1/media") -class MediaController( - private val mediaService: MediaService -) { - @GetMapping("/upload") - fun upload(@RequestBody requestDto: MediaRequestDto.Upload): ResponseEntity> { - return ResponseEntity.ok(mapOf( - "message" to "파일 업로드 링크를 성공적으로 생성했습니다.", - "url" to mediaService.upload(requestDto) - )) - } - - @GetMapping("/read") - fun read(@RequestBody requestDto: MediaRequestDto.Read): ResponseEntity> { - return ResponseEntity.ok(mapOf( - "message" to "파일 조회 링크를 성공적으로 생성했습니다.", - "url" to mediaService.read(requestDto) - )) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaRequestDto.kt b/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaRequestDto.kt index 5eb69e3..cc7cc56 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaRequestDto.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaRequestDto.kt @@ -25,9 +25,6 @@ class MediaRequestDto { ) data class Delete ( - @field:NotBlank(message = "회원명이 비어있습니다.") - val username: String, - @field:NotBlank(message = "파일명이 비어있습니다.") val filename: String ) diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaResponseDto.kt b/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaResponseDto.kt index 03eb6c5..3e0f7a9 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaResponseDto.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaResponseDto.kt @@ -1,15 +1,17 @@ package org.tenten.bittakotlin.media.dto -import jakarta.validation.constraints.NotBlank +import org.tenten.bittakotlin.media.entity.Media class MediaResponseDto { data class Read ( - @field:NotBlank(message = "조회 링크가 비어있습니다.") - val link: String + val filename: String, + + val url: String ) data class Upload ( - @field:NotBlank(message = "업로드 링크가 비어있습니다.") - val link: String + val url: String, + + val media: Media ) } \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/entity/Media.kt b/src/main/kotlin/org/tenten/bittakotlin/media/entity/Media.kt index cb359d5..5e2499d 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/media/entity/Media.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/media/entity/Media.kt @@ -5,6 +5,7 @@ import org.hibernate.annotations.ColumnDefault import org.springframework.data.annotation.CreatedDate import org.springframework.data.jpa.domain.support.AuditingEntityListener import org.tenten.bittakotlin.media.constant.MediaType +import org.tenten.bittakotlin.profile.entity.Profile import java.time.LocalDateTime @Entity @@ -29,9 +30,7 @@ data class Media ( @Column(nullable = false, updatable = false) val savedAt: LocalDateTime? = null, - /* - 회원과 1:N으로 연결할 예정입니다. - 임시로 문자열로 구성해놓았습니다. - */ - val member: String? = null + @ManyToOne + @JoinColumn + val profile: Profile ) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/repository/MediaRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/media/repository/MediaRepository.kt index 3367a19..eacba8a 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/media/repository/MediaRepository.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/media/repository/MediaRepository.kt @@ -11,7 +11,4 @@ import java.util.Optional interface MediaRepository : JpaRepository { @Query("SELECT m FROM Media m WHERE m.filename = :filename") fun findByFilename(@Param("filename") filename: String): Optional - - @Query("SELECT m FROM Media m WHERE m.filename = :filename AND m.member = :member") - fun findByFilenameAndUsername(@Param("filename") filename: String, @Param("member") member: String): Optional } \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaService.kt b/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaService.kt index 412674c..4e90e01 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaService.kt @@ -1,11 +1,13 @@ package org.tenten.bittakotlin.media.service import org.tenten.bittakotlin.media.dto.MediaRequestDto +import org.tenten.bittakotlin.media.dto.MediaResponseDto +import org.tenten.bittakotlin.profile.entity.Profile interface MediaService { - fun read(requestDto: MediaRequestDto.Read): String + fun read(requestDto: MediaRequestDto.Read): MediaResponseDto.Read - fun upload(requestDto: MediaRequestDto.Upload): String + fun upload(requestDto: MediaRequestDto.Upload, profile: Profile): MediaResponseDto.Upload - fun delete(requestDto: MediaRequestDto.Delete): Unit + fun delete(requestDto: MediaRequestDto.Delete): Long } \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaServiceImpl.kt index 0c4d5e7..ab79c19 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaServiceImpl.kt @@ -8,9 +8,11 @@ import org.springframework.stereotype.Service import org.tenten.bittakotlin.media.constant.MediaError import org.tenten.bittakotlin.media.constant.MediaType import org.tenten.bittakotlin.media.dto.MediaRequestDto +import org.tenten.bittakotlin.media.dto.MediaResponseDto import org.tenten.bittakotlin.media.entity.Media import org.tenten.bittakotlin.media.exception.MediaException import org.tenten.bittakotlin.media.repository.MediaRepository +import org.tenten.bittakotlin.profile.entity.Profile import java.util.* @Service @@ -29,40 +31,52 @@ class MediaServiceImpl( private val logger: Logger = LoggerFactory.getLogger(MediaServiceImpl::class.java) } - override fun read(requestDto: MediaRequestDto.Read): String { + override fun read(requestDto: MediaRequestDto.Read): MediaResponseDto.Read { val result: Media = mediaRepository.findByFilename(requestDto.filename) .orElseThrow { MediaException(MediaError.CANNOT_FOUND) } - return s3Service.getReadUrl(result.filename) + return MediaResponseDto.Read( + filename = result.filename, + url = s3Service.getReadUrl(result.filename) + ) } - override fun upload(requestDto: MediaRequestDto.Upload): String { + override fun upload(requestDto: MediaRequestDto.Upload, profile: Profile): MediaResponseDto.Upload { val filename: String = UUID.randomUUID().toString() val filetype: MediaType = checkMimetype(requestDto.mimetype) val filesize: Int = checkFileSize(requestDto.filesize, filetype) - mediaRepository.save(Media( + val media: Media = mediaRepository.save(Media( filename = filename, filetype = filetype, - filesize = filesize + filesize = filesize, + profile = profile )) - return s3Service.getUploadUrl(filename, requestDto.mimetype) + return MediaResponseDto.Upload( + media = media, + url = s3Service.getUploadUrl(filename, requestDto.mimetype) + ) } @Transactional - override fun delete(requestDto: MediaRequestDto.Delete) { + override fun delete(requestDto: MediaRequestDto.Delete): Long { val filename: String = requestDto.filename - val username: String = requestDto.username - val result: Media = mediaRepository.findByFilenameAndUsername(filename, username) + + val result: Media = mediaRepository.findByFilename(filename) .orElseThrow { - logger.warn("The file data does not exist in DB: filename=$filename, username=$username") + logger.warn("The file data does not exist in DB: filename=$filename") MediaException(MediaError.CANNOT_FOUND) } + val id = result.id!! + s3Service.delete(result.filename) - mediaRepository.deleteById(result.id!!) + + mediaRepository.deleteById(id) + + return id } private fun checkMimetype(mimetype: String): MediaType { diff --git a/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt b/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt index 47d9717..b65d44a 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt @@ -6,12 +6,13 @@ import org.springframework.security.access.AccessDeniedException import org.springframework.web.bind.annotation.* import org.tenten.bittakotlin.member.dto.MemberRequestDTO import org.tenten.bittakotlin.member.dto.MemberResponseDTO +import org.tenten.bittakotlin.member.exception.MemberException import org.tenten.bittakotlin.member.repository.MemberRepository import org.tenten.bittakotlin.member.service.MemberService import org.tenten.bittakotlin.security.jwt.JWTUtil @RestController -@RequestMapping("/api/member") +@RequestMapping("/api/v1/member") class MemberController( private val memberService: MemberService, private val jwtUtil: JWTUtil, @@ -32,17 +33,31 @@ class MemberController( return ResponseEntity(memberInfo, HttpStatus.OK) } - // 회원 정보 업데이트 + @PutMapping("/{id}") fun updateMember( @PathVariable id: Long, - @RequestBody updateRequest: MemberRequestDTO.UpdateMemberRequest + @RequestBody updateRequest: MemberRequestDTO.UpdateMemberRequest, + @RequestHeader("access") token: String // JWT 토큰을 헤더에서 추출 ): ResponseEntity { - // id는 updateRequest에서 가져오는 것이 아니라, PathVariable로 받아온 id를 그대로 사용 - memberService.updateMember(updateRequest.copy(id = id)) // copy() 메서드를 사용하여 새로운 인스턴스를 생성 + // 현재 로그인한 사용자 username 추출 + val usernameFromToken = jwtUtil.getUsername(token) + + // id로 회원 정보 조회 + val member = memberRepository.findById(id) + .orElseThrow { MemberException.NOT_FOUND.get() } + + // username 비교 + if (member.username != usernameFromToken) { + throw AccessDeniedException("You don't have permission to update this member.") + } + + // 유효성 검증 후 회원 정보 업데이트 + memberService.updateMember(updateRequest, id) // id를 포함하여 업데이트 메서드 호출 return ResponseEntity.ok().build() } + @DeleteMapping("/{id}") fun remove(@PathVariable id: Long, @RequestHeader("access") token: String): ResponseEntity { val username = jwtUtil.getUsername(token) diff --git a/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt index a6a7938..b3be9c4 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt @@ -31,11 +31,7 @@ class MemberRequestDTO { @Schema(title = "회원정보 수정 및 비밀번호 변경 DTO", description = "회원정보 수정 및 비밀번호 변경 요청에 사용하는 DTO입니다.") data class UpdateMemberRequest( - @Schema(title = "회원 ID (PK)", description = "수정할 회원의 기본키입니다.", example = "1") - val id: Long, - @Schema(title = "아이디", description = "비밀번호를 변경할 아이디입니다.", example = "username") - val username: String, @Schema(title = "새로운 별명", description = "새롭게 변경할 별명입니다.", example = "nickname") val nickname: String? = null, // nullable로 설정 diff --git a/src/main/kotlin/org/tenten/bittakotlin/member/service/MemberService.kt b/src/main/kotlin/org/tenten/bittakotlin/member/service/MemberService.kt index 5017990..7507dac 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/service/MemberService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/service/MemberService.kt @@ -9,7 +9,7 @@ interface MemberService { fun read(id: Long): MemberResponseDTO.Information - fun updateMember(request: MemberRequestDTO.UpdateMemberRequest) + fun updateMember(request: MemberRequestDTO.UpdateMemberRequest, id: Long) fun remove(id: Long) diff --git a/src/main/kotlin/org/tenten/bittakotlin/member/service/MemberServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/member/service/MemberServiceImpl.kt index b0432d5..016d298 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/service/MemberServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/service/MemberServiceImpl.kt @@ -63,8 +63,8 @@ class MemberServiceImpl ( ) } - override fun updateMember(request: MemberRequestDTO.UpdateMemberRequest) { - val member = memberRepository.findById(request.id) + override fun updateMember(request: MemberRequestDTO.UpdateMemberRequest, id: Long) { + val member = memberRepository.findById(id) .orElseThrow { MemberException.NOT_FOUND.get() } // 비밀번호 변경 요청이 있을 경우 @@ -80,7 +80,7 @@ class MemberServiceImpl ( request.nickname?.let { member.nickname = it } request.address?.let { member.address = it } - member.username = request.username // 아이디는 항상 업데이트 + // username은 변경할 수 없으므로 해당 줄 제거 memberRepository.save(member) // 수정 후 저장 } diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/controller/ProfileController.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/controller/ProfileController.kt index f6e5585..791e40b 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/controller/ProfileController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/controller/ProfileController.kt @@ -15,28 +15,20 @@ class ProfileController( private val profileService: ProfileServiceImpl ) { - @PostMapping - fun create(@RequestBody profileDTO: ProfileDTO): ResponseEntity { - logger.info("Received request to create profile for memberId=${profileDTO.memberId}") - val response = ResponseEntity.ok(profileService.createProfile(profileDTO)) - logger.info("Profile created successfully for memberId=${profileDTO.memberId}") + @GetMapping("/{profileId}") + fun get(@PathVariable profileId: Long): ResponseEntity { + logger.info("Received request to get profile for profileId=$profileId") + val response = ResponseEntity.ok(profileService.getProfile(profileId)) + logger.info("Profile fetched successfully for memberId=$profileId") return response } - @GetMapping("/{memberId}") - fun get(@PathVariable memberId: Long): ResponseEntity { - logger.info("Received request to get profile for memberId=$memberId") - val response = ResponseEntity.ok(profileService.getProfile(memberId)) - logger.info("Profile fetched successfully for memberId=$memberId") - return response - } - - @PutMapping("/{memberId}") - fun update(@PathVariable memberId: Long, @RequestBody profileDTO: ProfileDTO): ResponseEntity { - logger.info("Received request to update profile for memberId=$memberId") - val response = ResponseEntity.ok(profileService.updateProfile(memberId, profileDTO)) - logger.info("Profile updated successfully for memberId=$memberId") - return response + @PutMapping("/{profileId}") + fun update(@PathVariable profileId: Long, @RequestBody profileDTO: ProfileDTO): ResponseEntity { + logger.info("Received request to update profile for profileId=$profileId") + val updatedProfile = profileService.updateProfile(profileId, profileDTO) + logger.info("Profile updated successfully for profileId=$profileId") + return ResponseEntity.ok(updatedProfile) } companion object { diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/dto/ProfileDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/dto/ProfileDTO.kt index 4524c38..5118511 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/dto/ProfileDTO.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/dto/ProfileDTO.kt @@ -5,15 +5,10 @@ import jakarta.validation.constraints.NotNull import jakarta.validation.constraints.Size data class ProfileDTO( - @field:NotNull(message = "회원 ID는 누락될 수 없습니다.") - val memberId: Long, - - @field:NotBlank(message = "닉네임은 비워둘 수 없습니다.") - @field:Size(max = 20, message = "닉네임은 최대 20자까지 입력할 수 있습니다.") - val nickname: String, - + val profileId: Long? = null, + val nickname: String? = null, val profileUrl: String? = null, val description: String? = null, - val job: String? = null, - val socialMedia: String? = null + val socialMedia: String? = null, + val job: String? = null ) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt index 2487cdc..8fd0cfe 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/entity/Profile.kt @@ -6,6 +6,7 @@ import org.tenten.bittakotlin.apply.entity.Apply import org.tenten.bittakotlin.like.entity.Like import org.tenten.bittakotlin.member.entity.Member import org.tenten.bittakotlin.profile.constant.Job +import org.tenten.bittakotlin.scout.entity.ScoutRequest //data class 로 변경 @Entity @@ -37,5 +38,11 @@ class Profile( val apply: List = mutableListOf(), @OneToMany(mappedBy = "profile", fetch = FetchType.EAGER, cascade = [CascadeType.REMOVE], orphanRemoval = true) - val like: List = mutableListOf() + val like: List = mutableListOf(), + + @OneToMany(mappedBy = "sender", cascade = [CascadeType.ALL], orphanRemoval = true) + val sentScoutRequests: List = mutableListOf(), + + @OneToMany(mappedBy = "receiver", cascade = [CascadeType.ALL], orphanRemoval = true) + val receivedScoutRequests: List = mutableListOf() ) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/repository/ProfileRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/repository/ProfileRepository.kt index db048c1..ece4de2 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/repository/ProfileRepository.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/repository/ProfileRepository.kt @@ -12,4 +12,7 @@ interface ProfileRepository : JpaRepository { @Query("SELECT p FROM Profile p WHERE p.nickname = :nickname") fun findByNickname(@Param("nickname") nickname: String): Optional + + @Query("SELECT p FROM Profile p WHERE p.member.username = :username") + fun findByUsername(@Param("username") username: String): Optional } \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt index 347f6a9..4072f04 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt @@ -5,10 +5,11 @@ import org.tenten.bittakotlin.profile.dto.ProfileDTO import org.tenten.bittakotlin.profile.entity.Profile interface ProfileService { - fun createProfile(ProfileDTO: ProfileDTO): ProfileDTO - fun getProfile(memberId: Long): ProfileDTO - fun updateProfile(memberId: Long, profileDTO: ProfileDTO): ProfileDTO + fun getProfile(profileId: Long): ProfileDTO + fun updateProfile(profileId: Long, profileDTO: ProfileDTO): ProfileDTO fun createDefaultProfile(member: Member): ProfileDTO fun getByNickname(nickname: String): Profile + + fun getByPrincipal(): Profile } \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt index d48bfd1..6586ae0 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt @@ -12,21 +12,24 @@ import org.tenten.bittakotlin.profile.dto.ProfileDTO import org.tenten.bittakotlin.profile.entity.Profile import org.tenten.bittakotlin.profile.repository.ProfileRepository import org.tenten.bittakotlin.profile.constant.Job +import org.tenten.bittakotlin.security.service.PrincipalProvider @Service class ProfileServiceImpl( private val profileRepository: ProfileRepository, - private val memberRepository: MemberRepository + private val memberRepository: MemberRepository, + private val principalProvider: PrincipalProvider ) : ProfileService { //Member 생성시 Profile 도 같이 생성 @Transactional override fun createDefaultProfile(member: Member): ProfileDTO { + val nickname = generateUniqueNickname() val profile = Profile( member = member, - nickname = member.nickname, + nickname = nickname, profileUrl = null, description = "This is a default profile.", job = null, @@ -36,67 +39,55 @@ class ProfileServiceImpl( return toDto(savedProfile) } - - //자체적으로 profile 을 생성할경우. "테스트 용도" - @Transactional - override fun createProfile(profileDTO: ProfileDTO): ProfileDTO { - val member = memberRepository.findById(profileDTO.memberId).orElseThrow { - EntityNotFoundException("Member not found for memberId=${profileDTO.memberId}") - } - - val profile = Profile( - member = member, - nickname = profileDTO.nickname, - profileUrl = profileDTO.profileUrl, - description = profileDTO.description, - job = profileDTO.job?.let { Job.valueOf(it) }, - socialMedia = profileDTO.socialMedia - ) - - val savedProfile = profileRepository.save(profile) - logger.info("Profile created successfully for memberId=${profileDTO.memberId}") - - return toDto(savedProfile) + private fun generateUniqueNickname(): String { + var nickname: String + var counter = 1 + do { + nickname = "default#$counter" + counter++ + } while (profileRepository.findByNickname(nickname).isPresent) // 중복 검사 + return nickname } @Transactional(readOnly = true) - override fun getProfile(memberId: Long): ProfileDTO { - logger.info("Fetching profile for memberId=$memberId") + override fun getProfile(profileId: Long): ProfileDTO { + logger.info("Fetching profile for profileId=$profileId") - val profile = profileRepository.findByMemberId(memberId) - ?: throw EntityNotFoundException("Profile not found for memberId=$memberId") + val profile = profileRepository.findById(profileId) + .orElseThrow { EntityNotFoundException("Profile not found for profileId=$profileId") } - logger.info("Profile fetched successfully for memberId=$memberId") + logger.info("Profile fetched successfully for profileId=$profileId") return toDto(profile) } @Transactional - override fun updateProfile(memberId: Long, profileDTO: ProfileDTO): ProfileDTO { - logger.info("Updating profile for memberId=$memberId") + override fun updateProfile(profileId: Long, profileDTO: ProfileDTO): ProfileDTO { + val profile = profileRepository.findById(profileId) + .orElseThrow { EntityNotFoundException("Profile not found with id=$profileId") } + + profileDTO.nickname?.let { newNickname -> + if (profile.nickname != newNickname && profileRepository.findByNickname(newNickname).isPresent) { + throw IllegalArgumentException("Nickname '$newNickname' is already in use") + } + profile.nickname = newNickname + } - val profile = profileRepository.findByMemberId(memberId) - ?: throw EntityNotFoundException("Profile not found for memberId=$memberId") + profile.description = profileDTO.description ?: profile.description + profile.socialMedia = profileDTO.socialMedia ?: profile.socialMedia + profile.profileUrl = profileDTO.profileUrl ?: profile.profileUrl + profile.job = profileDTO.job?.let { Job.valueOf(it) } ?: profile.job - profile.nickname = profileDTO.nickname - profile.profileUrl = profileDTO.profileUrl - profile.description = profileDTO.description - profile.job = profileDTO.job?.let { Job.valueOf(it) } - profile.socialMedia = profileDTO.socialMedia val updatedProfile = profileRepository.save(profile) - logger.info("Profile updated successfully for memberId=$memberId") - return toDto(updatedProfile) } private fun toDto(profile: Profile): ProfileDTO { return ProfileDTO( - memberId = profile.member.id ?: throw IllegalStateException("Member ID is missing"), + profileId = profile.id, nickname = profile.nickname, - profileUrl = profile.profileUrl, description = profile.description, - job = profile.job?.name, socialMedia = profile.socialMedia ) } @@ -106,6 +97,11 @@ class ProfileServiceImpl( .orElseThrow { NoSuchElementException() } } + override fun getByPrincipal(): Profile { + return profileRepository.findByUsername(principalProvider.getUsername()!!) + .orElseThrow { NoSuchElementException() } + } + companion object { private val logger: Logger = LoggerFactory.getLogger(ProfileServiceImpl::class.java) } diff --git a/src/main/kotlin/org/tenten/bittakotlin/scout/controller/ScoutRequestController.kt b/src/main/kotlin/org/tenten/bittakotlin/scout/controller/ScoutRequestController.kt index baa857d..832adae 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/scout/controller/ScoutRequestController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/scout/controller/ScoutRequestController.kt @@ -1,5 +1,5 @@ package org.tenten.bittakotlin.scout.controller -/* + import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest @@ -44,4 +44,3 @@ class ScoutRequestController( } } - */ \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/scout/entity/ScoutRequest.kt b/src/main/kotlin/org/tenten/bittakotlin/scout/entity/ScoutRequest.kt index ab02b2b..dd18a8c 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/scout/entity/ScoutRequest.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/scout/entity/ScoutRequest.kt @@ -3,7 +3,8 @@ package org.tenten.bittakotlin.scout.entity import jakarta.persistence.* import org.springframework.data.annotation.CreatedDate import org.springframework.data.jpa.domain.support.AuditingEntityListener -import org.tenten.bittakotlin.member.entity.Member +import org.tenten.bittakotlin.feed.entity.Feed +import org.tenten.bittakotlin.profile.entity.Profile import java.time.LocalDateTime @Entity @@ -13,18 +14,17 @@ data class ScoutRequest( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, - //feed 마이그레이션 종료가 안되어 일단 주석처리 하겠습니다 - //@ManyToOne - //@JoinColumn(name = "feed_id", nullable = false) - //val feed: Feed, + @ManyToOne + @JoinColumn(name = "feed_id", nullable = false) + val feed: Feed, @ManyToOne @JoinColumn(name = "sender_id", nullable = false) - val sender: Member, + val sender: Profile, @ManyToOne @JoinColumn(name = "receiver_id", nullable = false) - val receiver: Member, + val receiver: Profile, @Lob val description: String? = null, diff --git a/src/main/kotlin/org/tenten/bittakotlin/scout/repository/ScoutRequestRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/scout/repository/ScoutRequestRepository.kt index 5252290..243ccbd 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/scout/repository/ScoutRequestRepository.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/scout/repository/ScoutRequestRepository.kt @@ -7,6 +7,6 @@ import org.springframework.data.jpa.repository.JpaRepository import org.tenten.bittakotlin.scout.entity.ScoutRequest interface ScoutRequestRepository : JpaRepository { - fun findBySenderIdOrderById(senderId: Long, pageable: Pageable): Page - fun findByReceiverIdOrderById(receiverId: Long, pageable: Pageable): Page + fun findBySender_IdOrderById(senderId: Long, pageable: Pageable): Page + fun findByReceiver_IdOrderById(receiverId: Long, pageable: Pageable): Page } \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt b/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt index 601b6a7..edc3647 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt @@ -1,69 +1,90 @@ package org.tenten.bittakotlin.scout.service -/* -//import com.prgrms2.java.bitta.feed.entity.Feed -//import com.prgrms2.java.bitta.member.entity.Member -//import com.prgrms2.java.bitta.member.service.MemberProvider -//import com.prgrms2.java.bitta.feed.service.FeedProvider + + import jakarta.persistence.EntityNotFoundException +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import org.tenten.bittakotlin.member.repository.MemberRepository +import org.tenten.bittakotlin.feed.repository.FeedRepository +import org.tenten.bittakotlin.profile.repository.ProfileRepository import org.tenten.bittakotlin.scout.dto.ScoutDTO import org.tenten.bittakotlin.scout.entity.ScoutRequest import org.tenten.bittakotlin.scout.repository.ScoutRequestRepository -import java.lang.reflect.Member + @Service class ScoutRequestService( private val scoutRequestRepository: ScoutRequestRepository, - // private val feedProvider: FeedProvider, - private val memberRepository: MemberRepository + private val feedRepository: FeedRepository, + private val profileRepository: ProfileRepository ) { + private val logger: Logger = LoggerFactory.getLogger(ScoutRequestService::class.java) + @Transactional fun sendScoutRequest(scoutDTO: ScoutDTO): ScoutDTO { + logger.info("Attempting to send scout request from senderId=${scoutDTO.senderId} to receiverId=${scoutDTO.receiverId}") + val request = dtoToEntity(scoutDTO) val savedRequest = scoutRequestRepository.save(request) + + logger.info("Scout request successfully saved with id=${savedRequest.id}") return entityToDto(savedRequest) } @Transactional(readOnly = true) fun getSentScoutRequests(senderId: Long, pageable: Pageable): Page { - return scoutRequestRepository.findBySenderIdOrderById(senderId, pageable) + logger.info("Fetching sent scout requests for senderId=$senderId") + + val sentRequests = scoutRequestRepository.findBySender_IdOrderById(senderId, pageable) .map { request -> entityToDto(request) } + + logger.info("Retrieved ${sentRequests.totalElements} sent scout requests for senderId=$senderId") + return sentRequests } @Transactional(readOnly = true) fun getReceivedScoutRequests(receiverId: Long, pageable: Pageable): Page { - return scoutRequestRepository.findByReceiverIdOrderById(receiverId, pageable) + logger.info("Fetching received scout requests for receiverId=$receiverId") + + val receivedRequests = scoutRequestRepository.findByReceiver_IdOrderById(receiverId, pageable) .map { request -> entityToDto(request) } + + logger.info("Retrieved ${receivedRequests.totalElements} received scout requests for receiverId=$receiverId") + return receivedRequests } private fun entityToDto(request: ScoutRequest): ScoutDTO { - // return ScoutDTO( - // id = request.id, - // senderId = request.sender?.id ?: throw IllegalStateException("Sender ID is missing"), - //receiverId = request.receiver?.id ?: throw IllegalStateException("Receiver ID is missing"), - // description = request.description, - //sentAt = request.sentAt + return ScoutDTO( + id = request.id, + feedId = request.feed.id ?: throw IllegalStateException("Feed ID is missing"), + senderId = request.sender.id ?: throw IllegalStateException("Sender Profile ID is missing"), + receiverId = request.receiver.id ?: throw IllegalStateException("Receiver Profile ID is missing"), + description = request.description, + sentAt = request.sentAt ) } private fun dtoToEntity(scoutDTO: ScoutDTO): ScoutRequest { - // val feed = feedRepository.findById(scoutDTO.feedId) - // .orElseThrow { EntityNotFoundException("Feed not found with id=${scoutDTO.feedId}") } + logger.info("Converting ScoutDTO to ScoutRequest entity") + + val feed = feedRepository.findById(scoutDTO.feedId) + .orElseThrow { EntityNotFoundException("Feed not found with id=${scoutDTO.feedId}") } - val sender = memberRepository.findById(scoutDTO.senderId) + val sender = profileRepository.findById(scoutDTO.senderId) .orElseThrow { EntityNotFoundException("Sender not found with id=${scoutDTO.senderId}") } - val receiver = memberRepository.findById(scoutDTO.receiverId) + val receiver = profileRepository.findById(scoutDTO.receiverId) .orElseThrow { EntityNotFoundException("Receiver not found with id=${scoutDTO.receiverId}") } + logger.info("ScoutRequest entity created with feedId=${feed.id}, senderId=${sender.id}, receiverId=${receiver.id}") + return ScoutRequest( id = scoutDTO.id, - // feed = feed, + feed = feed, sender = sender, receiver = receiver, description = scoutDTO.description, @@ -72,4 +93,3 @@ class ScoutRequestService( } } - */ \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt b/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt index 3e141a6..815d9a2 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/security/config/SecurityConfig.kt @@ -72,11 +72,23 @@ class SecurityConfig( auth .requestMatchers( "/", - "/api/member/login", - "/api/member/join", - "/api/member/reissue").permitAll() - .requestMatchers("/api/member/{id}/**").hasRole("USER") + "/api/v1/member/login", + "/member/login", + "/api/v1/member/join", + "/member/join", + "/api/v1/member/reissue").permitAll() + + .requestMatchers( + "/api/v1/member/{id}/**", + "member/{id}/**", + "/api/v1/job-post/**", + "/job-post/**", + "/api/v1/like/**").hasRole("USER") + .requestMatchers(HttpMethod.DELETE,"/api/member/{id}").authenticated() + .requestMatchers(HttpMethod.PUT,"/api/member/{id}").authenticated() + .requestMatchers("/api/v1/chat/**").authenticated() + .anyRequest().authenticated() } diff --git a/src/main/kotlin/org/tenten/bittakotlin/security/service/PrincipalProvider.kt b/src/main/kotlin/org/tenten/bittakotlin/security/service/PrincipalProvider.kt new file mode 100644 index 0000000..174e7ae --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/security/service/PrincipalProvider.kt @@ -0,0 +1,22 @@ +package org.tenten.bittakotlin.security.service + +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.tenten.bittakotlin.security.dto.CustomUserDetails + +@Component +class PrincipalProvider { + private fun getPrincipal(): CustomUserDetails? { + val authentication = SecurityContextHolder.getContext().authentication + + return authentication.principal as? CustomUserDetails + } + + fun getUsername(): String? { + return getPrincipal()?.username + } + + fun isAdmin(): Boolean { + return getPrincipal()?.authorities?.any { it.authority == "ROLE_ADMIN" } ?: false + } +} \ No newline at end of file