diff --git a/src/main/kotlin/gogo/gogostage/GogoStageApplication.kt b/src/main/kotlin/gogo/gogostage/GogoStageApplication.kt index 2f81fbea..cd3774c6 100644 --- a/src/main/kotlin/gogo/gogostage/GogoStageApplication.kt +++ b/src/main/kotlin/gogo/gogostage/GogoStageApplication.kt @@ -2,6 +2,7 @@ package gogo.gogostage import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableAsync import org.springframework.scheduling.annotation.EnableScheduling @EnableScheduling diff --git a/src/main/kotlin/gogo/gogostage/domain/community/board/application/BoardProcessor.kt b/src/main/kotlin/gogo/gogostage/domain/community/board/application/BoardProcessor.kt index 2a76ee77..4fce89a7 100644 --- a/src/main/kotlin/gogo/gogostage/domain/community/board/application/BoardProcessor.kt +++ b/src/main/kotlin/gogo/gogostage/domain/community/board/application/BoardProcessor.kt @@ -28,6 +28,7 @@ class BoardProcessor( isFiltered = false, createdAt = LocalDateTime.now(), imageUrl = writeCommunityBoardDto.imageUrl, + viewCount = 0 ) return boardRepository.save(board) diff --git a/src/main/kotlin/gogo/gogostage/domain/community/board/persistence/Board.kt b/src/main/kotlin/gogo/gogostage/domain/community/board/persistence/Board.kt index b37cdd9a..510d1d41 100644 --- a/src/main/kotlin/gogo/gogostage/domain/community/board/persistence/Board.kt +++ b/src/main/kotlin/gogo/gogostage/domain/community/board/persistence/Board.kt @@ -31,6 +31,9 @@ class Board( @Column(name = "like_count", nullable = false) var likeCount: Int, + @Column(name = "view_count", nullable = false) + var viewCount: Int, + @Column(name = "is_filtered", nullable = false) var isFiltered: Boolean, @@ -57,4 +60,8 @@ class Board( isFiltered = true } + fun plusViewCount() { + viewCount += 1 + } + } diff --git a/src/main/kotlin/gogo/gogostage/domain/community/boardview/persistence/BoardView.kt b/src/main/kotlin/gogo/gogostage/domain/community/boardview/persistence/BoardView.kt new file mode 100644 index 00000000..dc6f429b --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/community/boardview/persistence/BoardView.kt @@ -0,0 +1,21 @@ +package gogo.gogostage.domain.community.boardview.persistence + +import gogo.gogostage.domain.community.board.persistence.Board +import jakarta.persistence.* + +@Entity +@Table(name = "tbl_board_view") +class BoardView( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + val id: Long = 0, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id", nullable = false) + val board: Board, + + @Column(name = "student_id", nullable = false) + val studentId: Long, +) \ No newline at end of file diff --git a/src/main/kotlin/gogo/gogostage/domain/community/boardview/persistence/BoardViewRepository.kt b/src/main/kotlin/gogo/gogostage/domain/community/boardview/persistence/BoardViewRepository.kt new file mode 100644 index 00000000..7f04173c --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/community/boardview/persistence/BoardViewRepository.kt @@ -0,0 +1,8 @@ +package gogo.gogostage.domain.community.boardview.persistence + +import org.springframework.data.jpa.repository.JpaRepository + +interface BoardViewRepository: JpaRepository { + + fun existsByBoardIdAndStudentId(boardId: Long, studentId: Long): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/gogo/gogostage/domain/community/root/application/CommunityProcessor.kt b/src/main/kotlin/gogo/gogostage/domain/community/root/application/CommunityProcessor.kt index 82f78420..591d0793 100644 --- a/src/main/kotlin/gogo/gogostage/domain/community/root/application/CommunityProcessor.kt +++ b/src/main/kotlin/gogo/gogostage/domain/community/root/application/CommunityProcessor.kt @@ -1,14 +1,18 @@ package gogo.gogostage.domain.community.root.application +import gogo.gogostage.domain.community.board.application.BoardReader import gogo.gogostage.domain.community.board.persistence.Board import gogo.gogostage.domain.community.board.persistence.BoardRepository import gogo.gogostage.domain.community.boardlike.persistence.BoardLike import gogo.gogostage.domain.community.boardlike.persistence.BoardLikeRepository +import gogo.gogostage.domain.community.boardview.persistence.BoardView +import gogo.gogostage.domain.community.boardview.persistence.BoardViewRepository import gogo.gogostage.domain.community.comment.persistence.Comment import gogo.gogostage.domain.community.comment.persistence.CommentRepository import gogo.gogostage.domain.community.commentlike.persistence.CommentLike import gogo.gogostage.domain.community.commentlike.persistence.CommentLikeRepository import gogo.gogostage.domain.community.root.application.dto.* +import gogo.gogostage.domain.community.root.event.BoardViewEvent import gogo.gogostage.global.error.StageException import gogo.gogostage.global.internal.student.stub.StudentByIdStub import org.springframework.data.repository.findByIdOrNull @@ -24,6 +28,8 @@ class CommunityProcessor( private val commentLikeRepository: CommentLikeRepository, private val commentMapper: CommunityMapper, private val boardRepository: BoardRepository, + private val boardViewRepository: BoardViewRepository, + private val boardReader: BoardReader ) { fun likeBoard(studentId: Long, board: Board): LikeResDto { @@ -130,4 +136,22 @@ class CommunityProcessor( commentRepository.save(comment) } + + @Transactional + fun saveBoardView(event: BoardViewEvent) { + val board = boardReader.read(event.boardId) + + if (!boardViewRepository.existsByBoardIdAndStudentId(board.id, board.studentId)) { + val newBoardView = BoardView( + board = board, + studentId = board.studentId, + ) + + boardViewRepository.save(newBoardView) + + board.plusViewCount() + + boardRepository.save(board) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/gogo/gogostage/domain/community/root/application/CommunityServiceImpl.kt b/src/main/kotlin/gogo/gogostage/domain/community/root/application/CommunityServiceImpl.kt index c2d80f07..0230c57c 100644 --- a/src/main/kotlin/gogo/gogostage/domain/community/root/application/CommunityServiceImpl.kt +++ b/src/main/kotlin/gogo/gogostage/domain/community/root/application/CommunityServiceImpl.kt @@ -4,6 +4,7 @@ import gogo.gogostage.domain.community.board.application.BoardProcessor import gogo.gogostage.domain.community.board.application.BoardReader import gogo.gogostage.domain.community.root.application.dto.* import gogo.gogostage.domain.community.root.event.BoardCreateEvent +import gogo.gogostage.domain.community.root.event.BoardViewEvent import gogo.gogostage.domain.community.root.event.CommentCreateEvent import gogo.gogostage.domain.community.root.persistence.SortType import gogo.gogostage.domain.game.persistence.GameCategory @@ -55,7 +56,16 @@ class CommunityServiceImpl( val board = boardReader.read(boardId) stageValidator.validStage(student, board.community.stage.id) stageValidator.validProfanityFilter(student, board) - return communityReader.readBoardInfo(isFiltered, board, student) + val response = communityReader.readBoardInfo(isFiltered, board, student) + + applicationEventPublisher.publishEvent( + BoardViewEvent( + boardId = boardId, + studentId = student.studentId, + ) + ) + + return response } @Transactional diff --git a/src/main/kotlin/gogo/gogostage/domain/community/root/application/dto/CommunityDto.kt b/src/main/kotlin/gogo/gogostage/domain/community/root/application/dto/CommunityDto.kt index ae584aac..f23c290d 100644 --- a/src/main/kotlin/gogo/gogostage/domain/community/root/application/dto/CommunityDto.kt +++ b/src/main/kotlin/gogo/gogostage/domain/community/root/application/dto/CommunityDto.kt @@ -35,6 +35,7 @@ data class BoardDto( val gameCategory: GameCategory, val title: String, val likeCount: Int, + val viewCount: Int, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") val createdAt: LocalDateTime, val stageType: StageType, @@ -50,6 +51,7 @@ data class GetCommunityBoardInfoResDto( @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") val createdAt: LocalDateTime, val imageUrl: String?, + val viewCount: Int, val stage: StageDto, val commentCount: Int, val comment: List diff --git a/src/main/kotlin/gogo/gogostage/domain/community/root/event/BoardViewEvent.kt b/src/main/kotlin/gogo/gogostage/domain/community/root/event/BoardViewEvent.kt new file mode 100644 index 00000000..866d23d1 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/community/root/event/BoardViewEvent.kt @@ -0,0 +1,6 @@ +package gogo.gogostage.domain.community.root.event + +data class BoardViewEvent( + val boardId: Long, + val studentId: Long +) diff --git a/src/main/kotlin/gogo/gogostage/domain/community/root/event/handler/BoardViewEventHandler.kt b/src/main/kotlin/gogo/gogostage/domain/community/root/event/handler/BoardViewEventHandler.kt new file mode 100644 index 00000000..560b2236 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/community/root/event/handler/BoardViewEventHandler.kt @@ -0,0 +1,21 @@ +package gogo.gogostage.domain.community.root.event.handler + +import gogo.gogostage.domain.community.root.application.CommunityProcessor +import gogo.gogostage.domain.community.root.event.BoardViewEvent +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class BoardViewEventHandler( + private val communityProcessor: CommunityProcessor, +) { + + @Async("asyncExecutor") + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun eventHandler(event: BoardViewEvent) { + communityProcessor.saveBoardView(event) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/gogo/gogostage/domain/community/root/persistence/CommunityCustomRepositoryImpl.kt b/src/main/kotlin/gogo/gogostage/domain/community/root/persistence/CommunityCustomRepositoryImpl.kt index 2394ca96..7c053ed5 100644 --- a/src/main/kotlin/gogo/gogostage/domain/community/root/persistence/CommunityCustomRepositoryImpl.kt +++ b/src/main/kotlin/gogo/gogostage/domain/community/root/persistence/CommunityCustomRepositoryImpl.kt @@ -58,6 +58,7 @@ class CommunityCustomRepositoryImpl( createdAt = board.createdAt, stageType = board.community.stage.type, commentCount = board.commentCount, + viewCount = board.viewCount, ) }.toList() @@ -124,7 +125,8 @@ class CommunityCustomRepositoryImpl( stage = stageDto, commentCount = board.commentCount, comment = commentDto, - imageUrl = board.imageUrl + imageUrl = board.imageUrl, + viewCount = board.viewCount, ) return response diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/result/persistence/CouponResult.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/result/persistence/CouponResult.kt new file mode 100644 index 00000000..31c82eb3 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/result/persistence/CouponResult.kt @@ -0,0 +1,49 @@ +package gogo.gogostage.domain.coupon.result.persistence + +import gogo.gogostage.domain.coupon.root.persistence.Coupon +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "tbl_coupon_result") +class CouponResult ( + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + val id: Long = 0, + + @JoinColumn(name = "coupon_id", nullable = false) + @OneToOne(cascade = [(CascadeType.ALL)], fetch = FetchType.LAZY) + val coupon: Coupon, + + @Column(name = "student_id", nullable = false) + val studentId: Long, + + @Column(name = "is_gain", nullable = false) + val isGain: Boolean, + + @Column(name = "before_point", nullable = false) + val beforePoint: Long, + + @Column(name = "after_point", nullable = false) + val afterPoint: Long, + + @Column(name = "create_at", nullable = false) + val createAt: LocalDateTime = LocalDateTime.now(), + +) { + + companion object { + + fun of(coupon: Coupon, studentId: Long, isGain: Boolean, beforePoint: Long, afterPoint: Long) = CouponResult( + coupon = coupon, + studentId = studentId, + isGain = isGain, + beforePoint = beforePoint, + afterPoint = afterPoint + ) + + } + +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/result/persistence/CouponResultRepository.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/result/persistence/CouponResultRepository.kt new file mode 100644 index 00000000..909a1437 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/result/persistence/CouponResultRepository.kt @@ -0,0 +1,6 @@ +package gogo.gogostage.domain.coupon.result.persistence + +import org.springframework.data.repository.CrudRepository + +interface CouponResultRepository: CrudRepository { +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponMapper.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponMapper.kt new file mode 100644 index 00000000..4171b9b7 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponMapper.kt @@ -0,0 +1,29 @@ +package gogo.gogostage.domain.coupon.root.application + +import gogo.gogostage.domain.coupon.result.persistence.CouponResult +import gogo.gogostage.domain.coupon.root.application.dto.QueryCouponDto +import gogo.gogostage.domain.coupon.root.application.dto.UseCouponDto +import gogo.gogostage.domain.coupon.root.persistence.Coupon +import gogo.gogostage.domain.coupon.root.persistence.CouponType +import org.springframework.stereotype.Component + +@Component +class CouponMapper { + + fun map(coupon: Coupon) = QueryCouponDto( + isUsed = coupon.isUsed, + stageId = coupon.stage.id, + stageName = coupon.stage.name, + couponType = coupon.type, + point = if (coupon.type == CouponType.NORMAL) coupon.earnPoint else null, + ) + + fun mapResult(coupon: Coupon, couponResult: CouponResult) = UseCouponDto( + isGain = couponResult.isGain, + earnedPoint = if (couponResult.isGain) coupon.earnPoint else null, + lostedPoint = if (couponResult.isGain.not()) coupon.lostPoint else null, + beforePoint = couponResult.beforePoint, + afterPoint = couponResult.afterPoint, + ) + +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponProcessor.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponProcessor.kt new file mode 100644 index 00000000..71216f11 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponProcessor.kt @@ -0,0 +1,63 @@ +package gogo.gogostage.domain.coupon.root.application + +import gogo.gogostage.domain.coupon.result.persistence.CouponResult +import gogo.gogostage.domain.coupon.result.persistence.CouponResultRepository +import gogo.gogostage.domain.coupon.root.persistence.Coupon +import gogo.gogostage.domain.coupon.root.persistence.CouponRepository +import gogo.gogostage.domain.coupon.root.persistence.CouponType +import gogo.gogostage.domain.stage.participant.root.persistence.StageParticipant +import gogo.gogostage.domain.stage.participant.root.persistence.StageParticipantRepository +import gogo.gogostage.global.error.StageException +import gogo.gogostage.global.internal.student.stub.StudentByIdStub +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import kotlin.random.Random + +@Component +class CouponProcessor( + private val couponRepository: CouponRepository, + private val couponResultRepository: CouponResultRepository, + private val stageParticipantRepository: StageParticipantRepository +) { + + fun use(student: StudentByIdStub, coupon: Coupon): CouponResult { + coupon.used() + couponRepository.save(coupon) + + val isGain = + if (coupon.type == CouponType.RANDOM) Random.nextBoolean() + else true + + val stageParticipant = getStageParticipant(coupon, student) + + val beforePoint = stageParticipant.point + updatePoints(stageParticipant, coupon, isGain) + stageParticipantRepository.save(stageParticipant) + val afterPoint = stageParticipant.point + + val couponResult = CouponResult.of( + coupon = coupon, + studentId = student.studentId, + isGain = isGain, + beforePoint = beforePoint, + afterPoint = afterPoint + ) + return couponResultRepository.save(couponResult) + } + + private fun getStageParticipant(coupon: Coupon, student: StudentByIdStub) = + stageParticipantRepository.queryStageParticipantByStageIdAndStudentId(coupon.stage.id, student.studentId) + ?: throw StageException( + "Stage Participant Not Found, Stage Id = ${coupon.stage.id}, Student Id = ${student.studentId}", + HttpStatus.NOT_FOUND.value() + ) + + private fun updatePoints(participant: StageParticipant, coupon: Coupon, isGain: Boolean) { + if (isGain) { + participant.plusPoint(coupon.earnPoint) + } else { + participant.minusPointMust(coupon.lostPoint!!) + } + } + +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponReader.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponReader.kt new file mode 100644 index 00000000..b958a901 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponReader.kt @@ -0,0 +1,23 @@ +package gogo.gogostage.domain.coupon.root.application + +import gogo.gogostage.domain.coupon.root.persistence.Coupon +import gogo.gogostage.domain.coupon.root.persistence.CouponRepository +import gogo.gogostage.global.error.StageException +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component + +@Component +class CouponReader( + private val couponRepository: CouponRepository +) { + + fun read(couponId: String): Coupon = + couponRepository.findByIdOrNull(couponId) + ?: throw StageException("쿠폰을 찾을 수 없습니다. coupon id = $couponId", HttpStatus.NOT_FOUND.value()) + + fun readForUpdate(couponId: String): Coupon = + couponRepository.findByIdOrNullForUpdate(couponId) + ?: throw StageException("쿠폰을 찾을 수 없습니다. coupon id = $couponId", HttpStatus.NOT_FOUND.value()) + +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponService.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponService.kt new file mode 100644 index 00000000..a5d43223 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponService.kt @@ -0,0 +1,9 @@ +package gogo.gogostage.domain.coupon.root.application + +import gogo.gogostage.domain.coupon.root.application.dto.QueryCouponDto +import gogo.gogostage.domain.coupon.root.application.dto.UseCouponDto + +interface CouponService { + fun query(couponId: String): QueryCouponDto + fun use(couponId: String): UseCouponDto +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponServiceImpl.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponServiceImpl.kt new file mode 100644 index 00000000..9ecb225a --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponServiceImpl.kt @@ -0,0 +1,38 @@ +package gogo.gogostage.domain.coupon.root.application + +import gogo.gogostage.domain.coupon.root.application.dto.QueryCouponDto +import gogo.gogostage.domain.coupon.root.application.dto.UseCouponDto +import gogo.gogostage.domain.stage.root.application.StageValidator +import gogo.gogostage.global.util.UserContextUtil +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CouponServiceImpl( + private val userUtil: UserContextUtil, + private val stageValidator: StageValidator, + private val couponReader: CouponReader, + private val couponValidator: CouponValidator, + private val couponMapper: CouponMapper, + private val couponProcessor: CouponProcessor, +) : CouponService { + + @Transactional(readOnly = true) + override fun query(couponId: String): QueryCouponDto { + val student = userUtil.getCurrentStudent() + val coupon = couponReader.read(couponId) + stageValidator.validStage(student, coupon.stage.id) + return couponMapper.map(coupon) + } + + @Transactional + override fun use(couponId: String): UseCouponDto { + val student = userUtil.getCurrentStudent() + val coupon = couponReader.readForUpdate(couponId) + couponValidator.valid(coupon) + stageValidator.validStage(student, coupon.stage.id) + val couponResult = couponProcessor.use(student, coupon) + return couponMapper.mapResult(coupon, couponResult) + } + +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponValidator.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponValidator.kt new file mode 100644 index 00000000..d296c3ea --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/CouponValidator.kt @@ -0,0 +1,18 @@ +package gogo.gogostage.domain.coupon.root.application + +import gogo.gogostage.domain.coupon.root.persistence.Coupon +import gogo.gogostage.global.error.StageException +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component + +@Component +class CouponValidator { + + fun valid(coupon: Coupon) { + val isUsed = coupon.isUsed + if (isUsed) { + throw StageException("이미 사용된 쿠폰입니다.", HttpStatus.BAD_REQUEST.value()) + } + } + +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/dto/CouponDto.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/dto/CouponDto.kt new file mode 100644 index 00000000..5b063770 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/application/dto/CouponDto.kt @@ -0,0 +1,19 @@ +package gogo.gogostage.domain.coupon.root.application.dto + +import gogo.gogostage.domain.coupon.root.persistence.CouponType + +data class QueryCouponDto( + val isUsed: Boolean, + val stageId: Long, + val stageName: String, + val couponType: CouponType, + val point: Long? +) + +data class UseCouponDto( + val isGain: Boolean, + val earnedPoint: Long?, + val lostedPoint: Long?, + val beforePoint: Long, + val afterPoint: Long, +) diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/persistence/Coupon.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/persistence/Coupon.kt new file mode 100644 index 00000000..c5752577 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/persistence/Coupon.kt @@ -0,0 +1,61 @@ +package gogo.gogostage.domain.coupon.root.persistence + +import gogo.gogostage.domain.stage.root.persistence.Stage +import jakarta.persistence.* + +@Entity +@Table(name = "tbl_coupon") +class Coupon ( + + @Id + @Column(name = "id", nullable = false) + val id: String, + + @JoinColumn(name = "stage_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + val stage: Stage, + + @Column(name = "type", nullable = false) + @Enumerated(EnumType.STRING) + val type: CouponType, + + @Column(name = "earn_point", nullable = false) + val earnPoint: Long, + + @Column(name = "lost_point", nullable = true) + val lostPoint: Long? = null, + + @Column(name = "is_used", nullable = false) + var isUsed: Boolean = false, + +) { + + companion object { + + fun ofNormal(id: String, stage: Stage, earnPoint: Long) = Coupon( + id = id, + type = CouponType.NORMAL, + stage = stage, + earnPoint = earnPoint, + ) + + + fun ofRandom(id: String, stage: Stage, earnPoint: Long, lostPoint: Long) = Coupon( + id = id, + type = CouponType.RANDOM, + stage = stage, + earnPoint = earnPoint, + lostPoint = lostPoint, + ) + + } + + fun used() { + this.isUsed = true + } + +} + +enum class CouponType { + NORMAL, RANDOM +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/persistence/CouponRepository.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/persistence/CouponRepository.kt new file mode 100644 index 00000000..4b0c4529 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/persistence/CouponRepository.kt @@ -0,0 +1,12 @@ +package gogo.gogostage.domain.coupon.root.persistence + +import jakarta.persistence.LockModeType +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.CrudRepository + +interface CouponRepository: CrudRepository { + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT c FROM Coupon c WHERE c.id = :couponId") + fun findByIdOrNullForUpdate(couponId: String): Coupon? +} diff --git a/src/main/kotlin/gogo/gogostage/domain/coupon/root/presentation/CouponController.kt b/src/main/kotlin/gogo/gogostage/domain/coupon/root/presentation/CouponController.kt new file mode 100644 index 00000000..a5497f0a --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/domain/coupon/root/presentation/CouponController.kt @@ -0,0 +1,35 @@ +package gogo.gogostage.domain.coupon.root.presentation + +import gogo.gogostage.domain.coupon.root.application.CouponService +import gogo.gogostage.domain.coupon.root.application.dto.QueryCouponDto +import gogo.gogostage.domain.coupon.root.application.dto.UseCouponDto +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/stage") +class CouponController( + private val couponService: CouponService, +) { + + @GetMapping("/coupon") + fun query( + @RequestParam couponId: String, + ): ResponseEntity { + val response = couponService.query(couponId) + return ResponseEntity.ok(response) + } + + @PostMapping("/coupon") + fun use( + @RequestParam couponId: String, + ): ResponseEntity { + val response = couponService.use(couponId) + return ResponseEntity.ok(response) + } + +} diff --git a/src/main/kotlin/gogo/gogostage/domain/stage/participant/root/persistence/StageParticipant.kt b/src/main/kotlin/gogo/gogostage/domain/stage/participant/root/persistence/StageParticipant.kt index 0f3fc8be..1e52c7ee 100644 --- a/src/main/kotlin/gogo/gogostage/domain/stage/participant/root/persistence/StageParticipant.kt +++ b/src/main/kotlin/gogo/gogostage/domain/stage/participant/root/persistence/StageParticipant.kt @@ -46,6 +46,15 @@ class StageParticipant( this.point -= point } + fun minusPointMust(point: Long) { + if (this.point - point < 0) { + this.point = 0 + return + } + + this.point -= point + } + fun plusPoint(point: Long) { this.point += point } diff --git a/src/main/kotlin/gogo/gogostage/global/config/AsyncConfig.kt b/src/main/kotlin/gogo/gogostage/global/config/AsyncConfig.kt new file mode 100644 index 00000000..185914b0 --- /dev/null +++ b/src/main/kotlin/gogo/gogostage/global/config/AsyncConfig.kt @@ -0,0 +1,32 @@ +package gogo.gogostage.global.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.EnableAsync +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor +import java.util.concurrent.Executor +import java.util.concurrent.ThreadPoolExecutor + + +@Configuration +@EnableAsync +class AsyncConfig { + + @Bean(name = ["asyncExecutor"]) + fun asyncExecutor(): Executor { + val executor = ThreadPoolTaskExecutor() + executor.corePoolSize = 5 + executor.maxPoolSize = 20 + executor.queueCapacity = 50 + executor.keepAliveSeconds = 60 + executor.setAllowCoreThreadTimeOut(true) + executor.setPrestartAllCoreThreads(true) + executor.setWaitForTasksToCompleteOnShutdown(true) + executor.setAwaitTerminationSeconds(20) + executor.setRejectedExecutionHandler(ThreadPoolExecutor.CallerRunsPolicy()) + executor.initialize() + + return executor + } + +} \ No newline at end of file diff --git a/src/main/kotlin/gogo/gogostage/global/config/SecurityConfig.kt b/src/main/kotlin/gogo/gogostage/global/config/SecurityConfig.kt index 3554d0d4..751295d3 100644 --- a/src/main/kotlin/gogo/gogostage/global/config/SecurityConfig.kt +++ b/src/main/kotlin/gogo/gogostage/global/config/SecurityConfig.kt @@ -94,6 +94,10 @@ class SecurityConfig( // image httpRequests.requestMatchers(HttpMethod.POST, "/stage/image").hasAnyRole(Authority.USER.name, Authority.STAFF.name) + // coupon + httpRequests.requestMatchers(HttpMethod.GET, "/stage/coupon").hasAnyRole(Authority.USER.name, Authority.STAFF.name) + httpRequests.requestMatchers(HttpMethod.POST, "/stage/coupon").hasAnyRole(Authority.USER.name, Authority.STAFF.name) + // server to server httpRequests.requestMatchers(HttpMethod.GET, "/stage/api/point/{stage_id}").access { _, context -> hasIpAddress(context) } httpRequests.requestMatchers(HttpMethod.GET, "/stage/api/match/info").access { _, context -> hasIpAddress(context) }