아키텍처#

소스 구조#

플러그인의 모든 로직은 src/ 아래에 있습니다:

파일

역할

main.ts

플러그인 진입점: 설정을 불러오고, 사이드바/월간 뷰, 에디터 서제스트, 라이브 프리뷰 확장, 포스트프로세서를 등록합니다. 사이드바에서 캘린더 열기, 새 탭에서 캘린더 열기, 캘린더 새로고침 명령을 추가합니다.

types.ts

공유 타입: CalDavEvent, CalDavCalendar, CalDavAccount, CalDavTimezone, HarangCalendarSettings, CalendarScope, HrcalScope, NoteEvent, CalendarListItem.

settings.ts / settingsTab.ts

기본 설정값과 설정 UI. settingsTab.ts는 Obsidian의 더 최신 선언형 getSettingDefinitions() API(1.13.0+)만 구현하며, 기존의 명령형 display() 폴백은 없습니다 - 플러그인이 명시한 minAppVersion이 1.13.4이므로 안전합니다.

i18n.ts

공식 getLanguage() API로 현재 Obsidian UI 언어를 확인하고, 작은 ko/en 사전에서 일치하는 문자열을 반환합니다(지원하지 않는 언어는 영어로 대체).

caldav/client.ts

최소한의 읽기 전용 CalDAV 클라이언트입니다: Obsidian의 requestUrl을 통해 캘린더 검색(PROPFIND/current-user-principal/calendar-home-set)과 일정 가져오기(REPORT calendar-query, 필요시 시간 범위로 제한)를 수행합니다.

caldav/ics.ts

VEVENT 속성을 CalDavEvent로 추출하는 iCalendar(RFC 5545) 파서입니다. VALARM 등 하위 컴포넌트는 건너뛰어서 그 속성이 상위 이벤트로 새어 들어가지 않게 합니다.

caldav/recurrence.ts

RRULE이 있는 CalDavEventrrule 패키지를 통해 특정 범위 안의 구체적인 발생 일정으로 전개합니다(EXDATE 항목은 제외).

caldav/timezone.ts

계정에 설정된 시간대(Intl을 통한 IANA 시간대 이름, 또는 고정 UTC 오프셋)를 해석하고, 설정 드롭다운용으로 알려진 IANA 시간대 목록을 제공합니다.

caldav/store.ts

CalendarStore: 설정된 모든 계정/캘린더의 일정을 병합해 캐시하며, TTL 기반 만료, 월간 뷰의 범위 탐색을 위한 온디맨드 공백 채우기, 범위 지정/검색/조회 헬퍼를 제공합니다.

editorSuggest/HrcalEditorSuggest.ts, dateCandidates.ts

