Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
## ⏰ Timo ️
<img width="1440" height="929" alt="TimoLanding" src="https://github.com/user-attachments/assets/752a48b5-8c37-4891-ae48-954af645c243" />


## ⚙️ Features
<img width="1920" height="1080" alt="Timo-Flow1" src="https://github.com/user-attachments/assets/d68e92f8-2ad2-4fe7-8cab-1c33ccbc69a1" />

**📝 투두와 시간을 입력해 타임박스 생성**

해야 할 일과 예상 소요 시간을 입력하면 하루 계획이 타임박스 형태로 자동 생성됩니다.

<img width="1920" height="1080" alt="Timo-Flow2" src="https://github.com/user-attachments/assets/9f0133dd-5f52-4eda-9a24-bcc7df438adb" />

**⏱️ 타이머를 실행해 계획 수행**

각 타임박스를 실행하면 설정한 시간만큼 타이머가 시작되고 실제 수행 시간이 측정됩니다.

<img width="1920" height="1080" alt="Timo-Flow3" src="https://github.com/user-attachments/assets/25595452-4623-4a05-9931-180493708107" />

**📊 타임라인을 통해서 기록**

타이머로 수행한 시간만큼 타임라인에 실시간으로 기록되어 하루 동안 무엇을 했는지 한눈에 파악할 수 있습니다.

<img width="1920" height="1080" alt="Timo-Flow4" src="https://github.com/user-attachments/assets/aec2d817-14e1-4ddc-acbc-f20681de3429" />

**🤖 AI 피드백**

계획한 소요 시간과 실제 소요 시간을 비교해 AI가 사용자에게 맞는 시간 추천과 피드백을 제공합니다.

<img width="1920" height="1080" alt="Timo-Flow5" src="https://github.com/user-attachments/assets/0e639bef-dafb-4fcf-bd12-fefa69ffb6c8" />

**▶️ 완료하면 다음 타임박스를 시작**

하나의 투두를 완료하면 이어서 다음 타임박스로 자연스럽게 넘어가 계획한 하루를 끊김 없이 수행할 수 있습니다.

<img width="1920" height="1080" alt="Timo-Flow6" src="https://github.com/user-attachments/assets/00a33d33-4d4b-4023-860d-58b05c04c8a2" />

**📈 통계**

하루 동안 계획한 시간과 실제 수행 시간의 차이를 캘린더 뷰에서 한눈에 확인할 수 있습니다.

<img width="1920" height="1080" alt="Timo-Flow7" src="https://github.com/user-attachments/assets/97826a74-974f-478b-80ec-120d08136a17" />

**🎯 집중 모드**

하나의 투두에만 집중해서 수행할 수 있는 몰입 전용 화면을 제공합니다.

#### 할 일을 시간 단위로 관리하는 타임박싱 투두

Timo는 작업을 실행 가능한 시간 블록으로 변환해
계획한 일을 실제로 완료할 수 있도록 돕는 타임박스 기반 할 일 관리 서비스입니다.

기존의 투두 서비스와 달리, Timo는 각 작업을 추가할 때 예상 소요 시간을 입력하도록 하며
이를 바탕으로 하루 일정을 시간 블록 중심으로 구성합니다.

또한 사용자의 작업 및 시간 사용 패턴을 지속적으로 학습하여 AI 기반 소요 시간 추천을 제공합니다.
이를 통해 과도한 계획을 줄이고 보다 현실적이고 실행 가능한 일정을 세울 수 있도록 지원합니다.

&nbsp;

