Skip to content

[Feat] 훈련 달력 저장 시 날짜 개별 선택 기능#94

Merged
zweadfx merged 5 commits intomainfrom
feat/individual-training-dates
Apr 10, 2026
Merged

[Feat] 훈련 달력 저장 시 날짜 개별 선택 기능#94
zweadfx merged 5 commits intomainfrom
feat/individual-training-dates

Conversation

@zweadfx
Copy link
Copy Markdown
Owner

@zweadfx zweadfx commented Apr 10, 2026

어떤 변경사항인가요?

훈련 달력 저장 시 연속 날짜 자동 배치 대신 각 day별로 날짜를 개별 선택할 수 있도록 변경합니다.

작업 상세 내용

  • SavedPlan 모델: start_date (Date) → training_dates (JSON, list of date strings)
  • SavePlanRequest / SavedPlanResponse 스키마 변경 (training_dates: list[date])
  • plans.py 월 필터링 로직 변경 (training_dates 중 해당 월 포함 여부)
  • complete_plan_day day_number 범위 검증을 len(training_dates) 기준으로
  • startup 시 기존 saved_plans 테이블 자동 마이그레이션 (start_date 컬럼 감지 → drop 후 재생성)

체크리스트

  • self-test를 수행하였는가?
  • 관련 문서나 주석을 업데이트하였는가?
  • 설정한 코딩 컨벤션을 준수하였는가?

관련 이슈

리뷰 포인트

  • training_dates는 JSON 컬럼에 ["2026-04-10", "2026-04-12"] 형태로 저장됩니다.
  • total_dayslen(training_dates)로 자동 계산되어 저장됩니다.
  • 기존 DB 마이그레이션은 startup 시 start_date 컬럼 존재 여부로 판단하며, 마이그레이션 완료 후에는 no-op입니다.

참고사항 및 스크린샷(선택)

프론트엔드(별도 레포) 변경사항:

  • weekly: day별 개별 date input N개
  • skill: 단일 date input → training_dates: [date]로 변환
  • mypage: training_dates 직접 사용, addDays 유틸 제거

Summary by CodeRabbit

  • New Features

    • Plans now use explicit training dates instead of a single start date + total days.
    • Month-based plan listing now matches plans by those explicit training dates.
  • Bug Fixes

    • Completing a plan day is validated against actual training dates.
    • Save request now rejects duplicate training dates.
  • Chores

    • Automatic migration applied to existing saved plans on startup.

@zweadfx zweadfx linked an issue Apr 10, 2026 that may be closed by this pull request
8 tasks
@zweadfx zweadfx self-assigned this Apr 10, 2026
@zweadfx zweadfx added the feature 새로운 기능 구현 시 사용합니다. label Apr 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

The PR replaces range-based plans (start_date + total_days) with explicit training_dates (JSON array), updates the ORM model, request/response schemas, endpoint logic, and adds a startup migration that drops the old saved_plans table if it contains start_date.

Changes

Cohort / File(s) Summary
Database Model
src/db/models.py
Removed start_date column; added training_dates column stored as JSON (non-null) on SavedPlan.
Startup Migration
src/main.py
Added startup logic to inspect saved_plans and drop the table if old start_date column exists before running Base.metadata.create_all.
API Schemas
src/models/plan_schema.py
Changed SavePlanRequest to require training_dates: list[date] (min_length=1, no duplicates). Updated responses to include training_dates and preserve total_days/completed_days/created_at.
Endpoints
src/api/v1/endpoints/plans.py
save_plan persists training_dates and sets total_days=len(training_dates); get_plans fetches user plans and filters by month via training_dates string prefix (removed start_date range logic); complete_plan_day validates against len(training_dates).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant API as PlansAPI
  participant DB as Database

  Client->>API: POST /plans (training_dates[])
  API->>DB: INSERT SavedPlan {training_dates: [...], total_days: len(...)}
  DB-->>API: Insert OK / SavedPlan ID
  API-->>Client: 201 Created (SavedPlanResponse)

  Client->>API: GET /plans?year=YYYY&month=MM
  API->>DB: SELECT * FROM saved_plans WHERE user_id=...
  DB-->>API: [SavedPlan{training_dates:[...]}...]
  API->>API: filter plans where any training_dates entry startsWith "YYYY-MM"
  API-->>Client: 200 OK (filtered plans)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through tables, fields replaced,
From ranges to dates each carefully placed.
I dropped the old, then stitched the new,
Training days listed, bright as dew.
A rabbit cheers — the plan's in bloom! 🌱📅

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title '[Feat] 훈련 달력 저장 시 날짜 개별 선택 기능' (Feature: Individual date selection when saving training calendar) accurately describes the main change: replacing a continuous start_date with per-day date selection via a new training_dates field.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/individual-training-dates

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/api/v1/endpoints/plans.py (2)

26-27: Avoid persisting a second source of truth for plan length.