{{hrcal:에서 트리거됩니다. 별도의 @date[/@event[ 트리거 없이 단계별로 이어지는 하나의 서제스터로, 입력한 검색어를 :로 나눠 지금이 어느 단계인지(계정 이름 - 이름으로 검색해 그 계정의 id로 해석됩니다, 그 계정의 캘린더 이름, 그다음 날짜 후보/일정 제목 통합 검색) 판단해서 {{hrcal:<accountId>:<calendarName>:date:YYYY-MM-DD}} 또는 {{hrcal:<accountId>:<calendarName>:event:<uid>}}를 삽입합니다.

render/frontmatterScope.ts

노트의 harang-account/harang-calendar 프론트매터를 CalendarScope로 읽어들입니다. 현재 사용되지 않습니다 - {{hrcal:...}} 참조가 노트 프론트매터에 의존하는 대신 계정/캘린더를 직접 명시하므로, 이 함수를 호출하는 곳이 없습니다.

notes/noteEvents.ts

harang-date/harang-repeat/harang-time 프론트매터가 있는지 보관소를 스캔하고, 각각을 범위 안의 NoteEvent 발생 항목으로 전개합니다 — Obsidian 안에서만 일어나고 CalDAV 서버와는 무관합니다. caldav/recurrence.ts를 재사용하지 않고 종일 날짜에 안전한 자체 RRULE 전개 로직을 따로 만들었습니다(아래 참고). 위의 frontmatterScope.ts와는 무관합니다 - 다른 프론트매터 키를 쓰고, 목적도 다릅니다.

render/dateWidget.ts

{{hrcal:...:date:YYYY-MM-DD}} 카드 위젯을 만듭니다: 제목, 참조에 담긴 계정 id/캘린더 이름이 등록된 것과 일치하지 않을 때의 경고, 그리고 그 특정 계정/캘린더의 그날 일정을 클릭 가능한 행으로 표시합니다.

render/eventChip.ts / render/eventCard.ts

인라인 일정 칩과, 클릭하면 열리는 상세 팝업(직접 위치를 계산하는 플로팅 패널, 바깥 클릭이나 Esc로 닫힘)입니다. 조회 범위는 {{hrcal:<accountId>:<calendarName>:event:<uid>}} 참조 자체에 담긴 accountId/calendarName으로 정해집니다 — 참조가 이미 정확히 하나의 일정을 가리키기 때문입니다.

render/livePreview.ts

Live Preview에서 {{hrcal:...}} 범위(date/event 두 종류 모두, 정규식 하나로)를 위젯/칩으로 바꾸는 CodeMirror 6 ViewPlugin입니다. 커서나 선택 영역이 겹치는 범위는 건너뛰어서 원문 문법을 계속 편집할 수 있게 합니다.

render/postProcessor.ts

읽기 모드에서 같은 치환을 수행하는 마크다운 포스트프로세서로, 렌더링된 텍스트 노드를 직접 스캔합니다 - {{...}}는 위키링크 문법이 아니므로, 예전의 [[cal:...]]/[[event:...]] 문법과 달리 Obsidian이 이를 a.internal-link로 미리 파싱하는 일이 없고, Live Preview와 읽기 모드가 이제 완전히 동일한 원문 텍스트 매칭 경로를 공유합니다.

view/agenda.ts, view/AgendaItemView.ts, vue/AgendaView.vue

사이드바 아젠다 목록입니다: 고정 30일 범위의 CalendarListItem(CalDAV 일정과 노트 일정을 합친 것)을 로컬 날짜별로 묶어서, ItemView가 호스팅하는 Vue 3 컴포넌트로 렌더링합니다.

view/monthGrid.ts, view/MonthItemView.ts, vue/MonthView.vue

전체 탭 월간 그리드 뷰로(아젠다 목록과 같은 CalendarListItem 병합 사용), 키보드(방향키) 날짜 탐색과 월 경계 처리까지 포함합니다.

view/MonthPickerModal.ts

월간 뷰 제목에서 여는 연도/월 선택창으로, 클릭하면 편집 가능해지는 연도 입력란이 있습니다.

데이터 흐름#

settingsTab.ts          -->  CalDavAccount[] (server URL, credentials, calendars)
     |
     v
caldav/client.ts         -->  PROPFIND/REPORT over requestUrl
     |                        (discovery + event fetch)
     v
caldav/ics.ts              -->  parses each VEVENT into a CalDavEvent
     |
     v
caldav/recurrence.ts         -->  expands RRULE occurrences on demand
     |
     v
caldav/store.ts                -->  merged, cached, scoped CalDavEvent[]
     |
     +--> editorSuggest/*.ts          -->  autocomplete while typing
     |
     +--> render/livePreview.ts       -->  widget/chip (Live Preview)
     |
     +--> render/postProcessor.ts     -->  widget/chip (Reading view)
     |        |
     |        v
     |  render/eventChip.ts + eventCard.ts  -->  click-to-open detail popup
     |
     +--> view/AgendaItemView.ts / vue/AgendaView.vue   -->  sidebar list
     |
     +--> view/MonthItemView.ts / vue/MonthView.vue     -->  month grid

notes/noteEvents.ts (vault-wide frontmatter scan) --> NoteEvent[]
     |
     +--> (merged into a CalendarListItem[] alongside CalDavEvent[],
           inside AgendaItemView.ts/MonthItemView.ts, right before
           the sidebar list / month grid above render - nothing else
           in this diagram ever sees a NoteEvent)

참조 문법#

{{hrcal:<accountId>:<calendarName>:date:YYYY-MM-DD}}는 날짜 위젯으로, {{hrcal:<accountId>:<calendarName>:event:<uid>}}는 일정 칩으로 렌더링됩니다. 보통은 단계별 {{hrcal: 에디터 서제스트로 삽입되지만, 둘 다 직접 손으로 입력해도 됩니다 — 해석할 수 없는 참조는 조용히 실패하는 대신 흐릿한 점선 칩으로(또는 계정/캘린더가 등록된 것과 일치하지 않는 날짜라면 위젯 안의 경고로) 표시됩니다.

두 종류 모두 항상 참조에 직접 명시된 accountId/calendarName으로 범위가 정해집니다 - 노트 프론트매터(harang-account/harang-calendar, 사용법 참고)는 더 이상 여기서 아무 역할도 하지 않습니다. accountId는 표시 이름이 아니라 계정의 안정적인 내부 id로, 계정이 생성될 때 한 번 만들어져 이후로 바뀌지 않으므로, 이 id 기반 방식이 적용된 뒤에 삽입한 참조는 설정에서 계정 이름을 바꿔도 영향을 받지 않습니다. 반면 calendarName은 여전히 캘린더의 표시 이름이라, 캘린더 이름을 바꾸면 이 변경 이전과 마찬가지로 기존 참조가 깨질 수 있습니다. id-또는-이름 대체 처리(fallback)가 없으므로, 이 변경 이전에 작성된 참조 — 계정의 예전 이름 기반 구간을 담고 있는 — 는 더 이상 해석되지 않으며 {{hrcal: 에디터 서제스트로 지우고 다시 삽입해야 합니다.

노트 일정(harang-date/harang-repeat/harang-time)#

위 참조 문법과는 무관합니다: harang-date 프론트매터(사용법 참고)가 있는 노트는 NoteEvent가 되어 사이드바 아젠다 목록과 월간 그리드에만 항목으로 나타납니다 — {{hrcal:...}} 날짜 위젯이나 일정 칩에는 절대 나타나지 않고, CalDAV 서버에 쓰거나 읽는 일도 전혀 없습니다. harang-repeatcaldav/recurrence.ts와 같은 rrule 패키지를 재사용하지만, 그 모듈의 함수를 그대로 공유하는 대신 자체적으로 처음부터 작성한 전개 로직을 씁니다(위 notes/noteEvents.ts 참고) — 노트 일정은 기본적으로 시간 개념이 전혀 없어서, 이 프로젝트에서 여러 번 겪고(그리고 고친) 로컬/UTC 종일 날짜 불일치를 여기서도 겪지 않으려면 날짜 계산 전체를 일관되게 UTC 자정 기준으로 처리해야 하기 때문입니다. harang-time은 그 종일 날짜 계산 위에 선택적인 HH:MM-HH:MM 표시 범위를 얹을 뿐입니다 — NoteEvent가 시간대가 있는 CalDAV 일정과 나란히 어떻게 표시되고 정렬되는지에만 영향을 주고, (여전히 UTC 자정 기준인) 날짜 전개 자체는 바뀌지 않습니다.

rrule 외에는 런타임 의존성 없음#

Obsidian 자체가 제공하는 것(obsidian 패키지, CodeMirror 6, Vue 3)과 반복 일정 전개용 rrule 패키지 외에는 런타임 의존성이 없습니다 — CalDAV 클라이언트와 iCalendar 파서 모두 npm에서 가져오지 않고 직접 작성해서, 번들 크기를 작게 유지하고 서드파티 HTTP/XML 파싱 라이브러리 자체의 취약점에 노출되는 것을 피합니다.