Skip to content

Commit

Permalink
[Feature] 스케줄러를 통해서 커피챗 예정 시간이 지난 경우 커피챗 상태를 전환한다 (#174)
Browse files Browse the repository at this point in the history
* feat: 커피챗 자동 취소에 대한 CoffeeChatStatus 필드 추가

* feat: 커피챗 ID 상태별 조회, 상태 일괄 업데이트 쿼리

* refactor: 커피챗 ID 상태별 조회에 진행 시작 시간 조건 추가

* feat: 대기, 예정된 커피챗들에 대해서 시간이 지난 경우 상태를 업데이트하는 프로세서 컴포넌트

* feat: 대기, 예정 커피챗 상태 전환 스케줄러

* fix .editorconfig

* test: Kotest 단위 테스트 관련 `@UnitTestKt` 적용
  • Loading branch information
sjiwon authored Feb 26, 2024
1 parent f7ffdd7 commit fc4ff80
Show file tree
Hide file tree
Showing 19 changed files with 472 additions and 161 deletions.
101 changes: 3 additions & 98 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,107 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
tab_width = 4
indent_size = 4
indent_style = space
max_line_length = 180
trim_trailing_whitespace = true

ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = true
ij_smart_tabs = false
ij_visual_guides = none
ij_wrap_on_typing = false

[*.{kt,kts}]
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
[{*.kt,*.kts}]
ktlint_code_style = INTELLIJ_IDEA
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_assignment_wrap = normal
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = true
ij_kotlin_call_parameters_right_paren_on_new_line = true
ij_kotlin_call_parameters_wrap = on_every_item
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = split_into_lines
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_for_chained_calls = false
ij_kotlin_continuation_indent_for_expression_bodies = false
ij_kotlin_continuation_indent_in_argument_lists = false
ij_kotlin_continuation_indent_in_elvis = false
ij_kotlin_continuation_indent_in_if_conditions = false
ij_kotlin_continuation_indent_in_parameter_lists = false
ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = normal
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = true
ij_kotlin_import_nested_classes = false
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 0
ij_kotlin_keep_blank_lines_in_code = 1
ij_kotlin_keep_blank_lines_in_declarations = 1
ij_kotlin_keep_first_column_comment = true
ij_kotlin_keep_indents_on_empty_lines = false
ij_kotlin_keep_line_breaks = true
ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_break_after_multiline_when_entry = true
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_add_space_on_reformat = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
ij_kotlin_space_after_type_colon = true
ij_kotlin_space_before_catch_parentheses = true
ij_kotlin_space_before_comma = false
ij_kotlin_space_before_extend_colon = true
ij_kotlin_space_before_for_parentheses = true
ij_kotlin_space_before_if_parentheses = true
ij_kotlin_space_before_lambda_arrow = true
ij_kotlin_space_before_type_colon = false
ij_kotlin_space_before_when_parentheses = true
ij_kotlin_space_before_while_parentheses = true
ij_kotlin_spaces_around_additive_operators = true
ij_kotlin_spaces_around_assignment_operators = true
ij_kotlin_spaces_around_equality_operators = true
ij_kotlin_spaces_around_function_type_arrow = true
ij_kotlin_spaces_around_logical_operators = true
ij_kotlin_spaces_around_multiplicative_operators = true
ij_kotlin_spaces_around_range = false
ij_kotlin_spaces_around_relational_operators = true
ij_kotlin_spaces_around_unary_operator = false
ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 1
ij_kotlin_wrap_first_method_in_call_chain = false
ktlint_standard_no-blank-line-in-list = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ij_kotlin_packages_to_use_import_on_demand = unset

[{*.yml,*.yaml}]
tab_width = 2
Expand Down
2 changes: 0 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
kotlin.code.style=ktlint_official

### Application Version ###
applicationVersion=0.0.1-SNAPSHOT

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.koddy.server.coffeechat.application.scheduler

import com.koddy.server.coffeechat.domain.service.UpdateCoffeeChatStatusProcessor
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.time.LocalDateTime

@Component
class UpdateCoffeeChatStatusScheduler(
private val updateCoffeeChatStatusProcessor: UpdateCoffeeChatStatusProcessor,
) {
@Scheduled(cron = "0 0/10 * * * *", zone = "Asia/Seoul")
fun updateCoffeeChatStatus() {
val now: LocalDateTime = LocalDateTime.now()
updateCoffeeChatStatusProcessor.updateWaitingToAutoCancel(now)
updateCoffeeChatStatusProcessor.updateScheduledToComplete(now)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum class CoffeeChatStatus(
MENTOR_APPROVE(Category.SCHEDULED, Detail.APPROVE),
MENTEE_APPLY_COFFEE_CHAT_COMPLETE(Category.PASSED, Detail.COMPLETE),
CANCEL_FROM_MENTEE_FLOW(Category.PASSED, Detail.CANCEL),
AUTO_CANCEL_FROM_MENTEE_FLOW(Category.PASSED, Detail.CANCEL),

// MentorFlow
MENTOR_SUGGEST(Category.SUGGESTED, Detail.NONE),
Expand All @@ -22,6 +23,7 @@ enum class CoffeeChatStatus(
MENTOR_FINALLY_APPROVE(Category.SCHEDULED, Detail.APPROVE),
MENTOR_SUGGEST_COFFEE_CHAT_COMPLETE(Category.PASSED, Detail.COMPLETE),
CANCEL_FROM_MENTOR_FLOW(Category.PASSED, Detail.CANCEL),
AUTO_CANCEL_FROM_MENTOR_FLOW(Category.PASSED, Detail.CANCEL),
;

private val cancelableStatus: List<CoffeeChatStatus>
Expand All @@ -42,6 +44,7 @@ enum class CoffeeChatStatus(
MENTOR_APPROVE,
MENTEE_APPLY_COFFEE_CHAT_COMPLETE,
CANCEL_FROM_MENTEE_FLOW,
AUTO_CANCEL_FROM_MENTEE_FLOW,
)

fun isMenteeFlow(): Boolean = menteeFlow.any { it == this }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
import com.koddy.server.coffeechat.domain.model.CoffeeChat;
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus;
import com.koddy.server.coffeechat.exception.CoffeeChatException;
import com.koddy.server.global.annotation.KoddyWritableTransactional;
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 java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

Expand All @@ -16,6 +21,32 @@ default CoffeeChat getById(final Long id) {
.orElseThrow(() -> new CoffeeChatException(COFFEE_CHAT_NOT_FOUND));
}

// @Query
@Query("""
SELECT c.id
FROM CoffeeChat c
WHERE c.status = :status
AND c.reservation.start IS NOT NULL
AND c.reservation.start <= :standard
""")
List<Long> findIdsByStatusAndReservationStandard(
@Param("status") final CoffeeChatStatus status,
@Param("standard") final LocalDateTime standard
);

@KoddyWritableTransactional
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("""
UPDATE CoffeeChat c
SET c.status = :status
WHERE c.id IN :ids
""")
void updateStatusInBatch(
@Param("ids") final List<Long> ids,
@Param("status") final CoffeeChatStatus status
);

// Query Method
Optional<CoffeeChat> findByIdAndMentorId(final Long id, final Long mentorId);

default CoffeeChat getByIdAndMentorId(final Long id, final Long mentorId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.koddy.server.coffeechat.domain.service

import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus.AUTO_CANCEL_FROM_MENTEE_FLOW
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus.AUTO_CANCEL_FROM_MENTOR_FLOW
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus.MENTEE_APPLY
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus.MENTEE_APPLY_COFFEE_CHAT_COMPLETE
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus.MENTEE_PENDING
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus.MENTOR_APPROVE
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus.MENTOR_FINALLY_APPROVE
import com.koddy.server.coffeechat.domain.model.CoffeeChatStatus.MENTOR_SUGGEST_COFFEE_CHAT_COMPLETE
import com.koddy.server.coffeechat.domain.repository.CoffeeChatRepository
import com.koddy.server.global.annotation.KoddyWritableTransactional
import com.koddy.server.global.log.logger
import org.slf4j.Logger
import org.springframework.stereotype.Component
import java.time.LocalDateTime

@Component
class UpdateCoffeeChatStatusProcessor(
private val coffeeChatRepository: CoffeeChatRepository,
) {
private val log: Logger = logger()

@KoddyWritableTransactional
fun updateWaitingToAutoCancel(standard: LocalDateTime) {
// 1. MenteeFlow 신청
val applyCoffeeChatIds: List<Long> = coffeeChatRepository.findIdsByStatusAndReservationStandard(MENTEE_APPLY, standard)
log.info("[updateWaitingToAutoCancel] MENTEE_APPLY Ids = {}", applyCoffeeChatIds)
updateStatus(applyCoffeeChatIds, AUTO_CANCEL_FROM_MENTEE_FLOW)

// 2. MentorFlow 1차 수락
val pendingCoffeeChatIds: List<Long> = coffeeChatRepository.findIdsByStatusAndReservationStandard(MENTEE_PENDING, standard)
log.info("[updateWaitingToAutoCancel] MENTEE_PENDING Ids = {}", pendingCoffeeChatIds)
updateStatus(pendingCoffeeChatIds, AUTO_CANCEL_FROM_MENTOR_FLOW)
}

@KoddyWritableTransactional
fun updateScheduledToComplete(standard: LocalDateTime) {
// 1. MenteeFlow 수락
val approveCoffeeChatIds: List<Long> = coffeeChatRepository.findIdsByStatusAndReservationStandard(MENTOR_APPROVE, standard)
log.info("[updateScheduledToComplete] MENTOR_APPROVE Ids = {}", approveCoffeeChatIds)
updateStatus(approveCoffeeChatIds, MENTEE_APPLY_COFFEE_CHAT_COMPLETE)

// 2. MentorFlow 수락
val finallyApproveCoffeeChatIds: List<Long> = coffeeChatRepository.findIdsByStatusAndReservationStandard(MENTOR_FINALLY_APPROVE, standard)
log.info("[updateScheduledToComplete] MENTOR_FINALLY_APPROVE Ids = {}", finallyApproveCoffeeChatIds)
updateStatus(finallyApproveCoffeeChatIds, MENTOR_SUGGEST_COFFEE_CHAT_COMPLETE)
}

private fun updateStatus(
coffeeChatIds: List<Long>,
updateStatus: CoffeeChatStatus,
) {
if (coffeeChatIds.isNotEmpty()) {
coffeeChatRepository.updateStatusInBatch(coffeeChatIds, updateStatus)
}
}
}
17 changes: 6 additions & 11 deletions src/test/java/com/koddy/server/auth/utils/TokenExtractorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,30 @@ import jakarta.servlet.http.HttpServletRequest
@UnitTestKt
@DisplayName("Auth -> TokenExtractor 테스트")
internal class TokenExtractorTest : DescribeSpec({
val request: HttpServletRequest = mockk<HttpServletRequest>()
val request = mockk<HttpServletRequest>()

describe("TokenExtractor's extractAccessToken") {
context("Authorization HeaderBearer 타입 & AccessToken 둘다 없으면") {
every { request.getHeader(AuthToken.ACCESS_TOKEN_HEADER) } returns null

it("null을 응답한다") {
val token: String? = TokenExtractor.extractAccessToken(request)
token shouldBe null
TokenExtractor.extractAccessToken(request) shouldBe null
}
}

context("Authorization HeaderBearer 타입만 있으면") {
every { request.getHeader(AuthToken.ACCESS_TOKEN_HEADER) } returns AuthToken.TOKEN_TYPE

it("null을 응답한다") {
val token: String? = TokenExtractor.extractAccessToken(request)
token shouldBe null
TokenExtractor.extractAccessToken(request) shouldBe null
}
}

context("Authorization HeaderBearer 타입 & AccessToken 둘다 있으면") {
every { request.getHeader(AuthToken.ACCESS_TOKEN_HEADER) } returns "${AuthToken.TOKEN_TYPE} $ACCESS_TOKEN"

it("정상적으로 AccessToken을 추출한다") {
val token: String? = TokenExtractor.extractAccessToken(request)
token shouldBe ACCESS_TOKEN
TokenExtractor.extractAccessToken(request) shouldBe ACCESS_TOKEN
}
}
}
Expand All @@ -51,17 +48,15 @@ internal class TokenExtractorTest : DescribeSpec({
every { request.cookies } returns emptyArray()

it("null을 응답한다") {
val token: String? = TokenExtractor.extractRefreshToken(request)
token shouldBe null
TokenExtractor.extractRefreshToken(request) shouldBe null
}
}

context("Cookie HeaderRefreshToken이 있으면") {
every { request.cookies } returns arrayOf(Cookie(AuthToken.REFRESH_TOKEN_HEADER, REFRESH_TOKEN))

it("정상적으로 RefreshToken을 추출한다") {
val token: String? = TokenExtractor.extractRefreshToken(request)
token shouldBe REFRESH_TOKEN
TokenExtractor.extractRefreshToken(request) shouldBe REFRESH_TOKEN
}
}
}
Expand Down
Loading

0 comments on commit fc4ff80

Please sign in to comment.