Skip to content

두 사람의 좋은 날짜와 시간 찾기 - #12

Merged
seonghobae merged 4 commits into
mainfrom
agent/two-person-good-time
Jul 26, 2026
Merged

seonghobae merged 4 commits into
mainfrom
agent/two-person-good-time

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

무엇이 달라졌나요

  • 첫 화면에서 두 사람의 저장된 프로필을 고르거나 양력·한국 음력·윤달 출생 정보를 바로 입력할 수 있습니다.
  • 두 사람의 일지와 후보 날짜·시간 지지의 육합·삼합 계열을 투명하게 점수화하고, 육충 후보를 제외합니다.
  • 한 사람에게만 높은 후보를 피하도록 낮은 개인 점수를 더 크게 반영하고 하루 한 시간만 가까운 순으로 보여 줍니다.
  • 결과를 전용 CalDAV 캘린더로 저장하고 미리보기·반복 동기화할 수 있습니다.
  • 현재 시각보다 뒤에 끝나는 시간만 고른 뒤 날짜별 최고 후보를 결정합니다.
  • 한국어 설명을 기본으로 두고 한자는 펼침 보조 정보로만 표시합니다.

근거와 한계

Chao Wei-pang의 1946년 연구에 기록된 일주 중심 해석과 지지의 육합·삼합·충 관계를 문화사 자료로 참고했습니다. 점수는 저장소가 명시한 제품 관례이며 과학적 예측이나 관계의 객관적 판정으로 표시하지 않습니다.

검증

  • uv run --frozen pytest -q — 57 passed
  • uv run --frozen ruff check .
  • node --check app/static/app.js
  • 양력+음력 두 사람 신규 입력, 저장 프로필 재사용, 추천 캘린더 저장·미리보기 브라우저 검증
  • 1280px 데스크톱과 390px 모바일에서 가로 넘침·중복 ID·라벨 없는 폼 컨트롤 없음 확인
  • 라이브 acceptance smoke에 두 사람 추천 생성·CalDAV 동기화·ICS 읽기·정리 경로 추가

Summary by CodeRabbit

  • 새 기능

    • 두 사람의 출생 정보를 입력해 서로에게 균형 있게 맞는 날짜와 시간을 추천합니다.
    • 추천 점수, 관계 설명, 양쪽의 일지 정보를 함께 확인할 수 있습니다.
    • 추천 결과를 별도 CalDAV 캘린더로 저장하고 반복 동기화할 수 있습니다.
    • 현재 이후 1년 범위에서 날짜별 최적 시간을 미리보고 가까운 순으로 확인합니다.
  • 개선

    • 프로필 선택과 새 인물 입력 흐름을 간소화했습니다.
    • 모바일 화면과 결과·캘린더 저장 영역의 표시를 개선했습니다.
  • 문서

    • 두 사람 추천 규칙, 점수 해석 및 검색 범위를 문서화했습니다.

Copilot AI review requested due to automatic review settings July 26, 2026 10:28
@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

두 사람의 출생 차트를 비교해 현재 이후의 시간 창을 점수화하고, 날짜별 추천 후보를 미리보기·호환성 CalDAV 캘린더로 저장·동기화하는 API, 저장소, UI, 테스트가 추가되었습니다.

Changes

호환성 추천 흐름

