Repository navigation
[feat] #34 - 타이머 완료/종료 #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6d9593a
8737d59
628efc1
bb7a56a
385497a
8b448bb
782a6f7
4c24b96
4226273
a8b18a0
16573ce
040355c
6306ad2
5bf983c
e5a7594
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package com.Timo.Timo.domain.timer.docs; | ||
|
|
||
| import com.Timo.Timo.domain.timer.dto.response.TimerFinishResponse; | ||
| import com.Timo.Timo.global.auth.principal.CustomUserDetails; | ||
| import com.Timo.Timo.global.exception.dto.ErrorDto; | ||
| import com.Timo.Timo.global.response.BaseResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| 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.responses.ApiResponses; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
|
|
||
| public interface TimerCompleteControllerDocs { | ||
|
|
||
| @Operation( | ||
| summary = "타이머 시간 완료", | ||
| description = """ | ||
| 예상 소요 시간이 모두 경과하여 타이머를 자동 종료합니다. | ||
| 종료 시각 기록, 실제 수행 시간 계산 (status → COMPLETED) | ||
| 해당 날짜 TodoInstance 완료 처리 및 타이머 상태 초기화 | ||
| aiFeedback은 현재 null로 반환되며, 추후 AI 연동 예정 | ||
| """ | ||
| ) | ||
| @ApiResponses({ | ||
| @ApiResponse( | ||
| responseCode = "200", | ||
| description = "타이머 완료 성공", | ||
| useReturnTypeSchema = true | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "401", | ||
| description = "Access Token이 없거나 만료되었거나 유효하지 않은 경우", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "403", | ||
| description = "본인 소유의 타이머가 아닌 경우", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "404", | ||
| description = "존재하지 않는 타이머인 경우", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "409", | ||
| description = "이미 종료된 타이머인 경우", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "500", | ||
| description = "서버 내부 오류", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ) | ||
| }) | ||
| ResponseEntity<BaseResponse<TimerFinishResponse>> completeTimer( | ||
| @Parameter(description = "타이머 기록 ID", example = "10") | ||
| @PathVariable Long timerId, | ||
| @Parameter(hidden = true) CustomUserDetails userDetails | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package com.Timo.Timo.domain.timer.docs; | ||
|
|
||
| import com.Timo.Timo.domain.timer.dto.response.TimerFinishResponse; | ||
| import com.Timo.Timo.global.auth.principal.CustomUserDetails; | ||
| import com.Timo.Timo.global.exception.dto.ErrorDto; | ||
| import com.Timo.Timo.global.response.BaseResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| 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.responses.ApiResponses; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
|
|
||
| public interface TimerStopControllerDocs { | ||
|
|
||
| @Operation( | ||
| summary = "타이머 종료", | ||
| description = """ | ||
| 사용자의 요청으로 타이머를 종료합니다. | ||
| 종료 시각 기록, 실제 수행 시간 계산 (status → STOPPED) | ||
| 해당 날짜 TodoInstance 완료 처리 및 타이머 상태 초기화 | ||
| aiFeedback은 현재 null로 반환되며, 추후 AI 연동 예정 | ||
| """ | ||
| ) | ||
| @ApiResponses({ | ||
| @ApiResponse( | ||
| responseCode = "200", | ||
| description = "타이머 종료 성공", | ||
| useReturnTypeSchema = true | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "401", | ||
| description = "Access Token이 없거나 만료되었거나 유효하지 않은 경우", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "403", | ||
| description = "본인 소유의 타이머가 아닌 경우", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "404", | ||
| description = "존재하지 않는 타이머인 경우", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "409", | ||
| description = "이미 종료된 타이머인 경우", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ), | ||
| @ApiResponse( | ||
| responseCode = "500", | ||
| description = "서버 내부 오류", | ||
| content = @Content( | ||
| mediaType = "application/json", | ||
| schema = @Schema(implementation = ErrorDto.class) | ||
| ) | ||
| ) | ||
| }) | ||
| ResponseEntity<BaseResponse<TimerFinishResponse>> stopTimer( | ||
| @Parameter(description = "타이머 기록 ID", example = "10") | ||
| @PathVariable Long timerId, | ||
| @Parameter(hidden = true) CustomUserDetails userDetails | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.Timo.Timo.domain.timer.dto.response; | ||
|
|
||
| import com.Timo.Timo.domain.timer.entity.TimerRecord; | ||
|
|
||
| public record TimerFinishResponse( | ||
| Long timerId, | ||
| Long todoId, | ||
| String status, | ||
| Integer plannedSeconds, | ||
| Integer actualSeconds, | ||
| String aiFeedback | ||
| ) { | ||
| public static TimerFinishResponse of(TimerRecord timerRecord) { | ||
| return new TimerFinishResponse( | ||
| timerRecord.getId(), | ||
| timerRecord.getTodo().getId(), | ||
| timerRecord.getStatus().name(), | ||
| timerRecord.getPlannedSeconds(), | ||
| timerRecord.getActualSeconds(), | ||
| timerRecord.getAiFeedback() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package com.Timo.Timo.domain.timer.service; | ||
|
|
||
| import com.Timo.Timo.domain.timer.dto.response.TimerActiveResponse; | ||
| import com.Timo.Timo.domain.timer.dto.response.TimerFinishResponse; | ||
| import com.Timo.Timo.domain.timer.dto.response.TimerStartResponse; | ||
| import com.Timo.Timo.domain.timer.dto.response.TimerStatusResponse; | ||
| import com.Timo.Timo.domain.timer.entity.TimerRecord; | ||
|
|
@@ -143,4 +144,37 @@ private TodoInstance getOrCreateInstance(Todo todo, LocalDate date) { | |
| public boolean hasActiveTimer(Long todoId) { | ||
| return timerRecordRepository.existsByTodo_IdAndStatusIn(todoId, ACTIVE_STATUS); | ||
| } | ||
| } | ||
|
|
||
| @Transactional | ||
| public TimerFinishResponse completeTimer(Long userId, Long timerId) { | ||
| return finishTimer(userId, timerId, TimerStatus.COMPLETED); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 완료와 수동 종료의 공통 로직을 finishTimer()로 묶고 최종 상태만 전달하도록 구성 조은것 같아용 |
||
| } | ||
|
|
||
| @Transactional | ||
| public TimerFinishResponse stopTimer(Long userId, Long timerId) { | ||
| return finishTimer(userId, timerId, TimerStatus.STOPPED); | ||
| } | ||
|
|
||
| private TimerFinishResponse finishTimer(Long userId, Long timerId, TimerStatus targetStatus) { | ||
| TimerRecord timerRecord = timerRecordRepository.findByIdForUpdate(timerId) | ||
| .orElseThrow(() -> new CustomException(TimerErrorCode.TIMER_NOT_FOUND)); | ||
|
|
||
| if (!timerRecord.getUser().getId().equals(userId)) { | ||
| throw new CustomException(ErrorCode.FORBIDDEN); | ||
| } | ||
|
|
||
| LocalDateTime now = LocalDateTime.now(); | ||
| int actualSeconds = calculateElapsedSeconds(timerId, now); | ||
|
|
||
| timerSessionRepository.findByTimerRecordIdAndPausedAtIsNull(timerId) | ||
| .ifPresent(activeSession -> activeSession.pause(now)); | ||
|
|
||
| timerRecord.finish(targetStatus, now, actualSeconds, null); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3) RUNNING 상태의 타이머를 완료하거나 종료할 때 활성 TimerSession의 pausedAt은 갱신되지 않는 것 같아용
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 말씀주신대로 actualSeconds 계산 직후, 활성 세션이 있으면 종료 시각(now)으로 pausedAt을 채우도록 수정했습니다!! PAUSED 상태에서 종료하는 경우는 이미 활성 세션이 없는 상태라 별도 처리 없이 자연스럽게 넘어가게 하였습니다!! 감사합니다아:) |
||
|
|
||
| TodoInstance instance = getOrCreateInstance(timerRecord.getTodo(), timerRecord.getStartedAt().toLocalDate()); | ||
| instance.stopTimer(); | ||
| instance.markCompleted(); | ||
|
|
||
| return TimerFinishResponse.of(timerRecord); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
락 좋습니다아