두 사람의 좋은 날짜와 시간 찾기 - #12
Conversation
📝 WalkthroughWalkthrough두 사람의 출생 차트를 비교해 현재 이후의 시간 창을 점수화하고, 날짜별 추천 후보를 미리보기·호환성 CalDAV 캘린더로 저장·동기화하는 API, 저장소, UI, 테스트가 추가되었습니다. Changes호환성 추천 흐름
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: 캘린더 생성 응답
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
kindand an optionalsecondary_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.
There was a problem hiding this comment.
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 value9px 라벨은 가독성이 지나치게 낮습니다.
.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
📒 Files selected for processing (15)
README.mdapp/compatibility.pyapp/events.pyapp/main.pyapp/static/app.jsapp/static/index.htmlapp/static/styles.cssapp/store.pydocs/ARCHITECTURE.mddocs/research/README.mdscripts/acceptance_smoke.pytests/test_api.pytests/test_compatibility.pytests/test_events.pytests/test_store.py
| 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) | ||
|
|
There was a problem hiding this comment.
📐 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에서 재사용하세요.
| ${item.score | ||
| ? `· 조화 점수 ${escapeHtml(item.score)}점 · ${escapeHtml(item.label)}` | ||
| : `· 일지 ${escapeHtml(item.day_branch_korean)}, 시간 ${escapeHtml(item.hour_stem_korean)}`} |
There was a problem hiding this comment.
🎯 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.
| ${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.
| 보여 줍니다. 더 세밀한 천간·지지 조건은 아래의 `고급: 직접 조건 만들기`에서 | ||
| 계속 사용할 수 있습니다. |
There was a problem hiding this comment.
📐 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.
| 보여 줍니다. 더 세밀한 천간·지지 조건은 아래의 `고급: 직접 조건 만들기`에서 | |
| 계속 사용할 수 있습니다. | |
| 보여 줍니다. 더 세밀한 천간·지지 조건은 아래의 `고급 기능`에서 | |
| 계속 사용할 수 있습니다. |
🤖 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의 실제 라벨인 `고급 기능 · 명식과 조건을 직접 골라 캘린더 만들기`로 교체하세요.
| status, event = request( | ||
| "GET", | ||
| urljoin(caldav_base.rstrip("/") + "/", href.lstrip("/")), | ||
| username, | ||
| password, | ||
| ) |
There was a problem hiding this comment.
🎯 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.
| 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"] |
There was a problem hiding this comment.
🩺 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.pyRepository: 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"].
무엇이 달라졌나요
근거와 한계
Chao Wei-pang의 1946년 연구에 기록된 일주 중심 해석과 지지의 육합·삼합·충 관계를 문화사 자료로 참고했습니다. 점수는 저장소가 명시한 제품 관례이며 과학적 예측이나 관계의 객관적 판정으로 표시하지 않습니다.
검증
uv run --frozen pytest -q— 57 passeduv run --frozen ruff check .node --check app/static/app.jsSummary by CodeRabbit
새 기능
개선
문서