-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/완료된 일정 모아보기 #32
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
Merged
The head ref may contain hidden characters: "feat/\uC644\uB8CC\uB41C-\uC77C\uC815-\uBAA8\uC544\uBCF4\uAE30"
Merged
Feat/완료된 일정 모아보기 #32
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
d2ae895
docs: OpenApiConfig에서 API 설명 업데이트
GoGradually 6f6e952
docs: 지난 Task 모아보기 API 체크리스트 추가
GoGradually ba071dc
feat: 미완료 상태에서 일정 동기화 로직 추가
GoGradually 38020cb
docs: 완료된 일정 아카이브 기능 구현 및 API 계약 업데이트
GoGradually 509501d
feat: 커서 기반 완료 아카이브 조회 기능 추가
GoGradually 861c34f
test: 완료된 일정 아카이브 조회 기능에 대한 테스트 추가
GoGradually a05dfc6
feat: Task 엔티티에 완료된 일정 조회를 위한 인덱스 추가
GoGradually 6b95eab
feat: 완료된 일정 아카이브 조회를 위한 서비스 클래스 추가
GoGradually 4381262
test: 완료된 일정 아카이브 조회 기능에 대한 테스트 클래스 추가
GoGradually 640a880
feat: 완료된 일정 아카이브 조회를 위한 커서 페이지 응답 클래스 추가
GoGradually 0bfc6c6
feat: 완료된 작업 아카이브 조회를 위한 API 엔드포인트 추가
GoGradually 5ee4315
feat: 시스템 시계를 위한 ClockConfig 클래스 추가
GoGradually 6ede73b
test: 완료된 일정 아카이브 조회를 위한 통합 테스트 클래스 추가
GoGradually eb2caac
feat: 완료된 작업 조회를 위한 activeDate 필터 추가
GoGradually 1811c23
feat: 완료된 작업 소유자별 조회를 위한 필터 추가
GoGradually d280e9b
feat: 완료된 작업 조회를 위한 오늘 날짜 기준 필터 추가
GoGradually 3946f7d
feat: 완료된 작업 조회를 위한 현재 날짜 기준 필터 추가
GoGradually 192924e
docs: 완료되지 않은 작업 목록 및 현재 완료된 사항 추가
GoGradually 20f1876
feat: 작업 목록 조회 API 설명 업데이트
GoGradually 977f07b
docs: 처리해야 하는 완료된 작업 아카이빙 처리 방식 업데이트
GoGradually 649cac8
feat: 소유자 ID와 마감일 기준으로 작업 조회 기능 추가
GoGradually 84d26d8
refactor: 작업 인덱스 순서 변경
GoGradually ba452d9
test: 소유자 ID와 마감일 기준으로 작업 조회 테스트 추가
GoGradually 950f6c8
refactor: 작업 인덱스 이름 변경 및 불필요한 열 제거
GoGradually c7305be
feat: 소유자 ID와 마감일 기준으로 작업 조회 기능 추가
GoGradually 41f3482
test: 마감일 기준으로 작업 조회 테스트 추가
GoGradually 1ff9a50
feat: 마감 날짜 기준 작업 조회 기능 추가
GoGradually bd508e4
test: 마감일 기준 작업 조회 기능 테스트 추가
GoGradually 6d8201c
docs: 마감 날짜 기준 작업 조회 API 추가
GoGradually 32b7c72
feat: 작업 목록의 완료 상태 표시 업데이트
GoGradually 5ffda63
fix: 작업 완료 동기화 호출 제거
GoGradually File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # 지난 Task 모아보기(완료 + 마감일 1일 경과) API 체크리스트 | ||
|
|
||
| > 목적: 오늘 00:00 기준 이전(어제까지) 마감일이 지난 완료 Task를 커서 기반으로 모아볼 수 있는 클라이언트용 엔드포인트를 추가한다. | ||
|
|
||
| ## 요구 정의 | ||
|
|
||
| - [x] "1일" 기준을 확정한다: **사용자/요청 오프셋의 LocalDate 기준 now-1일 00:00** 을 컷오프로 사용한다. | ||
| - [x] 포함 범위를 정의한다: 일정 연결 여부와 무관하게 해당 회원의 모든 완료 Task(마감 필수) 포함. | ||
| - [x] 정렬 방향과 기본/최대 페이지 크기를 확정한다: `deadline_date DESC, id DESC`, default 20, max 100, 커서 `yyyy-MM-dd[+HH:mm]|taskId`. | ||
| - [x] 응답 필드 확정: `TaskResponseV2` 그대로 사용(마감 날짜+오프셋 노출, 완료 소스는 미노출로 합의). | ||
| - [x] 기존 Task 목록 API와의 역할 분리를 명문화한다: `/v2/tasks/completed` 는 마감 1일 경과 + 완료만, 기존 목록은 최신/진행중 포함. | ||
|
|
||
| ## 스키마 & 도메인 | ||
|
|
||
| - [x] 도메인에서 마감일(`TemporalConstraint.deadline`)을 손쉽게 `LocalDate`로 얻을 수 있는 헬퍼 확인(기존 `getDeadlineDate()` 재사용). | ||
| - [x] 1일 컷오프 계산 시 오프셋을 명시하고 서비스 책임으로 분리(`TaskArchiveService`, 테스트용 Clock 주입). | ||
| - [x] DB 인덱스 필요 여부를 검토한다: `(owner_id, completed, deadline_date, task_id)` 복합 인덱스 추가용 DDL 초안 작성( | ||
| `task-archive-index.sql`). | ||
|
|
||
| ## 레포지토리 & 쿼리 | ||
|
|
||
| - [x] `TaskRepository`에 완료 Task 전용 커서 쿼리를 추가한다: `completed=true`, `deadline_date <= cutoffDate`, 정렬 | ||
| `deadline_date DESC, id DESC`. | ||
| - [x] 커서 인코딩/디코딩 포맷을 정의한다(`yyyy-MM-dd|{id}` 기본, 오프셋 허용) 및 파싱 실패 예외 메시지를 규격화한다. | ||
| - [x] 커서 단위 테스트/통합 테스트로 동일 마감일 + ID 타이브레이킹이 안정적으로 동작하는지 검증한다. | ||
|
|
||
| ## 서비스 & 애플리케이션 레이어 | ||
|
|
||
| - [x] 1일 컷오프 계산 방식을 구현한다(요청/회원 오프셋 기준 LocalDate.now-1) 및 Clock 주입으로 테스트 가능. | ||
| - [x] `TaskArchiveService`에 아카이브용 커서 페이지 모델을 추가하고 DESC 정렬에 맞는 `hasNext`/`nextCursor`(size+1) 계산 완료. | ||
| - [x] 완료→재오픈 시 아카이브 대상에서 제외되도록 completed 플래그 의존(재오픈 시 false 처리 유지). | ||
|
|
||
| ## API 계약 & 컨트롤러 | ||
|
|
||
| - [x] V2에 `GET /v2/tasks/completed` 커서 엔드포인트 추가, `size(1~100)`, `cursor`, `offset` 검증 처리. | ||
| - [x] 응답 DTO는 `TaskArchiveCursorPageResponseV2` + `TaskResponseV2` 조합으로 확정. | ||
| - [x] Swagger/OAS 어노테이션에 커서 예제, 1일 컷오프 설명, 400 응답 반영. | ||
| - [x] Member 인증(`@MemberId`) 적용, 별도 레이트리밋/캐싱은 도입하지 않기로 결정. | ||
|
|
||
| ## 테스트 | ||
|
|
||
| - [x] 도메인/서비스 단위: 오프셋 기반 컷오프·커서 파싱·size 검증 테스트 추가. | ||
| - [x] 리포지토리 통합: 커서 정렬/필터/경계 테스트 추가. | ||
| - [x] 서비스 단위: 커서 직렬화/역직렬화, 잘못된 커서 예외, size 상한, cutoff 계산 검증. | ||
| - [x] 컨트롤러: 요청 검증(0 size), 커서 페이지 동작, 오프셋 파라미터 적용 통합 테스트. | ||
| - [x] 이벤트/로그: 별도 트래킹 미도입으로 N/A 확인. | ||
|
|
||
| ## 배포 & 운영 | ||
|
|
||
| - [x] 스키마 변경 없음(인덱스만 추가)으로 확정, 적용 DDL은 `task-archive-index.sql`에 기록. | ||
| - [x] 운영/스테이징은 수동 인덱스 적용 전제(`ddl-auto` 유지 시 위험 없도록 별도 적용 계획 필요). | ||
| - [x] 모니터링 포인트 제안: archive API latency/에러율, 응답 건수, deadline null 비율 알림. | ||
| - [x] 프런트/모바일 공지 사항 정리: 커서 포맷·1일 기준 오프셋·max size 100·응답 필드 기존과 동일(새 엔드포인트) 공유 필요. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| - 한번에 보유한 모든 작업을 조회하는 API는 비효율적이다. | ||
|
|
||
| - 의존관계 추가를 위해선, 데드라인 날짜 기준으로 작업 조회를 해야 한다. | ||
|
|
||
| - 따라서, 마감 날짜 기준 작업 조회 API가 필요하다. | ||
|
|
||
| - 이 API는, 특정 날짜에 마감일이 설정된 작업들을 조회하는 기능을 제공해야 한다. | ||
|
|
||
| ## 구현 메모 | ||
|
|
||
| - GET `/v2/tasks/by-deadline?date=YYYY-MM-DD` (v1 동일 경로)로 제공하며, 회원 ID 헤더를 받아 해당 날짜에 마감이 있는 모든 작업(완료/미완료 포함)을 반환한다. | ||
| - 응답 스키마는 기존 작업 조회 응답(TaskResponseV2/TaskResponse)과 동일하며, 의존관계 정보도 포함된다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| ### 현재 완료된 사항 | ||
|
|
||
| - [x] 지난 일정 모아보기 API 구현 완료 | ||
|
|
||
| ### 남은 작업 | ||
|
|
||
| - [x] 작업 탭에 기본으로 조회되어야 할 "작업 목록"이 "완료되지 않은 작업"/"마감일이 지나지 않은 완료된 작업"을 마감일 기준 오름차순으로 조회 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| - [ ] 아카이빙 해야 할 작업을 Cassandra DB와 같은 곳으로 이동 (배치 처리) | ||
| - 현재 DB에 완료된 작업이 계속 쌓이는 것을 방지 | ||
| - 현재 조회하는 아카이브 조회 방식을 그대로 Batch 처리를 위한 조회 메소드로 사용하기 | ||
| - 조회된 작업들을 Cassandra DB로 이동 | ||
| - 이동된 작업들은 기존 DB에서 삭제 | ||
| - 실제 사용자가 보게 될 아카이브 조회 API는 RDB가 아닌 Cassandra DB에서 조회하도록 변경 | ||
| - 해당 작업들은 변경 불가 |
108 changes: 108 additions & 0 deletions
108
src/main/java/me/gg/pinit/pinittask/application/task/service/TaskArchiveService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| package me.gg.pinit.pinittask.application.task.service; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import me.gg.pinit.pinittask.application.member.service.MemberService; | ||
| import me.gg.pinit.pinittask.domain.task.model.Task; | ||
| import me.gg.pinit.pinittask.domain.task.repository.TaskRepository; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.Clock; | ||
| import java.time.LocalDate; | ||
| import java.time.ZoneOffset; | ||
| import java.util.List; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class TaskArchiveService { | ||
|
|
||
| private static final int DEFAULT_PAGE_SIZE = 20; | ||
| private static final int MAX_PAGE_SIZE = 100; | ||
| private static final String CURSOR_DELIMITER = "|"; | ||
| private static final Pattern DATE_WITH_OPTIONAL_OFFSET_PATTERN = Pattern.compile("(?<date>\\d{4}-\\d{2}-\\d{2})(?<offset>[+-]\\d{2}:?\\d{2})?"); | ||
|
|
||
| private final TaskRepository taskRepository; | ||
| private final MemberService memberService; | ||
| private final Clock clock; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public CursorPage getCompletedArchive(Long ownerId, Integer sizeParam, String cursor, ZoneOffset requestOffset) { | ||
| int size = resolveSize(sizeParam); | ||
| ZoneOffset effectiveOffset = resolveOffset(ownerId, requestOffset); | ||
| LocalDate cutoffDate = LocalDate.now(clock.withZone(effectiveOffset)).minusDays(1); | ||
|
|
||
| Cursor decoded = decodeCursor(cursor, cutoffDate); | ||
| Pageable pageable = PageRequest.of(0, size + 1); | ||
| List<Task> tasks = taskRepository.findCompletedArchiveByCursor( | ||
| ownerId, | ||
| cutoffDate, | ||
| decoded.deadline(), | ||
| decoded.id(), | ||
| pageable | ||
| ); | ||
|
|
||
| boolean hasNext = tasks.size() > size; | ||
| List<Task> content = hasNext ? tasks.subList(0, size) : tasks; | ||
| String nextCursor = hasNext ? encodeCursor(content.getLast()) : null; | ||
| return new CursorPage(content, nextCursor, hasNext, cutoffDate, effectiveOffset); | ||
| } | ||
|
|
||
| private int resolveSize(Integer sizeParam) { | ||
| int size = sizeParam == null ? DEFAULT_PAGE_SIZE : sizeParam; | ||
| if (size <= 0 || size > MAX_PAGE_SIZE) { | ||
| throw new IllegalArgumentException("size는 1 이상 100 이하이어야 합니다."); | ||
| } | ||
| return size; | ||
| } | ||
|
|
||
| private ZoneOffset resolveOffset(Long ownerId, ZoneOffset requestOffset) { | ||
| if (requestOffset != null) { | ||
| return requestOffset; | ||
| } | ||
| return memberService.findZoneOffsetOfMember(ownerId); | ||
| } | ||
|
|
||
| private String encodeCursor(Task task) { | ||
| return task.getTemporalConstraint().getDeadlineDate() + CURSOR_DELIMITER + task.getId(); | ||
| } | ||
|
|
||
| private Cursor decodeCursor(String cursor, LocalDate cutoffDate) { | ||
| if (cursor == null || cursor.isBlank()) { | ||
| return new Cursor(cutoffDate.plusDays(1), Long.MAX_VALUE); | ||
| } | ||
| String[] parts = cursor.split("\\|"); | ||
| if (parts.length != 2) { | ||
| throw new IllegalArgumentException("커서는 'yyyy-MM-dd|id' 형식이어야 합니다."); | ||
| } | ||
| LocalDate deadline = parseDate(parts[0]); | ||
| long id = parseId(parts[1]); | ||
| return new Cursor(deadline, id); | ||
| } | ||
|
|
||
| private LocalDate parseDate(String raw) { | ||
| Matcher matcher = DATE_WITH_OPTIONAL_OFFSET_PATTERN.matcher(raw); | ||
| if (!matcher.matches()) { | ||
| throw new IllegalArgumentException("커서는 'yyyy-MM-dd|id' 형식이어야 합니다."); | ||
| } | ||
| return LocalDate.parse(matcher.group("date")); | ||
| } | ||
|
|
||
| private long parseId(String raw) { | ||
| try { | ||
| return Long.parseLong(raw); | ||
| } catch (NumberFormatException e) { | ||
| throw new IllegalArgumentException("커서의 ID가 올바르지 않습니다.", e); | ||
| } | ||
| } | ||
|
|
||
| public record CursorPage(List<Task> tasks, String nextCursor, boolean hasNext, LocalDate cutoffDate, | ||
| ZoneOffset offset) { | ||
| } | ||
|
|
||
| private record Cursor(LocalDate deadline, long id) { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
src/main/java/me/gg/pinit/pinittask/infrastructure/config/ClockConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package me.gg.pinit.pinittask.infrastructure.config; | ||
|
|
||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| import java.time.Clock; | ||
|
|
||
| @Configuration | ||
| public class ClockConfig { | ||
|
|
||
| @Bean | ||
| public Clock systemClock() { | ||
| return Clock.systemUTC(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ | |
| info = @Info( | ||
| title = "Pinit Task API", | ||
| version = "v2", | ||
| description = "일정 관리와 의존 관계 기능을 제공하는 API", | ||
| description = "일정/작업 관리와 의존 관계 기능을 제공하는 API", | ||
| contact = @Contact(name = "Pinit Team", email = "[email protected]") | ||
| ), | ||
| servers = { | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
(1) 문제점: markIncomplete()에서 syncScheduleOnTaskCompletion()을 호출하고 있어, 작업을 ‘미완료’로 되돌리는 동작에서 연결된 일정이 finish() 처리(완료 처리)될 수 있습니다.
(2) 영향: 작업은 미완료인데 일정은 완료로 남는 데이터 불일치가 발생하고, 일정이 진행중/중단 상태인 경우에는 ‘작업을 완료할 수 없습니다’ 예외가 발생해 되돌리기(reopen)가 차단될 수 있습니다.
(3) 수정 제안: markIncomplete()에서는 syncScheduleOnTaskCompletion() 호출을 제거하거나, 일정 상태를 미완료로 되돌리는 별도 로직(예: syncScheduleOnTaskReopen/markIncomplete 전용)을 구현해 해당 로직을 호출하도록 수정해 주세요.