total_days is derived from training_dates, but it is still stored separately. If those ever drift, this endpoint will return a different length than complete_plan_day enforces.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/v1/endpoints/plans.py` around lines 26 - 27, The code is persisting
total_days separately while it is derivable from training_dates (training_dates
and total_days in the plan creation), creating a second source of truth; remove
the total_days field from the persisted payload and any creation logic (where
total_days is set) and instead compute plan length on demand from
len(plan.training_dates) (or provide a property/method like
complete_plan_day/plan_length that returns len(training_dates)) so stored data
cannot drift from the derived value.

43-54: This monthly query now scans the user's full plan history in Python.

That will get slower as saved plans grow because the database only filters by user_id. Consider pushing the month predicate into SQL, or normalizing training_dates into a child table so calendar reads stay queryable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/v1/endpoints/plans.py` around lines 43 - 54, The current code loads
all SavedPlan rows for a user into Python and then filters training_dates, which
will scale poorly; modify the query that builds plans (the db.query(...) for
SavedPlan) to include the month predicate in SQL so only matching rows are
returned (e.g. add an additional filter that checks SavedPlan.training_dates for
any element starting with month_prefix using a database-side JSON/array/text
operator or a SQL func like jsonb_array_elements_text/LIKE), and keep the rest
of the logic (building result with SavedPlanResponse.model_validate) the same;
for long-term scalability consider normalizing the training_dates array into a
child table (e.g., a PlanTrainingDate model) and query that table with a JOIN or
EXISTS instead of scanning SavedPlan in Python.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.py`:
- Around line 43-52: The startup code currently unconditionally drops the
saved_plans table when it sees the legacy "start_date" column (inspect,
inspector, get_columns, start_date, conn.execute(text("DROP TABLE
saved_plans"))); instead stop deleting data and either fail fast or perform an
in-place migration/backfill. Replace the DROP TABLE branch with a call to a new
backfill_saved_plans(engine) utility (or raise a clear RuntimeError) that 1)
adds the new training_dates column if needed (ALTER TABLE), 2) iterates existing
saved_plans rows and derives training_dates from the legacy fields, and 3)
updates rows within a transaction; ensure to log progress (logger.info/error)
and only drop legacy columns after a successful backfill.

In `@src/models/plan_schema.py`:
- Line 13: The PlanSchema's training_dates field may contain duplicates or be
out-of-order which breaks day_number semantics; add a Pydantic validator (e.g.,
`@root_validator` or `@field_validator`) on PlanSchema that normalizes
training_dates by converting inputs to date objects, removing duplicates,
sorting ascending, and ensuring min_length>=1, and if normalization changes the
list update any dependent properties (like day_number calculation) or raise
ValidationError if invalid; reference the training_dates field and PlanSchema
class so the validator is applied before persisting.

---

Nitpick comments:
In `@src/api/v1/endpoints/plans.py`:
- Around line 26-27: The code is persisting total_days separately while it is
derivable from training_dates (training_dates and total_days in the plan
creation), creating a second source of truth; remove the total_days field from
the persisted payload and any creation logic (where total_days is set) and
instead compute plan length on demand from len(plan.training_dates) (or provide
a property/method like complete_plan_day/plan_length that returns
len(training_dates)) so stored data cannot drift from the derived value.
- Around line 43-54: The current code loads all SavedPlan rows for a user into
Python and then filters training_dates, which will scale poorly; modify the
query that builds plans (the db.query(...) for SavedPlan) to include the month
predicate in SQL so only matching rows are returned (e.g. add an additional
filter that checks SavedPlan.training_dates for any element starting with
month_prefix using a database-side JSON/array/text operator or a SQL func like
jsonb_array_elements_text/LIKE), and keep the rest of the logic (building result
with SavedPlanResponse.model_validate) the same; for long-term scalability
consider normalizing the training_dates array into a child table (e.g., a
PlanTrainingDate model) and query that table with a JOIN or EXISTS instead of
scanning SavedPlan in Python.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d9c81c9-5594-46b7-9ccb-a1954a38659b

📥 Commits

Reviewing files that changed from the base of the PR and between c9bc923 and 5cbfece.

📒 Files selected for processing (4)
  • src/api/v1/endpoints/plans.py
  • src/db/models.py
  • src/main.py
  • src/models/plan_schema.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/models/plan_schema.py (1)

28-28: Consider mirroring request constraints in response schema.

Optional: set training_dates as Field(..., min_length=1) in SavedPlanResponse too, so the API contract documents the non-empty invariant consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/models/plan_schema.py` at line 28, SavedPlanResponse currently defines
training_dates without the non-empty constraint; mirror the request-side
invariant by updating SavedPlanResponse.training_dates to use Field(...,
min_length=1) (keeping the list[date] type) so the response schema documents the
non-empty requirement; ensure you import Field from pydantic if not already and
update the SavedPlanResponse class accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/models/plan_schema.py`:
- Line 28: SavedPlanResponse currently defines training_dates without the
non-empty constraint; mirror the request-side invariant by updating
SavedPlanResponse.training_dates to use Field(..., min_length=1) (keeping the
list[date] type) so the response schema documents the non-empty requirement;
ensure you import Field from pydantic if not already and update the
SavedPlanResponse class accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: abaffab1-e386-4c9f-b7b4-8bd943c9780e

📥 Commits

Reviewing files that changed from the base of the PR and between 5cbfece and f5eb490.

📒 Files selected for processing (1)
  • src/models/plan_schema.py

@zweadfx zweadfx merged commit 8b284e6 into main Apr 10, 2026
2 checks passed
@zweadfx zweadfx deleted the feat/individual-training-dates branch April 10, 2026 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 새로운 기능 구현 시 사용합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 훈련 달력 저장 시 날짜 개별 선택 기능

1 participant