## ✨ Backend Developers
<table>
<tr>
<th>정윤아</th>
<th>박예나</th>
<th>장자윤</th>
</tr>
<tr>
<td>
<img width="200" height="300" alt="Timo-yuna" src="https://github.com/user-attachments/assets/81759350-5d56-4d01-8015-50d67cf8aa50" />
</td>
<td>
<img width="200" height="300" alt="Timo-yena" src="https://github.com/user-attachments/assets/74d55577-9b5b-495a-86dd-97af89c55841" />
</td>
<td>
<img width="200" height="300" alt="Timo-jayoon" src="https://github.com/user-attachments/assets/7c47c6ef-80c0-44b4-a42b-97698611861d" />
</td>
</tr>
<tr>
<td>Backend Leader</td>
<td>Backend Dev</td>
<td>Backend Dev</td>
</tr>
<tr>
<td><a href="https://github.com/laura-jung">@laura-jung</a></td>
<td><a href="https://github.com/aneykrap">@anykrap</a></td>
<td><a href="https://github.com/Jy000n">@Jy000n</a></td>
</tr>
</table>

&nbsp;

## 🛠️ Tech Stack
#### Backend & Auth
<img alt="SPRING BOOT" src="https://img.shields.io/badge/SPRING Boot-6DB33F.svg?&style=for-the-badge&logo=SpringBoot&logoColor=white"/> <img alt="SPRING SECURITY" src="https://img.shields.io/badge/Spring Security-6DB33F.svg?&style=for-the-badge&logo=springsecurity&logoColor=white"/> <img alt="JWT" src="https://img.shields.io/badge/JWT-000000?style=for-the-badge&logo=auth0&logoColor=white"/>

#### Database & Cache
<img alt="MYSQL" src="https://img.shields.io/badge/Mysql-4479A1.svg?&style=for-the-badge&logo=Mysql&logoColor=white"/> <img alt="REDIS" src="https://img.shields.io/badge/Redis-DC382D.svg?&style=for-the-badge&logo=Redis&logoColor=white"/>

#### Infra & Deploy
<img alt="DOCKER" src="https://img.shields.io/badge/Docker-2496ED.svg?&logo=Docker&style=for-the-badge&logoColor=white"/> <img alt="Nginx" src="https://img.shields.io/badge/NGINX-009639.svg?&style=for-the-badge&logo=nginx&logoColor=white"/> <img alt="AWS EC2" src="https://img.shields.io/badge/Amazon EC2-FF9900.svg?&style=for-the-badge&logo=amazonec2&logoColor=white"/> <img alt="AWS RDS" src="https://img.shields.io/badge/Amazon RDS-FFAA00.svg?&style=for-the-badge&logo=amazonrds&logoColor=white"/> <img alt="GHCR" src="https://img.shields.io/badge/GHCR-181717?style=for-the-badge&logo=github&logoColor=white&height=34"/>

#### Monitoring
<img alt="Prometheus" src="https://img.shields.io/badge/Prometheus-E6522C.svg?&style=for-the-badge&logo=prometheus&logoColor=white"/> <img alt="Grafana" src="https://img.shields.io/badge/Grafana-F46800.svg?&style=for-the-badge&logo=grafana&logoColor=white"/> <img alt="Sentry" src="https://img.shields.io/badge/Sentry-362D59.svg?&style=for-the-badge&logo=sentry&logoColor=white"/>

#### AI
<img alt="Google Gemini" src="https://img.shields.io/badge/Google Gemini-8E75B2.svg?&style=for-the-badge&logo=googlegemini&logoColor=white"/>

&nbsp;

## 🖥️ System Architecture
<img width="3297" height="2140" alt="Timo-system_architecture" src="https://github.com/user-attachments/assets/610c9409-cf61-4016-8594-277388d01403" />

&nbsp;

## 📊 ERD
<img width="2150" height="1072" alt="Timo-erd" src="https://github.com/user-attachments/assets/87ac9965-1e97-43e7-9aa6-be04588f5864" />


&nbsp;

## 📦️ Directory
```
📂com.Timo.Timo
├──📂domain
│ ├──📂timer
│ │ ├──📂controller # HTTP 요청/응답 처리
│ │ ├──📂docs # Swagger 문서 (Controller와 분리)
│ │ ├──📂dto # 요청/응답 DTO
│ │ ├──📂entity # JPA 엔티티
│ │ ├──📂enums # 도메인 전용 enum
│ │ ├──📂exception # 에러/성공 코드
│ │ ├──📂repository # DB 접근
│ │ └──📂service # 비즈니스 로직
│ ├──📂todo
│ ├──📂user
│ └── ...
└──📂global
├──📂auth # 인증/인가 공통 로직
├──📂common # 공통 베이스 클래스 (BaseTimeEntity 등)
├──📂config # Swagger, Security 등 설정
├──📂exception # 공통 예외 처리
├──📂jwt # JWT 유틸 (발급/검증)
├──📂logging # 요청/응답 및 예외 로깅
└──📂response # 공통 응답 포맷 (BaseResponse)
```