Layer / File(s) Summary
시간 창 생성과 절기 전환 처리
app/events.py, tests/test_events.py
시간 창 반복자가 추가되고, 경계 시각과 intraday 절기 전환에 따른 차트 계산 및 검증이 구현되었습니다.
호환성 관계 점수와 후보 선택
app/compatibility.py, tests/test_compatibility.py, docs/research/README.md, docs/ARCHITECTURE.md
일지·시간 관계를 점수화하고 충돌을 제외하며, 날짜별 최고 후보와 동점 처리·현재 이후 필터를 적용합니다.
호환성 API와 캘린더 저장
app/main.py, app/store.py, tests/test_store.py
호환성 미리보기·캘린더 생성 API와 kind, secondary_profile_id 저장 및 마이그레이션이 추가되었습니다.
두 사람 입력과 결과·저장 UI
app/static/index.html, app/static/app.js, app/static/styles.css, README.md
두 사람 입력, 추천 결과, 호환성 캘린더 저장 폼과 관련 프로필·반응형 UI가 추가되었습니다.
API·CalDAV 통합 검증
scripts/acceptance_smoke.py, tests/test_api.py
호환성 preview, calendar preview, sync, CalDAV 이벤트와 서로 다른 프로필 요구 조건을 검증합니다.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PairForm
  participant CompatibilityAPI
  participant CompatibilityScoring
  participant CalendarStore
  PairForm->>CompatibilityAPI: 두 프로필과 기간 제출
  CompatibilityAPI->>CompatibilityScoring: 호환성 후보 계산
  CompatibilityScoring-->>CompatibilityAPI: 점수·라벨·이유가 포함된 후보
  CompatibilityAPI-->>PairForm: 추천 결과 표시
  PairForm->>CompatibilityAPI: 호환성 캘린더 저장
  CompatibilityAPI->>CalendarStore: compatibility 캘린더 저장
  CalendarStore-->>CompatibilityAPI: 저장 결과
  CompatibilityAPI-->>PairForm: 캘린더 생성 응답
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 두 사람의 좋은 날짜·시간을 찾는 핵심 변경을 간결하게 잘 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 agent/two-person-good-time

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a new “two-person compatibility” flow that ranks upcoming date/time windows for two saved (or newly entered) profiles, explains the scoring in Korean, and allows saving/syncing the results as a dedicated CalDAV calendar type alongside existing rule-based calendars.

Changes:

  • Introduces a compatibility scoring/generation module and new API endpoints for previewing and creating “compatibility” calendars.
  • Extends calendar storage/migration to support calendar kind and an optional secondary_profile_id, and updates profile deletion to clean up dependent compatibility calendars.
  • Refactors window generation to reuse per-day chart components (via iter_chart_windows) and updates the web UI + acceptance smoke to cover the new flow.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_store.py Asserts calendar schema migration includes kind and secondary_profile_id.
tests/test_events.py Adds coverage for iter_chart_windows optimization matching prior midpoint logic.
tests/test_compatibility.py New unit tests for compatibility scoring and candidate selection constraints.
tests/test_api.py Adds API acceptance tests for compatibility preview/calendar creation and validation.
scripts/acceptance_smoke.py Extends smoke flow to generate/sync compatibility calendars and validate CalDAV output.
README.md Updates product description and adds two-person flow usage notes.
docs/research/README.md Documents two-person recommendation rules and adds a primary historical reference.
docs/ARCHITECTURE.md Updates architecture diagram and operational notes for compatibility flow.
app/store.py Migrates/records new calendar fields and cleans up calendars referencing deleted secondary profiles.
app/static/styles.css Adds new layout/styles for the two-person flow and results display.
app/static/index.html Adds the two-person input/result flow and moves advanced rule builder into a collapsible section.
app/static/app.js Implements client-side two-person profile resolution, compatibility preview rendering, and calendar saving.
app/main.py Adds compatibility API endpoints, calendar preview/sync support for compatibility calendars, and candidate JSON.
app/events.py Adds iter_chart_windows generator and refactors window generation to reuse per-day chart state.
app/compatibility.py New compatibility scoring and candidate generation logic with explanatory reasons/labels.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/compatibility.py
Comment thread app/static/app.js
Copilot AI review requested due to automatic review settings July 26, 2026 10:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Comment thread app/compatibility.py
Comment thread app/events.py Outdated
Copilot AI review requested due to automatic review settings July 26, 2026 10:50
Comment thread tests/test_compatibility.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Comment thread app/events.py Outdated
Copilot AI review requested due to automatic review settings July 26, 2026 11:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

@seonghobae
seonghobae merged commit 614957f into main Jul 26, 2026
30 of 31 checks passed
@seonghobae
seonghobae deleted the agent/two-person-good-time branch July 26, 2026 11:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (9)
tests/test_api.py (3)

240-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

events[0] 인덱싱 전에 비어 있지 않음을 단정하세요.

캘린더 preview가 빈 목록을 돌려주면 IndexError로 실패해 원인 파악이 어렵습니다.

💚 제안 수정
     assert calendar_preview.status_code == 200, calendar_preview.text
