[FEAT] 캘린더 개인 Todo — 생성·완료토글 (#234) - #236
Conversation
2026-08-06 배분된 캘린더 작업의 첫 조각. Figma 확인 결과 read-only
집계가 아니라 신규 CRUD 엔티티(개인 Todo)가 필요함이 드러났다 —
회의·AI 파생이 아닌 순수 개인용 할 일.
- V6.1.1 신규 테이블(title·date만, 홍근 확인 모달 필드 그대로)
- POST /api/todos, PATCH /api/todos/{id}/complete(토글)
- 조작 범위는 생성·조회·완료토글만, 수정·삭제는 스코프 밖
- 테스트 12건, 전체 스위트 그린
⚠️ V6.1.1 버전 번호는 운영 flyway_schema_history 확인 전까지
잠정치 — PO/DBA 확인 필요(머지 전).
이슈 #234
📝 WalkthroughWalkthrough개인 Todo 도메인과 저장소를 추가했습니다. Todo 생성 및 완료 상태 토글 유스케이스를 구현했습니다. 소유권 검증, 데이터베이스 마이그레이션, HTTP API와 계층별 테스트를 추가했습니다. Changes개인 Todo 기능
Estimated code review effort: 3 (Moderate) | ~25 minutes Suggested labels: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/com/module06/backend/calendar/application/service/PersonalTodoService.java`:
- Around line 40-50: Update PersonalTodoService.toggleComplete to prevent lost
updates during concurrent completion toggles by applying one supported strategy:
use a pessimistic-locking repository query, add optimistic versioning with retry
handling, or perform an atomic toggle update query. Ensure concurrent toggles
are serialized or both state changes are preserved, and add an integration test
covering simultaneous toggles of the same todo.
In
`@src/test/java/com/module06/backend/calendar/application/service/PersonalTodoServiceTest.java`:
- Around line 104-114: Update toggleThrowsWhenTodoBelongsToAnotherCompany to
also verify that personalTodoRepository.save is never called when ownership
validation fails, while preserving the existing BusinessException and
TODO_NOT_FOUND assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 927e34fd-a615-4be2-8128-bb1a6580d913
📒 Files selected for processing (17)
src/main/java/com/module06/backend/calendar/application/command/CreateTodoCommand.javasrc/main/java/com/module06/backend/calendar/application/service/PersonalTodoService.javasrc/main/java/com/module06/backend/calendar/application/usecase/CreateTodoUseCase.javasrc/main/java/com/module06/backend/calendar/application/usecase/ToggleTodoCompleteUseCase.javasrc/main/java/com/module06/backend/calendar/domain/model/PersonalTodo.javasrc/main/java/com/module06/backend/calendar/domain/repository/PersonalTodoRepository.javasrc/main/java/com/module06/backend/calendar/exception/CalendarErrorCode.javasrc/main/java/com/module06/backend/calendar/infrastructure/persistence/PersonalTodoJpaEntity.javasrc/main/java/com/module06/backend/calendar/infrastructure/persistence/PersonalTodoPersistenceAdapter.javasrc/main/java/com/module06/backend/calendar/infrastructure/persistence/SpringDataPersonalTodoRepository.javasrc/main/java/com/module06/backend/calendar/presentation/api/TodoController.javasrc/main/java/com/module06/backend/calendar/presentation/api/request/CreateTodoRequest.javasrc/main/java/com/module06/backend/calendar/presentation/api/response/TodoResponse.javasrc/main/resources/db/migration/V6.1.1__create_personal_todo.sqlsrc/test/java/com/module06/backend/calendar/application/service/PersonalTodoServiceTest.javasrc/test/java/com/module06/backend/calendar/infrastructure/persistence/PersonalTodoPersistenceAdapterTest.javasrc/test/java/com/module06/backend/calendar/presentation/api/TodoControllerTest.java
| public PersonalTodo toggleComplete(Long companyId, Long memberId, Long todoId) { | ||
| PersonalTodo todo = personalTodoRepository.findById(todoId) | ||
| .orElseThrow(() -> new BusinessException(CalendarErrorCode.TODO_NOT_FOUND)); | ||
|
|
||
| // 다른 회사·다른 사람의 Todo는 "없다"와 동일하게 취급한다 — 존재 자체를 노출하지 않는다. | ||
| if (!companyId.equals(todo.getCompanyId()) || !memberId.equals(todo.getMemberId())) { | ||
| throw new BusinessException(CalendarErrorCode.TODO_NOT_FOUND); | ||
| } | ||
|
|
||
| todo.toggleDone(); | ||
| return personalTodoRepository.save(todo); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/main/java/com/module06/backend/calendar/infrastructure/persistence/SpringDataPersonalTodoRepository.java --items all
rg -n -C 3 '`@Lock`|PESSIMISTIC|`@Version`|version|findById' \
src/main/java/com/module06/backend/calendar/infrastructure/persistence \
src/main/java/com/module06/backend/calendarRepository: Z-Groupware/BACKEND
Length of output: 4544
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- PersonalTodoJpaEntity ---'
fd -i 'PersonalTodoJpaEntity' .
printf '%s\n' '--- Persistence adapter ---'
cat -n src/main/java/com/module06/backend/calendar/infrastructure/persistence/PersonalTodoPersistenceAdapter.java
printf '%s\n' '--- Todo entity and migrations ---'
rg -n -C 4 'class PersonalTodoJpaEntity|`@Entity`|isDone|done|CREATE TABLE|personal_todo|todo' \
src/main/java src/main/resources
printf '%s\n' '--- Toggle tests and repository tests ---'
rg -n -C 5 'toggleComplete|toggleDone|TODO_NOT_FOUND|PersonalTodo' \
src/testRepository: Z-Groupware/BACKEND
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- PersonalTodoJpaEntity ---'
cat -n src/main/java/com/module06/backend/calendar/infrastructure/persistence/PersonalTodoJpaEntity.java
printf '%s\n' '--- Personal todo migration ---'
cat -n src/main/resources/db/migration/V6.1.1__create_personal_todo.sql
printf '%s\n' '--- Relevant test files ---'
fd -t f . src/test | rg 'calendar|todo|PersonalTodo'
while IFS= read -r file; do
printf '\n--- %s ---\n' "$file"
rg -n -C 4 'toggleComplete|toggleDone|PersonalTodo|TODO_NOT_FOUND' "$file" || true
done < <(fd -t f . src/test | rg 'calendar|todo|PersonalTodo')Repository: Z-Groupware/BACKEND
Length of output: 4885
완료 토글에 동시성 제어를 추가하십시오.
findById에는 비관 잠금이 없고 PersonalTodoJpaEntity와 personal_todo 테이블에도 @Version 또는 버전 컬럼이 없습니다. 두 요청이 같은 isDone 값을 읽으면 한 토글이 유실됩니다. 잠금이 적용된 조회, 낙관적 잠금과 재시도, 또는 원자적 토글 쿼리 중 하나를 적용하고 동시 토글 통합 테스트를 추가하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/module06/backend/calendar/application/service/PersonalTodoService.java`
around lines 40 - 50, Update PersonalTodoService.toggleComplete to prevent lost
updates during concurrent completion toggles by applying one supported strategy:
use a pessimistic-locking repository query, add optimistic versioning with retry
handling, or perform an atomic toggle update query. Ensure concurrent toggles
are serialized or both state changes are preserved, and add an integration test
covering simultaneous toggles of the same todo.
| @Test | ||
| void toggleThrowsWhenTodoBelongsToAnotherCompany() { | ||
| PersonalTodoService service = service(); | ||
| PersonalTodo otherCompanyTodo = PersonalTodo.reconstitute( | ||
| TODO_ID, 999L, MEMBER, "다른 회사 Todo", LocalDate.of(2026, 8, 20), false, null, null); | ||
| when(personalTodoRepository.findById(TODO_ID)).thenReturn(Optional.of(otherCompanyTodo)); | ||
|
|
||
| assertThatThrownBy(() -> service.toggleComplete(COMPANY, MEMBER, TODO_ID)) | ||
| .isInstanceOf(BusinessException.class) | ||
| .hasFieldOrPropertyWithValue("errorCode", CalendarErrorCode.TODO_NOT_FOUND); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
다른 회사 Todo의 저장 금지 검증을 추가하십시오.
현재 테스트는 예외만 확인합니다. 소유권 검사 전에 save가 호출되고 이후 예외가 발생해도 테스트가 통과할 수 있습니다. 다른 회사의 Todo 상태 변경을 막도록 save 미호출도 검증하십시오.
수정 예시
assertThatThrownBy(() -> service.toggleComplete(COMPANY, MEMBER, TODO_ID))
.isInstanceOf(BusinessException.class)
.hasFieldOrPropertyWithValue("errorCode", CalendarErrorCode.TODO_NOT_FOUND);
+
+ verify(personalTodoRepository, never()).save(any());📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Test | |
| void toggleThrowsWhenTodoBelongsToAnotherCompany() { | |
| PersonalTodoService service = service(); | |
| PersonalTodo otherCompanyTodo = PersonalTodo.reconstitute( | |
| TODO_ID, 999L, MEMBER, "다른 회사 Todo", LocalDate.of(2026, 8, 20), false, null, null); | |
| when(personalTodoRepository.findById(TODO_ID)).thenReturn(Optional.of(otherCompanyTodo)); | |
| assertThatThrownBy(() -> service.toggleComplete(COMPANY, MEMBER, TODO_ID)) | |
| .isInstanceOf(BusinessException.class) | |
| .hasFieldOrPropertyWithValue("errorCode", CalendarErrorCode.TODO_NOT_FOUND); | |
| } | |
| `@Test` | |
| void toggleThrowsWhenTodoBelongsToAnotherCompany() { | |
| PersonalTodoService service = service(); | |
| PersonalTodo otherCompanyTodo = PersonalTodo.reconstitute( | |
| TODO_ID, 999L, MEMBER, "다른 회사 Todo", LocalDate.of(2026, 8, 20), false, null, null); | |
| when(personalTodoRepository.findById(TODO_ID)).thenReturn(Optional.of(otherCompanyTodo)); | |
| assertThatThrownBy(() -> service.toggleComplete(COMPANY, MEMBER, TODO_ID)) | |
| .isInstanceOf(BusinessException.class) | |
| .hasFieldOrPropertyWithValue("errorCode", CalendarErrorCode.TODO_NOT_FOUND); | |
| verify(personalTodoRepository, never()).save(any()); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/test/java/com/module06/backend/calendar/application/service/PersonalTodoServiceTest.java`
around lines 104 - 114, Update toggleThrowsWhenTodoBelongsToAnotherCompany to
also verify that personalTodoRepository.save is never called when ownership
validation fails, while preserving the existing BusinessException and
TODO_NOT_FOUND assertions.
📌 연관 이슈
📝 작업 내용
V6.1.1__create_personal_todo.sql신규 테이블(title·date만, Figma 모달 필드 그대로)POST /api/todos,PATCH /api/todos/{todoId}/complete(토글)✅ 마이그레이션 버전 확인 완료
V6.1.1은 안전하다. DBA 확인 결과 아직 운영 환경 자체가 구성되지 않았음 — 첫 배포 시 빈 DB에V1부터 순서대로 전부 적용되므로FLYWAY_OUT_OF_ORDER관련 스킵 위험이 애초에 없다. (이전에 "머지 전 확인 필요" 블로커로 표시했던 것은 운영 DB가 이미 존재한다는 잘못된 전제에서 시작한 조사였음 — 정정.)🖥️ 프론트엔드 연동 가이드 (API 명세)
1. 주요 엔드포인트
POST/api/todos: Todo 생성PATCH/api/todos/{todoId}/complete: 완료 체크박스 토글2. 요청 파라미터 (Request)
LocalDate, 단일 날짜3. 정상 응답 예시 (200 OK)
응답 JSON 보기 (클릭)
{ "httpStatus": 201, "message": "Todo를 추가했습니다.", "data": { "id": 1, "title": "우유 사기", "date": "2026-08-20", "isDone": false } }4.⚠️ 프론트엔드 참고 및 주의사항
GET /api/calendar통합조회는 이 PR에 없다 — 다음 라운드에서 별도 구현.🚨 주요 에러 코드 및 예외 (Exceptions)
CAL-001(TODO_NOT_FOUND) : 존재하지 않거나 다른 회사·다른 사람 소유의 Todo id로 토글 시도💡 백엔드 리뷰 포인트 (Backend Review)
TODO_NOT_FOUND하나로 뭉갠 판단(존재 자체를 노출하지 않는 방향) 괜찮은지✅ 체크리스트