From 01474b48e21fc2aa1edca2582f6745bc66a5fc62 Mon Sep 17 00:00:00 2001 From: jungyoeal Date: Mon, 4 Nov 2024 09:47:40 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Feat:=20feed=20kotlin=20=EB=B3=80?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feed/controller/FeedController.kt | 108 ++++++++++++++ .../tenten/bittakotlin/feed/dto/FeedDTO.kt | 34 +++++ .../bittakotlin/feed/dto/FeedRequestDto.kt | 33 ++++ .../tenten/bittakotlin/feed/entity/Feed.kt | 39 +++++ .../feed/exception/FeedException.kt | 25 ++++ .../feed/exception/FeedTaskException.kt | 6 + .../feed/repository/FeedRepository.kt | 38 +++++ .../bittakotlin/feed/service/FeedProvider.kt | 18 +++ .../bittakotlin/feed/service/FeedService.kt | 23 +++ .../feed/service/FeedServiceImpl.kt | 141 ++++++++++++++++++ .../scout/service/ScoutRequestService.kt | 2 +- 11 files changed, 466 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/controller/FeedController.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedRequestDto.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/entity/Feed.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedTaskException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedProvider.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedService.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedServiceImpl.kt 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..876ce91 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/controller/FeedController.kt @@ -0,0 +1,108 @@ +package org.tenten.bittakotlin.feed.controller + +import org.tenten.bittakotlin.feed.dto.FeedDTO +import org.tenten.bittakotlin.feed.dto.FeedRequestDto.Modify +import org.tenten.bittakotlin.feed.service.FeedService +import org.tenten.bittakotlin.global.constants.ApiResponses.* +import org.tenten.bittakotlin.global.exception.AuthenticationException +import org.tenten.bittakotlin.global.util.AuthenticationProvider +import org.tenten.bittakotlin.member.entity.Role +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.Parameters +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import jakarta.validation.constraints.Min +import lombok.RequiredArgsConstructor +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile +import java.util.Map + +@Tag(name = "피드 API 컨트롤러", description = "피드와 관련된 REST API를 제공하는 컨틀롤러입니다.") +@RestController +@RequestMapping("/api/v1/feed") +@RequiredArgsConstructor +@Validated +class FeedController { + private val feedService: FeedService? = null + + + @GetMapping + fun getFeeds( + @RequestParam(required = false, defaultValue = "0", value = "page") page: Int, + @RequestParam(required = false, defaultValue = "10", value = "size") size: Int, + @RequestParam(required = false, value = "username") username: String?, + @RequestParam(required = false, value = "title") title: String? + ): ResponseEntity<*> { + val pageable: Pageable = PageRequest.of(page, size) + + return ResponseEntity.ok( + Map.of( + "message", "피드를 성공적으로 조회했습니다.", + "result", feedService.readAll(pageable, username, title) + ) + ) + } + + @GetMapping("/{id}") + fun getFeedById(@PathVariable("id") id: @Min(1) Long?): ResponseEntity<*> { + return ResponseEntity.ok( + Map.of("message", "피드를 성공적으로 조회했습니다.", "result", feedService.read(id)) + ) + } + + @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE]) + fun createFeed( + @RequestPart(value = "feed") feedDto: @Valid FeedDTO?, + @RequestPart(value = "files", required = false) files: List? + ): ResponseEntity<*> { + feedService.insert(feedDto, files) + + return ResponseEntity.ok().body(Map.of("message", "피드가 등록되었습니다.")) + } + + @PutMapping(value = ["/{id}"], consumes = [MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE]) + fun modifyFeed( + @PathVariable("id") id: @Min(1) Long?, + @RequestPart("feed") feedDTO: @Valid Modify, + @RequestPart("filesToUpload") filesToUpload: List?, + @RequestPart("filesToDelete") filesToDelete: List? + ): ResponseEntity<*> { + if (!checkPermission(id)) { + throw AuthenticationException.CANNOT_ACCESS.get() + } + + feedDTO.setId(id) + + feedService.update(feedDTO, filesToUpload, filesToDelete) + + return ResponseEntity.ok().body(Map.of("message", "피드가 수정되었습니다.")) + } + + @DeleteMapping("/{id}") + fun deleteFeed(@PathVariable("id") id: @Min(1) Long?): ResponseEntity<*> { + if (!checkPermission(id)) { + throw AuthenticationException.CANNOT_ACCESS.get() + } + + feedService.delete(id) + + return ResponseEntity.ok().body(Map.of("message", "피드가 삭제되었습니다.")) + } + + private fun checkPermission(id: Long?): Boolean { + if (AuthenticationProvider.getRoles() === Role.ROLE_ADMIN) { + return true + } + + return feedService.checkAuthority(id, AuthenticationProvider.getUsername()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedDTO.kt new file mode 100644 index 0000000..d0d74b5 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedDTO.kt @@ -0,0 +1,34 @@ +package org.tenten.bittakotlin.feed.dto + +import org.tenten.bittakotlin.media.dto.MediaRequestDto +import org.tenten.bittakotlin.media.dto.MediaResponseDto +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.* +import java.time.LocalDateTime + +@Schema(title = "피드 DTO", description = "피드의 요청 및 응답에 사용하는 DTO입니다.") +data class FeedDTO( + @field:Schema(title = "피드 ID (PK)", description = "피드의 고유 ID 입니다.", example = "1", minimum = "1") + @field:Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") + var id: Long? = null, + + @field:Schema(title = "피드 제목", description = "피드 제목입니다.", example = "Feed Title", minimum = "1", maximum = "50") + @field:NotBlank(message = "제목은 비우거나, 공백이 될 수 없습니다.") + @field:Size(min = 1, max = 50, message = "제목은 1 ~ 50자 이하여야 합니다.") + var title: String, + + @field:Schema(title = "피드 내용", description = "피드 내용입니다.", example = "Feed Content") + @field:NotNull + var content: String = "", + + @field:Schema(title = "회원 ID (FK)", description = "회원의 고유 ID 입니다.", example = "1", minimum = "1") + @field:Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") + @field:NotNull(message = "회원 ID는 누락될 수 없습니다.") + var memberId: Long, + + @field:Schema(title = "피드 생성일시", description = "피드가 생성된 날짜 및 시간입니다.", example = "2023-09-24T14:45:00") + var createdAt: LocalDateTime? = null, + + @field:Schema(title = "미디어 파일 목록", description = "피드에 포함된 사진 및 영상 목록입니다.") + var medias: List = emptyList() +) \ 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..e8752c6 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedRequestDto.kt @@ -0,0 +1,33 @@ +package org.tenten.bittakotlin.feed.dto + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import lombok.AllArgsConstructor +import lombok.Builder +import lombok.Data +import lombok.NoArgsConstructor + +class FeedRequestDto { + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + class Modify { + @Schema(title = "피드 ID (PK)", description = "피드의 고유 ID 입니다.", example = "1", minimum = "1") + val id: @Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") Long? = null + + @Schema(title = "피드 제목", description = "피드 제목입니다.", example = "Feed Title", minimum = "1", maximum = "50") + val title: @NotBlank(message = "제목은 비우거나, 공백이 될 수 없습니다.") @Size( + min = 1, + max = 50, + message = "제목은 1 ~ 50자 이하여야 합니다." + ) String? = null + + @Schema(title = "피드 내용", description = "피드 내용입니다.", example = "Feed Content") + @Builder.Default + val content: @NotNull String = "" + } +} \ 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..7add6c0 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/entity/Feed.kt @@ -0,0 +1,39 @@ +package org.tenten.bittakotlin.feed.entity + +import org.tenten.bittakotlin.media.entity.Media +import org.tenten.bittakotlin.member.entity.Member +import jakarta.persistence.* +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +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, + + @Lob + @Column(nullable = false) + var content: String, + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + var member: Optional, + + @CreatedDate + @Column(updatable = false, nullable = false) + var createdAt: LocalDateTime = LocalDateTime.now(), + + @OneToMany(mappedBy = "feed", cascade = [CascadeType.REMOVE]) + var medias: MutableList = mutableListOf() +) { + fun clearMedias() { + medias.clear() + } +} \ 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..4c2de1c --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedException.kt @@ -0,0 +1,25 @@ +package org.tenten.bittakotlin.feed.exception + +import com.prgrms2.java.bitta.feed.exception.FeedTaskException + + +enum class FeedException(code: Int, message: String) { + NOT_FOUND(404, "피드가 존재하지 않습니다."), + BAD_REQUEST(400, "잘못된 요청입니다."), + BAD_AUTHORITY(403, "권한이 없습니다."), + CANNOT_INSERT(400, "피드를 등록할 수 없습니다."), + CANNOT_FOUND(404, "피드가 존재하지 않습니다."), + CANNOT_MODIFY(400, "피드를 수정할 수 없습니다."), + CANNOT_DELETE(404, "삭제할 피드가 존재하지 않습니다"), + INTERNAL_ERROR(500, "서버 내부에 오류가 발생했습니다."); + + private val feedTaskException: FeedTaskException + + init { + feedTaskException = FeedTaskException(code, message) + } + + fun get(): FeedTaskException { + return feedTaskException + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedTaskException.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedTaskException.kt new file mode 100644 index 0000000..e310a6a --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedTaskException.kt @@ -0,0 +1,6 @@ +package com.prgrms2.java.bitta.feed.exception + +class FeedTaskException( + val code: Int, + override val message: String +) : RuntimeException(message) \ 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..e819307 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt @@ -0,0 +1,38 @@ +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.Modifying +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.member.username LIKE %:username%") + fun findAllLikeUsernameOrderByIdDesc(@Param("username") username: String, pageable: Pageable): Page + + @Query("SELECT f FROM Feed f WHERE f.title LIKE %:title%") + fun findAllLikeTitleOrderByIdDesc(@Param("title") title: String, pageable: Pageable): Page + + @Query("SELECT f FROM Feed f WHERE f.member.username LIKE %:username% AND f.title LIKE %:title%") + fun findAllLikeUsernameAndTitleOrderByIdDesc( + @Param("username") username: String, + @Param("title") title: String, + pageable: Pageable + ): Page + + fun findAllByOrderByIdDesc(pageable: Pageable): Page + + @Modifying + @Query("DELETE FROM Feed f WHERE f.id = :id") + fun deleteByIdAndReturnCount(@Param("id") id: Long): Int + + @Query(value = "SELECT * FROM feed ORDER BY RAND() LIMIT :limit", nativeQuery = true) + fun findRandomFeeds(@Param("limit") limit: Int): List + + fun existsByIdAndMember_Username(feedId: Long, username: String): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedProvider.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedProvider.kt new file mode 100644 index 0000000..ba3250d --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedProvider.kt @@ -0,0 +1,18 @@ +package org.tenten.bittakotlin.feed.service + +import org.tenten.bittakotlin.feed.entity.Feed +import org.tenten.bittakotlin.feed.repository.FeedRepository +import lombok.RequiredArgsConstructor +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@RequiredArgsConstructor +class FeedProvider { + private val feedRepository: FeedRepository? = null + + @Transactional(readOnly = true) + fun getById(id: Long): Feed? { + return feedRepository?.findById(id)?.orElse(null) + } +} \ 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..f6c3cf3 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedService.kt @@ -0,0 +1,23 @@ +package org.tenten.bittakotlin.feed.service + +import org.tenten.bittakotlin.feed.dto.FeedDTO +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.web.multipart.MultipartFile +import org.tenten.bittakotlin.feed.dto.FeedRequestDto + +interface FeedService { + fun read(id: Long): FeedDTO + + fun readAll(pageable: Pageable, username: String?, title: String?): Page + + fun insert(feedDto: FeedDTO, files: List) + + fun update(feedDto: FeedRequestDto.Modify, filesToUpload: List, filesToDeletes: List) + + fun delete(id: Long) + + fun readRandomFeeds(limit: Int): List + + fun checkAuthority(feedId: Long, memberId: String): Boolean +} \ 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..8b5a61f --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedServiceImpl.kt @@ -0,0 +1,141 @@ +package org.tenten.bittakotlin.feed.service + +import org.tenten.bittakotlin.feed.dto.FeedDTO +import org.tenten.bittakotlin.feed.entity.Feed +import org.tenten.bittakotlin.feed.exception.FeedException +import org.tenten.bittakotlin.feed.repository.FeedRepository +import org.tenten.bittakotlin.media.service.MediaService +import lombok.RequiredArgsConstructor +import lombok.extern.slf4j.Slf4j +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.springframework.web.multipart.MultipartFile +import org.tenten.bittakotlin.feed.dto.FeedRequestDto +import org.tenten.bittakotlin.member.repository.MemberRepository +import java.util.* +import java.util.stream.Collectors + +@Service +@RequiredArgsConstructor +@Slf4j +class FeedServiceImpl( + private val feedRepository: FeedRepository, + private val mediaService: MediaService, + private val memberRepository: MemberRepository, +) : FeedService { + + @Transactional(readOnly = true) + override fun read(id: Long): FeedDTO { + val feed = feedRepository.findById(id) + .orElseThrow { FeedException.CANNOT_FOUND.get() } + + return entityToDto(feed) + } + + + @Transactional(readOnly = true) + override fun readAll(pageable: Pageable, username: String?, title: String?): Page { + val feeds = when { + !username.isNullOrBlank() && !title.isNullOrBlank() -> + feedRepository.findAllLikeUsernameAndTitleOrderByIdDesc(username, title, pageable) + !username.isNullOrBlank() -> + feedRepository.findAllLikeUsernameOrderByIdDesc(username, pageable) + !title.isNullOrBlank() -> + feedRepository.findAllLikeTitleOrderByIdDesc(title, pageable) + else -> + feedRepository.findAllByOrderByIdDesc(pageable) + } + + return feeds.takeIf { it.hasContent() } + ?.map { entityToDto(it) } + ?: throw FeedException.CANNOT_FOUND.get() + } + + + @Transactional + override fun insert(feedDTO: FeedDTO, files: List) { + if (feedDTO.id != null) { + throw FeedException.BAD_REQUEST.get() + } + + var feed = dtoToEntity(feedDTO) + feed = feedRepository.save(feed) + + + mediaService.upload(files, feed.id) + } + + + + @Transactional + override fun update(feedDto: FeedRequestDto.Modify, filesToUpload: List, filesToDeletes: List) { + val feed = feedDto.id?.let { + feedRepository.findById(it) + .orElseThrow { FeedException.CANNOT_FOUND.get() } + } + + feed?.title = feedDto.title.toString() + feed?.content = feedDto.content + + if (!filesToDeletes.isNullOrEmpty()) { + val deleteMedias = mediaService.getMedias(filesToDeletes) + mediaService.deleteExistFiles(deleteMedias) + } + + feed?.clearMedias() + mediaService.uploads(filesToUpload, feedDto.id) + + feedRepository.save(feed) + } + + + @Transactional + override fun delete(id: Long?) { + val feed: Feed = feedRepository.findById(id) + .orElseThrow(FeedException.CANNOT_FOUND::get) + + if (feed.getMedias() != null) { + mediaService.deleteAll(feed.getMedias()) + } + + feed.setMember(null) + feedRepository.delete(feed) + } + + @Transactional(readOnly = true) + override fun readRandomFeeds(limit: Int): List { + val feeds: List = feedRepository.findRandomFeeds(limit) + return feeds.stream() + .map { feed: Feed -> this.entityToDto(feed) } + .collect, Any>(Collectors.toList()) + } + + override fun checkAuthority(feedId: Long, username: String): Boolean { + return feedRepository.existsByIdAndMember_Username(feedId, username) + } + + private fun dtoToEntity(feedDto: FeedDTO): Feed { + return Feed( + id = feedDto.id, + title = feedDto.title, + content = feedDto.content, + createdAt = feedDto.createdAt!!, + member = memberRepository.findById(feedDto.memberId), + medias = mediaService.getMedias(feedDto.medias) + ) + } + + private fun entityToDto(feed: Feed): FeedDTO { + return FeedDTO( + id = feed.id, + title = feed.title, + content = feed.content, + createdAt = feed.createdAt, + memberId = feed.member.id!!, + medias = mediaService.getUrls(feed.medias) + ) + } + +} \ 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 d45e3ae..1c68c25 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt @@ -3,7 +3,7 @@ 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 org.tenten.bittakotlin.feed.service.FeedProvider import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service From 4a9c6794c52ec827c3ef3eeb8bda03bc42246429 Mon Sep 17 00:00:00 2001 From: Preta3418 Date: Mon, 4 Nov 2024 12:40:34 +0900 Subject: [PATCH 02/10] =?UTF-8?q?Refactoring:=20feed,profile=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FeedProvider 가 삭제 되었기에 FeedRepository 로 연결 하였습니다. Member 또한 Profile 을 통해 연결하기로 한 고로 Profile 로 전부 변환 완료 하였습니다 Related to: prgrms-be-devcourse#2 --- .../bittakotlin/profile/entity/Profile.kt | 9 +++- .../controller/ScoutRequestController.kt | 3 +- .../bittakotlin/scout/entity/ScoutRequest.kt | 14 +++--- .../repository/ScoutRequestRepository.kt | 4 +- .../scout/service/ScoutRequestService.kt | 44 +++++++++---------- 5 files changed, 39 insertions(+), 35 deletions(-) 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/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..0793100 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt @@ -1,26 +1,24 @@ 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.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 ) { + @Transactional fun sendScoutRequest(scoutDTO: ScoutDTO): ScoutDTO { @@ -31,39 +29,40 @@ class ScoutRequestService( @Transactional(readOnly = true) fun getSentScoutRequests(senderId: Long, pageable: Pageable): Page { - return scoutRequestRepository.findBySenderIdOrderById(senderId, pageable) + return scoutRequestRepository.findBySender_IdOrderById(senderId, pageable) .map { request -> entityToDto(request) } } @Transactional(readOnly = true) fun getReceivedScoutRequests(receiverId: Long, pageable: Pageable): Page { - return scoutRequestRepository.findByReceiverIdOrderById(receiverId, pageable) + return scoutRequestRepository.findByReceiver_IdOrderById(receiverId, pageable) .map { request -> entityToDto(request) } } 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}") } + 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}") } return ScoutRequest( id = scoutDTO.id, - // feed = feed, + feed = feed, sender = sender, receiver = receiver, description = scoutDTO.description, @@ -72,4 +71,3 @@ class ScoutRequestService( } } - */ \ No newline at end of file From 9c054dd87e21d54a95592cfc1e175098b6e8af9a Mon Sep 17 00:00:00 2001 From: Preta3418 Date: Mon, 4 Nov 2024 12:42:39 +0900 Subject: [PATCH 03/10] Feat: Logger added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 유지보수를 위해 ScoutRequestService 단에 logger 를 설정하였습니다 Related to: prgrms-be-devcourse#2 --- .../scout/service/ScoutRequestService.kt | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) 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 0793100..edc3647 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/scout/service/ScoutRequestService.kt @@ -2,6 +2,8 @@ package org.tenten.bittakotlin.scout.service 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 @@ -12,31 +14,47 @@ import org.tenten.bittakotlin.scout.dto.ScoutDTO import org.tenten.bittakotlin.scout.entity.ScoutRequest import org.tenten.bittakotlin.scout.repository.ScoutRequestRepository + @Service class ScoutRequestService( private val scoutRequestRepository: ScoutRequestRepository, 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.findBySender_IdOrderById(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.findByReceiver_IdOrderById(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 { @@ -51,6 +69,8 @@ class ScoutRequestService( } private fun dtoToEntity(scoutDTO: ScoutDTO): ScoutRequest { + logger.info("Converting ScoutDTO to ScoutRequest entity") + val feed = feedRepository.findById(scoutDTO.feedId) .orElseThrow { EntityNotFoundException("Feed not found with id=${scoutDTO.feedId}") } @@ -60,6 +80,8 @@ class ScoutRequestService( 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, From fd58fe91cf6ef9a8d0f5602c548f3c54ef707b11 Mon Sep 17 00:00:00 2001 From: ghtndl Date: Mon, 4 Nov 2024 15:49:42 +0900 Subject: [PATCH 04/10] Refactor: member modify part --- .../member/controller/MemberController.kt | 23 +++++++++++++++---- .../member/dto/MemberRequestDTO.kt | 2 -- .../member/service/MemberService.kt | 2 +- .../member/service/MemberServiceImpl.kt | 6 ++--- 4 files changed, 23 insertions(+), 10 deletions(-) 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..4367867 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt @@ -6,6 +6,7 @@ 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 @@ -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..9a5e188 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt @@ -31,8 +31,6 @@ 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, 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) // 수정 후 저장 } From 089efb5324e2691aaf9335dd8d6ac874c5ba32f1 Mon Sep 17 00:00:00 2001 From: ghtndl Date: Mon, 4 Nov 2024 16:05:04 +0900 Subject: [PATCH 05/10] Refactor: MemberRequestDTO part --- .../org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt | 2 -- 1 file changed, 2 deletions(-) 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 9a5e188..b3be9c4 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/dto/MemberRequestDTO.kt @@ -32,8 +32,6 @@ class MemberRequestDTO { @Schema(title = "회원정보 수정 및 비밀번호 변경 DTO", description = "회원정보 수정 및 비밀번호 변경 요청에 사용하는 DTO입니다.") data class UpdateMemberRequest( - @Schema(title = "아이디", description = "비밀번호를 변경할 아이디입니다.", example = "username") - val username: String, @Schema(title = "새로운 별명", description = "새롭게 변경할 별명입니다.", example = "nickname") val nickname: String? = null, // nullable로 설정 From 6b997c58d70c69d29da3f0002ee9d7438708156c Mon Sep 17 00:00:00 2001 From: ghtndl Date: Mon, 4 Nov 2024 17:02:47 +0900 Subject: [PATCH 06/10] Refactor: MemberController api url --- .../controller/JobPostViewController.kt | 2 +- .../member/controller/MemberController.kt | 2 +- .../security/config/SecurityConfig.kt | 20 +++++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) 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/member/controller/MemberController.kt b/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt index 4367867..b65d44a 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/member/controller/MemberController.kt @@ -12,7 +12,7 @@ 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, 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() } From 3697a5870080cd8d6c35c6954acc5cc07905e91b Mon Sep 17 00:00:00 2001 From: Preta3418 Date: Mon, 4 Nov 2024 17:16:07 +0900 Subject: [PATCH 07/10] =?UTF-8?q?Feat:=20nickname=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 닉네임 자동 생성 + update 기능을 구현했습니다. 추가로 memberId 참조를 전부 profileId 참조로 변경했습니다 Related to: prgrms-be-devcourse#10 --- .../profile/controller/ProfileController.kt | 30 +++----- .../bittakotlin/profile/dto/ProfileDTO.kt | 13 ++-- .../profile/service/ProfileService.kt | 5 +- .../profile/service/ProfileServiceImpl.kt | 72 ++++++++----------- 4 files changed, 47 insertions(+), 73 deletions(-) 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/service/ProfileService.kt b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt index 347f6a9..79c71a7 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt @@ -5,9 +5,8 @@ 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 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..c1c4e77 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt @@ -17,16 +17,16 @@ import org.tenten.bittakotlin.profile.constant.Job @Service class ProfileServiceImpl( private val profileRepository: ProfileRepository, - private val memberRepository: MemberRepository ) : 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 +36,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 ) } From 8058ddee7df7429046afad903f93697347839b84 Mon Sep 17 00:00:00 2001 From: juwon-code Date: Tue, 5 Nov 2024 01:29:09 +0900 Subject: [PATCH 08/10] =?UTF-8?q?Feat:=20=ED=94=BC=EB=93=9C=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 피드 컴포넌트를 마이그레이션하고 잘못 또는 부족한 기능을 보완합니다. - 피드 목록 랜덤 조회 API 등록. - 피드-미디어 중간 엔티티로 관리. - 피드 수정, 삭제시 권한 확인하도록 설정. Related to: prgrms-be-devcourse/NBB1_2_3_Team10#22 --- .../bittakotlin/feed/constant/FeedError.kt | 8 + .../feed/controller/FeedController.kt | 120 +++++------- .../tenten/bittakotlin/feed/dto/FeedDTO.kt | 34 ---- .../bittakotlin/feed/dto/FeedRequestDto.kt | 46 ++--- .../bittakotlin/feed/dto/FeedResponseDto.kt | 28 +++ .../tenten/bittakotlin/feed/entity/Feed.kt | 29 ++- .../bittakotlin/feed/entity/FeedMedia.kt | 22 +++ .../feed/entity/key/FeedMediaId.kt | 11 ++ .../feed/exception/FeedException.kt | 31 +-- .../feed/exception/FeedTaskException.kt | 6 - .../feed/repository/FeedMediaRepository.kt | 16 ++ .../feed/repository/FeedRepository.kt | 23 +-- .../feed/service/FeedMediaService.kt | 16 ++ .../feed/service/FeedMediaServiceImpl.kt | 76 ++++++++ .../bittakotlin/feed/service/FeedProvider.kt | 18 -- .../bittakotlin/feed/service/FeedService.kt | 17 +- .../feed/service/FeedServiceImpl.kt | 179 ++++++++++-------- 17 files changed, 372 insertions(+), 308 deletions(-) create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/constant/FeedError.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedDTO.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedResponseDto.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/entity/FeedMedia.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/entity/key/FeedMediaId.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedTaskException.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedMediaRepository.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedMediaService.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedMediaServiceImpl.kt delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedProvider.kt 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 index 876ce91..064d109 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/controller/FeedController.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/controller/FeedController.kt @@ -1,108 +1,74 @@ package org.tenten.bittakotlin.feed.controller -import org.tenten.bittakotlin.feed.dto.FeedDTO -import org.tenten.bittakotlin.feed.dto.FeedRequestDto.Modify import org.tenten.bittakotlin.feed.service.FeedService -import org.tenten.bittakotlin.global.constants.ApiResponses.* -import org.tenten.bittakotlin.global.exception.AuthenticationException -import org.tenten.bittakotlin.global.util.AuthenticationProvider -import org.tenten.bittakotlin.member.entity.Role -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.Parameter -import io.swagger.v3.oas.annotations.Parameters -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.responses.ApiResponse -import io.swagger.v3.oas.annotations.tags.Tag -import jakarta.validation.Valid -import jakarta.validation.constraints.Min -import lombok.RequiredArgsConstructor import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable -import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* -import org.springframework.web.multipart.MultipartFile -import java.util.Map +import org.tenten.bittakotlin.feed.dto.FeedRequestDto +import org.tenten.bittakotlin.security.service.PrincipalProvider -@Tag(name = "피드 API 컨트롤러", description = "피드와 관련된 REST API를 제공하는 컨틀롤러입니다.") @RestController @RequestMapping("/api/v1/feed") -@RequiredArgsConstructor -@Validated -class FeedController { - private val feedService: FeedService? = null - - +class FeedController ( + private val feedService: FeedService +) { @GetMapping - fun getFeeds( + fun readAll( @RequestParam(required = false, defaultValue = "0", value = "page") page: Int, @RequestParam(required = false, defaultValue = "10", value = "size") size: Int, - @RequestParam(required = false, value = "username") username: String?, + @RequestParam(required = false, value = "nickname") nickname: String?, @RequestParam(required = false, value = "title") title: String? - ): ResponseEntity<*> { + ): ResponseEntity> { val pageable: Pageable = PageRequest.of(page, size) - return ResponseEntity.ok( - Map.of( - "message", "피드를 성공적으로 조회했습니다.", - "result", feedService.readAll(pageable, username, title) - ) - ) + return ResponseEntity.ok(mapOf( + "message" to "피드 목록을 성공적으로 조회했습니다.", + "result" to feedService.getAll(pageable, nickname, title) + )) } - @GetMapping("/{id}") - fun getFeedById(@PathVariable("id") id: @Min(1) Long?): ResponseEntity<*> { - return ResponseEntity.ok( - Map.of("message", "피드를 성공적으로 조회했습니다.", "result", feedService.read(id)) - ) + @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) + )) } - @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE]) - fun createFeed( - @RequestPart(value = "feed") feedDto: @Valid FeedDTO?, - @RequestPart(value = "files", required = false) files: List? - ): ResponseEntity<*> { - feedService.insert(feedDto, files) + @GetMapping("/{id}") + fun read(@PathVariable("id") id: Long): ResponseEntity> { + return ResponseEntity.ok(mapOf( + "message" to "피드를 성공적으로 조회했습니다.", + "result" to feedService.get(id) + )) + } - return ResponseEntity.ok().body(Map.of("message", "피드가 등록되었습니다.")) + @PostMapping + fun create(requestDto: FeedRequestDto.Create): ResponseEntity> { + return ResponseEntity.ok(mapOf( + "message" to "피드를 성공적으로 등록했습니다.", + "result" to feedService.save(requestDto) + )) } - @PutMapping(value = ["/{id}"], consumes = [MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE]) + @PutMapping fun modifyFeed( - @PathVariable("id") id: @Min(1) Long?, - @RequestPart("feed") feedDTO: @Valid Modify, - @RequestPart("filesToUpload") filesToUpload: List?, - @RequestPart("filesToDelete") filesToDelete: List? - ): ResponseEntity<*> { - if (!checkPermission(id)) { - throw AuthenticationException.CANNOT_ACCESS.get() - } - - feedDTO.setId(id) - - feedService.update(feedDTO, filesToUpload, filesToDelete) - - return ResponseEntity.ok().body(Map.of("message", "피드가 수정되었습니다.")) + @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") id: @Min(1) Long?): ResponseEntity<*> { - if (!checkPermission(id)) { - throw AuthenticationException.CANNOT_ACCESS.get() - } - + fun deleteFeed(@PathVariable id: Long): ResponseEntity> { feedService.delete(id) - return ResponseEntity.ok().body(Map.of("message", "피드가 삭제되었습니다.")) - } - - private fun checkPermission(id: Long?): Boolean { - if (AuthenticationProvider.getRoles() === Role.ROLE_ADMIN) { - return true - } - - return feedService.checkAuthority(id, AuthenticationProvider.getUsername()) + return ResponseEntity.ok(mapOf( + "message" to "피드를 성공적으로 삭제했습니다." + )) } } \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedDTO.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedDTO.kt deleted file mode 100644 index d0d74b5..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedDTO.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.tenten.bittakotlin.feed.dto - -import org.tenten.bittakotlin.media.dto.MediaRequestDto -import org.tenten.bittakotlin.media.dto.MediaResponseDto -import io.swagger.v3.oas.annotations.media.Schema -import jakarta.validation.constraints.* -import java.time.LocalDateTime - -@Schema(title = "피드 DTO", description = "피드의 요청 및 응답에 사용하는 DTO입니다.") -data class FeedDTO( - @field:Schema(title = "피드 ID (PK)", description = "피드의 고유 ID 입니다.", example = "1", minimum = "1") - @field:Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") - var id: Long? = null, - - @field:Schema(title = "피드 제목", description = "피드 제목입니다.", example = "Feed Title", minimum = "1", maximum = "50") - @field:NotBlank(message = "제목은 비우거나, 공백이 될 수 없습니다.") - @field:Size(min = 1, max = 50, message = "제목은 1 ~ 50자 이하여야 합니다.") - var title: String, - - @field:Schema(title = "피드 내용", description = "피드 내용입니다.", example = "Feed Content") - @field:NotNull - var content: String = "", - - @field:Schema(title = "회원 ID (FK)", description = "회원의 고유 ID 입니다.", example = "1", minimum = "1") - @field:Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") - @field:NotNull(message = "회원 ID는 누락될 수 없습니다.") - var memberId: Long, - - @field:Schema(title = "피드 생성일시", description = "피드가 생성된 날짜 및 시간입니다.", example = "2023-09-24T14:45:00") - var createdAt: LocalDateTime? = null, - - @field:Schema(title = "미디어 파일 목록", description = "피드에 포함된 사진 및 영상 목록입니다.") - var medias: List = emptyList() -) \ 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 index e8752c6..eb3f879 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedRequestDto.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/dto/FeedRequestDto.kt @@ -1,33 +1,23 @@ package org.tenten.bittakotlin.feed.dto -import io.swagger.v3.oas.annotations.media.Schema -import jakarta.validation.constraints.Min -import jakarta.validation.constraints.NotBlank -import jakarta.validation.constraints.NotNull -import jakarta.validation.constraints.Size -import lombok.AllArgsConstructor -import lombok.Builder -import lombok.Data -import lombok.NoArgsConstructor +import org.tenten.bittakotlin.media.dto.MediaRequestDto class FeedRequestDto { - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - class Modify { - @Schema(title = "피드 ID (PK)", description = "피드의 고유 ID 입니다.", example = "1", minimum = "1") - val id: @Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") Long? = null - - @Schema(title = "피드 제목", description = "피드 제목입니다.", example = "Feed Title", minimum = "1", maximum = "50") - val title: @NotBlank(message = "제목은 비우거나, 공백이 될 수 없습니다.") @Size( - min = 1, - max = 50, - message = "제목은 1 ~ 50자 이하여야 합니다." - ) String? = null - - @Schema(title = "피드 내용", description = "피드 내용입니다.", example = "Feed Content") - @Builder.Default - val content: @NotNull String = "" - } + 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 index 7add6c0..1b86fe8 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/entity/Feed.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/entity/Feed.kt @@ -1,10 +1,10 @@ package org.tenten.bittakotlin.feed.entity -import org.tenten.bittakotlin.media.entity.Media -import org.tenten.bittakotlin.member.entity.Member 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.* @@ -18,22 +18,21 @@ data class Feed( @Column(nullable = false, length = 50) var title: String, - @Lob - @Column(nullable = false) + @Column(nullable = false, length = 1000) var content: String, @ManyToOne - @JoinColumn(name = "member_id", nullable = false) - var member: Optional, + @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) - var createdAt: LocalDateTime = LocalDateTime.now(), - - @OneToMany(mappedBy = "feed", cascade = [CascadeType.REMOVE]) - var medias: MutableList = mutableListOf() -) { - fun clearMedias() { - medias.clear() - } -} \ No newline at end of file + 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 index 4c2de1c..9c4c10e 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedException.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedException.kt @@ -1,25 +1,14 @@ -package org.tenten.bittakotlin.feed.exception +package com.prgrms2.java.bitta.feed.exception -import com.prgrms2.java.bitta.feed.exception.FeedTaskException +import org.tenten.bittakotlin.feed.constant.FeedError +class FeedException( + val code: Int, -enum class FeedException(code: Int, message: String) { - NOT_FOUND(404, "피드가 존재하지 않습니다."), - BAD_REQUEST(400, "잘못된 요청입니다."), - BAD_AUTHORITY(403, "권한이 없습니다."), - CANNOT_INSERT(400, "피드를 등록할 수 없습니다."), - CANNOT_FOUND(404, "피드가 존재하지 않습니다."), - CANNOT_MODIFY(400, "피드를 수정할 수 없습니다."), - CANNOT_DELETE(404, "삭제할 피드가 존재하지 않습니다"), - INTERNAL_ERROR(500, "서버 내부에 오류가 발생했습니다."); - - private val feedTaskException: FeedTaskException - - init { - feedTaskException = FeedTaskException(code, message) - } - - fun get(): FeedTaskException { - return feedTaskException - } + 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/exception/FeedTaskException.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedTaskException.kt deleted file mode 100644 index e310a6a..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/exception/FeedTaskException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.prgrms2.java.bitta.feed.exception - -class FeedTaskException( - val code: Int, - override val message: String -) : RuntimeException(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 index e819307..999f652 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt @@ -4,7 +4,6 @@ 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.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository @@ -12,27 +11,15 @@ import org.springframework.stereotype.Repository @Repository interface FeedRepository : JpaRepository { - @Query("SELECT f FROM Feed f WHERE f.member.username LIKE %:username%") - fun findAllLikeUsernameOrderByIdDesc(@Param("username") username: String, pageable: Pageable): Page + @Query("SELECT f FROM Feed f WHERE f.profile.nickname LIKE %:nickname%") + fun findAllLikeNicknameOrderByIdDesc(@Param("nickname") nickname: String, pageable: Pageable): Page @Query("SELECT f FROM Feed f WHERE f.title LIKE %:title%") fun findAllLikeTitleOrderByIdDesc(@Param("title") title: String, pageable: Pageable): Page - @Query("SELECT f FROM Feed f WHERE f.member.username LIKE %:username% AND f.title LIKE %:title%") - fun findAllLikeUsernameAndTitleOrderByIdDesc( - @Param("username") username: String, - @Param("title") title: String, - pageable: Pageable - ): Page + @Query("SELECT f FROM Feed f WHERE f.profile.nickname LIKE %:nickname% AND f.title LIKE %:title%") + fun findAllLikeNicknameAndTitleOrderByIdDesc(@Param("nickname") nickname: String, @Param("title") title: String, + pageable: Pageable): Page fun findAllByOrderByIdDesc(pageable: Pageable): Page - - @Modifying - @Query("DELETE FROM Feed f WHERE f.id = :id") - fun deleteByIdAndReturnCount(@Param("id") id: Long): Int - - @Query(value = "SELECT * FROM feed ORDER BY RAND() LIMIT :limit", nativeQuery = true) - fun findRandomFeeds(@Param("limit") limit: Int): List - - fun existsByIdAndMember_Username(feedId: Long, username: String): Boolean } \ 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/FeedProvider.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedProvider.kt deleted file mode 100644 index ba3250d..0000000 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedProvider.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.tenten.bittakotlin.feed.service - -import org.tenten.bittakotlin.feed.entity.Feed -import org.tenten.bittakotlin.feed.repository.FeedRepository -import lombok.RequiredArgsConstructor -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional - -@Service -@RequiredArgsConstructor -class FeedProvider { - private val feedRepository: FeedRepository? = null - - @Transactional(readOnly = true) - fun getById(id: Long): Feed? { - return feedRepository?.findById(id)?.orElse(null) - } -} \ 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 index f6c3cf3..aca1356 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedService.kt @@ -1,23 +1,20 @@ package org.tenten.bittakotlin.feed.service -import org.tenten.bittakotlin.feed.dto.FeedDTO import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable -import org.springframework.web.multipart.MultipartFile import org.tenten.bittakotlin.feed.dto.FeedRequestDto +import org.tenten.bittakotlin.feed.dto.FeedResponseDto interface FeedService { - fun read(id: Long): FeedDTO + fun get(id: Long): FeedResponseDto.Read - fun readAll(pageable: Pageable, username: String?, title: String?): Page + fun getAll(pageable: Pageable, username: String?, title: String?): Page - fun insert(feedDto: FeedDTO, files: List) + fun getRandom(pageSize: Int): Page - fun update(feedDto: FeedRequestDto.Modify, filesToUpload: List, filesToDeletes: List) + fun save(requestDto: FeedRequestDto.Create): FeedResponseDto.Create - fun delete(id: Long) - - fun readRandomFeeds(limit: Int): List + fun update(feedId: Long, requestDto: FeedRequestDto.Modify): FeedResponseDto.Modify - fun checkAuthority(feedId: Long, memberId: String): Boolean + 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 index 8b5a61f..e63d104 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/service/FeedServiceImpl.kt @@ -1,141 +1,158 @@ package org.tenten.bittakotlin.feed.service -import org.tenten.bittakotlin.feed.dto.FeedDTO +import com.prgrms2.java.bitta.feed.exception.FeedException import org.tenten.bittakotlin.feed.entity.Feed -import org.tenten.bittakotlin.feed.exception.FeedException +import org.tenten.bittakotlin.feed.constant.FeedError import org.tenten.bittakotlin.feed.repository.FeedRepository -import org.tenten.bittakotlin.media.service.MediaService 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.springframework.web.multipart.MultipartFile import org.tenten.bittakotlin.feed.dto.FeedRequestDto -import org.tenten.bittakotlin.member.repository.MemberRepository -import java.util.* -import java.util.stream.Collectors +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 mediaService: MediaService, - private val memberRepository: MemberRepository, -) : FeedService { + private val profileService: ProfileService, + + private val feedMediaService: FeedMediaService, +) : FeedService { @Transactional(readOnly = true) - override fun read(id: Long): FeedDTO { + override fun get(id: Long): FeedResponseDto.Read { val feed = feedRepository.findById(id) - .orElseThrow { FeedException.CANNOT_FOUND.get() } + .orElseThrow { FeedException(FeedError.CANNOT_FOUND) } - return entityToDto(feed) + return toReadResponseDto(feed) } - @Transactional(readOnly = true) - override fun readAll(pageable: Pageable, username: String?, title: String?): Page { - val feeds = when { - !username.isNullOrBlank() && !title.isNullOrBlank() -> - feedRepository.findAllLikeUsernameAndTitleOrderByIdDesc(username, title, pageable) - !username.isNullOrBlank() -> - feedRepository.findAllLikeUsernameOrderByIdDesc(username, pageable) - !title.isNullOrBlank() -> - feedRepository.findAllLikeTitleOrderByIdDesc(title, pageable) - else -> - feedRepository.findAllByOrderByIdDesc(pageable) + 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) } - return feeds.takeIf { it.hasContent() } - ?.map { entityToDto(it) } - ?: throw FeedException.CANNOT_FOUND.get() + if (feeds.isEmpty) { + throw FeedException(FeedError.CANNOT_FOUND) + } + + return feeds.map { feed -> toReadResponseDto(feed) } } + override fun getRandom(pageSize: Int): Page { + val totalElements = feedRepository.count() - @Transactional - override fun insert(feedDTO: FeedDTO, files: List) { - if (feedDTO.id != null) { - throw FeedException.BAD_REQUEST.get() + if (totalElements == 0L) { + throw FeedException(FeedError.CANNOT_FOUND) } - var feed = dtoToEntity(feedDTO) - feed = feedRepository.save(feed) + val totalPages: Int = ((totalElements - 1) / pageSize).toInt() + val randomPage: Int = (0 until totalPages).random() - mediaService.upload(files, feed.id) - } + 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 update(feedDto: FeedRequestDto.Modify, filesToUpload: List, filesToDeletes: List) { - val feed = feedDto.id?.let { - feedRepository.findById(it) - .orElseThrow { FeedException.CANNOT_FOUND.get() } + 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()) } + } + + - feed?.title = feedDto.title.toString() - feed?.content = feedDto.content + @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 (!filesToDeletes.isNullOrEmpty()) { - val deleteMedias = mediaService.getMedias(filesToDeletes) - mediaService.deleteExistFiles(deleteMedias) + if (profile == feed.profile) { + throw FeedException(FeedError.CANNOT_MODIFY_BAD_AUTHORITY) } - feed?.clearMedias() - mediaService.uploads(filesToUpload, feedDto.id) + 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?) { + override fun delete(id: Long) { + val profile: Profile = profileService.getByPrincipal() + val feed: Feed = feedRepository.findById(id) - .orElseThrow(FeedException.CANNOT_FOUND::get) + .orElseThrow { FeedException(FeedError.CANNOT_FOUND) } - if (feed.getMedias() != null) { - mediaService.deleteAll(feed.getMedias()) + if (profile == feed.profile) { + throw FeedException(FeedError.CANNOT_DELETE_BAD_AUTHORITY) } - feed.setMember(null) - feedRepository.delete(feed) - } + feedMediaService.delete(id) - @Transactional(readOnly = true) - override fun readRandomFeeds(limit: Int): List { - val feeds: List = feedRepository.findRandomFeeds(limit) - return feeds.stream() - .map { feed: Feed -> this.entityToDto(feed) } - .collect, Any>(Collectors.toList()) - } - - override fun checkAuthority(feedId: Long, username: String): Boolean { - return feedRepository.existsByIdAndMember_Username(feedId, username) - } - - private fun dtoToEntity(feedDto: FeedDTO): Feed { - return Feed( - id = feedDto.id, - title = feedDto.title, - content = feedDto.content, - createdAt = feedDto.createdAt!!, - member = memberRepository.findById(feedDto.memberId), - medias = mediaService.getMedias(feedDto.medias) - ) + feedRepository.delete(feed) } - private fun entityToDto(feed: Feed): FeedDTO { - return FeedDTO( - id = feed.id, + private fun toReadResponseDto(feed: Feed): FeedResponseDto.Read { + return FeedResponseDto.Read( + id = feed.id!!, title = feed.title, content = feed.content, - createdAt = feed.createdAt, - memberId = feed.member.id!!, - medias = mediaService.getUrls(feed.medias) + author = feed.profile.nickname, + createdAt = feed.createdAt!!, + medias = feedMediaService.getMedias(feed.id!!) ) } - } \ No newline at end of file From 9fc450fad5cd15b8251d3a22818c1e300b305554 Mon Sep 17 00:00:00 2001 From: juwon-code Date: Tue, 5 Nov 2024 01:33:00 +0900 Subject: [PATCH 09/10] =?UTF-8?q?Feat:=20=EB=AF=B8=EB=94=94=EC=96=B4=20?= =?UTF-8?q?=EB=B0=8F=20=ED=94=84=EB=A1=9C=ED=95=84=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 피드 도메인의 기능의 동작에 맞추어 미디어 및 프로필의 기능을 수정합니다. - 미디어 <-> 프로필 간의 연관관계 설정. - 불필요한 미디어 컨트롤러 삭제. - 로그인한 회원의 정보를 제공하는 컴포넌트 작성. Related to: prgrms-be-devcourse/NBB1_2_3_Team10#22 --- .../media/controller/MediaController.kt | 31 ---------------- .../bittakotlin/media/dto/MediaRequestDto.kt | 3 -- .../bittakotlin/media/dto/MediaResponseDto.kt | 12 ++++--- .../tenten/bittakotlin/media/entity/Media.kt | 9 +++-- .../media/repository/MediaRepository.kt | 3 -- .../bittakotlin/media/service/MediaService.kt | 8 +++-- .../media/service/MediaServiceImpl.kt | 36 +++++++++++++------ .../profile/repository/ProfileRepository.kt | 3 ++ .../profile/service/ProfileService.kt | 2 ++ .../profile/service/ProfileServiceImpl.kt | 9 ++++- .../security/service/PrincipalProvider.kt | 22 ++++++++++++ 11 files changed, 76 insertions(+), 62 deletions(-) delete mode 100644 src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaController.kt create mode 100644 src/main/kotlin/org/tenten/bittakotlin/security/service/PrincipalProvider.kt 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/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..540a8e2 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileService.kt @@ -11,4 +11,6 @@ interface ProfileService { 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..e586a32 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/profile/service/ProfileServiceImpl.kt @@ -12,12 +12,14 @@ 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 도 같이 생성 @@ -106,6 +108,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/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 From f30c706d43b918658eedb87a9c105a40cfaf38a0 Mon Sep 17 00:00:00 2001 From: juwon-code Date: Tue, 5 Nov 2024 02:40:09 +0900 Subject: [PATCH 10/10] =?UTF-8?q?Feat:=20=EC=B1=84=ED=8C=85=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EC=8B=B1=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 채팅 조회에 슬라이싱을 적용하고, 피드 레포지토리의 ORDER BY가 누락된 조회 쿼리문을 수정했습니다. Related to: prgrms-be-devcourse/NBB1_2_3_Team10#28 --- .../chat/controller/ChatController.kt | 14 ++++++++++---- .../chat/repository/ChatRepository.kt | 6 ++++-- .../bittakotlin/chat/service/ChatService.kt | 3 ++- .../chat/service/ChatServiceImpl.kt | 18 ++++++++++-------- .../feed/repository/FeedRepository.kt | 6 +++--- 5 files changed, 29 insertions(+), 18 deletions(-) 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/repository/FeedRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt index 999f652..ee1bd49 100644 --- a/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt +++ b/src/main/kotlin/org/tenten/bittakotlin/feed/repository/FeedRepository.kt @@ -11,13 +11,13 @@ import org.springframework.stereotype.Repository @Repository interface FeedRepository : JpaRepository { - @Query("SELECT f FROM Feed f WHERE f.profile.nickname LIKE %:nickname%") + @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%") + @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%") + @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