From b96c58abee3e1c58f647e84cd889ae8c39487fbc Mon Sep 17 00:00:00 2001 From: JaeUk Date: Thu, 1 Jan 2026 13:02:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EB=AA=A8=EC=85=98=20?= =?UTF-8?q?=EC=9E=91=EA=B0=80=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20hidden=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20`isAuthorHidden`=20optional=20field=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../damaba/application/promotion/Command.kt | 7 +++++- .../application/promotion/PromotionService.kt | 3 ++- .../promotion/PromotionController.kt | 2 +- .../damaba/controller/promotion/Request.kt | 9 +++++-- .../damaba/controller/promotion/Response.kt | 9 +++++++ .../damaba/domain/promotion/Promotion.kt | 6 +++++ .../domain/promotion/PromotionDetail.kt | 1 + .../domain/promotion/PromotionListItem.kt | 1 + ...onAuthorHiddenPermissionDeniedException.kt | 10 ++++++++ .../promotion/PromotionJpaEntity.kt | 7 ++++++ .../damaba/damaba/mapper/PromotionMapper.kt | 5 ++++ src/main/resources/schema.sql | 25 ++++++++++--------- .../promotion/PromotionCommandTest.kt | 7 +++++- .../promotion/PromotionServiceTest.kt | 3 ++- ...thorHiddenPermissionDeniedExceptionTest.kt | 17 +++++++++++++ .../damaba/util/fixture/PromotionFixture.kt | 6 +++++ 16 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 src/main/kotlin/com/damaba/damaba/domain/promotion/exception/PromotionAuthorHiddenPermissionDeniedException.kt create mode 100644 src/test/kotlin/com/damaba/damaba/domain/promotion/exception/PromotionAuthorHiddenPermissionDeniedExceptionTest.kt diff --git a/src/main/kotlin/com/damaba/damaba/application/promotion/Command.kt b/src/main/kotlin/com/damaba/damaba/application/promotion/Command.kt index 06d16de2..277aa9df 100644 --- a/src/main/kotlin/com/damaba/damaba/application/promotion/Command.kt +++ b/src/main/kotlin/com/damaba/damaba/application/promotion/Command.kt @@ -6,24 +6,29 @@ import com.damaba.damaba.domain.file.File import com.damaba.damaba.domain.file.Image import com.damaba.damaba.domain.promotion.PromotionValidator import com.damaba.damaba.domain.promotion.constant.PromotionType +import com.damaba.damaba.domain.promotion.exception.PromotionAuthorHiddenPermissionDeniedException import com.damaba.damaba.domain.region.Region import com.damaba.damaba.domain.user.User import java.time.LocalDate data class PostPromotionCommand( - val authorId: Long, + val requestUser: User, val promotionType: PromotionType, val title: String, val content: String, val externalLink: String?, val startedAt: LocalDate?, val endedAt: LocalDate?, + val isAuthorHidden: Boolean, val photographyTypes: Set, val images: List, val activeRegions: Set, val hashtags: Set, ) { init { + if (isAuthorHidden && !requestUser.isAdmin) { + throw PromotionAuthorHiddenPermissionDeniedException() + } PromotionValidator.validateTitle(title) PromotionValidator.validateContent(content) if (images.isEmpty() || images.size > 10) { diff --git a/src/main/kotlin/com/damaba/damaba/application/promotion/PromotionService.kt b/src/main/kotlin/com/damaba/damaba/application/promotion/PromotionService.kt index fb82059a..015f10f5 100644 --- a/src/main/kotlin/com/damaba/damaba/application/promotion/PromotionService.kt +++ b/src/main/kotlin/com/damaba/damaba/application/promotion/PromotionService.kt @@ -70,13 +70,14 @@ class PromotionService( @Transactional fun postPromotion(command: PostPromotionCommand): Promotion = promotionRepo.create( Promotion.create( - authorId = command.authorId, + authorId = command.requestUser.id, promotionType = command.promotionType, title = command.title, content = command.content, externalLink = command.externalLink, startedAt = command.startedAt, endedAt = command.endedAt, + isAuthorHidden = command.isAuthorHidden, photographyTypes = command.photographyTypes, images = command.images.map { file -> Image(file.name, file.url) }, activeRegions = command.activeRegions.map { region -> Region(region.category, region.name) }.toSet(), diff --git a/src/main/kotlin/com/damaba/damaba/controller/promotion/PromotionController.kt b/src/main/kotlin/com/damaba/damaba/controller/promotion/PromotionController.kt index 00148764..6cded176 100644 --- a/src/main/kotlin/com/damaba/damaba/controller/promotion/PromotionController.kt +++ b/src/main/kotlin/com/damaba/damaba/controller/promotion/PromotionController.kt @@ -169,7 +169,7 @@ class PromotionController(private val promotionService: PromotionService) { @AuthenticationPrincipal requestUser: User, @RequestBody request: PostPromotionRequest, ): ResponseEntity { - val promotion = promotionService.postPromotion(request.toCommand(requestUser.id)) + val promotion = promotionService.postPromotion(request.toCommand(requestUser)) return ResponseEntity .created(URI.create("/api/v*/promotions/${promotion.id}")) .body(PromotionMapper.INSTANCE.toPromotionResponse(promotion)) diff --git a/src/main/kotlin/com/damaba/damaba/controller/promotion/Request.kt b/src/main/kotlin/com/damaba/damaba/controller/promotion/Request.kt index 505444c3..75e40238 100644 --- a/src/main/kotlin/com/damaba/damaba/controller/promotion/Request.kt +++ b/src/main/kotlin/com/damaba/damaba/controller/promotion/Request.kt @@ -6,6 +6,7 @@ import com.damaba.damaba.controller.common.ImageRequest import com.damaba.damaba.controller.region.RegionRequest import com.damaba.damaba.domain.common.constant.PhotographyType import com.damaba.damaba.domain.promotion.constant.PromotionType +import com.damaba.damaba.domain.user.User import com.damaba.damaba.mapper.ImageMapper import com.damaba.damaba.mapper.RegionMapper import io.swagger.v3.oas.annotations.media.Schema @@ -30,6 +31,9 @@ data class PostPromotionRequest( @Schema(description = "이벤트 종료일") val endedAt: LocalDate?, + @Schema(description = "작성자 정보 숨김 여부. 관리자만 true로 설정할 수 있습니다.", example = "false") + val isAuthorHidden: Boolean = false, + @Schema(description = "촬영 종류") val photographyTypes: Set, @@ -42,14 +46,15 @@ data class PostPromotionRequest( @Schema(description = "해시태그 리스트", example = "[\"수원핫플\", \"스냅사진\"]") val hashtags: Set, ) { - fun toCommand(requestUserId: Long) = PostPromotionCommand( - authorId = requestUserId, + fun toCommand(requestUser: User) = PostPromotionCommand( + requestUser = requestUser, promotionType = promotionType, title = title, content = content, externalLink = externalLink, startedAt = startedAt, endedAt = endedAt, + isAuthorHidden = isAuthorHidden, photographyTypes = photographyTypes, images = images.map { ImageMapper.INSTANCE.toImage(it) }, activeRegions = activeRegions.map { regionRequest -> RegionMapper.INSTANCE.toRegion(regionRequest) }.toSet(), diff --git a/src/main/kotlin/com/damaba/damaba/controller/promotion/Response.kt b/src/main/kotlin/com/damaba/damaba/controller/promotion/Response.kt index 7361cfbb..8eda7afb 100644 --- a/src/main/kotlin/com/damaba/damaba/controller/promotion/Response.kt +++ b/src/main/kotlin/com/damaba/damaba/controller/promotion/Response.kt @@ -37,6 +37,9 @@ data class PromotionResponse( @Schema(description = "조회수", example = "15") val viewCount: Long, + @Schema(description = "작성자 정보 숨김 여부", example = "false") + val isAuthorHidden: Boolean, + @Schema(description = "촬영 종류 리스트") val photographyTypes: Set, @@ -84,6 +87,9 @@ data class PromotionDetailResponse( @Schema(description = "게시글 저장 여부. 이미 저장한 게시글이라면 true") val isSaved: Boolean, + @Schema(description = "작성자 정보 숨김 여부", example = "false") + val isAuthorHidden: Boolean, + @Schema(description = "촬영 종류 리스트") val photographyTypes: Set, @@ -119,6 +125,9 @@ data class PromotionListItemResponse( @Schema(description = "게시글 저장 여부. 이미 저장한 게시글이라면 true") val isSaved: Boolean, + @Schema(description = "작성자 정보 숨김 여부", example = "false") + val isAuthorHidden: Boolean, + @Schema(description = "촬영 종류") val photographyTypes: Set, diff --git a/src/main/kotlin/com/damaba/damaba/domain/promotion/Promotion.kt b/src/main/kotlin/com/damaba/damaba/domain/promotion/Promotion.kt index 6a62837d..54129138 100644 --- a/src/main/kotlin/com/damaba/damaba/domain/promotion/Promotion.kt +++ b/src/main/kotlin/com/damaba/damaba/domain/promotion/Promotion.kt @@ -16,6 +16,7 @@ class Promotion( startedAt: LocalDate?, endedAt: LocalDate?, viewCount: Long, + isAuthorHidden: Boolean, photographyTypes: Set, images: List, activeRegions: Set, @@ -24,6 +25,9 @@ class Promotion( var authorId: Long? = authorId private set + var isAuthorHidden: Boolean = isAuthorHidden + private set + var promotionType: PromotionType = promotionType private set @@ -102,6 +106,7 @@ class Promotion( externalLink: String?, startedAt: LocalDate?, endedAt: LocalDate?, + isAuthorHidden: Boolean, photographyTypes: Set, images: List, activeRegions: Set, @@ -116,6 +121,7 @@ class Promotion( startedAt = startedAt, endedAt = endedAt, viewCount = 0, + isAuthorHidden = isAuthorHidden, photographyTypes = photographyTypes, images = images, activeRegions = activeRegions, diff --git a/src/main/kotlin/com/damaba/damaba/domain/promotion/PromotionDetail.kt b/src/main/kotlin/com/damaba/damaba/domain/promotion/PromotionDetail.kt index d5b2f8e2..72f06e41 100644 --- a/src/main/kotlin/com/damaba/damaba/domain/promotion/PromotionDetail.kt +++ b/src/main/kotlin/com/damaba/damaba/domain/promotion/PromotionDetail.kt @@ -19,6 +19,7 @@ data class PromotionDetail( val viewCount: Long, val saveCount: Long, val isSaved: Boolean, + val isAuthorHidden: Boolean, val photographyTypes: Set, val images: List, val activeRegions: Set, diff --git a/src/main/kotlin/com/damaba/damaba/domain/promotion/PromotionListItem.kt b/src/main/kotlin/com/damaba/damaba/domain/promotion/PromotionListItem.kt index eed1c29b..4957348a 100644 --- a/src/main/kotlin/com/damaba/damaba/domain/promotion/PromotionListItem.kt +++ b/src/main/kotlin/com/damaba/damaba/domain/promotion/PromotionListItem.kt @@ -17,6 +17,7 @@ data class PromotionListItem( val endedAt: LocalDate?, val saveCount: Long, val isSaved: Boolean, + val isAuthorHidden: Boolean, val photographyTypes: Set, val images: List, val activeRegions: Set, diff --git a/src/main/kotlin/com/damaba/damaba/domain/promotion/exception/PromotionAuthorHiddenPermissionDeniedException.kt b/src/main/kotlin/com/damaba/damaba/domain/promotion/exception/PromotionAuthorHiddenPermissionDeniedException.kt new file mode 100644 index 00000000..5ae3d3b1 --- /dev/null +++ b/src/main/kotlin/com/damaba/damaba/domain/promotion/exception/PromotionAuthorHiddenPermissionDeniedException.kt @@ -0,0 +1,10 @@ +package com.damaba.damaba.domain.promotion.exception + +import com.damaba.damaba.domain.exception.CustomException + +class PromotionAuthorHiddenPermissionDeniedException : + CustomException( + httpStatusCode = 403, + code = "PROMOTION_AUTHOR_HIDDEN_PERMISSION_DENIED", + message = "작성자 정보 숨김은 관리자만 설정할 수 있습니다.", + ) diff --git a/src/main/kotlin/com/damaba/damaba/infrastructure/promotion/PromotionJpaEntity.kt b/src/main/kotlin/com/damaba/damaba/infrastructure/promotion/PromotionJpaEntity.kt index 15bb1147..6b71b552 100644 --- a/src/main/kotlin/com/damaba/damaba/infrastructure/promotion/PromotionJpaEntity.kt +++ b/src/main/kotlin/com/damaba/damaba/infrastructure/promotion/PromotionJpaEntity.kt @@ -31,6 +31,7 @@ class PromotionJpaEntity( startedAt: LocalDate?, endedAt: LocalDate?, viewCount: Long, + isAuthorHidden: Boolean, ) : TimeTrackedJpaEntity() { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -70,6 +71,10 @@ class PromotionJpaEntity( var viewCount: Long = viewCount private set + @Column(name = "is_author_hidden", nullable = false) + var isAuthorHidden: Boolean = isAuthorHidden + private set + @Column(name = "deleted_at") var deletedAt: LocalDateTime? = null private set @@ -105,6 +110,7 @@ class PromotionJpaEntity( startedAt = this.startedAt, endedAt = this.endedAt, viewCount = this.viewCount, + isAuthorHidden = this.isAuthorHidden, photographyTypes = this.photographyTypes.map { it.type }.toCollection(LinkedHashSet()), images = this.images.map { it.toImage() }, activeRegions = this.activeRegions.map { it.toRegion() }.toCollection(LinkedHashSet()), @@ -183,6 +189,7 @@ class PromotionJpaEntity( startedAt = promotion.startedAt, endedAt = promotion.endedAt, viewCount = promotion.viewCount, + isAuthorHidden = promotion.isAuthorHidden, ) promotionJpaEntity.photographyTypes.addAll( promotion.photographyTypes.map { diff --git a/src/main/kotlin/com/damaba/damaba/mapper/PromotionMapper.kt b/src/main/kotlin/com/damaba/damaba/mapper/PromotionMapper.kt index 157b7d2c..53f085ed 100644 --- a/src/main/kotlin/com/damaba/damaba/mapper/PromotionMapper.kt +++ b/src/main/kotlin/com/damaba/damaba/mapper/PromotionMapper.kt @@ -13,15 +13,19 @@ import org.mapstruct.factory.Mappers @Mapper(uses = [UserMapper::class, ImageMapper::class, RegionMapper::class]) interface PromotionMapper { + @Mapping(source = "authorHidden", target = "isAuthorHidden") fun toPromotionResponse(promotion: Promotion): PromotionResponse @Mapping(source = "saved", target = "isSaved") + @Mapping(source = "authorHidden", target = "isAuthorHidden") fun toPromotionDetailResponse(promotionDetail: PromotionDetail): PromotionDetailResponse @Mapping(source = "saved", target = "isSaved") + @Mapping(source = "authorHidden", target = "isAuthorHidden") fun toPromotionListItemResponse(promotionListItem: PromotionListItem): PromotionListItemResponse @Mapping(source = "promotion.id", target = "id") + @Mapping(source = "promotion.authorHidden", target = "isAuthorHidden") fun toPromotionDetail( promotion: Promotion, author: User?, @@ -30,6 +34,7 @@ interface PromotionMapper { ): PromotionDetail @Mapping(source = "promotion.id", target = "id") + @Mapping(source = "promotion.authorHidden", target = "isAuthorHidden") fun toPromotionListItem( promotion: Promotion, author: User?, diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index bd2c9883..39b6f191 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -112,18 +112,19 @@ CREATE INDEX idx__photographer_save__photographer_id ON photographer_save (photo CREATE TABLE promotion ( - id BIGINT NOT NULL AUTO_INCREMENT, - author_id BIGINT COMMENT '(FK) id of user(author)', - promotion_type VARCHAR(255) NOT NULL, - title VARCHAR(20) NOT NULL, - content VARCHAR(500) NOT NULL, - external_link VARCHAR(255), - started_at DATE, - ended_at DATE, - view_count BIGINT NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NOT NULL, - deleted_at TIMESTAMP, + id BIGINT NOT NULL AUTO_INCREMENT, + author_id BIGINT COMMENT '(FK) id of user(author)', + is_author_hidden BOOLEAN NOT NULL DEFAULT FALSE, + promotion_type VARCHAR(255) NOT NULL, + title VARCHAR(20) NOT NULL, + content VARCHAR(500) NOT NULL, + external_link VARCHAR(255), + started_at DATE, + ended_at DATE, + view_count BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + deleted_at TIMESTAMP, PRIMARY KEY (id) ); diff --git a/src/test/kotlin/com/damaba/damaba/application/promotion/PromotionCommandTest.kt b/src/test/kotlin/com/damaba/damaba/application/promotion/PromotionCommandTest.kt index 3d5264ea..a23839c2 100644 --- a/src/test/kotlin/com/damaba/damaba/application/promotion/PromotionCommandTest.kt +++ b/src/test/kotlin/com/damaba/damaba/application/promotion/PromotionCommandTest.kt @@ -6,9 +6,11 @@ import com.damaba.damaba.domain.file.File import com.damaba.damaba.domain.file.Image import com.damaba.damaba.domain.promotion.constant.PromotionType import com.damaba.damaba.domain.region.Region +import com.damaba.damaba.domain.user.User import com.damaba.damaba.util.RandomTestUtils.Companion.randomLong import com.damaba.damaba.util.RandomTestUtils.Companion.randomString import com.damaba.damaba.util.fixture.FileFixture.createImage +import com.damaba.damaba.util.fixture.UserFixture.createUser import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable import org.junit.jupiter.params.ParameterizedTest @@ -59,21 +61,24 @@ class PostPromotionUseCaseCommandTest { ) private fun createCommand( + requestUser: User = createUser(), title: String = "Valid title", content: String = "Valid content", promotionType: PromotionType = PromotionType.FREE, + isAuthorHidden: Boolean = false, photographyTypes: Set = setOf(PhotographyType.SNAP), images: List = List(3) { createImage() }, activeRegions: Set = setOf(Region("서울", "강남구")), hashtags: Set = setOf("tag1", "tag2"), ) = PostPromotionCommand( - authorId = randomLong(), + requestUser = requestUser, promotionType = promotionType, title = title, content = content, externalLink = "https://example.com", startedAt = LocalDate.now(), endedAt = LocalDate.now().plusDays(1), + isAuthorHidden = isAuthorHidden, photographyTypes = photographyTypes, images = images, activeRegions = activeRegions, diff --git a/src/test/kotlin/com/damaba/damaba/application/promotion/PromotionServiceTest.kt b/src/test/kotlin/com/damaba/damaba/application/promotion/PromotionServiceTest.kt index e7441a20..184aa2f1 100644 --- a/src/test/kotlin/com/damaba/damaba/application/promotion/PromotionServiceTest.kt +++ b/src/test/kotlin/com/damaba/damaba/application/promotion/PromotionServiceTest.kt @@ -507,13 +507,14 @@ class PromotionServiceTest { } private fun createPostPromotionCommand() = PostPromotionCommand( - authorId = randomLong(), + requestUser = createUser(), promotionType = PromotionType.FREE, title = randomString(len = 10), content = randomString(), externalLink = randomString(), startedAt = randomLocalDate(), endedAt = randomLocalDate(), + isAuthorHidden = false, photographyTypes = setOf(PhotographyType.SNAP), images = generateRandomList(maxSize = 10) { createImage() }, activeRegions = generateRandomSet(maxSize = 5) { createRegion() }, diff --git a/src/test/kotlin/com/damaba/damaba/domain/promotion/exception/PromotionAuthorHiddenPermissionDeniedExceptionTest.kt b/src/test/kotlin/com/damaba/damaba/domain/promotion/exception/PromotionAuthorHiddenPermissionDeniedExceptionTest.kt new file mode 100644 index 00000000..61aad885 --- /dev/null +++ b/src/test/kotlin/com/damaba/damaba/domain/promotion/exception/PromotionAuthorHiddenPermissionDeniedExceptionTest.kt @@ -0,0 +1,17 @@ +package com.damaba.damaba.domain.promotion.exception + +import org.assertj.core.api.Assertions.assertThat +import kotlin.test.Test + +class PromotionAuthorHiddenPermissionDeniedExceptionTest { + @Test + fun `PromotionAuthorHiddenPermissionDeniedException 생성 시 올바른 속성값을 가진다`() { + // given & when + val exception = PromotionAuthorHiddenPermissionDeniedException() + + // then + assertThat(exception.httpStatusCode).isEqualTo(403) + assertThat(exception.code).isEqualTo("PROMOTION_AUTHOR_HIDDEN_PERMISSION_DENIED") + assertThat(exception.message).isEqualTo("작성자 정보 숨김은 관리자만 설정할 수 있습니다.") + } +} diff --git a/src/test/kotlin/com/damaba/damaba/util/fixture/PromotionFixture.kt b/src/test/kotlin/com/damaba/damaba/util/fixture/PromotionFixture.kt index 6610bda4..c16cfb2e 100644 --- a/src/test/kotlin/com/damaba/damaba/util/fixture/PromotionFixture.kt +++ b/src/test/kotlin/com/damaba/damaba/util/fixture/PromotionFixture.kt @@ -31,6 +31,7 @@ object PromotionFixture { startedAt: LocalDate? = randomLocalDate(), endedAt: LocalDate? = randomLocalDate(), viewCount: Long = randomLong(), + isAuthorHidden: Boolean = false, photographyTypes: Set = setOf(PhotographyType.SNAP), images: List = generateRandomList(maxSize = 10) { createImage() }, activeRegions: Set = generateRandomSet(maxSize = 5) { createRegion() }, @@ -45,6 +46,7 @@ object PromotionFixture { startedAt = startedAt, endedAt = endedAt, viewCount = viewCount, + isAuthorHidden = isAuthorHidden, photographyTypes = photographyTypes, images = images, activeRegions = activeRegions, @@ -63,6 +65,7 @@ object PromotionFixture { viewCount: Long = randomLong(), saveCount: Long = randomLong(), isSaved: Boolean = randomBoolean(), + isAuthorHidden: Boolean = false, photographyTypes: Set = setOf(PhotographyType.SNAP), images: List = generateRandomList(maxSize = 10) { createImage() }, activeRegions: Set = generateRandomSet(maxSize = 5) { createRegion() }, @@ -79,6 +82,7 @@ object PromotionFixture { viewCount = viewCount, saveCount = saveCount, isSaved = isSaved, + isAuthorHidden = isAuthorHidden, photographyTypes = photographyTypes, images = images, activeRegions = activeRegions, @@ -93,6 +97,7 @@ object PromotionFixture { endedAt: LocalDate? = randomLocalDate(), saveCount: Long = randomLong(), isSaved: Boolean = randomBoolean(), + isAuthorHidden: Boolean = false, photographyTypes: Set = setOf(PhotographyType.SNAP), images: List = generateRandomList(maxSize = 10) { createImage() }, activeRegions: Set = generateRandomSet(maxSize = 5) { createRegion() }, @@ -105,6 +110,7 @@ object PromotionFixture { endedAt = endedAt, saveCount = saveCount, isSaved = isSaved, + isAuthorHidden = isAuthorHidden, photographyTypes = photographyTypes, images = images, activeRegions = activeRegions,