&nbsp;

## 📋 Convention
[🌟 깃 컨벤션 보러가기](https://app.notion.com/p/Timo-Git-Convention-3877196f19708068b15fe97386a80e01)<br>
[🌟 코드 컨벤션 보러가기](https://app.notion.com/p/Timo-spring-code-convention-3877196f19708031b1bcd531a93119fd)
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public record TodoResponse(
String priority,
TagResponse tag,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
boolean hasSubtask,
boolean hasMemo,
@JsonProperty("isRepeated")
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
boolean isRepeated,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public record TodayTodoResponse(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
boolean isRepeated,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
boolean hasSubtask,
boolean hasMemo,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
TodoTimerStatus timerStatus,
Integer sortOrder,
Expand All @@ -78,7 +78,7 @@ public static TodayTodoResponse from(LocalDate date, TodoResponse todo) {
todo.priority(),
todo.tag(),
todo.isRepeated(),
todo.hasSubtask(),
todo.hasMemo(),
todo.timerStatus(),
todo.sortOrder(),
todo.subtasks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,19 @@ public TodoResponse toResponse(TodoContext context) {
todo.getDurationSeconds(),
todo.getPriority() != null ? todo.getPriority().name() : null,
mapTag(context.tag()),
!todo.getSubtasks().isEmpty(),
hasMemo(todo, instance),
todo.getRepeatType() != RepeatType.NONE,
resolveTimerStatus(instance),
instance != null ? instance.getSortOrder() : context.defaultSortOrder(),
mapSubtasks(todo.getSubtasks(), context.completedSubtaskIds())
);
}

private boolean hasMemo(Todo todo, TodoInstance instance) {
String memo = instance != null ? instance.resolveMemo() : todo.getMemo();
return memo != null && !memo.isBlank();
}

private TagResponse mapTag(Tag tag) {
return tag != null ? TagResponse.from(tag) : null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.Timo.Timo.domain.timer.entity.TimerRecord;
import com.Timo.Timo.domain.timer.enums.TimerStatus;
import jakarta.persistence.LockModeType;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
Expand All @@ -18,6 +19,9 @@ public interface TimerRecordRepository extends JpaRepository<TimerRecord, Long>

boolean existsByTodo_IdAndStatusIn(Long todoId, List<TimerStatus> statuses);

boolean existsByTodo_IdAndTargetDateAndStatusIn(
Long todoId, LocalDate targetDate, List<TimerStatus> statuses);

@Modifying(clearAutomatically = true)
@Query("delete from TimerRecord r where r.todo.id = :todoId")
void deleteByTodoId(@Param("todoId") Long todoId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ public boolean hasActiveTimer(Long todoId) {
return timerRecordRepository.existsByTodo_IdAndStatusIn(todoId, ACTIVE_STATUS);
}

public boolean hasActiveTimerOn(Long todoId, LocalDate date) {
return timerRecordRepository.existsByTodo_IdAndTargetDateAndStatusIn(todoId, date, ACTIVE_STATUS);
}

@Transactional
public void deleteTimersByTodo(Long todoId) {
timerSessionRepository.deleteByTodoId(todoId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public TodoStatusChangeResponse changeCompletion(Long userId, Long todoId, TodoS
throw new CustomException(TodoErrorCode.TODO_NOT_FOUND);
}

if (timerService.hasActiveTimer(todoId)) {
if (timerService.hasActiveTimerOn(todoId, date)) {
throw new CustomException(TodoErrorCode.TIMER_RUNNING);
}

Expand Down
Loading