-    assert calendar_preview.json()["events"][0]["reasons"]
+    calendar_events = calendar_preview.json()["events"]
+    assert calendar_events, calendar_preview.text
+    assert calendar_events[0]["reasons"]
🤖 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 `@tests/test_api.py` around lines 240 - 246, Update the calendar preview
assertions after the POST request to first assert that the JSON response’s
events collection is non-empty, including the response text for diagnostics,
before indexing events[0]. Preserve the existing assertion that the first event
contains reasons.

77-97: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_create_profile과 거의 동일한 헬퍼가 복제되었습니다.

이름과 생일만 다르므로 기존 헬퍼에 기본값 인자를 추가하는 편이 낫습니다.

♻️ 제안 리팩터
-def _create_profile(client: TestClient) -> dict[str, object]:
+def _create_profile(
+    client: TestClient,
+    name: str = "공개 테스트 예시",
+    birth_day: int = 1,
+) -> dict[str, object]:
     response = client.post(
         "/api/profiles",
         auth=_auth(),
         json={
-            "name": "공개 테스트 예시",
+            "name": name,
             ...
-            "birth_day": 1,
+            "birth_day": birth_day,
🤖 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 `@tests/test_api.py` around lines 77 - 97, Replace the duplicated
_create_secondary_profile helper by extending _create_profile with optional name
and birth-data defaults, then reuse it for the secondary profile while
overriding only the differing values. Preserve the existing request payload and
behavior for callers that rely on _create_profile’s current defaults.

202-221: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

과거 구간을 명시해 테스트하므로 “현재 이후만 추천” 규칙이 검증되지 않습니다.

PR 목표의 핵심 제약(현재 시각 이후에 끝나는 창만 선택)은 start_date/end_date를 생략한 기본 경로에서만 동작합니다. 여기서는 2000-01 구간을 명시해 그 경로를 우회하므로, 회귀가 나도 잡히지 않습니다. 범위를 생략한 케이스를 하나 추가해 모든 이벤트 start가 현재 이후인지 확인해 주세요. 원하시면 해당 테스트를 작성해 드리겠습니다.

🤖 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 `@tests/test_api.py` around lines 202 - 221, Update the compatibility preview
test around the POST request to add a case that omits start_date and end_date,
exercising the default future-window selection path. Assert every returned
event’s start value is at or after the current time, while preserving the
existing explicit historical-range assertions.
app/static/app.js (2)

439-443: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

같은 사람 선택 검증을 프로필 생성 이전으로 옮기는 편이 안전합니다.

현재는 resolvePairProfile이 두 번 실행되어 신규 프로필을 서버에 생성한 뒤에야 동일 여부를 검사합니다. 저장된 프로필을 양쪽에 동일하게 고른 경우는 생성이 없어 무해하지만, 한쪽이 신규 생성 중 후속 단계가 실패하면 고아 프로필이 남습니다. 선택된 *_profile_id 값을 먼저 비교하면 불필요한 쓰기를 줄일 수 있습니다.

♻️ 제안 변경
   try {
+    const primaryChoiceId = form.elements.namedItem("primary_profile_id").value;
+    const secondaryChoiceId = form.elements.namedItem("secondary_profile_id").value;
+    if (primaryChoiceId && primaryChoiceId === secondaryChoiceId) {
+      throw new Error("서로 다른 두 사람을 선택하세요.");
+    }
     const primary = await resolvePairProfile(form, "primary");
     const secondary = await resolvePairProfile(form, "secondary");
     if (primary.id === secondary.id) {
       throw new Error("서로 다른 두 사람을 선택하세요.");
     }
🤖 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 `@app/static/app.js` around lines 439 - 443, Move the same-person validation
before the two resolvePairProfile calls by comparing the selected
primary_profile_id and secondary_profile_id values from form. Reject only when
both IDs are present and equal, then continue resolving profiles so new-profile
creation is skipped for this invalid selection.

101-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

renderProfiles()의 조기 반환이 페어 선택지 갱신까지 막습니다.

프로필이 0개일 때 Line 104-108에서 반환되므로 [data-profile-choice] 재구성과 toggleNewPersonFields 호출이 실행되지 않습니다. 초기 상태에서는 초기 HTML과 동일해 눈에 띄지 않지만, 프로필이 모두 제거되는 경로에서는 이전 옵션이 그대로 남습니다. 단일 선택 패널 렌더링과 페어 선택지 렌더링을 분리해 두면 안전합니다.

♻️ 제안 리팩터
 function renderProfiles() {
+  renderProfileChoices();
   const select = $("`#profile-select`");
   const selected = select.value;
   if (!state.profiles.length) {
     select.innerHTML = '<option value="">먼저 출생 프로필을 저장하세요</option>';
     $("`#chart-result`").hidden = true;
     return;
   }
   ...
-  document.querySelectorAll("[data-profile-choice]").forEach((choice) => {
-    ...
-  });
 }
+
+function renderProfileChoices() {
+  document.querySelectorAll("[data-profile-choice]").forEach((choice) => {
+    const previous = choice.value;
+    choice.innerHTML = [
+      '<option value="">새로운 사람을 입력할게요</option>',
+      ...state.profiles.map((profile) => profileOption(profile)),
+    ].join("");
+    choice.value = state.profiles.some((profile) => profile.id === previous) ? previous : "";
+    toggleNewPersonFields(choice);
+  });
+}
🤖 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 `@app/static/app.js` around lines 101 - 126, Update renderProfiles so the
zero-profile branch only handles the profile-select and chart-result state
without returning before the [data-profile-choice] elements are rebuilt. Ensure
the pair-choice option rendering and toggleNewPersonFields calls run for both
empty and non-empty state.profiles, while preserving the existing
selected-profile behavior when profiles exist.
app/static/styles.css (1)

378-381: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

9px 라벨은 가독성이 지나치게 낮습니다.

.score-badge span의 font-size: 9px는 본문 최소 권장치를 크게 밑돌아 저시력 사용자에게 사실상 읽히지 않습니다. 배지 크기(60px)를 유지하려면 11~12px 정도로 올리거나, 텍스트를 시각적으로 숨기고 aria-label만 남기는 편이 낫습니다.

🤖 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 `@app/static/styles.css` around lines 378 - 381, Update the .score-badge span
styling to increase its font-size from 9px to an accessible 11–12px value while
preserving the existing 60px badge dimensions and other styling.
scripts/acceptance_smoke.py (1)

75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

next()가 StopIteration으로 실패하면 원인 파악이 어렵습니다.

Line 73에서 .ics 부분 문자열만 확인하므로 href 요소에 .ics가 없는 응답에서는 진단 메시지 없이 StopIteration이 납니다. next(..., None) + assert로 바꾸면 실패 응답을 함께 남길 수 있습니다.

♻️ 제안 변경
-    href = next(
+    href = next(
         (
             element.text
             for element in root.iter()
             if element.tag.endswith("href")
             and element.text
             and element.text.endswith(".ics")
-        )
+        ),
+        None,
     )
+    assert href, listing[:500]
🤖 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 `@scripts/acceptance_smoke.py` around lines 75 - 81, Update the href extraction
expression around root.iter() to use next(..., None) instead of allowing
StopIteration, then assert that a matching .ics href was found with a diagnostic
message including the response content. Preserve the existing element.text and
.ics filtering behavior.
app/static/index.html (1)

61-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

두 인물 필드셋이 접두사만 다른 완전 중복 마크업입니다.

primary/secondary 카드가 약 150줄 동일 구조로 복제되어 있어, 필드 추가·문구 수정 시 두 곳을 동시에 고쳐야 합니다. <template> + JS 복제(접두사 치환) 또는 서버 템플릿 부분화를 검토해 보세요. 지금 당장 동작 문제는 없으니 후속 작업으로 미뤄도 됩니다.

🤖 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 `@app/static/index.html` around lines 61 - 215, Refactor the duplicated
person-card markup in the primary and secondary sections by extracting the
shared structure into a reusable template or server-side partial, then render
each card with its person-specific prefix, labels, defaults, and data
attributes. Preserve all existing field names, validation behavior, Korean text,
and primary/secondary distinctions while ensuring generated IDs and selectors
remain unique.
app/compatibility.py (1)

152-203: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

limit과 무관하게 전체 날짜 범위를 항상 스캔합니다.

not_before/limit을 최종적으로만 적용하고, 실제로는 start_date~end_date(기본 365일) 전체 윈도우를 계산한 뒤 정렬해 자릅니다. 미리보기 기본값 limit=12인 경우에도 항상 1년치 창을 전부 계산하게 되어 사용자 대기 시간에 불필요한 부하가 생깁니다. iter_chart_windows가 날짜 오름차순으로 창을 내보내므로, 날짜가 바뀌는 시점에 이미 len(best_by_date) >= limit이면 조기 종료할 수 있습니다.

♻️ 제안하는 조기 종료 로직
     best_by_date: dict[date, CompatibilityCandidate] = {}
+    seen_date: date | None = None
     for window in iter_chart_windows(
         start_date,
         end_date,
         timezone,
         time_mode,
         longitude,
     ):
+        window_date = window.start.date()
+        if seen_date is not None and window_date != seen_date and len(best_by_date) >= limit:
+            break
+        seen_date = window_date
         if not_before is not None and window.end <= not_before:
             continue
🤖 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 `@app/compatibility.py` around lines 152 - 203, Update
generate_compatibility_candidates to stop scanning iter_chart_windows once
len(best_by_date) reaches limit at a date boundary, relying on its ascending
date order. Track the current candidate date or equivalent boundary state, break
only after completing all windows for that date, and preserve not_before
filtering, per-day best-candidate selection, sorting, and limiting behavior.
🤖 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 `@app/main.py`:
- Around line 94-105: 중복된 호환성 캘린더 기본값과 요청 재구성 로직을 공통 상수·헬퍼로 통합하세요.
CompatibilityCalendarCreate.limit, _windows, preview_calendar에서 동일한 limit 기본값을
재사용하고, 저장된 calendar로부터 CompatibilityRequest를 만드는 로직은 하나의 헬퍼로 추출해 두 분기에서 호출하세요.
"balanced_branch_harmony"도 공통 상수로 정의해 preview_calendar, preview_compatibility,
create_compatibility_calendar에서 재사용하세요.

In `@app/static/app.js`:
- Around line 547-549: Update the score branch in the item rendering template to
check whether item.score exists rather than whether it is truthy, so a score of
0 uses the score display and does not access rule-calendar fields such as
hour_stem_korean.

In `@README.md`:
- Around line 30-31: README의 해당 안내 문구에서 UI에 표시되지 않는 `고급: 직접 조건 만들기` 라벨을 제거하고,
index.html의 실제 라벨인 `고급 기능 · 명식과 조건을 직접 골라 캘린더 만들기`로 교체하세요.

In `@scripts/acceptance_smoke.py`:
- Around line 82-87: Update the request flow around request so server-root
absolute href values are joined directly with the origin rather than
caldav_base, avoiding duplicated path prefixes. Remove the now-unnecessary
caldav_base parameter from the relevant function signature and both call sites,
while preserving the existing username and password handling.
- Around line 251-297: Update the compatibility preview and calendar sync flow
in the acceptance smoke test to use identical fixed start_date and end_date
values for both requests, ensuring both event counts are calculated over the
same time window before comparing pair_synced["event_count"] with
pair_preview["count"].

---

Nitpick comments:
In `@app/compatibility.py`:
- Around line 152-203: Update generate_compatibility_candidates to stop scanning
iter_chart_windows once len(best_by_date) reaches limit at a date boundary,
relying on its ascending date order. Track the current candidate date or
equivalent boundary state, break only after completing all windows for that
date, and preserve not_before filtering, per-day best-candidate selection,
sorting, and limiting behavior.

In `@app/static/app.js`:
- Around line 439-443: Move the same-person validation before the two
resolvePairProfile calls by comparing the selected primary_profile_id and
secondary_profile_id values from form. Reject only when both IDs are present and
equal, then continue resolving profiles so new-profile creation is skipped for
this invalid selection.
- Around line 101-126: Update renderProfiles so the zero-profile branch only
handles the profile-select and chart-result state without returning before the
[data-profile-choice] elements are rebuilt. Ensure the pair-choice option
rendering and toggleNewPersonFields calls run for both empty and non-empty
state.profiles, while preserving the existing selected-profile behavior when
profiles exist.

In `@app/static/index.html`:
- Around line 61-215: Refactor the duplicated person-card markup in the primary
and secondary sections by extracting the shared structure into a reusable
template or server-side partial, then render each card with its person-specific
prefix, labels, defaults, and data attributes. Preserve all existing field
names, validation behavior, Korean text, and primary/secondary distinctions
while ensuring generated IDs and selectors remain unique.

In `@app/static/styles.css`:
- Around line 378-381: Update the .score-badge span styling to increase its
font-size from 9px to an accessible 11–12px value while preserving the existing
60px badge dimensions and other styling.

In `@scripts/acceptance_smoke.py`:
- Around line 75-81: Update the href extraction expression around root.iter() to
use next(..., None) instead of allowing StopIteration, then assert that a
matching .ics href was found with a diagnostic message including the response
content. Preserve the existing element.text and .ics filtering behavior.

In `@tests/test_api.py`:
- Around line 240-246: Update the calendar preview assertions after the POST
request to first assert that the JSON response’s events collection is non-empty,
including the response text for diagnostics, before indexing events[0]. Preserve
the existing assertion that the first event contains reasons.
- Around line 77-97: Replace the duplicated _create_secondary_profile helper by
extending _create_profile with optional name and birth-data defaults, then reuse
it for the secondary profile while overriding only the differing values.
Preserve the existing request payload and behavior for callers that rely on
_create_profile’s current defaults.
- Around line 202-221: Update the compatibility preview test around the POST
request to add a case that omits start_date and end_date, exercising the default
future-window selection path. Assert every returned event’s start value is at or
after the current time, while preserving the existing explicit historical-range
assertions.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ef8e4b2-186a-46ac-9b34-b5a9f1a61079

📥 Commits

Reviewing files that changed from the base of the PR and between 2560407 and f99910f.

📒 Files selected for processing (15)
  • README.md
  • app/compatibility.py
  • app/events.py
  • app/main.py
  • app/static/app.js
  • app/static/index.html
  • app/static/styles.css
  • app/store.py
  • docs/ARCHITECTURE.md
  • docs/research/README.md
  • scripts/acceptance_smoke.py
  • tests/test_api.py
  • tests/test_compatibility.py
  • tests/test_events.py
  • tests/test_store.py

Comment thread app/main.py
Comment on lines +94 to +105
class CompatibilityCalendarCreate(BaseModel):
primary_profile_id: str = Field(min_length=1, max_length=80)
secondary_profile_id: str = Field(min_length=1, max_length=80)
name: str = Field(min_length=1, max_length=100)
slug: str = Field(
min_length=1,
max_length=80,
pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$",
)
visibility: Literal["private", "confidential", "public"] = "private"
limit: int = Field(default=36, ge=1, le=96)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

호환성 캘린더의 기본값·문자열이 3곳에 중복되어 있습니다.

limit 기본값 36이 CompatibilityCalendarCreate.limit(Line 104), _windows(Line 271), preview_calendar(Line 463)에 각각 하드코딩되어 있고, 저장된 calendar에서 CompatibilityRequest를 재구성하는 로직 자체도 _windows와 preview_calendar에 거의 동일하게 복제되어 있습니다. "balanced_branch_harmony" 문자열도 preview_calendar, preview_compatibility, create_compatibility_calendar 세 곳에 반복됩니다. 한 곳만 수정하면 나머지가 조용히 어긋날 수 있으므로 공통 헬퍼와 상수로 묶는 것을 권장합니다.

♻️ 제안하는 리팩터링
+DEFAULT_COMPATIBILITY_LIMIT = 36
+COMPATIBILITY_METHOD = "balanced_branch_harmony"
+
+
+def _compatibility_request_from_calendar(
+    calendar: dict[str, object], requested: DateRange
+) -> CompatibilityRequest:
+    settings = dict(calendar["rule"])
+    return CompatibilityRequest(
+        primary_profile_id=str(calendar["profile_id"]),
+        secondary_profile_id=str(calendar["secondary_profile_id"]),
+        start_date=requested.start_date,
+        end_date=requested.end_date,
+        limit=int(settings.get("limit", DEFAULT_COMPATIBILITY_LIMIT)),
+    )

그 뒤 _windows와 preview_calendar의 compatibility 분기에서 이 헬퍼를 재사용합니다.

Also applies to: 256-275, 451-475, 492-504, 506-536

🤖 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 `@app/main.py` around lines 94 - 105, 중복된 호환성 캘린더 기본값과 요청 재구성 로직을 공통 상수·헬퍼로
통합하세요. CompatibilityCalendarCreate.limit, _windows, preview_calendar에서 동일한 limit
기본값을 재사용하고, 저장된 calendar로부터 CompatibilityRequest를 만드는 로직은 하나의 헬퍼로 추출해 두 분기에서
호출하세요. "balanced_branch_harmony"도 공통 상수로 정의해 preview_calendar,
preview_compatibility, create_compatibility_calendar에서 재사용하세요.

Comment thread app/static/app.js
Comment on lines +547 to +549
${item.score
? `· 조화 점수 ${escapeHtml(item.score)}점 · ${escapeHtml(item.label)}`
: `· 일지 ${escapeHtml(item.day_branch_korean)}, 시간 ${escapeHtml(item.hour_stem_korean)}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

item.score의 truthy 검사는 점수 0에서 잘못된 분기를 탑니다.

호환성 캘린더 이벤트라도 score가 0이면 else 분기로 넘어가 규칙 캘린더 전용 필드(hour_stem_korean)를 읽게 되고, 값이 없으면 undefined가 그대로 노출됩니다. 존재 여부로 판정하세요.

🐛 제안 수정
-          ${item.score
+          ${item.score !== undefined && item.score !== null
             ? `· 조화 점수 ${escapeHtml(item.score)}점 · ${escapeHtml(item.label)}`
             : `· 일지 ${escapeHtml(item.day_branch_korean)}, 시간 ${escapeHtml(item.hour_stem_korean)}`}
📝 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.

Suggested change
${item.score
? `· 조화 점수 ${escapeHtml(item.score)}점 · ${escapeHtml(item.label)}`
: `· 일지 ${escapeHtml(item.day_branch_korean)}, 시간 ${escapeHtml(item.hour_stem_korean)}`}
${item.score !== undefined && item.score !== null
? `· 조화 점수 ${escapeHtml(item.score)}점 · ${escapeHtml(item.label)}`
: `· 일지 ${escapeHtml(item.day_branch_korean)}, 시간 ${escapeHtml(item.hour_stem_korean)}`}
🤖 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 `@app/static/app.js` around lines 547 - 549, Update the score branch in the
item rendering template to check whether item.score exists rather than whether
it is truthy, so a score of 0 uses the score display and does not access
rule-calendar fields such as hour_stem_korean.

Comment thread README.md
Comment on lines +30 to +31
보여 줍니다. 더 세밀한 천간·지지 조건은 아래의 `고급: 직접 조건 만들기`에서
계속 사용할 수 있습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

UI에 없는 라벨을 인용하고 있습니다.

app/static/index.html Line 275-276의 실제 문구는 “고급 기능 · 명식과 조건을 직접 골라 캘린더 만들기”입니다. README의 고급: 직접 조건 만들기와 일치하지 않아 사용자가 해당 항목을 찾기 어렵습니다.

📝 제안 수정
-보여 줍니다. 더 세밀한 천간·지지 조건은 아래의 `고급: 직접 조건 만들기`에서
+보여 줍니다. 더 세밀한 천간·지지 조건은 아래의 `고급 기능`에서
 계속 사용할 수 있습니다.
📝 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.

Suggested change
보여 줍니다. 더 세밀한 천간·지지 조건은 아래의 `고급: 직접 조건 만들기`에서
계속 사용할 수 있습니다.
보여 줍니다. 더 세밀한 천간·지지 조건은 아래의 `고급 기능`에서
계속 사용할 수 있습니다.
🤖 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 `@README.md` around lines 30 - 31, README의 해당 안내 문구에서 UI에 표시되지 않는 `고급: 직접 조건
만들기` 라벨을 제거하고, index.html의 실제 라벨인 `고급 기능 · 명식과 조건을 직접 골라 캘린더 만들기`로 교체하세요.

Comment on lines +82 to +87
status, event = request(
"GET",
urljoin(caldav_base.rstrip("/") + "/", href.lstrip("/")),
username,
password,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

caldav_base에 경로 접두어가 있으면 href 결합이 어긋납니다.

PROPFIND가 돌려주는 href는 서버 루트 기준 절대 경로(예: /dav/operator/smoke-1/x.ics)입니다. href.lstrip("/")로 상대화한 뒤 caldav_base(예: http://host/dav/)에 붙이면 접두어가 중복됩니다. 절대 href는 그대로 오리진에 결합되도록 urljoin에 맡기는 편이 안전합니다.

🐛 제안 수정
     status, event = request(
         "GET",
-        urljoin(caldav_base.rstrip("/") + "/", href.lstrip("/")),
+        urljoin(collection_url, href),
         username,
         password,
     )

이 경우 caldav_base 매개변수가 불필요해지므로 시그니처와 두 호출부(Line 216-222, Line 298-304)에서도 제거할 수 있습니다.

🤖 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 `@scripts/acceptance_smoke.py` around lines 82 - 87, Update the request flow
around request so server-root absolute href values are joined directly with the
origin rather than caldav_base, avoiding duplicated path prefixes. Remove the
now-unnecessary caldav_base parameter from the relevant function signature and
both call sites, while preserving the existing username and password handling.

Comment on lines +251 to +297
status, pair_preview = api_json(
"POST",
app_base,
"/api/compatibility/preview",
app_user,
app_password,
{
"primary_profile_id": profile_id,
"secondary_profile_id": secondary_profile_id,
"limit": 12,
},
)
assert status == 200 and isinstance(pair_preview, dict), (status, pair_preview)
assert int(pair_preview["count"]) > 0

status, pair_calendar = api_json(
"POST",
app_base,
"/api/compatibility/calendars",
app_user,
app_password,
{
"primary_profile_id": profile_id,
"secondary_profile_id": secondary_profile_id,
"name": "둘이 좋은 시간",
"slug": f"pair-smoke-{suffix}",
"visibility": visibility,
"limit": 12,
},
)
assert status == 201 and isinstance(pair_calendar, dict), (
status,
pair_calendar,
)
status, pair_synced = api_json(
"POST",
app_base,
f"/api/calendars/{pair_calendar['id']}/sync",
app_user,
app_password,
{},
)
assert status == 200 and isinstance(pair_synced, dict), (
status,
pair_synced,
)
assert pair_synced["event_count"] == pair_preview["count"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 호환성 후보 선택이 현재 시각 기준인지, DateRange 기본값이 무엇인지 확인
rg -n -C4 'now|utcnow|datetime\.now' app/compatibility.py app/events.py
ast-grep run --pattern 'class DateRange($$$): $$$' --lang python app/main.py

Repository: ContextualWisdomLab/saju-caldav

Length of output: 310


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 후보 선택/동기화 관련 파일 위치와 핵심 정의를 확인
fd -a 'compatibility.py|events.py|main.py' app .

echo '--- app/compatibility.py outline ---'
ast-grep outline app/compatibility.py --view expanded || true

echo '--- app/events.py outline ---'
ast-grep outline app/events.py --view expanded || true

echo '--- app/main.py outline ---'
ast-grep outline app/main.py --view expanded | sed -n '1,220p' || true

echo '--- relevant excerpts from app/compatibility.py ---'
cat -n app/compatibility.py | sed -n '1,260p'

echo '--- relevant excerpts from app/events.py ---'
cat -n app/events.py | sed -n '1,260p'

echo '--- DateRange definition in app/main.py ---'
cat -n app/main.py | sed -n '75,110p'

Repository: ContextualWisdomLab/saju-caldav

Length of output: 17929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- app/main.py _resolve_date_range and compatibility flow ---'
cat -n app/main.py | sed -n '107,300p'

echo '--- tests around compatibility behavior ---'
cat -n tests/test_compatibility.py | sed -n '1,260p'

Repository: ContextualWisdomLab/saju-caldav

Length of output: 14714


preview/sync 개수 비교는 고정 기준시각이 없어 흔들릴 수 있습니다. preview와 sync가 각각 현재 시각을 다시 기준으로 후보를 계산하고, start_date가 비어 있으면 이후 창만 남기기 때문에 두 호출 사이에 시간이 지나면 pair_synced["event_count"]가 pair_preview["count"]보다 달라질 수 있습니다. 같은 start_date/end_date를 넘기거나, 등식을 더 느슨한 조건으로 바꾸세요.

🤖 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 `@scripts/acceptance_smoke.py` around lines 251 - 297, Update the compatibility
preview and calendar sync flow in the acceptance smoke test to use identical
fixed start_date and end_date values for both requests, ensuring both event
counts are calculated over the same time window before comparing
pair_synced["event_count"] with pair_preview["count"].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants