add abandoned status to distinguish given-up habits from completed ones

Completed and abandoned habits were both lumped under a single terminal
state, making it impossible to tell "finished successfully" apart from
"gave up" when reviewing habit history.
This commit is contained in:
2026-07-17 20:19:35 +09:00
parent d0dd530e26
commit 55918d56d1
11 changed files with 167 additions and 3 deletions
+2
View File
@@ -18,6 +18,7 @@ class HabitType(str, enum.Enum):
class HabitStatus(str, enum.Enum):
ACTIVE = "active"
COMPLETED = "completed"
ABANDONED = "abandoned"
class Habit(Base):
@@ -38,6 +39,7 @@ class Habit(Base):
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now(), nullable=False)
completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
abandoned_at: Mapped[datetime | None] = mapped_column(nullable=True)
logs: Mapped[list["HabitLog"]] = relationship(
back_populates="habit", cascade="all, delete-orphan", passive_deletes=True
+6
View File
@@ -69,6 +69,12 @@ def complete_habit(habit_id: int, db: Session = Depends(get_db), current_user: U
return habit_service.complete_habit(db, habit)
@router.post("/{habit_id}/abandon", response_model=HabitOut)
def abandon_habit(habit_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_login)):
habit = _get_habit_or_404(db, habit_id, current_user.id)
return habit_service.abandon_habit(db, habit)
@router.post("/{habit_id}/reactivate", response_model=HabitOut)
def reactivate_habit(habit_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_login)):
habit = _get_habit_or_404(db, habit_id, current_user.id)
+30 -3
View File
@@ -118,6 +118,8 @@ def habits_page(request: Request, tab: str = "build", db: Session = Depends(get_
if tab == "completed":
habits = habit_service.list_habits(db, current.id, status=HabitStatus.COMPLETED)
elif tab == "abandoned":
habits = habit_service.list_habits(db, current.id, status=HabitStatus.ABANDONED)
else:
if tab not in ("build", "quit"):
tab = "build"
@@ -139,6 +141,14 @@ def habits_page(request: Request, tab: str = "build", db: Session = Depends(get_
)
def _tab_for_habit(habit) -> str:
if habit.status == HabitStatus.COMPLETED:
return "completed"
if habit.status == HabitStatus.ABANDONED:
return "abandoned"
return habit.habit_type.value
@router.post("/habits/new")
def create_habit_page(
request: Request,
@@ -208,7 +218,7 @@ def edit_habit_page(
return HTMLResponse("입력값을 확인해주세요")
habit_service.update_habit(db, habit, data)
tab = "completed" if habit.status == HabitStatus.COMPLETED else habit.habit_type.value
tab = _tab_for_habit(habit)
response = Response(status_code=200)
response.headers["HX-Redirect"] = f"/habits?tab={tab}"
return response
@@ -230,6 +240,22 @@ def complete_habit_page(request: Request, habit_id: int, db: Session = Depends(g
return response
@router.post("/habits/{habit_id}/abandon")
def abandon_habit_page(request: Request, habit_id: int, db: Session = Depends(get_db)):
current = _current_user_or_redirect(request, db)
if isinstance(current, RedirectResponse):
return current
habit = habit_service.get_habit(db, habit_id, current.id)
if habit is None:
return Response(status_code=404)
tab = habit.habit_type.value
habit_service.abandon_habit(db, habit)
response = Response(status_code=200)
response.headers["HX-Redirect"] = f"/habits?tab={tab}"
return response
@router.post("/habits/{habit_id}/reactivate")
def reactivate_habit_page(request: Request, habit_id: int, db: Session = Depends(get_db)):
current = _current_user_or_redirect(request, db)
@@ -239,9 +265,10 @@ def reactivate_habit_page(request: Request, habit_id: int, db: Session = Depends
habit = habit_service.get_habit(db, habit_id, current.id)
if habit is None:
return Response(status_code=404)
tab = _tab_for_habit(habit)
habit_service.reactivate_habit(db, habit)
response = Response(status_code=200)
response.headers["HX-Redirect"] = "/habits?tab=completed"
response.headers["HX-Redirect"] = f"/habits?tab={tab}"
return response
@@ -254,7 +281,7 @@ def delete_habit_page(request: Request, habit_id: int, db: Session = Depends(get
habit = habit_service.get_habit(db, habit_id, current.id)
if habit is None:
return Response(status_code=404)
tab = "completed" if habit.status == HabitStatus.COMPLETED else habit.habit_type.value
tab = _tab_for_habit(habit)
habit_service.delete_habit(db, habit)
response = Response(status_code=200)
response.headers["HX-Redirect"] = f"/habits?tab={tab}"
+1
View File
@@ -56,6 +56,7 @@ class HabitOut(BaseModel):
reminder_time: time | None
created_at: datetime
completed_at: datetime | None
abandoned_at: datetime | None
class HabitReorderRequest(BaseModel):
+9
View File
@@ -79,9 +79,18 @@ def complete_habit(db: Session, habit: Habit) -> Habit:
return habit
def abandon_habit(db: Session, habit: Habit) -> Habit:
habit.status = HabitStatus.ABANDONED
habit.abandoned_at = datetime.now()
db.commit()
db.refresh(habit)
return habit
def reactivate_habit(db: Session, habit: Habit) -> Habit:
habit.status = HabitStatus.ACTIVE
habit.completed_at = None
habit.abandoned_at = None
db.commit()
db.refresh(habit)
return habit
+2
View File
@@ -8,6 +8,7 @@
<a href="/habits?tab=build" class="{{ 'active' if tab == 'build' }}">만들고 싶은 습관</a>
<a href="/habits?tab=quit" class="{{ 'active' if tab == 'quit' }}">멈추고 싶은 습관</a>
<a href="/habits?tab=completed" class="{{ 'active' if tab == 'completed' }}">완료된 습관</a>
<a href="/habits?tab=abandoned" class="{{ 'active' if tab == 'abandoned' }}">포기한 습관</a>
</div>
{% if tab in ("build", "quit") %}
@@ -25,6 +26,7 @@
<div class="empty-state">
{% if tab == "build" %}만들고 싶은 습관이 아직 없어요. 위에서 추가해보세요.
{% elif tab == "quit" %}끊고 싶은 습관이 아직 없어요. 위에서 추가해보세요.
{% elif tab == "abandoned" %}아직 포기한 습관이 없어요.
{% else %}아직 완료된 습관이 없어요.{% endif %}
</div>
{% endif %}
+8
View File
@@ -41,6 +41,14 @@
hx-swap="none"
hx-confirm="'{{ habit.name }}' 습관을 완료 처리할까요?"
>완료 처리</button>
<button
type="button"
class="btn btn-secondary"
hx-post="/habits/{{ habit.id }}/abandon"
hx-target="body"
hx-swap="none"
hx-confirm="'{{ habit.name }}' 습관을 포기할까요?"
>포기</button>
{% else %}
<button
type="button"