├── .cursorrules ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README-ko.md ├── README.md ├── esbuild.config.mjs ├── manifest.json ├── package-lock.json ├── package.json ├── screenshots ├── Editing Note.gif └── Keyboard Navigating.gif ├── src ├── __tests__ │ ├── CardRender.test.ts │ ├── CardRenderTest.ts │ ├── mocks │ │ └── services.ts │ └── setup.ts ├── application │ ├── factories │ │ ├── CardFactory.ts │ │ └── CardSetFactory.ts │ ├── manager │ │ ├── CardDisplayManager.ts │ │ ├── CardManager.ts │ │ ├── CardRenderManager.ts │ │ ├── FocusManager.ts │ │ └── PresetManager.ts │ ├── services │ │ ├── application │ │ │ ├── ActiveFileWatcher.ts │ │ │ ├── CardFocusService.ts │ │ │ ├── CardNavigatorService.ts │ │ │ ├── ClipboardService.ts │ │ │ ├── EventDispatcherService.ts │ │ │ ├── FileService.ts │ │ │ ├── FocusService.ts │ │ │ ├── LayoutService.ts │ │ │ ├── PresetService.ts │ │ │ ├── ScrollService.ts │ │ │ ├── SearchService.ts │ │ │ ├── SettingsService.ts │ │ │ ├── SortService.ts │ │ │ ├── ToolbarService.ts │ │ │ └── ViewService.ts │ │ └── domain │ │ │ ├── CacheService.ts │ │ │ ├── CardInteractionService.ts │ │ │ ├── CardSelectionService.ts │ │ │ ├── CardService.ts │ │ │ └── CardSetService.ts │ └── viewmodels │ │ └── CardNavigatorViewModel.ts ├── domain │ ├── errors │ │ ├── CacheServiceError.ts │ │ ├── CardNavigatorError.ts │ │ ├── CardServiceError.ts │ │ ├── CardSetError.ts │ │ ├── CardSetServiceError.ts │ │ ├── ContainerError.ts │ │ ├── DomainError.ts │ │ ├── ErrorHandlerError.ts │ │ ├── EventError.ts │ │ ├── EventErrorType.ts │ │ ├── FocusServiceError.ts │ │ ├── LayoutServiceError.ts │ │ ├── PresetError.ts │ │ ├── PresetServiceError.ts │ │ ├── SearchServiceError.ts │ │ └── SortServiceError.ts │ ├── events │ │ ├── ActiveFileEvents.ts │ │ ├── CacheEvents.ts │ │ ├── CardEvents.ts │ │ ├── CardInteractionEvents.ts │ │ ├── CardSetEvents.ts │ │ ├── ClipboardEvents.ts │ │ ├── DomainEvent.ts │ │ ├── DomainEventDispatcher.ts │ │ ├── DomainEventType.ts │ │ ├── EventBus.ts │ │ ├── EventHandler.ts │ │ ├── FileEvents.ts │ │ ├── FocusEvents.ts │ │ ├── IDomainEvent.ts │ │ ├── IDomainEventHandler.ts │ │ ├── IEventMetadata.ts │ │ ├── LayoutEvents.ts │ │ ├── PresetEvents.ts │ │ ├── ScrollEvents.ts │ │ ├── SearchEvents.ts │ │ ├── SettingsEvents.ts │ │ ├── SortEvents.ts │ │ ├── ToolbarEvents.ts │ │ └── ViewEvents.ts │ ├── factories │ │ ├── ICardFactory.ts │ │ └── ICardSetFactory.ts │ ├── infrastructure │ │ ├── IAnalyticsService.ts │ │ ├── IErrorHandler.ts │ │ ├── IEventDispatcher.ts │ │ ├── ILoggingService.ts │ │ └── IPerformanceMonitor.ts │ ├── managers │ │ ├── ICardDisplayManager.ts │ │ ├── ICardManager.ts │ │ ├── ICardRenderManager.ts │ │ ├── IFocusManager.ts │ │ └── IPresetManager.ts │ ├── models │ │ ├── Card.ts │ │ ├── CardNavigatorState.ts │ │ ├── CardSet.ts │ │ ├── Layout.ts │ │ ├── PluginSettings.ts │ │ ├── Preset.ts │ │ ├── Search.ts │ │ └── Sort.ts │ ├── services │ │ ├── application │ │ │ ├── IActiveFileWatcher.ts │ │ │ ├── ICardFocusService.ts │ │ │ ├── ICardNavigatorService.ts │ │ │ ├── IClipboardService.ts │ │ │ ├── IFileService.ts │ │ │ ├── IFocusService.ts │ │ │ ├── ILayoutService.ts │ │ │ ├── IPresetService.ts │ │ │ ├── IScrollService.ts │ │ │ ├── ISearchService.ts │ │ │ ├── ISettingsService.ts │ │ │ ├── ISortService.ts │ │ │ ├── IToolbarService.ts │ │ │ └── IViewService.ts │ │ └── domain │ │ │ ├── ICacheService.ts │ │ │ ├── ICardInteractionService.ts │ │ │ ├── ICardSelectionService.ts │ │ │ ├── ICardService.ts │ │ │ └── ICardSetService.ts │ ├── utils │ │ ├── Debouncer.ts │ │ ├── Throttler.ts │ │ ├── fileSystemUtils.ts │ │ ├── layoutUtils.ts │ │ ├── markdownRenderer.ts │ │ └── settingsUtils.ts │ └── viewmodels │ │ └── ICardNavigatorViewModel.ts ├── infrastructure │ ├── AnalyticsService.ts │ ├── ErrorHandler.ts │ ├── LoggingService.ts │ ├── PerformanceMonitor.ts │ ├── di │ │ ├── Container.ts │ │ └── register.ts │ └── events │ │ ├── EventBus.ts │ │ └── EventDispatcher.ts ├── locales │ ├── en.json │ └── ko.json ├── main.ts └── ui │ ├── interfaces │ ├── ICardNavigatorView.ts │ └── ICardNavigatorViewModel.ts │ ├── modals │ ├── FolderSuggestModal.ts │ └── TagSuggestModal.ts │ ├── settings │ ├── CardNavigatorSettingTab.ts │ ├── components │ │ └── CardPreview.ts │ └── sections │ │ ├── CardSetSettingsSection.ts │ │ ├── CardSettingsSection.ts │ │ ├── LayoutSettingsSection.ts │ │ ├── PresetSettingsSection.ts │ │ ├── SearchSettingsSection.ts │ │ └── SortSettingsSection.ts │ └── views │ └── CardNavigatorView.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs ├── versions.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/ban-ts-comment": "off", 19 | "no-prototype-builtins": "off", 20 | "@typescript-eslint/no-empty-function": "off", 21 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | main.js 6 | *.js.map 7 | 8 | # IDE 9 | .idea/ 10 | .vscode/ 11 | *.swp 12 | *.swo 13 | 14 | # OS 15 | .DS_Store 16 | Thumbs.db 17 | 18 | # Logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Environment variables 25 | .env 26 | .env.local 27 | .env.*.local 28 | 29 | # Coverage 30 | coverage/ 31 | 32 | # Cache 33 | .cache/ 34 | .eslintcache 35 | 36 | # Intellij 37 | *.iml 38 | 39 | # Exclude sourcemaps 40 | *.map 41 | 42 | # obsidian 43 | data.json 44 | 45 | # Exclude macOS Finder (System Explorer) View States 46 | .DS_Store 47 | old/ 48 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 wakeyi-git 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-ko.md: -------------------------------------------------------------------------------- 1 | # Card Navigator 2 | 3 | Card Navigator는 노트를 시각화하고 탐색하는 독특한 방법을 제공하는 Obsidian 플러그인입니다. 노트를 가로 또는 세로로 스크롤 가능한 카드 형태로 표시하여 콘텐츠를 더 쉽게 탐색하고 관리할 수 있습니다. 4 | 5 | ## 기능 6 | 7 | - 사용자 지정 가능한 정보(파일 이름, 첫 번째 헤더, 내용)로 노트를 카드로 표시 8 | - 가로 또는 세로 스크롤 뷰 9 | - 뷰당 카드 수 사용자 지정 가능 10 | - 다양한 기준(이름, 생성 날짜, 수정 날짜)으로 카드 정렬 11 | - 키보드 단축키를 사용한 빠른 카드 간 탐색 12 | - 현재 폴더 내 검색 기능 13 | - 쉬운 노트 연결을 위한 드래그 앤 드롭 지원 14 | - 사용자 지정 가능한 카드 외관(글꼴 크기, 내용 길이 등) 15 | - 다양한 설정 간 빠른 전환을 위한 프리셋 관리 (1.0.1) 16 | - 특정 폴더의 카드 보기를 위한 폴더 선택 17 | - 카드 내용을 HTML로 렌더링하는 옵션 18 | - 균일한 모양을 위한 카드 높이 정렬 19 | - 접근성 향상을 위한 키보드 내비게이션 지원 (1.0.2) 20 | - 카드에 대한 빠른 작업을 위한 컨텍스트 메뉴 (1.0.2) 21 | - 영어와 한국어 다국어 지원 22 | - 다양한 레이아웃 옵션: 자동, 리스트, 그리드, 메이슨리 (1.0.3) 23 | 24 | ![alt text]() 25 | ![alt text]() 26 | 27 | ## 설치 28 | 29 | 1. 이 플러그인은 아직 Obsidian의 검토 과정을 통과하지 않았습니다. 30 | 2. Releases 섹션에서 main.js, manifest.json, styles.css 파일을 다운로드합니다. 31 | 3. .obsidian/plugins/ 폴더 안에 적절한 이름의 새 폴더를 만들고 2단계에서 다운로드한 파일들을 그 안에 붙여넣습니다. 32 | 4. Obsidian을 열고 설정으로 이동합니다. 33 | 5. 커뮤니티 플러그인으로 이동하여 안전 모드를 비활성화합니다. 34 | 6. 플러그인을 활성화합니다. 35 | 36 | ## 사용법 37 | 38 | 설치 후, 다음과 같은 방법으로 Card Navigator 뷰를 열 수 있습니다: 39 | 40 | 1. 왼쪽 사이드바의 Card Navigator 아이콘 클릭 41 | 2. 명령 팔레트를 사용하여 "Open Card Navigator" 검색 42 | 43 | ### 기본 탐색 44 | 45 | - 스크롤 휠이나 트랙패드를 사용하여 카드 간 이동 46 | - 카드를 클릭하여 해당 노트 열기 47 | - 툴바의 검색 바를 사용하여 카드 필터링 48 | 49 | ### 사용자 지정 50 | 51 | 1. Card Navigator 툴바의 설정 아이콘 클릭 52 | 2. 뷰당 카드 수, 카드 외관, 표시 옵션 등의 설정 조정 53 | 3. 빠른 설정 전환을 위한 프리셋 생성 및 관리 54 | 55 | ### 키보드 단축키 56 | 57 | Card Navigator는 다양한 탐색 키보드 단축키를 지원합니다: 58 | 59 | - 카드 위로 스크롤 60 | - 카드 아래로 스크롤 61 | - 카드 왼쪽으로 스크롤 62 | - 카드 오른쪽으로 스크롤 63 | - 페이지 위/왼쪽으로 스크롤 64 | - 페이지 아래/오른쪽으로 스크롤 65 | - 활성 카드 중앙 정렬 66 | - Card Navigator 포커스 (1.0.2) 67 | - 위/아래 화살표: 카드 간 수직 이동 (1.0.2) 68 | - 왼쪽/오른쪽 화살표: 카드 간 수평 이동 (1.0.2) 69 | - Enter: 포커스된 카드 열기 (1.0.2) 70 | - 컨텍스트 메뉴 키 또는 Cmd/Ctrl + E: 포커스된 카드의 컨텍스트 메뉴 열기 (1.0.2) 71 | 72 | Obsidian의 단축키 설정에서 이러한 단축키를 사용자 지정할 수 있습니다. 이 작업에 대한 단축키를 설정하려면: 73 | 74 | 1. 설정 → 단축키로 이동 75 | 2. "Card Navigator" 검색 76 | 3. 각 작업에 원하는 키 조합 할당 77 | 78 | ## 설정 79 | 80 | Card Navigator는 다양한 사용자 지정 옵션을 제공합니다: 81 | 82 | - **컨테이너 설정**: 뷰당 카드 수 및 카드 정렬 조정 83 | - **카드 설정**: 카드에 표시되는 정보 사용자 지정 (파일 이름, 첫 번째 헤더, 내용) 84 | - **외관**: 다양한 카드 요소의 글꼴 크기 설정 85 | - **정렬**: 카드의 기본 정렬 방법 선택 86 | - **폴더 선택**: 활성 파일의 폴더 또는 선택한 폴더 사용 옵션 87 | - **프리셋**: 다양한 구성 저장 및 불러오기 88 | - **레이아웃**: 자동, 리스트, 그리드, 메이슨리 레이아웃 중 선택 (1.0.3) 89 | 90 | ## 다국어 지원 91 | 92 | Card Navigator는 이제 다음 언어를 지원합니다: 93 | 94 | - 영어 95 | - 한국어 96 | 97 | 플러그인은 지원되는 경우 자동으로 Obsidian 인터페이스 언어를 사용합니다. GitHub 저장소에 풀 리퀘스트를 제출하여 다른 언어의 번역에 기여할 수 있습니다. 98 | 99 | ## 프리셋 100 | 101 | Card Navigator는 이제 프리셋을 지원하여 다양한 설정을 저장하고 빠르게 전환할 수 있습니다. 이 기능은 노트를 보고 상호작용하는 방식에 유연성을 제공하여 워크플로우를 향상시킵니다. 102 | 103 | ### 주요 기능 104 | 105 | 1. **사용자 정의 프리셋 생성**: 현재 Card Navigator 설정을 이름이 지정된 프리셋으로 저장하여 나중에 사용할 수 있습니다. 106 | 107 | 2. **전역 프리셋**: 모든 폴더에 적용되는 기본 프리셋을 설정할 수 있습니다 (폴더별 설정으로 재정의되지 않는 한). 108 | 109 | 3. **폴더별 프리셋**: 특정 폴더에 다른 프리셋을 할당하여 폴더 구조에 따라 맞춤형 뷰를 제공합니다. 110 | 111 | 4. **프리셋 자동 적용**: 폴더 간 이동 시 적절한 프리셋을 자동으로 적용합니다. 112 | 113 | 5. **프리셋 가져오기/내보내기**: 프리셋 파일을 가져오거나 내보내 프리셋을 공유하거나 설정을 백업할 수 있습니다. 114 | 115 | 6. **프리셋 관리**: 설정 패널에서 직접 기존 프리셋을 편집, 복제 또는 삭제할 수 있습니다. 116 | 117 | ### 프리셋 사용 방법 118 | 119 | 1. **프리셋 생성**: 120 | - Card Navigator 설정으로 이동 121 | - 원하는 설정을 구성 122 | - 프리셋 관리 섹션에서 "+" 버튼 클릭 123 | - 프리셋 이름을 지정하고 선택적으로 설명 추가 124 | 125 | 2. **프리셋 적용**: 126 | - 전역: 전역 기본값으로 사용할 프리셋 토글 127 | - 폴더별: 폴더 프리셋 섹션에서 특정 폴더에 프리셋 할당 128 | 129 | 3. **프리셋 자동 적용**: 130 | - 설정에서 "프리셋 자동 적용" 활성화 131 | - Card Navigator가 현재 폴더에 따라 적절한 프리셋으로 자동 전환 132 | 133 | 4. **프리셋 가져오기/내보내기**: 134 | - 프리셋 관리 섹션의 가져오기/내보내기 버튼 사용 135 | - 내보낸 프리셋은 쉽게 공유하거나 백업할 수 있는 JSON 파일로 저장됨 136 | 137 | 프리셋을 사용하면 보관함의 다른 부분이나 다른 유형의 노트에 맞게 Card Navigator의 동작을 조정할 수 있어 생산성과 노트 작성 경험을 향상시킬 수 있습니다. 138 | 139 | ### 키보드 내비게이션 (1.0.2) 140 | 141 | Card Navigator는 이제 포괄적인 키보드 내비게이션 지원을 제공하여 마우스를 사용하지 않고도 효율적으로 노트를 탐색하고 상호 작용할 수 있습니다. 키보드 내비게이션 기능 사용법은 다음과 같습니다: 142 | 143 | 1. **Card Navigator 포커싱**: 144 | - 할당된 단축키를 사용하여 Card Navigator에 포커스 (Obsidian의 단축키 설정에서 구성 가능). 145 | - 포커스되면 현재 카드가 강조 표시됩니다. 146 | 147 | 2. **카드 간 이동**: 148 | - 화살표 키를 사용하여 카드 간 이동: 149 | - 왼쪽/오른쪽: 카드 간 수평 이동 150 | - 위/아래: 카드 간 수직 이동 151 | - PageUp/PageDown: 한 페이지의 카드를 위 또는 아래로 스크롤 152 | - Home: 첫 번째 카드로 이동 153 | - End: 마지막 카드로 이동 154 | 155 | 3. **카드와 상호 작용**: 156 | - Enter: Obsidian에서 포커스된 카드 열기 157 | - 컨텍스트 메뉴 키 또는 사용자 지정 단축키: 포커스된 카드의 컨텍스트 메뉴 열기 158 | 159 | 4. **컨텍스트 메뉴 작업**: 160 | - 컨텍스트 메뉴가 열려 있을 때, 화살표 키를 사용하여 메뉴 항목 탐색 161 | - Enter: 강조 표시된 메뉴 항목 선택 162 | 163 | 5. **Card Navigator 포커스 종료**: 164 | - Tab을 누르거나 Card Navigator 외부를 클릭하여 포커스 모드 종료 165 | 166 | 키보드 내비게이션은 모든 레이아웃 옵션(자동, 리스트, 그리드, 메이슨리)과 원활하게 작동하며, 현재 레이아웃에 따라 동작을 조정합니다. 167 | 168 | ### 레이아웃 옵션 (1.0.3) 169 | 170 | Card Navigator는 이제 사용자의 선호도에 맞는 다양한 레이아웃 옵션을 제공합니다: 171 | 172 | 1. 자동: 사용 가능한 공간에 따라 리스트와 그리드 레이아웃 사이를 자동으로 조정 173 | 2. 리스트: 카드를 단일 열로 표시, 세로 또는 가로로 표시 가능 174 | 3. 그리드: 카드를 고정 열 그리드 레이아웃으로 배열 175 | 4. 메이슨리: 카드의 높이가 다양할 수 있는 동적 그리드 생성 176 | 177 | 레이아웃을 변경하려면: 178 | 179 | 1. Card Navigator 설정 열기 180 | 2. "레이아웃 설정" 섹션으로 이동 181 | 3. "기본 레이아웃" 드롭다운에서 원하는 레이아웃 선택 182 | 4. 각 레이아웃 유형에 특정한 추가 설정 조정 (예: 그리드 및 메이슨리 레이아웃의 열 수) 183 | 184 | ## 피드백 및 지원 185 | 186 | 문제가 발생하거나 개선을 위한 제안이 있으면 [GitHub 저장소](https://github.com/wakeyi-git/obsidian-card-navigator-plugin)를 방문하여 이슈를 열거나 프로젝트에 기여해 주세요. 187 | 188 | ## 라이선스 189 | 190 | [MIT](LICENSE) 191 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | loader: { 42 | '.html': 'text' 43 | } 44 | }); 45 | 46 | if (prod) { 47 | await context.rebuild(); 48 | process.exit(0); 49 | } else { 50 | await context.watch(); 51 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-card-navigator", 3 | "name": "Card Navigator", 4 | "version": "1.2.6", 5 | "minAppVersion": "1.8.0", 6 | "description": "노트를 시각화하고 탐색하는 독특한 방법을 제공하는 Obsidian 플러그인입니다.", 7 | "author": "wakeyi", 8 | "authorUrl": "https://github.com/wakeyi", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-card-navigator", 3 | "version": "1.2.6", 4 | "description": "노트를 시각화하고 탐색하는 독특한 방법을 제공하는 Obsidian 플러그인입니다.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json", 10 | "test": "vitest", 11 | "test:watch": "vitest watch", 12 | "test:coverage": "vitest run --coverage" 13 | }, 14 | "keywords": [ 15 | "obsidian", 16 | "plugin", 17 | "card", 18 | "navigator", 19 | "visualization" 20 | ], 21 | "author": "wakeyi", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@testing-library/dom": "^10.4.0", 25 | "@testing-library/jest-dom": "^6.6.3", 26 | "@testing-library/react": "^16.3.0", 27 | "@types/node": "^16.11.6", 28 | "@types/react": "^19.1.1", 29 | "@types/react-dom": "^19.1.2", 30 | "@typescript-eslint/eslint-plugin": "^5.29.0", 31 | "@typescript-eslint/parser": "^5.29.0", 32 | "builtin-modules": "^3.2.0", 33 | "esbuild": "0.17.3", 34 | "jsdom": "^26.0.0", 35 | "obsidian": "latest", 36 | "react": "^19.1.0", 37 | "react-dom": "^19.1.0", 38 | "tslib": "^2.8.1", 39 | "typescript": "4.7.4", 40 | "vitest": "^3.1.1" 41 | }, 42 | "dependencies": { 43 | "@popperjs/core": "^2.11.8", 44 | "@types/handlebars": "^4.0.40", 45 | "@types/uuid": "^10.0.0", 46 | "handlebars": "^4.7.8", 47 | "i18next": "^23.15.1", 48 | "inversify": "^7.5.0", 49 | "lucide": "^0.485.0", 50 | "reflect-metadata": "^0.2.2", 51 | "rxjs": "^7.8.2", 52 | "tsyringe": "^4.9.1", 53 | "uuid": "^11.1.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /screenshots/Editing Note.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakeyi-git/obsidian-card-navigator-plugin/2b4dd95ed3ba62d0a9f0bcc953c63fe8992f31ef/screenshots/Editing Note.gif -------------------------------------------------------------------------------- /screenshots/Keyboard Navigating.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakeyi-git/obsidian-card-navigator-plugin/2b4dd95ed3ba62d0a9f0bcc953c63fe8992f31ef/screenshots/Keyboard Navigating.gif -------------------------------------------------------------------------------- /src/__tests__/CardRender.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { CardRenderManager } from '@/application/manager/CardRenderManager'; 3 | import { ICard, IRenderConfig, IRenderState, RenderStatus, RenderType } from '@/domain/models/Card'; 4 | import { Container } from '@/infrastructure/di/Container'; 5 | import { mockErrorHandler, mockLoggingService, mockPerformanceMonitor, mockAnalyticsService } from './mocks/services'; 6 | import { EventBus } from '@/domain/events/EventBus'; 7 | 8 | describe('카드 렌더링 테스트', () => { 9 | let cardRenderManager: CardRenderManager; 10 | let mockCard: ICard; 11 | let mockRenderConfig: IRenderConfig; 12 | let eventBus: EventBus; 13 | 14 | beforeEach(() => { 15 | const container = Container.getInstance(); 16 | container.registerInstance('IErrorHandler', mockErrorHandler); 17 | container.registerInstance('ILoggingService', mockLoggingService); 18 | container.registerInstance('IPerformanceMonitor', mockPerformanceMonitor); 19 | container.registerInstance('IAnalyticsService', mockAnalyticsService); 20 | eventBus = EventBus.getInstance(); 21 | container.registerInstance('EventBus', eventBus); 22 | 23 | cardRenderManager = CardRenderManager.getInstance(); 24 | cardRenderManager.initialize(); 25 | 26 | mockCard = { 27 | id: 'test-card-1', 28 | file: null, 29 | filePath: '/test/path.md', 30 | title: '테스트 카드', 31 | fileName: 'test.md', 32 | firstHeader: '# 테스트 카드', 33 | content: '테스트 카드 내용', 34 | tags: ['테스트', '카드'], 35 | properties: {}, 36 | createdAt: new Date(), 37 | updatedAt: new Date(), 38 | validate: () => true, 39 | preview: () => ({ 40 | id: 'test-card-1', 41 | file: null, 42 | filePath: '/test/path.md', 43 | title: '테스트 카드', 44 | fileName: 'test.md', 45 | firstHeader: '# 테스트 카드', 46 | content: '테스트 카드 내용', 47 | tags: ['테스트', '카드'], 48 | properties: {}, 49 | createdAt: new Date(), 50 | updatedAt: new Date() 51 | }), 52 | toString: () => '테스트 카드' 53 | }; 54 | 55 | mockRenderConfig = { 56 | type: RenderType.TEXT, 57 | contentLengthLimitEnabled: false, 58 | contentLengthLimit: 0, 59 | style: { 60 | classes: ['card'], 61 | backgroundColor: '#ffffff', 62 | fontSize: '14px', 63 | color: '#000000', 64 | border: { 65 | width: '1px', 66 | color: '#cccccc', 67 | style: 'solid', 68 | radius: '4px' 69 | }, 70 | padding: '10px', 71 | boxShadow: 'none', 72 | lineHeight: '1.5', 73 | fontFamily: 'inherit' 74 | }, 75 | state: { 76 | status: RenderStatus.COMPLETED, 77 | startTime: Date.now(), 78 | endTime: Date.now(), 79 | error: null, 80 | timestamp: Date.now() 81 | } 82 | }; 83 | }); 84 | 85 | afterEach(() => { 86 | cardRenderManager.cleanup(); 87 | eventBus.cleanup(); 88 | }); 89 | 90 | it('카드 렌더링 매니저 초기화 테스트', () => { 91 | expect(cardRenderManager.isInitialized()).toBe(true); 92 | }); 93 | 94 | it('카드 렌더링 상태 등록 테스트', () => { 95 | const renderState: IRenderState = { 96 | status: RenderStatus.COMPLETED, 97 | startTime: Date.now(), 98 | endTime: Date.now(), 99 | error: null, 100 | timestamp: Date.now() 101 | }; 102 | 103 | cardRenderManager.registerRenderState(mockCard.id, renderState); 104 | const state = cardRenderManager.getRenderState(mockCard.id); 105 | 106 | expect(state).toEqual(renderState); 107 | }); 108 | 109 | it('카드 렌더링 테스트', () => { 110 | const cardElement = cardRenderManager.renderCard(mockCard); 111 | 112 | expect(cardElement).toBeDefined(); 113 | expect(cardElement.classList.contains('card-navigator-card')).toBe(true); 114 | expect(cardElement.querySelector('.card-navigator-card-header')).toBeDefined(); 115 | expect(cardElement.querySelector('.card-navigator-card-body')).toBeDefined(); 116 | expect(cardElement.querySelector('.card-navigator-card-footer')).toBeDefined(); 117 | }); 118 | 119 | it('카드 렌더링 이벤트 구독 테스트', () => { 120 | let eventReceived = false; 121 | 122 | cardRenderManager.subscribeToRenderEvents((event) => { 123 | if (event.type === 'render' && event.cardId === mockCard.id) { 124 | eventReceived = true; 125 | } 126 | }); 127 | 128 | cardRenderManager.renderCard(mockCard); 129 | 130 | expect(eventReceived).toBe(true); 131 | }); 132 | 133 | it('카드 렌더링 리소스 관리 테스트', () => { 134 | const testResource = { data: 'test' }; 135 | 136 | cardRenderManager.registerRenderResource(mockCard.id, testResource); 137 | const resource = cardRenderManager.getRenderResource(mockCard.id); 138 | 139 | expect(resource).toEqual(testResource); 140 | 141 | cardRenderManager.unregisterRenderResource(mockCard.id); 142 | const removedResource = cardRenderManager.getRenderResource(mockCard.id); 143 | 144 | expect(removedResource).toBeNull(); 145 | }); 146 | }); -------------------------------------------------------------------------------- /src/__tests__/CardRenderTest.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { CardRenderManager } from '@/application/manager/CardRenderManager'; 3 | import { ICard, IRenderConfig, IRenderState, RenderStatus, RenderType } from '@/domain/models/Card'; 4 | import { Container } from '@/infrastructure/di/Container'; 5 | import { IErrorHandler } from '@/domain/infrastructure/IErrorHandler'; 6 | import { ILoggingService } from '@/domain/infrastructure/ILoggingService'; 7 | import { IPerformanceMonitor } from '@/domain/infrastructure/IPerformanceMonitor'; 8 | import { IAnalyticsService } from '@/domain/infrastructure/IAnalyticsService'; 9 | 10 | describe('카드 렌더링 테스트', () => { 11 | let cardRenderManager: CardRenderManager; 12 | let mockCard: ICard; 13 | let mockRenderConfig: IRenderConfig; 14 | 15 | beforeEach(() => { 16 | const container = Container.getInstance(); 17 | cardRenderManager = CardRenderManager.getInstance(); 18 | cardRenderManager.initialize(); 19 | 20 | mockCard = { 21 | id: 'test-card-1', 22 | file: null, 23 | filePath: '/test/path.md', 24 | title: '테스트 카드', 25 | fileName: 'test.md', 26 | firstHeader: '# 테스트 카드', 27 | content: '테스트 카드 내용', 28 | tags: ['테스트', '카드'], 29 | properties: {}, 30 | createdAt: new Date(), 31 | updatedAt: new Date(), 32 | validate: () => true, 33 | preview: () => ({ 34 | id: 'test-card-1', 35 | file: null, 36 | filePath: '/test/path.md', 37 | title: '테스트 카드', 38 | fileName: 'test.md', 39 | firstHeader: '# 테스트 카드', 40 | content: '테스트 카드 내용', 41 | tags: ['테스트', '카드'], 42 | properties: {}, 43 | createdAt: new Date(), 44 | updatedAt: new Date() 45 | }), 46 | toString: () => '테스트 카드' 47 | }; 48 | 49 | mockRenderConfig = { 50 | type: RenderType.TEXT, 51 | contentLengthLimitEnabled: false, 52 | contentLengthLimit: 0, 53 | style: { 54 | classes: ['card'], 55 | backgroundColor: '#ffffff', 56 | fontSize: '14px', 57 | color: '#000000', 58 | border: { 59 | width: '1px', 60 | color: '#cccccc', 61 | style: 'solid', 62 | radius: '4px' 63 | }, 64 | padding: '10px', 65 | boxShadow: 'none', 66 | lineHeight: '1.5', 67 | fontFamily: 'inherit' 68 | }, 69 | state: { 70 | status: RenderStatus.COMPLETED, 71 | startTime: Date.now(), 72 | endTime: Date.now(), 73 | error: null, 74 | timestamp: Date.now() 75 | } 76 | }; 77 | }); 78 | 79 | afterEach(() => { 80 | cardRenderManager.cleanup(); 81 | }); 82 | 83 | it('카드 렌더링 매니저 초기화 테스트', () => { 84 | expect(cardRenderManager.isInitialized()).toBe(true); 85 | }); 86 | 87 | it('카드 렌더링 상태 등록 테스트', () => { 88 | const renderState: IRenderState = { 89 | status: RenderStatus.COMPLETED, 90 | startTime: Date.now(), 91 | endTime: Date.now(), 92 | error: null, 93 | timestamp: Date.now() 94 | }; 95 | 96 | cardRenderManager.registerRenderState(mockCard.id, renderState); 97 | const state = cardRenderManager.getRenderState(mockCard.id); 98 | 99 | expect(state).toEqual(renderState); 100 | }); 101 | 102 | it('카드 렌더링 테스트', () => { 103 | const cardElement = cardRenderManager.renderCard(mockCard); 104 | 105 | expect(cardElement).toBeDefined(); 106 | expect(cardElement.classList.contains('card')).toBe(true); 107 | expect(cardElement.querySelector('.card-header')).toBeDefined(); 108 | expect(cardElement.querySelector('.card-body')).toBeDefined(); 109 | expect(cardElement.querySelector('.card-footer')).toBeDefined(); 110 | }); 111 | 112 | it('카드 렌더링 이벤트 구독 테스트', () => { 113 | let eventReceived = false; 114 | 115 | cardRenderManager.subscribeToRenderEvents((event) => { 116 | if (event.type === 'render' && event.cardId === mockCard.id) { 117 | eventReceived = true; 118 | } 119 | }); 120 | 121 | cardRenderManager.renderCard(mockCard); 122 | 123 | expect(eventReceived).toBe(true); 124 | }); 125 | 126 | it('카드 렌더링 리소스 관리 테스트', () => { 127 | const testResource = { data: 'test' }; 128 | 129 | cardRenderManager.registerRenderResource(mockCard.id, testResource); 130 | const resource = cardRenderManager.getRenderResource(mockCard.id); 131 | 132 | expect(resource).toEqual(testResource); 133 | 134 | cardRenderManager.unregisterRenderResource(mockCard.id); 135 | const removedResource = cardRenderManager.getRenderResource(mockCard.id); 136 | 137 | expect(removedResource).toBeNull(); 138 | }); 139 | }); -------------------------------------------------------------------------------- /src/__tests__/mocks/services.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | import { IErrorHandler } from '@/domain/infrastructure/IErrorHandler'; 3 | import { ILoggingService } from '@/domain/infrastructure/ILoggingService'; 4 | import { IPerformanceMonitor } from '@/domain/infrastructure/IPerformanceMonitor'; 5 | import { IAnalyticsService } from '@/domain/infrastructure/IAnalyticsService'; 6 | 7 | export const mockErrorHandler: IErrorHandler = { 8 | handleError: vi.fn() 9 | }; 10 | 11 | export const mockLoggingService: ILoggingService = { 12 | debug: vi.fn(), 13 | info: vi.fn(), 14 | warn: vi.fn(), 15 | error: vi.fn() 16 | }; 17 | 18 | export const mockPerformanceMonitor: IPerformanceMonitor = { 19 | startTimer: vi.fn().mockReturnValue({ stop: vi.fn() }), 20 | measure: vi.fn(), 21 | measureAsync: vi.fn(), 22 | getMeasurement: vi.fn().mockReturnValue({ 23 | count: 0, 24 | average: 0, 25 | min: 0, 26 | max: 0, 27 | total: 0 28 | }), 29 | getAllMeasurements: vi.fn().mockReturnValue({}), 30 | clearMeasurement: vi.fn(), 31 | clearAllMeasurements: vi.fn(), 32 | logMemoryUsage: vi.fn(), 33 | clearMetrics: vi.fn() 34 | }; 35 | 36 | export const mockAnalyticsService: IAnalyticsService = { 37 | trackEvent: vi.fn(), 38 | getEvents: vi.fn().mockReturnValue([]), 39 | getEventCount: vi.fn().mockReturnValue(0), 40 | reset: vi.fn(), 41 | exportData: vi.fn().mockReturnValue('{}') 42 | }; -------------------------------------------------------------------------------- /src/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import { expect, afterEach } from 'vitest'; 2 | import { cleanup } from '@testing-library/react'; 3 | import * as matchers from '@testing-library/jest-dom/matchers'; 4 | 5 | // DOM 테스트를 위한 matchers 추가 6 | expect.extend(matchers); 7 | 8 | // 각 테스트 후 DOM 정리 9 | afterEach(() => { 10 | cleanup(); 11 | }); -------------------------------------------------------------------------------- /src/application/factories/CardSetFactory.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { ICardSet, CardSetType, ICardSetConfig, CardSet } from '@/domain/models/CardSet'; 3 | import { ICardSetFactory } from '@/domain/factories/ICardSetFactory'; 4 | 5 | /** 6 | * 카드셋 생성을 위한 팩토리 클래스 7 | */ 8 | @injectable() 9 | export class CardSetFactory implements ICardSetFactory { 10 | private static instance: CardSetFactory; 11 | 12 | private constructor() {} 13 | 14 | /** 15 | * 싱글톤 인스턴스 반환 16 | */ 17 | static getInstance(): CardSetFactory { 18 | if (!CardSetFactory.instance) { 19 | CardSetFactory.instance = new CardSetFactory(); 20 | } 21 | return CardSetFactory.instance; 22 | } 23 | 24 | /** 25 | * 카드셋을 생성합니다. 26 | * @param type 카드셋 타입 27 | * @param config 카드셋 설정 28 | * @returns 생성된 카드셋 29 | */ 30 | create(type: CardSetType, config: ICardSetConfig): ICardSet { 31 | return new CardSet(type, config); 32 | } 33 | } -------------------------------------------------------------------------------- /src/domain/errors/CacheServiceError.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from './DomainError'; 2 | 3 | /** 4 | * 캐시 서비스 관련 에러 5 | */ 6 | export class CacheServiceError extends DomainError { 7 | constructor(code: string, message: string) { 8 | super('CACHE_SERVICE', code, message); 9 | } 10 | } -------------------------------------------------------------------------------- /src/domain/errors/CardNavigatorError.ts: -------------------------------------------------------------------------------- 1 | import { DomainError } from './DomainError'; 2 | 3 | /** 4 | * 카드 내비게이터 에러 클래스 5 | */ 6 | export class CardNavigatorError extends DomainError { 7 | constructor(message: string) { 8 | super('CardNavigator', 'CARD_NAVIGATOR_ERROR', message); 9 | } 10 | } -------------------------------------------------------------------------------- /src/domain/errors/CardServiceError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 카드 서비스 에러 클래스 3 | */ 4 | export class CardServiceError extends Error { 5 | constructor( 6 | message: string, 7 | public readonly cardId?: string, 8 | public readonly fileName?: string, 9 | public readonly operation?: 'create' | 'update' | 'delete' | 'load' | 'render' | 'initialize' | 'cleanup' | 'display' | 'select' | 'focus' | 'scroll' | 'updateStyle' | 'updateRenderConfig' | 'updateVisibility' | 'updateZIndex' | 'updateCache' | 'removeCache' | 'clearCache' | 'updateConfig' | 'subscribe' | 'unsubscribe' | 'requestRender', 10 | public readonly cause?: Error 11 | ) { 12 | super(message); 13 | this.name = 'CardServiceError'; 14 | } 15 | 16 | /** 17 | * 에러 정보를 문자열로 변환 18 | */ 19 | toString(): string { 20 | const details = [ 21 | this.message, 22 | this.cardId && `카드 ID: ${this.cardId}`, 23 | this.fileName && `파일명: ${this.fileName}`, 24 | this.operation && `작업: ${this.operation}`, 25 | this.cause && `원인: ${this.cause.message}` 26 | ].filter(Boolean); 27 | 28 | return details.join('\n'); 29 | } 30 | } -------------------------------------------------------------------------------- /src/domain/errors/CardSetError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 카드셋 에러 클래스 3 | */ 4 | export class CardSetError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'CardSetError'; 8 | } 9 | 10 | /** 11 | * 에러 정보를 문자열로 변환 12 | */ 13 | toString(): string { 14 | const details = [ 15 | this.message, 16 | ].filter(Boolean); 17 | 18 | return details.join('\n'); 19 | } 20 | } -------------------------------------------------------------------------------- /src/domain/errors/CardSetServiceError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 카드셋 서비스 오류 클래스 3 | */ 4 | export class CardSetServiceError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'CardSetServiceError'; 8 | } 9 | } -------------------------------------------------------------------------------- /src/domain/errors/ContainerError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 컨테이너 에러 타입 3 | */ 4 | export type ContainerErrorType = 'register' | 'unregister' | 'resolve' | 'clear'; 5 | 6 | /** 7 | * 컨테이너 에러 클래스 8 | */ 9 | export class ContainerError extends Error { 10 | constructor( 11 | message: string, 12 | public readonly token: string, 13 | public readonly errorType: ContainerErrorType, 14 | public readonly metadata: Record = {}, 15 | public readonly cause?: Error 16 | ) { 17 | super(message); 18 | this.name = 'ContainerError'; 19 | } 20 | 21 | /** 22 | * 에러 정보를 문자열로 변환 23 | */ 24 | toString(): string { 25 | const details = [ 26 | this.message, 27 | `서비스: ${this.token}`, 28 | `에러 타입: ${this.errorType}`, 29 | Object.keys(this.metadata).length > 0 && `메타데이터: ${JSON.stringify(this.metadata)}`, 30 | this.cause && `원인: ${this.cause.message}` 31 | ].filter(Boolean); 32 | 33 | return details.join('\n'); 34 | } 35 | } -------------------------------------------------------------------------------- /src/domain/errors/DomainError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 도메인 에러 기본 클래스 3 | */ 4 | export class DomainError extends Error { 5 | constructor( 6 | public readonly domain: string, 7 | public readonly code: string, 8 | message: string 9 | ) { 10 | super(message); 11 | this.name = 'DomainError'; 12 | } 13 | } -------------------------------------------------------------------------------- /src/domain/errors/ErrorHandlerError.ts: -------------------------------------------------------------------------------- 1 | export class ErrorHandlerError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = 'ErrorHandlerError'; 5 | } 6 | } -------------------------------------------------------------------------------- /src/domain/errors/EventError.ts: -------------------------------------------------------------------------------- 1 | import { EventErrorType } from './EventErrorType'; 2 | 3 | /** 4 | * 이벤트 에러 클래스 5 | */ 6 | export class EventError extends Error { 7 | constructor( 8 | message: string, 9 | public readonly eventType: string, 10 | public readonly errorType: EventErrorType, 11 | public readonly metadata: Record = {}, 12 | public readonly cause?: Error 13 | ) { 14 | super(message); 15 | this.name = 'EventError'; 16 | } 17 | 18 | /** 19 | * 에러 정보를 문자열로 변환 20 | */ 21 | toString(): string { 22 | const details = [ 23 | this.message, 24 | `이벤트 타입: ${this.eventType}`, 25 | `에러 타입: ${this.errorType}`, 26 | Object.keys(this.metadata).length > 0 && `메타데이터: ${JSON.stringify(this.metadata)}`, 27 | this.cause && `원인: ${this.cause.message}` 28 | ].filter(Boolean); 29 | 30 | return details.join('\n'); 31 | } 32 | } -------------------------------------------------------------------------------- /src/domain/errors/EventErrorType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 이벤트 에러 타입 3 | */ 4 | export enum EventErrorType { 5 | /** 6 | * 이벤트 발송 실패 7 | */ 8 | DISPATCH = 'dispatch', 9 | 10 | /** 11 | * 이벤트 핸들러 등록 실패 12 | */ 13 | SUBSCRIBE = 'subscribe', 14 | 15 | /** 16 | * 이벤트 핸들러 해제 실패 17 | */ 18 | UNSUBSCRIBE = 'unsubscribe', 19 | 20 | /** 21 | * 이벤트 핸들러 실행 실패 22 | */ 23 | HANDLE = 'handle', 24 | 25 | /** 26 | * 이벤트 유효성 검사 실패 27 | */ 28 | VALIDATION = 'validation' 29 | } -------------------------------------------------------------------------------- /src/domain/errors/FocusServiceError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 포커스 서비스 에러 3 | */ 4 | export class FocusServiceError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'FocusServiceError'; 8 | } 9 | } -------------------------------------------------------------------------------- /src/domain/errors/LayoutServiceError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 레이아웃 서비스 에러 클래스 3 | */ 4 | export class LayoutServiceError extends Error { 5 | constructor( 6 | message: string, 7 | public readonly layoutType?: string, 8 | public readonly viewportSize?: { width: number; height: number }, 9 | public readonly cardCount?: number, 10 | public readonly cause?: Error 11 | ) { 12 | super(message); 13 | this.name = 'LayoutServiceError'; 14 | } 15 | 16 | /** 17 | * 에러 정보를 문자열로 변환 18 | */ 19 | toString(): string { 20 | const details = [ 21 | this.message, 22 | this.layoutType && `레이아웃 타입: ${this.layoutType}`, 23 | this.viewportSize && `뷰포트 크기: ${this.viewportSize.width}x${this.viewportSize.height}`, 24 | this.cardCount && `카드 수: ${this.cardCount}`, 25 | this.cause && `원인: ${this.cause.message}` 26 | ].filter(Boolean); 27 | 28 | return details.join('\n'); 29 | } 30 | } -------------------------------------------------------------------------------- /src/domain/errors/PresetError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 프리셋 에러 클래스 3 | */ 4 | export class PresetError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'PresetError'; 8 | } 9 | 10 | /** 11 | * 에러 정보를 문자열로 변환 12 | */ 13 | toString(): string { 14 | const details = [ 15 | this.message, 16 | ].filter(Boolean); 17 | 18 | return details.join('\n'); 19 | } 20 | } -------------------------------------------------------------------------------- /src/domain/errors/PresetServiceError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 프리셋 서비스 에러 클래스 3 | */ 4 | export class PresetServiceError extends Error { 5 | constructor( 6 | message: string, 7 | public readonly presetId?: string, 8 | public readonly presetName?: string, 9 | public readonly operation?: 'create' | 'update' | 'delete' | 'load' | 'apply' | 'initialize' | 'cleanup' | 'export' | 'import' | 'mapFolder' | 'mapTag' | 'removeMapping' | 'updatePriority', 10 | public readonly cause?: Error 11 | ) { 12 | super(message); 13 | this.name = 'PresetServiceError'; 14 | } 15 | 16 | /** 17 | * 에러 정보를 문자열로 변환 18 | */ 19 | toString(): string { 20 | const details = [ 21 | this.message, 22 | this.presetId && `프리셋 ID: ${this.presetId}`, 23 | this.presetName && `프리셋 이름: ${this.presetName}`, 24 | this.operation && `작업: ${this.operation}`, 25 | this.cause && `원인: ${this.cause.message}` 26 | ].filter(Boolean); 27 | 28 | return details.join('\n'); 29 | } 30 | } -------------------------------------------------------------------------------- /src/domain/errors/SearchServiceError.ts: -------------------------------------------------------------------------------- 1 | import { ISearchCriteria } from '../models/Search'; 2 | 3 | /** 4 | * 검색 서비스 에러 클래스 5 | */ 6 | export class SearchServiceError extends Error { 7 | constructor( 8 | message: string, 9 | public readonly criteria?: ISearchCriteria, 10 | public readonly operation?: 'search' | 'filter' | 'highlight', 11 | public readonly cause?: Error 12 | ) { 13 | super(message); 14 | this.name = 'SearchServiceError'; 15 | } 16 | 17 | /** 18 | * 에러 정보를 문자열로 변환 19 | */ 20 | toString(): string { 21 | const details = [ 22 | this.message, 23 | this.criteria?.query && `검색어: ${this.criteria.query}`, 24 | this.criteria?.scope && `범위: ${this.criteria.scope}`, 25 | this.criteria?.caseSensitive && `대소문자 구분: ${this.criteria.caseSensitive}`, 26 | this.criteria?.useRegex && `정규식 사용: ${this.criteria.useRegex}`, 27 | this.criteria?.wholeWord && `전체 단어 일치: ${this.criteria.wholeWord}`, 28 | this.operation && `작업: ${this.operation}`, 29 | this.cause && `원인: ${this.cause.message}` 30 | ].filter(Boolean); 31 | 32 | return details.join('\n'); 33 | } 34 | } -------------------------------------------------------------------------------- /src/domain/errors/SortServiceError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 정렬 서비스 오류 클래스 3 | */ 4 | export class SortServiceError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'SortServiceError'; 8 | } 9 | } -------------------------------------------------------------------------------- /src/domain/events/ActiveFileEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { TFile } from 'obsidian'; 3 | import { DomainEventType } from './DomainEventType'; 4 | 5 | /** 6 | * 활성 파일 변경 이벤트 7 | */ 8 | export class ActiveFileChangedEvent extends DomainEvent { 9 | constructor(file: TFile | null) { 10 | super(DomainEventType.ACTIVE_FILE_CHANGED, { file }); 11 | } 12 | } 13 | 14 | /** 15 | * 활성 파일 감시 시작 이벤트 16 | */ 17 | export class ActiveFileWatchStartedEvent extends DomainEvent { 18 | constructor(file: TFile | null) { 19 | super(DomainEventType.ACTIVE_FILE_WATCH_STARTED, { file }); 20 | } 21 | } 22 | 23 | /** 24 | * 활성 파일 감시 중지 이벤트 25 | */ 26 | export class ActiveFileWatchStoppedEvent extends DomainEvent { 27 | constructor(file: TFile | null) { 28 | super(DomainEventType.ACTIVE_FILE_WATCH_STOPPED, { file }); 29 | } 30 | } -------------------------------------------------------------------------------- /src/domain/events/CacheEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | 4 | /** 5 | * 캐시 초기화 이벤트 6 | */ 7 | export class CacheInitializedEvent extends DomainEvent { 8 | constructor() { 9 | super(DomainEventType.CACHE_INITIALIZED, {}); 10 | } 11 | } 12 | 13 | /** 14 | * 캐시 정리 이벤트 15 | */ 16 | export class CacheCleanedEvent extends DomainEvent { 17 | constructor() { 18 | super(DomainEventType.CACHE_CLEANED, {}); 19 | } 20 | } 21 | 22 | /** 23 | * 캐시 데이터 저장 이벤트 24 | */ 25 | export class CacheDataStoredEvent extends DomainEvent { 26 | constructor(key: string, data: unknown) { 27 | super(DomainEventType.CACHE_DATA_STORED, { key, data }); 28 | } 29 | } 30 | 31 | /** 32 | * 캐시 데이터 삭제 이벤트 33 | */ 34 | export class CacheDataDeletedEvent extends DomainEvent { 35 | constructor(key: string) { 36 | super(DomainEventType.CACHE_DATA_DELETED, { key }); 37 | } 38 | } 39 | 40 | /** 41 | * 캐시 데이터 초기화 이벤트 42 | */ 43 | export class CacheDataClearedEvent extends DomainEvent { 44 | constructor() { 45 | super(DomainEventType.CACHE_DATA_CLEARED, {}); 46 | } 47 | } -------------------------------------------------------------------------------- /src/domain/events/CardInteractionEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { ICard } from '../models/Card'; 4 | 5 | /** 6 | * 카드 선택 이벤트 7 | */ 8 | export class CardSelectedEvent extends DomainEvent { 9 | constructor(card: ICard) { 10 | super(DomainEventType.CARD_SELECTED, { card }); 11 | } 12 | } 13 | 14 | /** 15 | * 카드 포커스 이벤트 16 | */ 17 | export class CardFocusedEvent extends DomainEvent { 18 | constructor(card: ICard) { 19 | super(DomainEventType.CARD_FOCUSED, { card }); 20 | } 21 | } 22 | 23 | /** 24 | * 카드 드래그 시작 이벤트 25 | */ 26 | export class CardDragStartEvent extends DomainEvent { 27 | constructor(card: ICard) { 28 | super(DomainEventType.CARD_DRAG_START, { card }); 29 | } 30 | } 31 | 32 | /** 33 | * 카드 드롭 이벤트 34 | */ 35 | export class CardDropEvent extends DomainEvent { 36 | constructor(card: ICard) { 37 | super(DomainEventType.CARD_DROP, { card }); 38 | } 39 | } 40 | 41 | /** 42 | * 카드 컨텍스트 메뉴 열림 이벤트 43 | */ 44 | export class CardContextMenuOpenedEvent extends DomainEvent { 45 | constructor(card: ICard) { 46 | super(DomainEventType.CARD_CONTEXT_MENU, { card }); 47 | } 48 | } 49 | 50 | /** 51 | * 카드 인라인 편집 시작 이벤트 52 | */ 53 | export class CardInlineEditStartedEvent extends DomainEvent { 54 | constructor(card: ICard) { 55 | super(DomainEventType.CARD_INLINE_EDIT_STARTED, { card }); 56 | } 57 | } 58 | 59 | /** 60 | * 카드 인라인 편집 종료 이벤트 61 | */ 62 | export class CardInlineEditEndedEvent extends DomainEvent { 63 | constructor(card: ICard) { 64 | super(DomainEventType.CARD_INLINE_EDIT_ENDED, { card }); 65 | } 66 | } 67 | 68 | /** 69 | * 카드 링크 생성 이벤트 70 | */ 71 | export class CardLinkCreatedEvent extends DomainEvent { 72 | constructor(card: ICard) { 73 | super(DomainEventType.CARD_LINK_CREATED, { card }); 74 | } 75 | } -------------------------------------------------------------------------------- /src/domain/events/ClipboardEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { TFile } from 'obsidian'; 4 | 5 | /** 6 | * 파일 링크 복사 이벤트 7 | */ 8 | export class FileLinkCopiedEvent extends DomainEvent { 9 | constructor(file: TFile, link: string) { 10 | super(DomainEventType.FILE_LINK_COPIED, { file, link }); 11 | } 12 | } 13 | 14 | /** 15 | * 파일 내용 복사 이벤트 16 | */ 17 | export class FileContentCopiedEvent extends DomainEvent { 18 | constructor(file: TFile, content: string) { 19 | super(DomainEventType.FILE_CONTENT_COPIED, { file, content }); 20 | } 21 | } 22 | 23 | /** 24 | * 여러 파일 링크 복사 이벤트 25 | */ 26 | export class FileLinksCopiedEvent extends DomainEvent { 27 | constructor(files: TFile[], links: string) { 28 | super(DomainEventType.FILE_LINKS_COPIED, { files, links }); 29 | } 30 | } 31 | 32 | /** 33 | * 여러 파일 내용 복사 이벤트 34 | */ 35 | export class FileContentsCopiedEvent extends DomainEvent { 36 | constructor(files: TFile[], contents: string) { 37 | super(DomainEventType.FILE_CONTENTS_COPIED, { files, contents }); 38 | } 39 | } -------------------------------------------------------------------------------- /src/domain/events/DomainEvent.ts: -------------------------------------------------------------------------------- 1 | import { DomainEventType, EventDataType } from './DomainEventType'; 2 | import { ICard } from '../models/Card'; 3 | import { IPreset } from '../models/Preset'; 4 | import { TFile } from 'obsidian'; 5 | import { ILayoutConfig } from '../models/Layout'; 6 | import { ICardSet } from '../models/CardSet'; 7 | 8 | /** 9 | * 이벤트 데이터 타입 10 | */ 11 | export type EventData = 12 | | ICard 13 | | IPreset 14 | | TFile 15 | | ICardSet 16 | | null 17 | | void 18 | | { expiredCount: number; currentSize: number } 19 | | { key: string; currentSize: number } 20 | | { previousSize: number; currentSize: number } 21 | | { sourceCard: ICard; targetCard: ICard } 22 | | { viewType: string } 23 | | { action: string } 24 | | { width: number; height: number; layoutConfig: ILayoutConfig } 25 | | { file: TFile; link: string } 26 | | { file: TFile; content: string } 27 | | { files: TFile[]; links: string } 28 | | { files: TFile[]; contents: string }; 29 | 30 | /** 31 | * 도메인 이벤트 인터페이스 32 | * - 모든 도메인 이벤트가 구현해야 하는 인터페이스 33 | * @template T 이벤트 타입 34 | */ 35 | export interface IDomainEvent { 36 | /** 37 | * 이벤트 이름 38 | */ 39 | readonly eventName: T; 40 | 41 | /** 42 | * 이벤트 발생 시간 43 | */ 44 | readonly timestamp: number; 45 | 46 | /** 47 | * 이벤트 데이터 48 | */ 49 | readonly data: T extends keyof EventDataType ? EventDataType[T] : never; 50 | 51 | /** 52 | * 이벤트 미리보기 정보 반환 53 | * @returns 이벤트 미리보기 정보 54 | */ 55 | preview(): Record; 56 | } 57 | 58 | /** 59 | * 도메인 이벤트 기본 클래스 60 | * - 모든 도메인 이벤트의 기본 구현체 61 | * @template T 이벤트 타입 62 | */ 63 | export class DomainEvent implements IDomainEvent { 64 | constructor( 65 | public readonly eventName: T, 66 | public readonly data: T extends keyof EventDataType ? EventDataType[T] : never, 67 | public readonly timestamp: number = Date.now() 68 | ) {} 69 | 70 | /** 71 | * 이벤트 정보를 문자열로 변환 72 | * @returns 문자열 형태의 이벤트 정보 73 | */ 74 | toString(): string { 75 | return `[${this.eventName}] ${JSON.stringify(this.preview())}`; 76 | } 77 | 78 | /** 79 | * 이벤트 복제 80 | * @returns 복제된 이벤트 81 | */ 82 | clone(): DomainEvent { 83 | return new DomainEvent(this.eventName, this.data, this.timestamp); 84 | } 85 | 86 | /** 87 | * 이벤트 미리보기 정보 반환 88 | * @returns 이벤트 미리보기 정보 89 | */ 90 | preview(): Record { 91 | return { 92 | eventName: this.eventName, 93 | data: this.data, 94 | timestamp: this.timestamp 95 | }; 96 | } 97 | } 98 | 99 | /** 100 | * 이벤트 메타데이터 인터페이스 101 | */ 102 | export interface IEventMetadata { 103 | source?: string; 104 | target?: string; 105 | context?: Record; 106 | error?: Error; 107 | } 108 | 109 | /** 110 | * 이벤트 핸들러 인터페이스 111 | */ 112 | export interface IEventHandler> { 113 | handle(event: T): Promise | void; 114 | } 115 | 116 | /** 117 | * 이벤트 디스패처 인터페이스 118 | */ 119 | export interface IEventDispatcher { 120 | /** 121 | * 초기화 122 | */ 123 | initialize(): void; 124 | 125 | /** 126 | * 초기화 여부 확인 127 | * @returns 초기화 여부 128 | */ 129 | isInitialized(): boolean; 130 | 131 | /** 132 | * 정리 133 | */ 134 | cleanup(): void; 135 | 136 | /** 137 | * 이벤트 구독 138 | * @param eventName 이벤트 이름 139 | * @param callback 콜백 함수 140 | */ 141 | subscribe(eventName: T, callback: (event: DomainEvent) => void): void; 142 | 143 | /** 144 | * 이벤트 구독 해제 145 | * @param eventName 이벤트 이름 146 | * @param callback 콜백 함수 147 | */ 148 | unsubscribe(eventName: T, callback: (event: DomainEvent) => void): void; 149 | 150 | /** 151 | * 이벤트 발송 152 | * @param event 이벤트 객체 153 | */ 154 | dispatch(event: DomainEvent): void; 155 | } -------------------------------------------------------------------------------- /src/domain/events/EventBus.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent, IEventHandler, IEventDispatcher } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | /** 6 | * 이벤트 버스 구현체 7 | */ 8 | export class EventBus implements IEventDispatcher { 9 | private static instance: EventBus; 10 | private handlers: Map>>> = new Map(); 11 | private initialized: boolean = false; 12 | 13 | private constructor() {} 14 | 15 | /** 16 | * 싱글톤 인스턴스 반환 17 | */ 18 | public static getInstance(): EventBus { 19 | if (!EventBus.instance) { 20 | EventBus.instance = new EventBus(); 21 | } 22 | return EventBus.instance; 23 | } 24 | 25 | /** 26 | * 초기화 27 | */ 28 | initialize(): void { 29 | this.initialized = true; 30 | } 31 | 32 | /** 33 | * 초기화 여부 확인 34 | * @returns 초기화 여부 35 | */ 36 | isInitialized(): boolean { 37 | return this.initialized; 38 | } 39 | 40 | /** 41 | * 정리 42 | */ 43 | cleanup(): void { 44 | this.handlers.clear(); 45 | this.initialized = false; 46 | } 47 | 48 | /** 49 | * 이벤트 구독 50 | * @param eventName 이벤트 이름 51 | * @param callback 콜백 함수 52 | */ 53 | subscribe(eventName: T, callback: (event: DomainEvent) => void | Promise): Subscription { 54 | if (!this.handlers.has(eventName)) { 55 | this.handlers.set(eventName, new Set()); 56 | } 57 | 58 | const handler: IEventHandler> = { 59 | handle: async (event: DomainEvent) => { 60 | callback(event as unknown as DomainEvent); 61 | } 62 | }; 63 | 64 | this.handlers.get(eventName)?.add(handler); 65 | 66 | return new Subscription(() => { 67 | this.unsubscribe(eventName, callback); 68 | }); 69 | } 70 | 71 | /** 72 | * 이벤트 구독 해제 73 | * @param eventName 이벤트 이름 74 | * @param callback 콜백 함수 75 | */ 76 | unsubscribe(eventName: T, callback: (event: DomainEvent) => void): void { 77 | const handlers = this.handlers.get(eventName); 78 | if (!handlers) return; 79 | 80 | for (const handler of handlers) { 81 | if ((handler.handle as unknown as (event: DomainEvent) => void) === callback) { 82 | handlers.delete(handler); 83 | break; 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * 이벤트 발송 90 | * @param event 이벤트 객체 91 | */ 92 | dispatch(event: DomainEvent): void { 93 | const handlers = this.handlers.get(event.eventName); 94 | if (!handlers) return; 95 | 96 | for (const handler of handlers) { 97 | handler.handle(event as unknown as DomainEvent); 98 | } 99 | } 100 | 101 | /** 102 | * 이벤트를 발행합니다. 103 | * @param eventName 이벤트 이름 104 | * @param data 이벤트 데이터 105 | */ 106 | publish(eventName: T, data: any): void { 107 | const event = new DomainEvent(eventName, data); 108 | this.dispatch(event); 109 | } 110 | 111 | /** 112 | * 이벤트 핸들러 등록 113 | * @param eventName 이벤트 이름 114 | * @param handler 이벤트 핸들러 115 | */ 116 | registerHandler(eventName: T, handler: IEventHandler>): Subscription { 117 | if (!this.handlers.has(eventName)) { 118 | this.handlers.set(eventName, new Set()); 119 | } 120 | 121 | this.handlers.get(eventName)?.add(handler as IEventHandler>); 122 | 123 | return new Subscription(() => { 124 | this.unregisterHandler(eventName, handler); 125 | }); 126 | } 127 | 128 | /** 129 | * 이벤트 핸들러 해제 130 | * @param eventName 이벤트 이름 131 | * @param handler 이벤트 핸들러 132 | */ 133 | unregisterHandler(eventName: T, handler: IEventHandler>): void { 134 | const handlers = this.handlers.get(eventName); 135 | if (!handlers) return; 136 | 137 | handlers.delete(handler as IEventHandler>); 138 | } 139 | 140 | /** 141 | * 이벤트 핸들러 목록 조회 142 | * @param eventName 이벤트 이름 143 | * @returns 이벤트 핸들러 목록 144 | */ 145 | getHandlers(eventName: T): Set>> { 146 | return (this.handlers.get(eventName) || new Set()) as Set>>; 147 | } 148 | 149 | /** 150 | * 이벤트 핸들러 수 조회 151 | * @param eventName 이벤트 이름 152 | * @returns 이벤트 핸들러 수 153 | */ 154 | getHandlerCount(eventName: string): number { 155 | return this.handlers.get(eventName)?.size || 0; 156 | } 157 | 158 | /** 159 | * 이벤트 핸들러 존재 여부 확인 160 | * @param eventName 이벤트 이름 161 | * @returns 이벤트 핸들러 존재 여부 162 | */ 163 | hasHandlers(eventName: string): boolean { 164 | return this.handlers.has(eventName) && (this.handlers.get(eventName)?.size || 0) > 0; 165 | } 166 | 167 | /** 168 | * 이벤트 핸들러 목록 초기화 169 | * @param eventName 이벤트 이름 170 | */ 171 | clearHandlers(eventName: string): void { 172 | this.handlers.delete(eventName); 173 | } 174 | 175 | /** 176 | * 모든 이벤트 핸들러 목록 초기화 177 | */ 178 | clearAllHandlers(): void { 179 | this.handlers.clear(); 180 | } 181 | } -------------------------------------------------------------------------------- /src/domain/events/EventHandler.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent, IEventHandler } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | 4 | /** 5 | * 이벤트 핸들러 추상 클래스 6 | */ 7 | export abstract class EventHandler implements IEventHandler> { 8 | /** 9 | * 이벤트 처리 10 | * @param event 이벤트 객체 11 | */ 12 | abstract handle(event: DomainEvent): Promise | void; 13 | } -------------------------------------------------------------------------------- /src/domain/events/FileEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { TFile } from 'obsidian'; 4 | 5 | /** 6 | * 파일 열기 이벤트 7 | */ 8 | export class FileOpenedEvent extends DomainEvent { 9 | constructor(file: TFile) { 10 | super(DomainEventType.FILE_OPENED, { file }); 11 | } 12 | } 13 | 14 | /** 15 | * 여러 파일 열기 이벤트 16 | */ 17 | export class FilesOpenedEvent extends DomainEvent { 18 | constructor(files: TFile[]) { 19 | super(DomainEventType.FILES_OPENED, { files }); 20 | } 21 | } 22 | 23 | /** 24 | * 파일 편집을 위해 열기 이벤트 25 | */ 26 | export class FileOpenedForEditingEvent extends DomainEvent { 27 | constructor(file: TFile) { 28 | super(DomainEventType.FILE_OPENED_FOR_EDITING, { file }); 29 | } 30 | } 31 | 32 | /** 33 | * 편집창에 링크 삽입 이벤트 34 | */ 35 | export class LinkInsertedToEditorEvent extends DomainEvent { 36 | constructor(file: TFile) { 37 | super(DomainEventType.LINK_INSERTED_TO_EDITOR, { file }); 38 | } 39 | } 40 | 41 | /** 42 | * 파일에 링크 삽입 이벤트 43 | */ 44 | export class LinkInsertedToFileEvent extends DomainEvent { 45 | constructor(sourceFile: TFile, targetFile: TFile) { 46 | super(DomainEventType.LINK_INSERTED_TO_FILE, { sourceFile, targetFile }); 47 | } 48 | } -------------------------------------------------------------------------------- /src/domain/events/FocusEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from '@/domain/events/DomainEvent'; 2 | import { ICard } from '@/domain/models/Card'; 3 | import { DomainEventType } from '@/domain/events/DomainEventType'; 4 | 5 | /** 6 | * 포커스 변경 이벤트 7 | */ 8 | export class FocusChangedEvent extends DomainEvent { 9 | constructor(card: ICard) { 10 | super(DomainEventType.FOCUS_CHANGED, { card }); 11 | } 12 | } 13 | 14 | /** 15 | * 포커스 블러 이벤트 16 | */ 17 | export class FocusBlurredEvent extends DomainEvent { 18 | constructor(card: ICard) { 19 | super(DomainEventType.FOCUS_BLURRED, { card }); 20 | } 21 | } 22 | 23 | /** 24 | * 포커스 상태 업데이트 이벤트 25 | */ 26 | export class FocusStateUpdatedEvent extends DomainEvent { 27 | constructor(card: ICard, previousCard?: ICard) { 28 | super(DomainEventType.FOCUS_STATE_UPDATED, { card, previousCard }); 29 | } 30 | } -------------------------------------------------------------------------------- /src/domain/events/IDomainEvent.ts: -------------------------------------------------------------------------------- 1 | import { IEventMetadata } from '@/domain/events/IEventMetadata'; 2 | import { DomainEventType } from './DomainEventType'; 3 | 4 | /** 5 | * 도메인 이벤트 인터페이스 6 | */ 7 | export interface IDomainEvent { 8 | /** 9 | * 이벤트 ID 10 | */ 11 | readonly eventId: string; 12 | 13 | /** 14 | * 이벤트 타입 15 | */ 16 | readonly type: DomainEventType; 17 | 18 | /** 19 | * 이벤트 발생 시간 20 | */ 21 | readonly occurredOn: Date; 22 | 23 | /** 24 | * 이벤트 메타데이터 25 | */ 26 | readonly metadata: IEventMetadata; 27 | 28 | /** 29 | * 이벤트를 문자열로 변환 30 | */ 31 | toString(): string; 32 | 33 | /** 34 | * 이벤트 ID 생성 35 | */ 36 | generateEventId(): string; 37 | 38 | /** 39 | * 이벤트 복제 40 | */ 41 | clone(): IDomainEvent; 42 | } -------------------------------------------------------------------------------- /src/domain/events/IDomainEventHandler.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | 4 | /** 5 | * 도메인 이벤트 핸들러 인터페이스 6 | */ 7 | export interface IDomainEventHandler> { 8 | /** 9 | * 이벤트 처리 10 | */ 11 | handle(event: T): Promise; 12 | } -------------------------------------------------------------------------------- /src/domain/events/IEventMetadata.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 이벤트 메타데이터 컨텍스트 타입 3 | */ 4 | export type EventMetadataContext = { 5 | timestamp?: number; 6 | duration?: number; 7 | count?: number; 8 | status?: string; 9 | message?: string; 10 | details?: string; 11 | [key: string]: unknown; 12 | }; 13 | 14 | /** 15 | * 이벤트 메타데이터 인터페이스 16 | */ 17 | export interface IEventMetadata { 18 | /** 19 | * 이벤트 소스 20 | */ 21 | source?: string; 22 | 23 | /** 24 | * 이벤트 대상 25 | */ 26 | target?: string; 27 | 28 | /** 29 | * 추가 컨텍스트 정보 30 | */ 31 | context?: EventMetadataContext; 32 | 33 | /** 34 | * 에러 정보 35 | */ 36 | error?: Error; 37 | } -------------------------------------------------------------------------------- /src/domain/events/LayoutEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { ILayoutConfig } from '../models/Layout'; 4 | 5 | /** 6 | * 레이아웃 설정 업데이트 이벤트 7 | */ 8 | export class LayoutConfigUpdatedEvent extends DomainEvent { 9 | constructor(layoutConfig: ILayoutConfig) { 10 | super(DomainEventType.LAYOUT_CONFIG_UPDATED, { layoutConfig }); 11 | } 12 | } 13 | 14 | /** 15 | * 레이아웃 모드 변경 이벤트 16 | */ 17 | export class LayoutModeChangedEvent extends DomainEvent { 18 | constructor(layoutConfig: ILayoutConfig) { 19 | super(DomainEventType.LAYOUT_MODE_CHANGED, { layoutConfig }); 20 | } 21 | } 22 | 23 | /** 24 | * 레이아웃 카드 너비 변경 이벤트 25 | */ 26 | export class LayoutCardWidthChangedEvent extends DomainEvent { 27 | constructor(layoutConfig: ILayoutConfig) { 28 | super(DomainEventType.LAYOUT_CARD_WIDTH_CHANGED, { layoutConfig }); 29 | } 30 | } 31 | 32 | /** 33 | * 레이아웃 카드 높이 변경 이벤트 34 | */ 35 | export class LayoutCardHeightChangedEvent extends DomainEvent { 36 | constructor(layoutConfig: ILayoutConfig) { 37 | super(DomainEventType.LAYOUT_CARD_HEIGHT_CHANGED, { layoutConfig }); 38 | } 39 | } 40 | 41 | /** 42 | * 레이아웃 변경 이벤트 43 | */ 44 | export class LayoutChangedEvent extends DomainEvent { 45 | constructor(layoutConfig: ILayoutConfig) { 46 | super(DomainEventType.LAYOUT_CHANGED, { layoutConfig }); 47 | } 48 | } 49 | 50 | /** 51 | * 레이아웃 크기 변경 이벤트 52 | */ 53 | export class LayoutResizedEvent extends DomainEvent { 54 | constructor(layoutConfig: ILayoutConfig) { 55 | super(DomainEventType.LAYOUT_RESIZED, { layoutConfig }); 56 | } 57 | } 58 | 59 | /** 60 | * 레이아웃 카드 위치 업데이트 이벤트 61 | */ 62 | export class LayoutCardPositionUpdatedEvent extends DomainEvent { 63 | constructor(cardId: string, x: number, y: number, layoutConfig: ILayoutConfig) { 64 | super(DomainEventType.LAYOUT_CARD_POSITION_UPDATED, { cardId, x, y, layoutConfig }); 65 | } 66 | } 67 | 68 | /** 69 | * 뷰포트 크기 업데이트 이벤트 70 | */ 71 | export class ViewportDimensionsUpdatedEvent extends DomainEvent { 72 | constructor(width: number, height: number, layoutConfig: ILayoutConfig) { 73 | super(DomainEventType.VIEWPORT_DIMENSIONS_UPDATED, { width, height, layoutConfig }); 74 | } 75 | } 76 | 77 | /** 78 | * 레이아웃 카드 스타일 업데이트 이벤트 79 | */ 80 | export class LayoutCardStyleUpdatedEvent extends DomainEvent { 81 | constructor( 82 | public readonly cardId: string, 83 | public readonly style: 'normal' | 'active' | 'focused' 84 | ) { 85 | super(DomainEventType.CARD_STYLE_UPDATED, { cardId, style }); 86 | } 87 | } -------------------------------------------------------------------------------- /src/domain/events/PresetEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { IPreset } from '../models/Preset'; 3 | import { DomainEventType } from './DomainEventType'; 4 | 5 | /** 6 | * 프리셋 생성 이벤트 7 | */ 8 | export class PresetCreatedEvent extends DomainEvent { 9 | constructor(preset: IPreset) { 10 | super(DomainEventType.PRESET_CREATED, { preset }); 11 | } 12 | } 13 | 14 | /** 15 | * 프리셋 업데이트 이벤트 16 | */ 17 | export class PresetUpdatedEvent extends DomainEvent { 18 | constructor(preset: IPreset) { 19 | super(DomainEventType.PRESET_UPDATED, { preset }); 20 | } 21 | } 22 | 23 | /** 24 | * 프리셋 삭제 이벤트 25 | */ 26 | export class PresetDeletedEvent extends DomainEvent { 27 | constructor(preset: IPreset) { 28 | super(DomainEventType.PRESET_DELETED, { preset }); 29 | } 30 | } 31 | 32 | /** 33 | * 프리셋 적용 이벤트 34 | */ 35 | export class PresetAppliedEvent extends DomainEvent { 36 | constructor(preset: IPreset) { 37 | super(DomainEventType.PRESET_APPLIED, { preset }); 38 | } 39 | } -------------------------------------------------------------------------------- /src/domain/events/ScrollEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { ICard } from '../models/Card'; 4 | 5 | /** 6 | * 카드 중앙 정렬 이벤트 7 | */ 8 | export class CardCenteredEvent extends DomainEvent { 9 | constructor(card: ICard) { 10 | super(DomainEventType.CARD_CENTERED, { card }); 11 | } 12 | } 13 | 14 | /** 15 | * 스크롤 위치 업데이트 이벤트 16 | */ 17 | export class ScrollPositionUpdatedEvent extends DomainEvent { 18 | constructor(position: number) { 19 | super(DomainEventType.SCROLL_POSITION_UPDATED, { position }); 20 | } 21 | } 22 | 23 | /** 24 | * 스크롤 동작 변경 이벤트 25 | */ 26 | export class ScrollBehaviorChangedEvent extends DomainEvent { 27 | constructor(behavior: string) { 28 | super(DomainEventType.SCROLL_BEHAVIOR_CHANGED, { behavior }); 29 | } 30 | } 31 | 32 | /** 33 | * 부드러운 스크롤 설정 변경 이벤트 34 | */ 35 | export class SmoothScrollChangedEvent extends DomainEvent { 36 | constructor(smooth: boolean) { 37 | super(DomainEventType.SMOOTH_SCROLL_CHANGED, { smooth }); 38 | } 39 | } -------------------------------------------------------------------------------- /src/domain/events/SearchEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { ISearchConfig, ISearchCriteria, ISearchResult } from '../models/Search'; 4 | import { ICard } from '../models/Card'; 5 | import { ICardSet } from '../models/CardSet'; 6 | 7 | /** 8 | * 검색 시작 이벤트 9 | */ 10 | export class SearchStartedEvent extends DomainEvent { 11 | constructor(query: string, config: ISearchConfig) { 12 | super(DomainEventType.SEARCH_STARTED, { query, config }); 13 | } 14 | } 15 | 16 | /** 17 | * 검색 완료 이벤트 18 | */ 19 | export class SearchCompletedEvent extends DomainEvent { 20 | constructor(result: ISearchResult) { 21 | super(DomainEventType.SEARCH_COMPLETED, { result }); 22 | } 23 | } 24 | 25 | /** 26 | * 검색 실패 이벤트 27 | */ 28 | export class SearchFailedEvent extends DomainEvent { 29 | constructor(error: Error, query: string, config: ISearchConfig) { 30 | super(DomainEventType.SEARCH_FAILED, { error, query, config }); 31 | } 32 | } 33 | 34 | /** 35 | * 검색 결과 필터링 이벤트 36 | */ 37 | export class SearchResultsFilteredEvent extends DomainEvent { 38 | constructor(result: ISearchResult, criteria: ISearchCriteria) { 39 | super(DomainEventType.SEARCH_RESULTS_FILTERED, { result, criteria }); 40 | } 41 | } 42 | 43 | /** 44 | * 검색 결과 정렬 이벤트 45 | */ 46 | export class SearchResultsSortedEvent extends DomainEvent { 47 | constructor(result: ISearchResult, criteria: ISearchCriteria) { 48 | super(DomainEventType.SEARCH_RESULTS_SORTED, { result, criteria }); 49 | } 50 | } 51 | 52 | /** 53 | * 검색 인덱스 업데이트 이벤트 54 | */ 55 | export class SearchIndexUpdatedEvent extends DomainEvent { 56 | constructor(card: ICard) { 57 | super(DomainEventType.SEARCH_INDEX_UPDATED, { card }); 58 | } 59 | } 60 | 61 | /** 62 | * 검색 인덱스 삭제 이벤트 63 | */ 64 | export class SearchIndexRemovedEvent extends DomainEvent { 65 | constructor(cardId: string) { 66 | super(DomainEventType.SEARCH_INDEX_REMOVED, { cardId }); 67 | } 68 | } -------------------------------------------------------------------------------- /src/domain/events/SettingsEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { IPluginSettings } from '../models/PluginSettings'; 4 | import { ICardStyle, ICardSection, ICardStateStyle } from '../models/Card'; 5 | import { ICardSetConfig } from '../models/CardSet'; 6 | import { ILayoutConfig } from '../models/Layout'; 7 | import { ISortConfig } from '../models/Sort'; 8 | import { ISearchConfig } from '../models/Search'; 9 | import { ICardDisplayOptions } from '../models/Card'; 10 | import { TitleSource } from '../models/Card'; 11 | 12 | /** 13 | * 설정 변경 이벤트 14 | */ 15 | export class SettingsChangedEvent extends DomainEvent { 16 | constructor(oldSettings: IPluginSettings, newSettings: IPluginSettings) { 17 | super(DomainEventType.SETTINGS_CHANGED, { oldSettings, newSettings }); 18 | } 19 | } 20 | 21 | /** 22 | * 카드 설정 변경 이벤트 23 | */ 24 | export class CardConfigChangedEvent extends DomainEvent { 25 | constructor(oldConfig: ICardStateStyle, newConfig: ICardStateStyle) { 26 | super(DomainEventType.CARD_CONFIG_CHANGED, { oldConfig, newConfig }); 27 | } 28 | } 29 | 30 | /** 31 | * 카드셋 설정 변경 이벤트 32 | */ 33 | export class CardSetConfigChangedEvent extends DomainEvent { 34 | constructor(type: string, oldConfig: ICardSetConfig, newConfig: ICardSetConfig) { 35 | super(DomainEventType.CARD_SET_CONFIG_CHANGED, { type, oldConfig, newConfig }); 36 | } 37 | } 38 | 39 | /** 40 | * 레이아웃 설정 변경 이벤트 41 | */ 42 | export class LayoutConfigChangedEvent extends DomainEvent { 43 | constructor(oldConfig: ILayoutConfig, newConfig: ILayoutConfig) { 44 | super(DomainEventType.LAYOUT_CONFIG_CHANGED, { oldConfig, newConfig }); 45 | } 46 | } 47 | 48 | /** 49 | * 정렬 설정 변경 이벤트 50 | */ 51 | export class SortConfigChangedEvent extends DomainEvent { 52 | constructor(oldConfig: ISortConfig, newConfig: ISortConfig) { 53 | super(DomainEventType.SORT_CONFIG_CHANGED, { oldConfig, newConfig }); 54 | } 55 | } 56 | 57 | /** 58 | * 검색 설정 변경 이벤트 59 | */ 60 | export class SearchConfigChangedEvent extends DomainEvent { 61 | constructor(oldConfig: ISearchConfig, newConfig: ISearchConfig) { 62 | super(DomainEventType.SEARCH_CONFIG_CHANGED, { oldConfig, newConfig }); 63 | } 64 | } 65 | 66 | /** 67 | * 카드 스타일 변경 이벤트 68 | */ 69 | export class CardStyleChangedEvent extends DomainEvent { 70 | constructor( 71 | public readonly section: 'card' | 'header' | 'body' | 'footer', 72 | public readonly state: 'normal' | 'active' | 'focused' | 'style', 73 | public readonly property: string, 74 | public readonly value: string | number 75 | ) { 76 | super(DomainEventType.CARD_STYLE_CHANGED, { section, state, property, value }); 77 | } 78 | } 79 | 80 | /** 81 | * 카드 섹션 표시 변경 이벤트 82 | */ 83 | export class CardSectionDisplayChangedEvent extends DomainEvent { 84 | constructor( 85 | public readonly section: 'header' | 'body' | 'footer', 86 | public readonly property: keyof ICardDisplayOptions | 'titleSource', 87 | public readonly oldValue: boolean | TitleSource, 88 | public readonly newValue: boolean | TitleSource 89 | ) { 90 | super(DomainEventType.CARD_SECTION_DISPLAY_CHANGED, { section, property, oldValue, newValue }); 91 | } 92 | } 93 | 94 | /** 95 | * 타이틀 소스 변경 이벤트 96 | */ 97 | export class TitleSourceChangedEvent extends DomainEvent { 98 | constructor( 99 | public readonly oldValue: TitleSource, 100 | public readonly newValue: TitleSource 101 | ) { 102 | super(DomainEventType.TITLE_SOURCE_CHANGED, { 103 | oldSource: oldValue, 104 | newSource: newValue 105 | }); 106 | } 107 | } -------------------------------------------------------------------------------- /src/domain/events/ToolbarEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | import { CardSetType } from '../models/CardSet'; 4 | import { ISearchConfig } from '../models/Search'; 5 | import { ISortConfig } from '../models/Sort'; 6 | import { ICardStyle, ICardSection } from '../models/Card'; 7 | import { ILayoutConfig } from '../models/Layout'; 8 | 9 | /** 10 | * 툴바 액션 이벤트 11 | */ 12 | export class ToolbarActionEvent extends DomainEvent { 13 | constructor( 14 | cardSetType: CardSetType, 15 | searchConfig: ISearchConfig, 16 | sortConfig: ISortConfig, 17 | cardSection: ICardSection, 18 | cardStyle: ICardStyle, 19 | layoutConfig: ILayoutConfig 20 | ) { 21 | super(DomainEventType.TOOLBAR_ACTION, { 22 | cardSetType, 23 | searchConfig, 24 | sortConfig, 25 | cardSection, 26 | cardStyle, 27 | layoutConfig 28 | }); 29 | } 30 | } 31 | 32 | /** 33 | * 카드셋 타입 변경 이벤트 34 | */ 35 | export class CardSetTypeChangedEvent extends DomainEvent { 36 | constructor(oldType: CardSetType, newType: CardSetType) { 37 | super(DomainEventType.TOOLBAR_CARD_SET_TYPE_CHANGED, { oldType, newType }); 38 | } 39 | } 40 | 41 | /** 42 | * 검색 설정 변경 이벤트 43 | */ 44 | export class SearchConfigChangedEvent extends DomainEvent { 45 | constructor(oldConfig: ISearchConfig, newConfig: ISearchConfig) { 46 | super(DomainEventType.TOOLBAR_SEARCH_CONFIG_CHANGED, { oldConfig, newConfig }); 47 | } 48 | } 49 | 50 | /** 51 | * 정렬 설정 변경 이벤트 52 | */ 53 | export class SortConfigChangedEvent extends DomainEvent { 54 | constructor(oldConfig: ISortConfig, newConfig: ISortConfig) { 55 | super(DomainEventType.TOOLBAR_SORT_CONFIG_CHANGED, { oldConfig, newConfig }); 56 | } 57 | } 58 | 59 | /** 60 | * 카드 섹션 변경 이벤트 61 | */ 62 | export class CardSectionChangedEvent extends DomainEvent { 63 | constructor(oldSection: ICardSection, newSection: ICardSection) { 64 | super(DomainEventType.TOOLBAR_CARD_SECTION_CHANGED, { oldSection, newSection }); 65 | } 66 | } 67 | 68 | /** 69 | * 카드 스타일 변경 이벤트 70 | */ 71 | export class CardStyleChangedEvent extends DomainEvent { 72 | constructor(oldStyle: ICardStyle, newStyle: ICardStyle) { 73 | super(DomainEventType.TOOLBAR_CARD_STYLE_CHANGED, { oldStyle, newStyle }); 74 | } 75 | } 76 | 77 | /** 78 | * 레이아웃 설정 변경 이벤트 79 | */ 80 | export class LayoutConfigChangedEvent extends DomainEvent { 81 | constructor(oldConfig: ILayoutConfig, newConfig: ILayoutConfig) { 82 | super(DomainEventType.TOOLBAR_LAYOUT_CONFIG_CHANGED, { oldConfig, newConfig }); 83 | } 84 | } -------------------------------------------------------------------------------- /src/domain/events/ViewEvents.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './DomainEvent'; 2 | import { DomainEventType } from './DomainEventType'; 3 | 4 | /** 5 | * 뷰 변경 이벤트 6 | */ 7 | export class ViewChangedEvent extends DomainEvent { 8 | constructor(view: string) { 9 | super(DomainEventType.VIEW_CHANGED, { view }); 10 | } 11 | } 12 | 13 | /** 14 | * 뷰 활성화 이벤트 15 | */ 16 | export class ViewActivatedEvent extends DomainEvent { 17 | constructor(view: string) { 18 | super(DomainEventType.VIEW_ACTIVATED, { view }); 19 | } 20 | } 21 | 22 | /** 23 | * 뷰 비활성화 이벤트 24 | */ 25 | export class ViewDeactivatedEvent extends DomainEvent { 26 | constructor(view: string) { 27 | super(DomainEventType.VIEW_DEACTIVATED, { view }); 28 | } 29 | } -------------------------------------------------------------------------------- /src/domain/factories/ICardFactory.ts: -------------------------------------------------------------------------------- 1 | import { ICard, ICardCreateConfig } from '../models/Card'; 2 | import { TFile } from 'obsidian'; 3 | 4 | /** 5 | * 카드 생성 담당 6 | * - 카드 객체 생성 7 | * - 파일 기반 카드 생성 8 | */ 9 | export interface ICardFactory { 10 | /** 11 | * 기본 카드 생성 12 | * @param id 카드 ID 13 | * @param file 파일 객체 14 | * @param filePath 파일 경로 15 | * @param title 노트 제목 16 | * @param fileName 파일명 17 | * @param firstHeader 첫 번째 헤더 18 | * @param content 내용 19 | * @param tags 태그 목록 20 | * @param properties 속성 21 | * @param createdAt 생성일 22 | * @param updatedAt 수정일 23 | * @param config 카드 생성 설정 24 | */ 25 | create( 26 | id: string, 27 | file: TFile | null, 28 | filePath: string, 29 | title: string, 30 | fileName: string, 31 | firstHeader: string | null, 32 | content: string, 33 | tags: string[], 34 | properties: Record, 35 | createdAt: Date, 36 | updatedAt: Date, 37 | config: ICardCreateConfig 38 | ): ICard; 39 | 40 | /** 41 | * 파일 기반 카드 생성 42 | * @param filePath 파일 경로 43 | * @param config 카드 생성 설정 44 | */ 45 | createFromFile(filePath: string, config: ICardCreateConfig): Promise; 46 | 47 | /** 48 | * 카드 ID 생성 49 | * @param filePath 파일 경로 50 | * @returns 카드 ID 51 | */ 52 | generateCardId(filePath: string): string; 53 | 54 | /** 55 | * 카드 제목 생성 56 | * @param fileName 파일명 57 | * @param firstHeader 첫 번째 헤더 58 | * @param config 카드 생성 설정 59 | * @returns 카드 제목 60 | */ 61 | generateCardTitle( 62 | fileName: string, 63 | firstHeader: string | null, 64 | config: ICardCreateConfig 65 | ): string; 66 | 67 | createCard(file: TFile): Promise; 68 | createCards(files: TFile[]): Promise; 69 | updateCard(card: ICard, file: TFile): Promise; 70 | updateCards(cards: ICard[], files: TFile[]): Promise; 71 | } -------------------------------------------------------------------------------- /src/domain/factories/ICardSetFactory.ts: -------------------------------------------------------------------------------- 1 | import { ICardSet, CardSetType, ICardSetConfig } from '@/domain/models/CardSet'; 2 | 3 | /** 4 | * 카드셋 생성을 위한 팩토리 인터페이스 5 | */ 6 | export interface ICardSetFactory { 7 | /** 8 | * 카드셋을 생성합니다. 9 | * @param type 카드셋 타입 10 | * @param config 카드셋 설정 11 | * @returns 생성된 카드셋 12 | */ 13 | create(type: CardSetType, config: ICardSetConfig): ICardSet; 14 | } -------------------------------------------------------------------------------- /src/domain/infrastructure/IAnalyticsService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 분석 서비스 인터페이스 3 | * - 사용자 행동과 성능 데이터 수집 및 분석 담당 4 | */ 5 | export interface IAnalyticsService { 6 | /** 7 | * 이벤트를 기록합니다. 8 | * @param name 이벤트 이름 9 | * @param properties 이벤트 속성 10 | */ 11 | trackEvent(name: string, properties?: Record): void; 12 | 13 | /** 14 | * 특정 기간 동안의 이벤트를 조회합니다. 15 | * @param startTime 시작 시간 16 | * @param endTime 종료 시간 17 | * @returns 이벤트 목록 18 | */ 19 | getEvents(startTime: number, endTime: number): Array<{ 20 | name: string; 21 | timestamp: number; 22 | properties?: Record; 23 | }>; 24 | 25 | /** 26 | * 특정 이벤트의 발생 횟수를 반환합니다. 27 | * @param name 이벤트 이름 28 | * @returns 발생 횟수 29 | */ 30 | getEventCount(name: string): number; 31 | 32 | /** 33 | * 모든 이벤트 데이터를 초기화합니다. 34 | */ 35 | reset(): void; 36 | 37 | /** 38 | * 이벤트 데이터를 내보냅니다. 39 | * @returns 이벤트 데이터 JSON 문자열 40 | */ 41 | exportData(): string; 42 | } -------------------------------------------------------------------------------- /src/domain/infrastructure/IErrorHandler.ts: -------------------------------------------------------------------------------- 1 | export interface IErrorHandler { 2 | handleError(error: Error, context: string): void; 3 | } -------------------------------------------------------------------------------- /src/domain/infrastructure/IEventDispatcher.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from '../events/DomainEvent'; 2 | import { DomainEventType } from '../events/DomainEventType'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | /** 6 | * 이벤트 핸들러 인터페이스 7 | */ 8 | export interface IEventHandler> { 9 | /** 10 | * 이벤트 처리 11 | * @param event 이벤트 12 | */ 13 | handle(event: T): Promise | void; 14 | } 15 | 16 | /** 17 | * 이벤트 디스패처 인터페이스 18 | * - 이벤트 발송 및 구독 관리 19 | * - 이벤트 핸들러 등록 및 해제 20 | */ 21 | export interface IEventDispatcher { 22 | /** 23 | * 초기화 24 | */ 25 | initialize(): void; 26 | 27 | /** 28 | * 초기화 여부 확인 29 | * @returns 초기화 여부 30 | */ 31 | isInitialized(): boolean; 32 | 33 | /** 34 | * 정리 35 | */ 36 | cleanup(): void; 37 | 38 | /** 39 | * 이벤트를 발행합니다. 40 | * @param eventName 이벤트 이름 41 | * @param data 이벤트 데이터 42 | */ 43 | publish(eventName: T, data: any): void; 44 | 45 | /** 46 | * 이벤트를 구독합니다. 47 | * @param eventName 이벤트 이름 48 | * @param handler 이벤트 핸들러 49 | * @returns 구독 객체 50 | */ 51 | subscribe(eventName: T, handler: (event: DomainEvent) => void | Promise): Subscription; 52 | 53 | /** 54 | * 이벤트 발송 55 | * @param event 이벤트 객체 56 | */ 57 | dispatch(event: DomainEvent): void; 58 | 59 | /** 60 | * 이벤트 핸들러 등록 61 | * @param eventName 이벤트 이름 62 | * @param handler 이벤트 핸들러 63 | * @returns 구독 객체 64 | */ 65 | registerHandler(eventName: T, handler: IEventHandler>): Subscription; 66 | 67 | /** 68 | * 이벤트 핸들러 해제 69 | * @param eventName 이벤트 이름 70 | * @param handler 이벤트 핸들러 71 | */ 72 | unregisterHandler(eventName: T, handler: IEventHandler>): void; 73 | 74 | /** 75 | * 이벤트 핸들러 목록 조회 76 | * @param eventName 이벤트 이름 77 | * @returns 이벤트 핸들러 목록 78 | */ 79 | getHandlers(eventName: T): Set>>; 80 | 81 | /** 82 | * 이벤트 핸들러 수 조회 83 | * @param eventName 이벤트 이름 84 | * @returns 이벤트 핸들러 수 85 | */ 86 | getHandlerCount(eventName: string): number; 87 | 88 | /** 89 | * 이벤트 핸들러 존재 여부 확인 90 | * @param eventName 이벤트 이름 91 | * @returns 이벤트 핸들러 존재 여부 92 | */ 93 | hasHandlers(eventName: string): boolean; 94 | 95 | /** 96 | * 이벤트 핸들러 목록 초기화 97 | * @param eventName 이벤트 이름 98 | */ 99 | clearHandlers(eventName: string): void; 100 | 101 | /** 102 | * 모든 이벤트 핸들러 목록 초기화 103 | */ 104 | clearAllHandlers(): void; 105 | } -------------------------------------------------------------------------------- /src/domain/infrastructure/ILoggingService.ts: -------------------------------------------------------------------------------- 1 | export interface ILoggingService { 2 | debug(message: string, data?: any): void; 3 | info(message: string, data?: any): void; 4 | warn(message: string, data?: any): void; 5 | error(message: string, data?: any): void; 6 | } -------------------------------------------------------------------------------- /src/domain/infrastructure/IPerformanceMonitor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 성능 모니터링 서비스 인터페이스 3 | */ 4 | export interface IPerformanceMonitor { 5 | /** 6 | * 성능 측정 시작 7 | * @param name 측정 이름 8 | * @returns 성능 측정기 9 | */ 10 | startTimer(name: string): { 11 | /** 12 | * 성능 측정 중지 13 | */ 14 | stop(): void; 15 | }; 16 | 17 | /** 18 | * 성능 측정 19 | * @param name 측정 이름 20 | * @param callback 측정할 함수 21 | * @returns 측정 결과 22 | */ 23 | measure(name: string, callback: () => T): T; 24 | 25 | /** 26 | * 성능 측정 (비동기) 27 | * @param name 측정 이름 28 | * @param callback 측정할 함수 29 | * @returns 측정 결과 30 | */ 31 | measureAsync(name: string, callback: () => Promise): Promise; 32 | 33 | /** 34 | * 성능 측정 결과 조회 35 | * @param name 측정 이름 36 | * @returns 측정 결과 37 | */ 38 | getMeasurement(name: string): { 39 | /** 40 | * 측정 횟수 41 | */ 42 | count: number; 43 | 44 | /** 45 | * 평균 실행 시간 (ms) 46 | */ 47 | average: number; 48 | 49 | /** 50 | * 최소 실행 시간 (ms) 51 | */ 52 | min: number; 53 | 54 | /** 55 | * 최대 실행 시간 (ms) 56 | */ 57 | max: number; 58 | 59 | /** 60 | * 총 실행 시간 (ms) 61 | */ 62 | total: number; 63 | }; 64 | 65 | /** 66 | * 모든 성능 측정 결과 조회 67 | * @returns 모든 측정 결과 68 | */ 69 | getAllMeasurements(): Record; 95 | 96 | /** 97 | * 성능 측정 결과 초기화 98 | * @param name 측정 이름 99 | */ 100 | clearMeasurement(name: string): void; 101 | 102 | /** 103 | * 모든 성능 측정 결과 초기화 104 | */ 105 | clearAllMeasurements(): void; 106 | 107 | /** 108 | * 메모리 사용량 로깅 109 | */ 110 | logMemoryUsage(): void; 111 | 112 | /** 113 | * 성능 메트릭 초기화 114 | */ 115 | clearMetrics(): void; 116 | } -------------------------------------------------------------------------------- /src/domain/managers/ICardDisplayManager.ts: -------------------------------------------------------------------------------- 1 | import { ICard, ICardStyle } from '../models/Card'; 2 | import { ILayoutConfig } from '../models/Layout'; 3 | 4 | /** 5 | * 카드 표시 상태를 나타내는 인터페이스 6 | */ 7 | export interface ICardDisplayState { 8 | /** 카드 요소 */ 9 | readonly element: HTMLElement | null; 10 | /** 표시 여부 */ 11 | readonly isVisible: boolean; 12 | /** Z-인덱스 */ 13 | readonly zIndex: number; 14 | /** 활성 상태 */ 15 | readonly isActive: boolean; 16 | /** 포커스 상태 */ 17 | readonly isFocused: boolean; 18 | /** 선택 상태 */ 19 | readonly isSelected: boolean; 20 | } 21 | 22 | /** 23 | * 상호작용 스타일을 나타내는 인터페이스 24 | */ 25 | export interface IInteractionStyle { 26 | /** 호버 효과 */ 27 | readonly hoverEffect: string; 28 | /** 활성 효과 */ 29 | readonly activeEffect: string; 30 | /** 포커스 효과 */ 31 | readonly focusEffect: string; 32 | /** 선택 효과 */ 33 | readonly selectedEffect: string; 34 | /** 전환 지속 시간 */ 35 | readonly transitionDuration: string; 36 | } 37 | 38 | /** 39 | * 카드 표시 관리 담당 40 | * - 카드 DOM 요소 관리 41 | * - 카드 이벤트 리스너 관리 42 | * - 카드 스타일 관리 43 | * - 카드 상태 관리 44 | */ 45 | export interface ICardDisplayManager { 46 | /** 47 | * 표시 매니저를 초기화합니다. 48 | */ 49 | initialize(): void; 50 | 51 | /** 52 | * 표시 매니저를 정리합니다. 53 | */ 54 | cleanup(): void; 55 | 56 | /** 57 | * 표시 매니저가 초기화되었는지 확인합니다. 58 | * @returns 초기화 여부 59 | */ 60 | isInitialized(): boolean; 61 | 62 | /** 63 | * 카드 DOM 요소를 등록합니다. 64 | * @param cardId 카드 ID 65 | * @param element 카드 DOM 요소 66 | */ 67 | registerCardElement(cardId: string, element: HTMLElement): void; 68 | 69 | /** 70 | * 카드 DOM 요소를 등록 해제합니다. 71 | * @param cardId 카드 ID 72 | */ 73 | unregisterCardElement(cardId: string): void; 74 | 75 | /** 76 | * 카드 DOM 요소를 가져옵니다. 77 | * @param cardId 카드 ID 78 | * @returns 카드 DOM 요소 79 | */ 80 | getCardElement(cardId: string): HTMLElement | null; 81 | 82 | /** 83 | * 카드 이벤트 리스너를 추가합니다. 84 | * @param cardId 카드 ID 85 | * @param type 이벤트 타입 86 | * @param listener 이벤트 리스너 87 | */ 88 | addCardEventListener(cardId: string, type: string, listener: EventListener): void; 89 | 90 | /** 91 | * 카드 이벤트 리스너를 제거합니다. 92 | * @param cardId 카드 ID 93 | * @param type 이벤트 타입 94 | * @param listener 이벤트 리스너 95 | */ 96 | removeCardEventListener(cardId: string, type: string, listener: EventListener): void; 97 | 98 | /** 99 | * 카드 스타일을 적용합니다. 100 | * @param cardId 카드 ID 101 | * @param style 카드 스타일 102 | */ 103 | applyCardStyle(cardId: string, style: ICardStyle): void; 104 | 105 | /** 106 | * 카드 상호작용 스타일을 적용합니다. 107 | * @param cardId 카드 ID 108 | * @param style 상호작용 스타일 109 | */ 110 | applyInteractionStyle(cardId: string, style: IInteractionStyle): void; 111 | 112 | /** 113 | * 카드 레이아웃 스타일을 적용합니다. 114 | * @param cardId 카드 ID 115 | * @param layout 레이아웃 설정 116 | */ 117 | applyLayoutStyle(cardId: string, layout: ILayoutConfig): void; 118 | 119 | /** 120 | * 컨테이너의 크기를 가져옵니다. 121 | * @returns 컨테이너의 너비와 높이 122 | */ 123 | getContainerDimensions(): { width: number; height: number }; 124 | 125 | /** 126 | * 카드 스타일을 가져옵니다. 127 | * @returns 카드 스타일 128 | */ 129 | getCardStyle(): ICardStyle; 130 | 131 | /** 132 | * 카드의 표시 상태를 가져옵니다. 133 | * @param cardId 카드 ID 134 | * @returns 표시 상태 135 | */ 136 | getCardState(cardId: string): ICardDisplayState | null; 137 | 138 | /** 139 | * 카드의 표시 상태를 업데이트합니다. 140 | * @param cardId 카드 ID 141 | * @param state 업데이트할 상태 142 | */ 143 | updateCardState(cardId: string, state: Partial): void; 144 | 145 | /** 146 | * 카드 스타일을 업데이트합니다. 147 | * @param card 카드 148 | * @param style 스타일 149 | */ 150 | updateCardStyle(card: ICard, style: 'normal' | 'active' | 'focused'): void; 151 | } -------------------------------------------------------------------------------- /src/domain/managers/ICardManager.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '../models/Card'; 2 | import { TFile } from 'obsidian'; 3 | 4 | /** 5 | * 카드 상태 인터페이스 6 | */ 7 | export interface ICardState { 8 | /** 모든 카드 */ 9 | readonly cards: Map; 10 | /** 활성 카드 ID */ 11 | readonly activeCardId: string | null; 12 | /** 포커스된 카드 ID */ 13 | readonly focusedCardId: string | null; 14 | /** 선택된 카드 ID 목록 */ 15 | readonly selectedCardIds: Set; 16 | /** 마지막 업데이트 시간 */ 17 | readonly lastUpdated: number; 18 | } 19 | 20 | /** 21 | * 카드 관리자 인터페이스 22 | * - 카드 상태 관리 23 | * - 활성/포커스/선택 상태 관리 24 | * - 카드 목록 관리 25 | * - 상태 변경 이벤트 발송 26 | * - 카드 생명주기 관리 27 | * - 카드 등록/해제 28 | * - 카드 상태 변경 29 | * - 카드 캐시 관리 30 | */ 31 | export interface ICardManager { 32 | /** 33 | * 초기화 34 | */ 35 | initialize(): void; 36 | 37 | /** 38 | * 정리 39 | */ 40 | cleanup(): void; 41 | 42 | /** 43 | * 카드 상태 구독 44 | * @param callback 상태 변경 콜백 45 | */ 46 | subscribe(callback: (state: ICardState) => void): void; 47 | 48 | /** 49 | * 카드 상태 구독 해제 50 | * @param callback 상태 변경 콜백 51 | */ 52 | unsubscribe(callback: (state: ICardState) => void): void; 53 | 54 | /** 55 | * 카드를 등록합니다. 56 | * @param card 카드 객체 57 | */ 58 | registerCard(card: ICard): void; 59 | 60 | /** 61 | * 카드를 해제합니다. 62 | * @param cardId 카드 ID 63 | */ 64 | unregisterCard(cardId: string): void; 65 | 66 | /** 67 | * 파일로부터 카드 객체를 가져옵니다. 68 | * @param file 파일 69 | * @returns 카드 객체 또는 undefined 70 | */ 71 | getCardByFile(file: TFile): ICard | undefined; 72 | 73 | /** 74 | * 카드 ID로부터 카드 객체를 가져옵니다. 75 | * @param cardId 카드 ID 76 | * @returns 카드 객체 또는 undefined 77 | */ 78 | getCardById(cardId: string): ICard | undefined; 79 | 80 | /** 81 | * 모든 카드 객체를 가져옵니다. 82 | * @returns 카드 객체 배열 83 | */ 84 | getAllCards(): ICard[]; 85 | 86 | /** 87 | * 활성 카드를 설정합니다. 88 | * @param cardId 카드 ID 89 | */ 90 | setActiveCard(cardId: string | null): void; 91 | 92 | /** 93 | * 포커스된 카드를 설정합니다. 94 | * @param cardId 카드 ID 95 | */ 96 | setFocusedCard(cardId: string | null): void; 97 | 98 | /** 99 | * 카드를 선택합니다. 100 | * @param cardId 카드 ID 101 | * @param selected 선택 여부 102 | */ 103 | selectCard(cardId: string, selected: boolean): void; 104 | 105 | /** 106 | * 모든 카드 선택을 해제합니다. 107 | */ 108 | clearSelection(): void; 109 | 110 | /** 111 | * 카드 캐시를 갱신합니다. 112 | */ 113 | refreshCache(): void; 114 | 115 | /** 116 | * 카드 상태를 가져옵니다. 117 | * @returns 카드 상태 118 | */ 119 | getState(): ICardState; 120 | 121 | /** 122 | * 두 카드 간에 링크를 생성합니다. 123 | * @param sourceCard 소스 카드 124 | * @param targetCard 타겟 카드 125 | */ 126 | createLinkBetweenCards(sourceCard: ICard, targetCard: ICard): void; 127 | } -------------------------------------------------------------------------------- /src/domain/managers/ICardRenderManager.ts: -------------------------------------------------------------------------------- 1 | import { IRenderConfig, IRenderState } from '../models/Card'; 2 | 3 | /** 4 | * 카드 렌더링 관리 담당 5 | * - 렌더링 상태 관리 6 | * - 렌더링 이벤트 관리 7 | * - 렌더링 리소스 관리 8 | */ 9 | export interface ICardRenderManager { 10 | /** 11 | * 렌더링 매니저를 초기화합니다. 12 | */ 13 | initialize(): void; 14 | 15 | /** 16 | * 렌더링 매니저를 정리합니다. 17 | */ 18 | cleanup(): void; 19 | 20 | /** 21 | * 렌더링 매니저가 초기화되었는지 확인합니다. 22 | * @returns 초기화 여부 23 | */ 24 | isInitialized(): boolean; 25 | 26 | /** 27 | * 렌더링 상태를 등록합니다. 28 | * @param cardId 카드 ID 29 | * @param state 렌더링 상태 30 | */ 31 | registerRenderState(cardId: string, state: IRenderState): void; 32 | 33 | /** 34 | * 렌더링 상태를 등록 해제합니다. 35 | * @param cardId 카드 ID 36 | */ 37 | unregisterRenderState(cardId: string): void; 38 | 39 | /** 40 | * 렌더링 상태를 가져옵니다. 41 | * @param cardId 카드 ID 42 | * @returns 렌더링 상태 43 | */ 44 | getRenderState(cardId: string): IRenderState | null; 45 | 46 | /** 47 | * 렌더링 상태를 업데이트합니다. 48 | * @param cardId 카드 ID 49 | * @param state 업데이트할 상태 50 | */ 51 | updateRenderState(cardId: string, state: Partial): void; 52 | 53 | /** 54 | * 렌더링 이벤트를 구독합니다. 55 | * @param callback 이벤트 콜백 56 | */ 57 | subscribeToRenderEvents(callback: (event: { 58 | type: 'render' | 'cache-update'; 59 | cardId?: string; 60 | data?: { 61 | status: string; 62 | config?: IRenderConfig; 63 | error?: string; 64 | }; 65 | }) => void): void; 66 | 67 | /** 68 | * 렌더링 이벤트 구독을 해제합니다. 69 | * @param callback 이벤트 콜백 70 | */ 71 | unsubscribeFromRenderEvents(callback: (event: any) => void): void; 72 | 73 | /** 74 | * 렌더링 리소스를 등록합니다. 75 | * @param cardId 카드 ID 76 | * @param resource 리소스 77 | */ 78 | registerRenderResource(cardId: string, resource: any): void; 79 | 80 | /** 81 | * 렌더링 리소스를 등록 해제합니다. 82 | * @param cardId 카드 ID 83 | */ 84 | unregisterRenderResource(cardId: string): void; 85 | 86 | /** 87 | * 렌더링 리소스를 가져옵니다. 88 | * @param cardId 카드 ID 89 | * @returns 렌더링 리소스 90 | */ 91 | getRenderResource(cardId: string): any | null; 92 | 93 | /** 94 | * 렌더링 설정을 가져옵니다. 95 | * @returns 렌더링 설정 96 | */ 97 | getRenderConfig(): IRenderConfig; 98 | } -------------------------------------------------------------------------------- /src/domain/managers/IFocusManager.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '@/domain/models/Card'; 2 | import { FocusChangedEvent, FocusBlurredEvent, FocusStateUpdatedEvent } from '@/domain/events/FocusEvents'; 3 | 4 | /** 5 | * 포커스 상태 인터페이스 6 | */ 7 | export interface IFocusState { 8 | cardId: string; 9 | isFocused: boolean; 10 | timestamp: Date; 11 | } 12 | 13 | /** 14 | * 포커스 매니저 인터페이스 15 | */ 16 | export interface IFocusManager { 17 | initialize(): void; 18 | cleanup(): void; 19 | registerFocusState(cardId: string, isFocused: boolean): void; 20 | unregisterFocusState(cardId: string): void; 21 | updateFocusState(cardId: string, isFocused: boolean): void; 22 | getFocusState(cardId: string): IFocusState | undefined; 23 | getAllFocusStates(): IFocusState[]; 24 | subscribeToFocusEvents(callback: (event: FocusChangedEvent | FocusBlurredEvent | FocusStateUpdatedEvent) => void): void; 25 | unsubscribeFromFocusEvents(callback: (event: FocusChangedEvent | FocusBlurredEvent | FocusStateUpdatedEvent) => void): void; 26 | scrollToCard(card: ICard): void; 27 | focusCard(card: ICard): void; 28 | unfocusCard(card: ICard): void; 29 | getFocusedCard(): ICard | null; 30 | isCardFocused(card: ICard): boolean; 31 | focusNextCard(): void; 32 | focusPreviousCard(): void; 33 | focusFirstCard(): void; 34 | focusLastCard(): void; 35 | focusCardById(cardId: string): void; 36 | focusCardByIndex(index: number): void; 37 | scrollToFocusedCard(): void; 38 | centerFocusedCard(): void; 39 | } -------------------------------------------------------------------------------- /src/domain/managers/IPresetManager.ts: -------------------------------------------------------------------------------- 1 | import { IPreset } from '../models/Preset'; 2 | 3 | /** 4 | * 프리셋 이벤트 타입 5 | */ 6 | export type PresetEventType = 7 | | 'preset_created' 8 | | 'preset_updated' 9 | | 'preset_deleted' 10 | | 'preset_applied' 11 | | 'preset_mapping_added' 12 | | 'preset_mapping_removed' 13 | | 'preset_mapping_priority_updated'; 14 | 15 | /** 16 | * 프리셋 이벤트 17 | */ 18 | export interface IPresetEvent { 19 | type: PresetEventType; 20 | presetId: string; 21 | mappingId?: string; 22 | timestamp: Date; 23 | } 24 | 25 | /** 26 | * 프리셋 상태 27 | */ 28 | export interface IPresetState { 29 | preset: IPreset; 30 | isActive: boolean; 31 | lastAppliedAt?: Date; 32 | } 33 | 34 | /** 35 | * 프리셋 매핑 상태 36 | */ 37 | export interface IPresetMappingState { 38 | mappingId: string; 39 | presetId: string; 40 | targetType: 'folder' | 'tag' | 'created_date' | 'modified_date' | 'property'; 41 | targetValue: string; 42 | priority: number; 43 | includeSubfolders?: boolean; 44 | useRegex?: boolean; 45 | startDate?: Date; 46 | endDate?: Date; 47 | } 48 | 49 | /** 50 | * 프리셋 관리자 인터페이스 51 | * 52 | * 프리셋 상태와 매핑을 관리하는 매니저 53 | */ 54 | export interface IPresetManager { 55 | /** 56 | * 초기화 57 | */ 58 | initialize(): void; 59 | 60 | /** 61 | * 정리 62 | */ 63 | cleanup(): void; 64 | 65 | /** 66 | * 초기화 여부 확인 67 | * @returns 초기화 여부 68 | */ 69 | isInitialized(): boolean; 70 | 71 | /** 72 | * 프리셋 상태 등록 73 | * @param presetId 프리셋 ID 74 | * @param state 프리셋 상태 75 | */ 76 | registerPresetState(presetId: string, state: IPresetState): void; 77 | 78 | /** 79 | * 프리셋 상태 등록 해제 80 | * @param presetId 프리셋 ID 81 | */ 82 | unregisterPresetState(presetId: string): void; 83 | 84 | /** 85 | * 프리셋 상태 조회 86 | * @param presetId 프리셋 ID 87 | * @returns 프리셋 상태 88 | */ 89 | getPresetState(presetId: string): IPresetState | null; 90 | 91 | /** 92 | * 모든 프리셋 상태 조회 93 | * @returns 프리셋 상태 Map 94 | */ 95 | getPresetStates(): Map; 96 | 97 | /** 98 | * 프리셋 상태 업데이트 99 | * @param presetId 프리셋 ID 100 | * @param state 업데이트할 프리셋 상태 101 | */ 102 | updatePresetState(presetId: string, state: Partial): void; 103 | 104 | /** 105 | * 프리셋 매핑 상태 등록 106 | * @param mappingId 매핑 ID 107 | * @param state 매핑 상태 108 | */ 109 | registerPresetMappingState(mappingId: string, state: IPresetMappingState): void; 110 | 111 | /** 112 | * 프리셋 매핑 상태 등록 해제 113 | * @param mappingId 매핑 ID 114 | */ 115 | unregisterPresetMappingState(mappingId: string): void; 116 | 117 | /** 118 | * 프리셋 매핑 상태 조회 119 | * @param mappingId 매핑 ID 120 | * @returns 매핑 상태 121 | */ 122 | getPresetMappingState(mappingId: string): IPresetMappingState | null; 123 | 124 | /** 125 | * 모든 프리셋 매핑 상태 조회 126 | * @returns 매핑 상태 Map 127 | */ 128 | getPresetMappingStates(): Map; 129 | 130 | /** 131 | * 프리셋 매핑 상태 업데이트 132 | * @param mappingId 매핑 ID 133 | * @param state 업데이트할 매핑 상태 134 | */ 135 | updatePresetMappingState(mappingId: string, state: Partial): void; 136 | 137 | /** 138 | * 프리셋 이벤트 구독 139 | * @param callback 이벤트 콜백 140 | */ 141 | subscribeToPresetEvents(callback: (event: IPresetEvent) => void): void; 142 | 143 | /** 144 | * 프리셋 이벤트 구독 해제 145 | * @param callback 이벤트 콜백 146 | */ 147 | unsubscribeFromPresetEvents(callback: (event: IPresetEvent) => void): void; 148 | } -------------------------------------------------------------------------------- /src/domain/models/CardNavigatorState.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from './Card'; 2 | import { ICardSet } from './CardSet'; 3 | import { IPluginSettings, DEFAULT_PLUGIN_SETTINGS } from './PluginSettings'; 4 | 5 | /** 6 | * 카드 내비게이터 상태 인터페이스 7 | */ 8 | export interface ICardNavigatorState { 9 | /** 활성 카드셋 */ 10 | readonly activeCardSet: ICardSet | null; 11 | /** 활성 카드 */ 12 | readonly activeCard: ICard | null; 13 | /** 포커스된 카드 */ 14 | readonly focusedCard: ICard | null; 15 | /** 선택된 카드 ID 목록 */ 16 | readonly selectedCards: Set; 17 | /** 플러그인 설정 */ 18 | readonly settings: IPluginSettings; 19 | } 20 | 21 | /** 22 | * 기본 카드 내비게이터 상태 23 | */ 24 | export const DEFAULT_CARD_NAVIGATOR_STATE: ICardNavigatorState = { 25 | activeCardSet: null, 26 | activeCard: null, 27 | focusedCard: null, 28 | selectedCards: new Set(), 29 | settings: DEFAULT_PLUGIN_SETTINGS 30 | }; -------------------------------------------------------------------------------- /src/domain/models/Layout.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from './Card'; 2 | 3 | /** 4 | * 레이아웃 타입 열거형 5 | */ 6 | export enum LayoutType { 7 | /** 그리드 레이아웃 - 카드 높이 고정 */ 8 | GRID = 'grid', 9 | /** 메이슨리 레이아웃 - 카드 높이 자동 */ 10 | MASONRY = 'masonry' 11 | } 12 | 13 | /** 14 | * 레이아웃 방향 열거형 15 | */ 16 | export enum LayoutDirection { 17 | /** 수직 방향 - 세로 스크롤 */ 18 | VERTICAL = 'vertical', 19 | /** 수평 방향 - 가로 스크롤 */ 20 | HORIZONTAL = 'horizontal' 21 | } 22 | 23 | /** 24 | * 레이아웃 설정 인터페이스 25 | */ 26 | export interface ILayoutConfig { 27 | /** 레이아웃 타입 */ 28 | readonly type: LayoutType; 29 | /** 카드 높이 고정 여부 */ 30 | readonly fixedCardHeight: boolean; 31 | /** 카드 임계 너비 (픽셀) */ 32 | readonly cardThresholdWidth: number; 33 | /** 카드 임계 높이 (픽셀) */ 34 | readonly cardThresholdHeight: number; 35 | /** 카드 간격 (픽셀) */ 36 | readonly cardGap: number; 37 | /** 여백 (픽셀) */ 38 | readonly padding: number; 39 | } 40 | 41 | /** 42 | * 기본 레이아웃 설정 43 | */ 44 | export const DEFAULT_LAYOUT_CONFIG: ILayoutConfig = { 45 | type: LayoutType.GRID, 46 | fixedCardHeight: true, 47 | cardThresholdWidth: 300, 48 | cardThresholdHeight: 200, 49 | cardGap: 16, 50 | padding: 16 51 | }; 52 | 53 | /** 54 | * 레이아웃 인터페이스 55 | */ 56 | export interface ILayout { 57 | readonly config: ILayoutConfig; 58 | 59 | /** 60 | * 레이아웃 유효성 검사 61 | */ 62 | validate(): boolean; 63 | 64 | /** 65 | * 레이아웃 계산 66 | * @param cards 카드 목록 67 | * @param viewportWidth 뷰포트 너비 68 | * @param viewportHeight 뷰포트 높이 69 | * @returns 레이아웃 계산 결과 70 | */ 71 | calculateLayout( 72 | cards: readonly ICard[], 73 | viewportWidth: number, 74 | viewportHeight: number 75 | ): ILayoutResult; 76 | } 77 | 78 | /** 79 | * 카드 위치 인터페이스 80 | */ 81 | export interface ICardPosition { 82 | /** 카드 ID */ 83 | readonly cardId: ICard['id']; 84 | /** 열 인덱스 */ 85 | readonly columnIndex: number; 86 | /** 행 인덱스 */ 87 | readonly rowIndex: number; 88 | /** X 좌표 */ 89 | readonly x: number; 90 | /** Y 좌표 */ 91 | readonly y: number; 92 | /** 너비 */ 93 | readonly width: number; 94 | /** 높이 */ 95 | readonly height: number; 96 | } 97 | 98 | /** 99 | * 레이아웃 결과 인터페이스 100 | */ 101 | export interface ILayoutResult { 102 | /** 레이아웃 타입 */ 103 | readonly type: LayoutType; 104 | /** 레이아웃 방향 */ 105 | readonly direction: LayoutDirection; 106 | /** 열 수 */ 107 | readonly columnCount: number; 108 | /** 행 수 */ 109 | readonly rowCount: number; 110 | /** 카드 너비 */ 111 | readonly cardWidth: number; 112 | /** 카드 높이 */ 113 | readonly cardHeight: number; 114 | /** 카드 위치 목록 */ 115 | readonly cardPositions: readonly ICardPosition[]; 116 | /** 스크롤 방향 */ 117 | readonly scrollDirection: LayoutDirection; 118 | /** 뷰포트 너비 */ 119 | readonly viewportWidth: number; 120 | /** 뷰포트 높이 */ 121 | readonly viewportHeight: number; 122 | } 123 | 124 | /** 125 | * 레이아웃 계산 로직: 126 | * 1. 메이슨리 레이아웃 (type = MASONRY) 127 | * - 세로 레이아웃만 적용 (direction = VERTICAL) 128 | * - 뷰포트 너비와 카드 임계 너비로 열 수 결정 129 | * - 모든 열이 뷰포트 안에 들어오도록 함 130 | * - 카드의 높이는 컨텐츠에 따라 자동 결정 131 | * - 세로 방향으로 스크롤 (scrollDirection = VERTICAL) 132 | * 133 | * 2. 그리드 레이아웃 (type = GRID) 134 | * - 뷰포트 가로 > 세로: 가로 레이아웃 (direction = HORIZONTAL) 135 | * - 뷰포트 높이와 카드 임계 높이로 행 수 결정 136 | * - 모든 행이 뷰포트 안에 들어오도록 함 137 | * - 카드의 너비는 임계 너비로 고정 138 | * - 가로 방향으로 스크롤 (scrollDirection = HORIZONTAL) 139 | * - 뷰포트 세로 > 가로: 세로 레이아웃 (direction = VERTICAL) 140 | * - 뷰포트 너비와 카드 임계 너비로 열 수 결정 141 | * - 모든 열이 뷰포트 안에 들어오도록 함 142 | * - 카드의 높이는 임계 높이로 고정 143 | * - 세로 방향으로 스크롤 (scrollDirection = VERTICAL) 144 | */ -------------------------------------------------------------------------------- /src/domain/models/PluginSettings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICardDomainSettings, 3 | DEFAULT_CARD_DOMAIN_SETTINGS 4 | } from './Card'; 5 | import { CardSetType, ICardSetConfig, DEFAULT_CARD_SET_CONFIG } from './CardSet'; 6 | import { ILayoutConfig, DEFAULT_LAYOUT_CONFIG } from './Layout'; 7 | import { ISearchConfig, DEFAULT_SEARCH_CONFIG } from './Search'; 8 | import { ISortConfig, DEFAULT_SORT_CONFIG } from './Sort'; 9 | import { IPresetContentConfig, DEFAULT_PRESET_CONTENT_CONFIG, IPresetMapping } from './Preset'; 10 | 11 | /** 12 | * 카드셋 도메인 설정 인터페이스 13 | */ 14 | export interface ICardSetDomainSettings { 15 | type: CardSetType; 16 | config: ICardSetConfig; 17 | } 18 | 19 | /** 20 | * 기본 카드셋 도메인 설정 21 | */ 22 | export const DEFAULT_CARD_SET_DOMAIN_SETTINGS: ICardSetDomainSettings = { 23 | type: CardSetType.FOLDER, 24 | config: DEFAULT_CARD_SET_CONFIG 25 | }; 26 | 27 | /** 28 | * 레이아웃 도메인 설정 인터페이스 29 | */ 30 | export interface ILayoutDomainSettings { 31 | /** 레이아웃 설정 */ 32 | readonly config: ILayoutConfig; 33 | } 34 | 35 | /** 36 | * 기본 레이아웃 도메인 설정 37 | */ 38 | export const DEFAULT_LAYOUT_DOMAIN_SETTINGS: ILayoutDomainSettings = { 39 | config: DEFAULT_LAYOUT_CONFIG 40 | }; 41 | 42 | /** 43 | * 검색 도메인 설정 인터페이스 44 | */ 45 | export interface ISearchDomainSettings { 46 | /** 검색 설정 */ 47 | readonly config: ISearchConfig; 48 | } 49 | 50 | /** 51 | * 기본 검색 도메인 설정 52 | */ 53 | export const DEFAULT_SEARCH_DOMAIN_SETTINGS: ISearchDomainSettings = { 54 | config: DEFAULT_SEARCH_CONFIG 55 | }; 56 | 57 | /** 58 | * 정렬 도메인 설정 인터페이스 59 | */ 60 | export interface ISortDomainSettings { 61 | /** 정렬 설정 */ 62 | readonly config: ISortConfig; 63 | } 64 | 65 | /** 66 | * 기본 정렬 도메인 설정 67 | */ 68 | export const DEFAULT_SORT_DOMAIN_SETTINGS: ISortDomainSettings = { 69 | config: DEFAULT_SORT_CONFIG 70 | }; 71 | 72 | /** 73 | * 프리셋 도메인 설정 인터페이스 74 | */ 75 | export interface IPresetDomainSettings { 76 | /** 프리셋 설정 */ 77 | readonly config: IPresetContentConfig; 78 | /** 프리셋 매핑 목록 */ 79 | readonly mappings: readonly IPresetMapping[]; 80 | } 81 | 82 | /** 83 | * 기본 프리셋 도메인 설정 84 | */ 85 | export const DEFAULT_PRESET_DOMAIN_SETTINGS: IPresetDomainSettings = { 86 | config: DEFAULT_PRESET_CONTENT_CONFIG, 87 | mappings: [] 88 | }; 89 | 90 | /** 91 | * 플러그인 설정 인터페이스 92 | */ 93 | export interface IPluginSettings { 94 | card: ICardDomainSettings; 95 | cardSet: ICardSetDomainSettings; 96 | layout: ILayoutDomainSettings; 97 | sort: ISortDomainSettings; 98 | search: ISearchDomainSettings; 99 | preset: IPresetDomainSettings; 100 | } 101 | 102 | /** 103 | * 기본 플러그인 설정 104 | */ 105 | export const DEFAULT_PLUGIN_SETTINGS: IPluginSettings = { 106 | card: DEFAULT_CARD_DOMAIN_SETTINGS, 107 | cardSet: DEFAULT_CARD_SET_DOMAIN_SETTINGS, 108 | layout: DEFAULT_LAYOUT_DOMAIN_SETTINGS, 109 | search: DEFAULT_SEARCH_DOMAIN_SETTINGS, 110 | sort: DEFAULT_SORT_DOMAIN_SETTINGS, 111 | preset: DEFAULT_PRESET_DOMAIN_SETTINGS 112 | }; 113 | 114 | /** 115 | * 플러그인 설정 클래스 116 | */ 117 | export class PluginSettings implements IPluginSettings { 118 | constructor( 119 | public readonly card: ICardDomainSettings, 120 | public readonly cardSet: ICardSetDomainSettings, 121 | public readonly layout: ILayoutDomainSettings, 122 | public readonly search: ISearchDomainSettings, 123 | public readonly sort: ISortDomainSettings, 124 | public readonly preset: IPresetDomainSettings 125 | ) {} 126 | 127 | /** 128 | * 설정 유효성 검사 129 | */ 130 | validate(): boolean { 131 | return ( 132 | !!this.card.sections.header && 133 | !!this.card.sections.body && 134 | !!this.card.sections.footer && 135 | !!this.card.renderConfig && 136 | !!this.card.stateStyle && 137 | !!this.cardSet.config && 138 | !!this.layout.config && 139 | !!this.search.config && 140 | !!this.sort.config && 141 | !!this.preset.config 142 | ); 143 | } 144 | } -------------------------------------------------------------------------------- /src/domain/models/Search.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from './Card'; 2 | 3 | /** 4 | * 검색 결과 항목 인터페이스 5 | * 6 | * @remarks 7 | * ISearchResult의 items 배열에 포함되는 각 카드의 상세 검색 결과를 나타냅니다. 8 | * card.id는 ISearchResult의 cardIds 배열에 포함되어야 합니다. 9 | */ 10 | export interface ISearchResultItem { 11 | /** 카드 (ICard의 id는 ISearchResult의 cardIds에 포함되어야 함) */ 12 | readonly card: ICard; 13 | /** 검색어 매칭 위치 */ 14 | readonly matches: Array<{ 15 | /** 시작 위치 */ 16 | readonly start: number; 17 | /** 끝 위치 */ 18 | readonly end: number; 19 | /** 매칭된 텍스트 */ 20 | readonly text: string; 21 | }>; 22 | /** 검색 결과 순위 점수 */ 23 | readonly score: number; 24 | } 25 | 26 | /** 27 | * 검색 기준 인터페이스 28 | */ 29 | export interface ISearchCriteria { 30 | /** 검색어 */ 31 | readonly query: string; 32 | /** 검색 범위 */ 33 | readonly scope: 'all' | 'current'; 34 | /** 대소문자 구분 */ 35 | readonly caseSensitive: boolean; 36 | /** 정규식 사용 */ 37 | readonly useRegex: boolean; 38 | /** 전체 단어 일치 */ 39 | readonly wholeWord: boolean; 40 | } 41 | 42 | /** 43 | * 검색 설정 인터페이스 44 | */ 45 | export interface ISearchConfig { 46 | /** 검색 기준 */ 47 | readonly criteria: ISearchCriteria; 48 | /** 검색 히스토리 */ 49 | readonly history: readonly ISearchCriteria[]; 50 | /** 최대 히스토리 수 */ 51 | readonly maxHistory: number; 52 | } 53 | 54 | /** 55 | * 검색 결과 인터페이스 56 | * 57 | * @remarks 58 | * 검색 결과는 검색된 카드들의 ID 목록과 각 카드의 상세 검색 결과를 포함합니다. 59 | * cardIds는 ICard의 id와 일치해야 하며, 이를 통해 ISearchResultItem에서 각 카드의 상세 정보를 찾을 수 있습니다. 60 | */ 61 | export interface ISearchResult { 62 | /** 검색된 카드 ID 목록 (ICard의 id와 일치해야 함) */ 63 | readonly cardIds: readonly string[]; 64 | /** 검색 쿼리 */ 65 | readonly query: string; 66 | /** 검색 설정 */ 67 | readonly config: ISearchConfig; 68 | /** 각 카드의 상세 검색 결과 (ISearchResultItem) */ 69 | readonly items: readonly ISearchResultItem[]; 70 | } 71 | 72 | /** 73 | * 기본 검색 기준 74 | */ 75 | export const DEFAULT_SEARCH_CRITERIA: ISearchCriteria = { 76 | query: '', 77 | scope: 'all', 78 | caseSensitive: false, 79 | useRegex: false, 80 | wholeWord: false 81 | }; 82 | 83 | /** 84 | * 기본 검색 설정 85 | */ 86 | export const DEFAULT_SEARCH_CONFIG: ISearchConfig = { 87 | criteria: DEFAULT_SEARCH_CRITERIA, 88 | history: [], 89 | maxHistory: 10 90 | }; -------------------------------------------------------------------------------- /src/domain/models/Sort.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 정렬 필드 타입 3 | */ 4 | export type SortField = 'fileName' | 'created' | 'modified' | 'custom'; 5 | 6 | /** 7 | * 정렬 방향 타입 8 | */ 9 | export type SortDirection = 'asc' | 'desc'; 10 | 11 | /** 12 | * 정렬 타입 열거형 13 | */ 14 | export enum SortType { 15 | NAME = 'name', 16 | DATE = 'date' 17 | } 18 | 19 | /** 20 | * 정렬 순서 열거형 21 | */ 22 | export enum SortOrder { 23 | ASC = 'asc', 24 | DESC = 'desc' 25 | } 26 | 27 | /** 28 | * 정렬 설정 인터페이스 29 | */ 30 | export interface ISortConfig { 31 | /** 정렬 타입 */ 32 | readonly type: SortType; 33 | /** 정렬 순서 */ 34 | readonly order: SortOrder; 35 | /** 정렬 필드 */ 36 | readonly field: SortField; 37 | /** 정렬 방향 */ 38 | readonly direction: SortDirection; 39 | /** 사용자 정의 정렬 필드 */ 40 | readonly customField?: string; 41 | /** 우선순위 태그 */ 42 | readonly priorityTags: readonly string[]; 43 | /** 우선순위 폴더 */ 44 | readonly priorityFolders: readonly string[]; 45 | } 46 | 47 | /** 48 | * 기본 정렬 설정 49 | */ 50 | export const DEFAULT_SORT_CONFIG: ISortConfig = { 51 | type: SortType.NAME, 52 | order: SortOrder.ASC, 53 | field: 'fileName', 54 | direction: 'asc', 55 | priorityTags: [], 56 | priorityFolders: [] 57 | }; 58 | 59 | /** 60 | * 정렬 설정 클래스 61 | */ 62 | export class SortConfig implements ISortConfig { 63 | constructor( 64 | public readonly type: SortType, 65 | public readonly order: SortOrder, 66 | public readonly field: SortField, 67 | public readonly direction: SortDirection, 68 | public readonly customField?: string, 69 | public readonly priorityTags: readonly string[] = [], 70 | public readonly priorityFolders: readonly string[] = [] 71 | ) {} 72 | 73 | /** 74 | * 정렬 설정 유효성 검사 75 | */ 76 | validate(): boolean { 77 | if (!this.field || !['fileName', 'created', 'modified', 'custom'].includes(this.field)) { 78 | return false; 79 | } 80 | 81 | if (!this.direction || !['asc', 'desc'].includes(this.direction)) { 82 | return false; 83 | } 84 | 85 | if (this.field === 'custom' && !this.customField) { 86 | return false; 87 | } 88 | 89 | return true; 90 | } 91 | 92 | /** 93 | * 정렬 설정 미리보기 94 | */ 95 | preview(): ISortConfig { 96 | return { 97 | type: this.type, 98 | order: this.order, 99 | field: this.field, 100 | direction: this.direction, 101 | customField: this.customField, 102 | priorityTags: this.priorityTags, 103 | priorityFolders: this.priorityFolders 104 | }; 105 | } 106 | } -------------------------------------------------------------------------------- /src/domain/services/application/IActiveFileWatcher.ts: -------------------------------------------------------------------------------- 1 | import { TFile } from 'obsidian'; 2 | 3 | /** 4 | * 활성 파일 감시자 인터페이스 5 | * - 옵시디언의 활성 파일 상태를 감시 6 | * - 활성 파일 변경 이벤트 처리 7 | */ 8 | export interface IActiveFileWatcher { 9 | /** 10 | * 초기화 11 | */ 12 | initialize(): void; 13 | 14 | /** 15 | * 초기화 여부 확인 16 | */ 17 | isInitialized(): boolean; 18 | 19 | /** 20 | * 정리 21 | */ 22 | cleanup(): void; 23 | 24 | /** 25 | * 현재 활성 파일 조회 26 | */ 27 | getActiveFile(): TFile | null; 28 | 29 | /** 30 | * 활성 파일 변경 이벤트 발생 31 | */ 32 | notifyFileChange(file: TFile | null): void; 33 | 34 | /** 35 | * 감시 시작 36 | */ 37 | start(): void; 38 | 39 | /** 40 | * 감시 중지 41 | */ 42 | stop(): void; 43 | } -------------------------------------------------------------------------------- /src/domain/services/application/ICardFocusService.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '@/domain/models/Card'; 2 | 3 | /** 4 | * 카드 포커스 서비스 인터페이스 5 | */ 6 | export interface ICardFocusService { 7 | /** 8 | * 카드에 포커스 설정 9 | * @param card 카드 10 | */ 11 | focusCard(card: ICard): void; 12 | 13 | /** 14 | * 포커스 해제 15 | */ 16 | blurCard(): void; 17 | 18 | /** 19 | * 포커스된 카드 조회 20 | * @returns 포커스된 카드 또는 null 21 | */ 22 | getFocusedCard(): ICard | null; 23 | 24 | /** 25 | * 포커스 이벤트 구독 26 | * @param callback 콜백 함수 27 | */ 28 | subscribeToFocusEvents(callback: (event: IFocusEvent) => void): void; 29 | 30 | /** 31 | * 포커스 이벤트 구독 해제 32 | * @param callback 콜백 함수 33 | */ 34 | unsubscribeFromFocusEvents(callback: (event: IFocusEvent) => void): void; 35 | } 36 | 37 | /** 38 | * 포커스 이벤트 인터페이스 39 | */ 40 | export interface IFocusEvent { 41 | type: 'focus' | 'blur'; 42 | cardId: string; 43 | previousCardId?: string; 44 | timestamp: Date; 45 | } -------------------------------------------------------------------------------- /src/domain/services/application/ICardNavigatorService.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '../../models/Card'; 2 | import { ICardSet } from '../../models/CardSet'; 3 | import { ISearchConfig } from '../../models/Search'; 4 | import { ISortConfig } from '../../models/Sort'; 5 | import { IPluginSettings } from '../../models/PluginSettings'; 6 | import { IRenderConfig } from '../../models/Card'; 7 | import { ICardStyle } from '../../models/Card'; 8 | 9 | /** 10 | * 카드 내비게이터 서비스 인터페이스 11 | * 카드 내비게이션과 관련된 모든 기능을 제공합니다. 12 | */ 13 | export interface ICardNavigatorService { 14 | /** 15 | * 카드셋에 해당하는 카드들을 가져옵니다. 16 | * @param cardSet 카드셋 17 | * @returns 카드 배열 18 | */ 19 | getCards(cardSet: ICardSet): Promise; 20 | 21 | /** 22 | * ID로 카드를 가져옵니다. 23 | * @param cardId 카드 ID 24 | * @returns 카드 또는 null 25 | */ 26 | getCardById(cardId: string): Promise; 27 | 28 | /** 29 | * 검색 결과에 해당하는 카드들을 가져옵니다. 30 | * @param query 검색어 31 | * @param config 검색 설정 32 | * @returns 카드 배열 33 | */ 34 | searchCards(query: string, config: ISearchConfig): Promise; 35 | 36 | /** 37 | * 카드들을 정렬합니다. 38 | * @param cards 카드 배열 39 | * @param config 정렬 설정 40 | * @returns 정렬된 카드 배열 41 | */ 42 | sortCards(cards: ICard[], config: ISortConfig): ICard[]; 43 | 44 | /** 45 | * 카드를 렌더링합니다. 46 | * @param card 카드 47 | * @returns 렌더링된 HTML 요소 48 | */ 49 | renderCard(card: ICard): HTMLElement; 50 | 51 | /** 52 | * 카드 표시 옵션을 적용합니다. 53 | * @param card 카드 54 | * @param settings 플러그인 설정 55 | */ 56 | applyDisplayOptions(card: ICard, settings: IPluginSettings): void; 57 | 58 | /** 59 | * 카드에 포커스를 설정합니다. 60 | * @param card 카드 61 | */ 62 | focusCard(card: ICard): void; 63 | 64 | /** 65 | * 프리셋을 저장합니다. 66 | * @param name 프리셋 이름 67 | * @param settings 플러그인 설정 68 | */ 69 | savePreset(name: string, settings: IPluginSettings): Promise; 70 | 71 | /** 72 | * 프리셋을 불러옵니다. 73 | * @param name 프리셋 이름 74 | * @returns 플러그인 설정 75 | */ 76 | loadPreset(name: string): Promise; 77 | 78 | /** 79 | * 폴더에 프리셋을 매핑합니다. 80 | * @param folderPath 폴더 경로 81 | * @param presetName 프리셋 이름 82 | */ 83 | mapPresetToFolder(folderPath: string, presetName: string): Promise; 84 | 85 | /** 86 | * 태그에 프리셋을 매핑합니다. 87 | * @param tag 태그 88 | * @param presetName 프리셋 이름 89 | */ 90 | mapPresetToTag(tag: string, presetName: string): Promise; 91 | 92 | /** 93 | * 카드 간 링크를 생성합니다. 94 | * @param sourceCard 소스 카드 95 | * @param targetCard 타겟 카드 96 | */ 97 | createLinkBetweenCards(sourceCard: ICard, targetCard: ICard): void; 98 | 99 | /** 100 | * 카드로 스크롤합니다. 101 | * @param card 카드 102 | */ 103 | scrollToCard(card: ICard): void; 104 | 105 | /** 106 | * 컨테이너의 크기를 가져옵니다. 107 | * @returns 컨테이너의 너비와 높이 108 | */ 109 | getContainerDimensions(): { width: number; height: number }; 110 | 111 | /** 112 | * 렌더링 설정을 가져옵니다. 113 | * @returns 렌더링 설정 114 | */ 115 | getRenderConfig(): IRenderConfig; 116 | 117 | /** 118 | * 카드 스타일을 가져옵니다. 119 | * @returns 카드 스타일 120 | */ 121 | getCardStyle(): ICardStyle; 122 | } -------------------------------------------------------------------------------- /src/domain/services/application/IClipboardService.ts: -------------------------------------------------------------------------------- 1 | import { TFile } from 'obsidian'; 2 | 3 | /** 4 | * 클립보드 서비스 인터페이스 5 | */ 6 | export interface IClipboardService { 7 | /** 8 | * 서비스 초기화 9 | */ 10 | initialize(): void; 11 | 12 | /** 13 | * 서비스 정리 14 | */ 15 | cleanup(): void; 16 | 17 | /** 18 | * 파일 링크 복사 19 | * @param file 파일 20 | */ 21 | copyLink(file: TFile): Promise; 22 | 23 | /** 24 | * 파일 내용 복사 25 | * @param file 파일 26 | */ 27 | copyContent(file: TFile): Promise; 28 | 29 | /** 30 | * 여러 파일의 링크 복사 31 | * @param files 파일 목록 32 | */ 33 | copyLinks(files: TFile[]): Promise; 34 | 35 | /** 36 | * 여러 파일의 내용 복사 37 | * @param files 파일 목록 38 | */ 39 | copyContents(files: TFile[]): Promise; 40 | } -------------------------------------------------------------------------------- /src/domain/services/application/IFileService.ts: -------------------------------------------------------------------------------- 1 | import { TFile } from 'obsidian'; 2 | 3 | /** 4 | * 파일 서비스 인터페이스 5 | */ 6 | export interface IFileService { 7 | /** 8 | * 파일을 엽니다. 9 | * @param file - 열 파일 10 | */ 11 | openFile(file: TFile): Promise; 12 | 13 | /** 14 | * 여러 파일을 엽니다. 15 | * @param files - 열 파일 목록 16 | */ 17 | openFiles(files: TFile[]): Promise; 18 | 19 | /** 20 | * 파일을 편집 모드로 엽니다. 21 | * @param file - 편집할 파일 22 | */ 23 | openFileForEditing(file: TFile): Promise; 24 | 25 | /** 26 | * 에디터에 링크를 삽입합니다. 27 | * @param file - 링크할 파일 28 | */ 29 | insertLinkToEditor(file: TFile): Promise; 30 | 31 | /** 32 | * 파일에 링크를 삽입합니다. 33 | * @param sourceFile - 링크를 삽입할 파일 34 | * @param targetFile - 링크할 파일 35 | */ 36 | insertLinkToFile(sourceFile: TFile, targetFile: TFile): Promise; 37 | } -------------------------------------------------------------------------------- /src/domain/services/application/IFocusService.ts: -------------------------------------------------------------------------------- 1 | import { TFile } from 'obsidian'; 2 | import { ICard } from '../../models/Card'; 3 | 4 | /** 5 | * 포커스 방향 6 | */ 7 | export enum FocusDirection { 8 | UP = 'UP', 9 | DOWN = 'DOWN', 10 | LEFT = 'LEFT', 11 | RIGHT = 'RIGHT' 12 | } 13 | 14 | /** 15 | * 포커스 서비스 인터페이스 16 | * 17 | * 포커스 조작을 담당하는 서비스 18 | */ 19 | export interface IFocusService { 20 | /** 21 | * 초기화 22 | */ 23 | initialize(): Promise; 24 | 25 | /** 26 | * 정리 27 | */ 28 | cleanup(): Promise; 29 | 30 | /** 31 | * 파일로 포커스 설정 32 | * @param file 파일 33 | */ 34 | focusByFile(file: TFile): Promise; 35 | 36 | /** 37 | * 카드로 포커스 설정 38 | * @param card 카드 39 | */ 40 | focusCard(card: ICard): Promise; 41 | 42 | /** 43 | * 방향으로 포커스 이동 44 | * @param direction 방향 45 | */ 46 | moveFocus(direction: FocusDirection): Promise; 47 | 48 | /** 49 | * 포커스된 카드 반환 50 | * @returns 포커스된 카드 51 | */ 52 | getFocusedCard(): ICard | null; 53 | 54 | /** 55 | * 포커스 이벤트 구독 56 | * @param callback 이벤트 콜백 57 | */ 58 | subscribeToFocusEvents(callback: (event: { 59 | type: 'focus' | 'blur'; 60 | card: ICard; 61 | previousCard?: ICard; 62 | }) => void): void; 63 | 64 | /** 65 | * 포커스 이벤트 구독 해제 66 | * @param callback 이벤트 콜백 67 | */ 68 | unsubscribeFromFocusEvents(callback: (event: { 69 | type: 'focus' | 'blur'; 70 | card: ICard; 71 | previousCard?: ICard; 72 | }) => void): void; 73 | } -------------------------------------------------------------------------------- /src/domain/services/application/ILayoutService.ts: -------------------------------------------------------------------------------- 1 | import { LayoutType, LayoutDirection, ILayoutConfig } from '../../models/Layout'; 2 | import { ICardSet } from '../../models/CardSet'; 3 | import { ICard } from '../../models/Card'; 4 | 5 | /** 6 | * 레이아웃 결과 인터페이스 7 | */ 8 | export interface ILayoutResult { 9 | /** 레이아웃 타입 */ 10 | readonly type: LayoutType; 11 | /** 레이아웃 방향 */ 12 | readonly direction: LayoutDirection; 13 | /** 열 수 */ 14 | readonly columnCount: number; 15 | /** 행 수 */ 16 | readonly rowCount: number; 17 | /** 카드 너비 */ 18 | readonly cardWidth: number; 19 | /** 카드 높이 */ 20 | readonly cardHeight: number; 21 | /** 카드 위치 */ 22 | readonly cardPositions: Map; 23 | } 24 | 25 | /** 26 | * 레이아웃 서비스 인터페이스 27 | * - 레이아웃 계산 및 관리 28 | * - 카드 위치 관리 29 | * - 레이아웃 설정 관리 30 | */ 31 | export interface ILayoutService { 32 | /** 33 | * 레이아웃 서비스를 초기화합니다. 34 | */ 35 | initialize(): void; 36 | 37 | /** 38 | * 서비스의 초기화 상태를 확인합니다. 39 | * @returns 서비스가 초기화되었으면 true, 그렇지 않으면 false 40 | */ 41 | isInitialized(): boolean; 42 | 43 | /** 44 | * 레이아웃 서비스를 정리합니다. 45 | */ 46 | cleanup(): void; 47 | 48 | /** 49 | * 현재 레이아웃 설정을 가져옵니다. 50 | * @returns 레이아웃 설정 51 | */ 52 | getLayoutConfig(): ILayoutConfig; 53 | 54 | /** 55 | * 레이아웃 설정을 업데이트합니다. 56 | * @param config 새 레이아웃 설정 57 | */ 58 | updateLayoutConfig(config: ILayoutConfig): void; 59 | 60 | /** 61 | * 카드셋에 대한 레이아웃을 계산합니다. 62 | * @param cardSet 카드셋 63 | * @param containerWidth 컨테이너 너비 64 | * @param containerHeight 컨테이너 높이 65 | * @returns 계산된 레이아웃 정보 66 | */ 67 | calculateLayout(cardSet: ICardSet, containerWidth: number, containerHeight: number): ILayoutResult; 68 | 69 | /** 70 | * 뷰포트 크기를 업데이트합니다. 71 | * @param width 새 너비 72 | * @param height 새 높이 73 | */ 74 | updateViewportDimensions(width: number, height: number): void; 75 | 76 | /** 77 | * 카드의 위치를 업데이트합니다. 78 | * @param cardId 카드 ID 79 | * @param x 새 x 좌표 80 | * @param y 새 y 좌표 81 | */ 82 | updateCardPosition(cardId: string, x: number, y: number): void; 83 | 84 | /** 85 | * 모든 카드의 위치를 초기화합니다. 86 | */ 87 | resetCardPositions(): void; 88 | 89 | /** 90 | * 기본 레이아웃 설정을 가져옵니다. 91 | * @returns 기본 레이아웃 설정 92 | */ 93 | getDefaultLayoutConfig(): ILayoutConfig; 94 | 95 | /** 96 | * 컨테이너 너비에 따른 열 수를 계산합니다. 97 | * @param containerWidth 컨테이너 너비 98 | * @param config 레이아웃 설정 99 | * @returns 계산된 열 수 100 | */ 101 | calculateColumnCount(containerWidth: number, config: ILayoutConfig): number; 102 | 103 | /** 104 | * 컨테이너 높이에 따른 행 수를 계산합니다. 105 | * @param containerHeight 컨테이너 높이 106 | * @param config 레이아웃 설정 107 | * @returns 계산된 행 수 108 | */ 109 | calculateRowCount(containerHeight: number, config: ILayoutConfig): number; 110 | 111 | /** 112 | * 다음 카드를 가져옵니다. 113 | * @param currentCard 현재 카드 114 | * @param direction 이동 방향 115 | * @returns 다음 카드 116 | */ 117 | getNextCard(currentCard: ICard, direction: LayoutDirection): ICard | null; 118 | 119 | /** 120 | * 카드 위치 조회 121 | * @param card 카드 122 | * @returns 카드 위치 또는 null 123 | */ 124 | getCardPosition(card: ICard): { x: number; y: number } | null; 125 | 126 | /** 127 | * 카드 스타일을 업데이트합니다. 128 | * @param card 카드 129 | * @param style 스타일 타입 130 | */ 131 | updateCardStyle(card: ICard, style: 'normal' | 'active' | 'focused'): void; 132 | } -------------------------------------------------------------------------------- /src/domain/services/application/IPresetService.ts: -------------------------------------------------------------------------------- 1 | import { IPreset, IPresetMapping } from '../../models/Preset'; 2 | import { ICardSection } from '../../models/Card'; 3 | import { ICardSetConfig } from '../../models/CardSet'; 4 | import { ILayoutConfig } from '../../models/Layout'; 5 | import { ISortConfig } from '../../models/Sort'; 6 | import { ISearchConfig } from '../../models/Search'; 7 | 8 | /** 9 | * 프리셋 서비스 인터페이스 10 | * - 프리셋 생성 및 관리 11 | * - 프리셋 매핑 12 | * - 프리셋 우선순위 13 | */ 14 | export interface IPresetService { 15 | /** 16 | * 서비스 초기화 17 | */ 18 | initialize(): Promise; 19 | 20 | /** 21 | * 서비스 정리 22 | */ 23 | cleanup(): Promise; 24 | 25 | /** 26 | * 현재 프리셋 가져오기 27 | */ 28 | getCurrentPreset(): Promise; 29 | 30 | /** 31 | * 기본 프리셋 로드 32 | */ 33 | loadDefaultPreset(): Promise; 34 | 35 | /** 36 | * 프리셋 생성 37 | */ 38 | createPreset( 39 | name: string, 40 | description: string, 41 | category: string, 42 | cardSection: ICardSection, 43 | cardSetConfig: ICardSetConfig, 44 | layoutConfig: ILayoutConfig, 45 | sortConfig: ISortConfig, 46 | searchConfig: ISearchConfig 47 | ): Promise; 48 | 49 | /** 50 | * 프리셋 업데이트 51 | */ 52 | updatePreset( 53 | preset: IPreset, 54 | cardSection: ICardSection, 55 | cardSetConfig: ICardSetConfig, 56 | layoutConfig: ILayoutConfig, 57 | sortConfig: ISortConfig, 58 | searchConfig: ISearchConfig 59 | ): Promise; 60 | 61 | /** 62 | * 프리셋 삭제 63 | */ 64 | deletePreset(presetId: string): Promise; 65 | 66 | /** 67 | * 프리셋 복제 68 | */ 69 | clonePreset(presetId: string, newName: string): Promise; 70 | 71 | /** 72 | * 프리셋 적용 73 | */ 74 | applyPreset(presetId: string): Promise; 75 | 76 | /** 77 | * 프리셋 가져오기 78 | */ 79 | getPreset(presetId: string): Promise; 80 | 81 | /** 82 | * 모든 프리셋 가져오기 83 | */ 84 | getAllPresets(): Promise; 85 | 86 | /** 87 | * 프리셋 유효성 검사 88 | */ 89 | validatePreset(preset: IPreset): boolean; 90 | 91 | /** 92 | * 프리셋 내보내기 93 | * @param presetId 프리셋 ID 94 | */ 95 | exportPreset(presetId: string): Promise; 96 | 97 | /** 98 | * 프리셋 가져오기 99 | * @param presetJson 프리셋 JSON 100 | */ 101 | importPreset(presetJson: string): Promise; 102 | 103 | /** 104 | * 프리셋 매핑 생성 105 | * @param presetId 프리셋 ID 106 | * @param mapping 매핑 107 | */ 108 | createPresetMapping( 109 | presetId: string, 110 | mapping: Omit 111 | ): Promise; 112 | 113 | /** 114 | * 프리셋 매핑 업데이트 115 | * @param mappingId 매핑 ID 116 | * @param mapping 매핑 117 | */ 118 | updatePresetMapping( 119 | mappingId: string, 120 | mapping: Partial 121 | ): Promise; 122 | 123 | /** 124 | * 프리셋 매핑 삭제 125 | * @param mappingId 매핑 ID 126 | */ 127 | deletePresetMapping(mappingId: string): Promise; 128 | 129 | /** 130 | * 매핑 우선순위 업데이트 131 | * @param mappingIds 매핑 ID 목록 132 | */ 133 | updateMappingPriority(mappingIds: string[]): Promise; 134 | 135 | /** 136 | * 프리셋 이벤트 구독 137 | * @param callback 콜백 함수 138 | */ 139 | subscribeToPresetEvents(callback: (event: any) => void): void; 140 | 141 | /** 142 | * 프리셋 이벤트 구독 해제 143 | * @param callback 콜백 함수 144 | */ 145 | unsubscribeFromPresetEvents(callback: (event: any) => void): void; 146 | } -------------------------------------------------------------------------------- /src/domain/services/application/IScrollService.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '@/domain/models/Card'; 2 | 3 | /** 4 | * 스크롤 서비스 인터페이스 5 | */ 6 | export interface IScrollService { 7 | /** 8 | * 초기화 9 | */ 10 | initialize(): void; 11 | 12 | /** 13 | * 정리 14 | */ 15 | cleanup(): void; 16 | 17 | /** 18 | * 초기화 여부 확인 19 | * @returns 초기화 완료 여부 20 | */ 21 | isInitialized(): boolean; 22 | 23 | /** 24 | * 카드를 뷰포트 중앙에 위치 25 | * @param card 카드 26 | */ 27 | centerCard(card: ICard): void; 28 | 29 | /** 30 | * 카드를 스크롤 위치로 이동 31 | * @param card 카드 32 | */ 33 | scrollToCard(card: ICard): void; 34 | 35 | /** 36 | * 부드러운 스크롤 여부 설정 37 | * @param enabled 부드러운 스크롤 사용 여부 38 | */ 39 | setSmoothScroll(enabled: boolean): void; 40 | 41 | /** 42 | * 부드러운 스크롤 여부 확인 43 | * @returns 부드러운 스크롤 사용 여부 44 | */ 45 | isSmoothScrollEnabled(): boolean; 46 | 47 | /** 48 | * 스크롤 동작 설정 49 | * @param behavior 스크롤 동작 50 | */ 51 | setScrollBehavior(behavior: 'auto' | 'smooth' | 'instant'): void; 52 | 53 | /** 54 | * 스크롤 동작 확인 55 | * @returns 스크롤 동작 56 | */ 57 | getScrollBehavior(): 'auto' | 'smooth' | 'instant'; 58 | 59 | /** 60 | * 스크롤 컨테이너 설정 61 | * @param container 스크롤 컨테이너 요소 62 | */ 63 | setScrollContainer(container: HTMLElement): void; 64 | 65 | /** 66 | * 스크롤 컨테이너 확인 67 | * @returns 스크롤 컨테이너 요소 68 | */ 69 | getScrollContainer(): HTMLElement | null; 70 | } -------------------------------------------------------------------------------- /src/domain/services/application/ISearchService.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '../../models/Card'; 2 | import { ISearchConfig, ISearchResult } from '../../models/Search'; 3 | import { TFile } from 'obsidian'; 4 | 5 | /** 6 | * 검색 서비스 인터페이스 7 | * 8 | * @remarks 9 | * 검색 서비스는 검색 실행, 실시간 검색, 파일 검색, 검색 결과 필터링 및 유효성 검사를 담당합니다. 10 | */ 11 | export interface ISearchService { 12 | /** 13 | * 초기화 14 | */ 15 | initialize(): void; 16 | 17 | /** 18 | * 정리 19 | */ 20 | cleanup(): void; 21 | 22 | /** 23 | * 검색 실행 24 | * @param query 검색어 25 | * @param config 검색 설정 26 | * @returns 검색 결과 27 | */ 28 | search(query: string, config: ISearchConfig): Promise; 29 | 30 | /** 31 | * 실시간 검색 32 | * @param query 검색어 33 | * @param config 검색 설정 34 | * @returns 검색 결과 35 | */ 36 | searchRealtime(query: string, config: ISearchConfig): Promise; 37 | 38 | /** 39 | * 파일 검색 40 | * @param file 검색할 파일 41 | * @param query 검색어 42 | * @param config 검색 설정 43 | * @returns 검색 결과 44 | */ 45 | searchInFile(file: TFile, query: string, config: ISearchConfig): Promise; 46 | 47 | /** 48 | * 검색 결과 필터링 49 | * @param result 검색 결과 50 | * @param config 검색 설정 51 | * @returns 필터링된 검색 결과 52 | */ 53 | filterResults(result: ISearchResult, config: ISearchConfig): Promise; 54 | 55 | /** 56 | * 검색 결과 유효성 검사 57 | * @param result 검색 결과 58 | * @returns 유효성 여부 59 | */ 60 | validateResults(result: ISearchResult): boolean; 61 | 62 | /** 63 | * 검색 결과 하이라이트 64 | * @param card 카드 65 | * @param query 검색어 66 | * @param config 검색 설정 67 | * @returns 하이라이트된 텍스트 68 | */ 69 | highlightSearchResults(card: ICard, query: string, config: ISearchConfig): Promise; 70 | 71 | /** 72 | * 검색 인덱스 업데이트 73 | * @param card 카드 74 | */ 75 | updateSearchIndex(card: ICard): Promise; 76 | 77 | /** 78 | * 검색 인덱스에서 제거 79 | * @param cardId 카드 ID 80 | */ 81 | removeFromSearchIndex(cardId: string): Promise; 82 | } -------------------------------------------------------------------------------- /src/domain/services/application/ISettingsService.ts: -------------------------------------------------------------------------------- 1 | import { IPluginSettings, ICardSetDomainSettings, ILayoutDomainSettings, ISearchDomainSettings, ISortDomainSettings, IPresetDomainSettings } from '../../models/PluginSettings'; 2 | import { ICardSection, ICardStyle } from '../../models/Card'; 3 | import { CardSetType } from '../../models/CardSet'; 4 | 5 | /** 6 | * 설정 서비스 인터페이스 7 | */ 8 | export interface ISettingsService { 9 | /** 10 | * 초기화 11 | */ 12 | initialize(): void; 13 | 14 | /** 15 | * 정리 16 | */ 17 | cleanup(): void; 18 | 19 | /** 20 | * 설정 로드 21 | * @returns 플러그인 설정 22 | */ 23 | loadSettings(): Promise; 24 | 25 | /** 26 | * 설정 저장 27 | * @param settings 저장할 설정 28 | */ 29 | saveSettings(settings: IPluginSettings): Promise; 30 | 31 | /** 32 | * 설정 가져오기 33 | * @returns 플러그인 설정 34 | */ 35 | getSettings(): IPluginSettings; 36 | 37 | /** 38 | * 설정 변경 이벤트 구독 39 | * @param callback 콜백 함수 40 | * @returns 구독 해제 함수 41 | */ 42 | onSettingsChanged(callback: (data: {oldSettings: IPluginSettings, newSettings: IPluginSettings}) => void): () => void; 43 | 44 | /** 45 | * 카드셋 도메인 설정 가져오기 46 | * @param type 카드셋 타입 47 | * @returns 카드셋 도메인 설정 48 | */ 49 | getCardSetDomainSettings(type: CardSetType): ICardSetDomainSettings; 50 | 51 | /** 52 | * 카드셋 도메인 설정 업데이트 53 | * @param type 카드셋 타입 54 | * @param settings 카드셋 도메인 설정 55 | */ 56 | updateCardSetDomainSettings(type: CardSetType, settings: ICardSetDomainSettings): Promise; 57 | 58 | /** 59 | * 레이아웃 도메인 설정 가져오기 60 | * @returns 레이아웃 도메인 설정 61 | */ 62 | getLayoutDomainSettings(): ILayoutDomainSettings; 63 | 64 | /** 65 | * 레이아웃 도메인 설정 업데이트 66 | * @param settings 레이아웃 도메인 설정 67 | */ 68 | updateLayoutDomainSettings(settings: ILayoutDomainSettings): Promise; 69 | 70 | /** 71 | * 정렬 도메인 설정 가져오기 72 | * @returns 정렬 도메인 설정 73 | */ 74 | getSortDomainSettings(): ISortDomainSettings; 75 | 76 | /** 77 | * 정렬 도메인 설정 업데이트 78 | * @param settings 정렬 도메인 설정 79 | */ 80 | updateSortDomainSettings(settings: ISortDomainSettings): Promise; 81 | 82 | /** 83 | * 검색 도메인 설정 가져오기 84 | * @returns 검색 도메인 설정 85 | */ 86 | getSearchDomainSettings(): ISearchDomainSettings; 87 | 88 | /** 89 | * 검색 도메인 설정 업데이트 90 | * @param settings 검색 도메인 설정 91 | */ 92 | updateSearchDomainSettings(settings: ISearchDomainSettings): Promise; 93 | 94 | /** 95 | * 프리셋 도메인 설정 가져오기 96 | * @returns 프리셋 도메인 설정 97 | */ 98 | getPresetDomainSettings(): IPresetDomainSettings; 99 | 100 | /** 101 | * 프리셋 도메인 설정 업데이트 102 | * @param settings 프리셋 도메인 설정 103 | */ 104 | updatePresetDomainSettings(settings: IPresetDomainSettings): Promise; 105 | 106 | /** 107 | * 설정 유효성 검사 108 | * @param settings 검사할 설정 109 | * @returns 유효성 여부 110 | */ 111 | validateSettings(settings: IPluginSettings): boolean; 112 | 113 | /** 114 | * 설정 변경 이벤트 구독 115 | * @param callback 콜백 함수 116 | */ 117 | subscribeToSettingsChange(callback: (settings: IPluginSettings) => void): void; 118 | 119 | /** 120 | * 설정 변경 이벤트 구독 해제 121 | * @param callback 콜백 함수 122 | */ 123 | unsubscribeFromSettingsChange(callback: (settings: IPluginSettings) => void): void; 124 | 125 | /** 126 | * 중첩된 설정 업데이트 127 | * @param path 설정 경로 128 | * @param value 값 129 | */ 130 | updateNestedSettings(path: string, value: any): Promise; 131 | 132 | /** 133 | * 카드 스타일 업데이트 134 | * @param state 카드 상태 (normal, active, focused) 135 | * @param property 스타일 속성 136 | * @param value 값 137 | */ 138 | updateCardStyle(state: 'normal' | 'active' | 'focused', property: keyof ICardStyle, value: string): Promise; 139 | 140 | /** 141 | * 카드 섹션 표시 설정 업데이트 142 | * @param section 섹션 타입 143 | * @param property 표시 옵션 속성 144 | * @param value 값 145 | */ 146 | updateCardSectionDisplay( 147 | section: 'header' | 'body' | 'footer', 148 | property: keyof ICardSection['displayOptions'], 149 | value: boolean 150 | ): Promise; 151 | } 152 | -------------------------------------------------------------------------------- /src/domain/services/application/ISortService.ts: -------------------------------------------------------------------------------- 1 | import { ISortConfig } from '../../models/Sort'; 2 | import { ICard } from '../../models/Card'; 3 | 4 | /** 5 | * 정렬 서비스 인터페이스 6 | * 7 | * @remarks 8 | * 정렬 서비스는 카드 목록의 정렬과 정렬 설정의 유효성 검사를 담당합니다. 9 | */ 10 | export interface ISortService { 11 | /** 12 | * 초기화 13 | */ 14 | initialize(): void; 15 | 16 | /** 17 | * 정리 18 | */ 19 | cleanup(): void; 20 | 21 | /** 22 | * 카드 목록 정렬 23 | * @param cards 정렬할 카드 목록 24 | * @param config 정렬 설정 25 | * @returns 정렬된 카드 목록 26 | */ 27 | sortCards(cards: readonly ICard[], config: ISortConfig): Promise; 28 | 29 | /** 30 | * 정렬 설정 유효성 검사 31 | * @param config 검사할 정렬 설정 32 | * @returns 유효성 여부 33 | */ 34 | validateSortConfig(config: ISortConfig): boolean; 35 | } -------------------------------------------------------------------------------- /src/domain/services/application/IToolbarService.ts: -------------------------------------------------------------------------------- 1 | import { CardSetType } from '../../models/CardSet'; 2 | import { ICardSection, ICardStyle } from '../../models/Card'; 3 | import { ILayoutConfig } from '../../models/Layout'; 4 | import { ISearchConfig } from '../../models/Search'; 5 | import { ISortConfig } from '../../models/Sort'; 6 | 7 | /** 8 | * 툴바 액션 타입 9 | */ 10 | export enum ToolbarActionType { 11 | /** 카드셋 타입 변경 */ 12 | CHANGE_CARD_SET_TYPE = 'CHANGE_CARD_SET_TYPE', 13 | /** 검색 */ 14 | SEARCH = 'SEARCH', 15 | /** 정렬 */ 16 | SORT = 'SORT', 17 | /** 설정 토글 */ 18 | TOGGLE_SETTING = 'TOGGLE_SETTING' 19 | } 20 | 21 | /** 22 | * 툴바 서비스 인터페이스 23 | */ 24 | export interface IToolbarService { 25 | /** 26 | * 서비스 초기화 27 | */ 28 | initialize(): void; 29 | 30 | /** 31 | * 초기화 여부 확인 32 | * @returns 초기화 여부 33 | */ 34 | isInitialized(): boolean; 35 | 36 | /** 37 | * 서비스 정리 38 | */ 39 | cleanup(): void; 40 | 41 | /** 42 | * 카드셋 타입 변경 43 | * @param type 카드셋 타입 44 | */ 45 | changeCardSetType(type: CardSetType): void; 46 | 47 | /** 48 | * 현재 카드셋 타입 가져오기 49 | * @returns 현재 카드셋 타입 50 | */ 51 | getCurrentCardSetType(): CardSetType; 52 | 53 | /** 54 | * 검색 설정 업데이트 55 | * @param config 검색 설정 56 | */ 57 | updateSearchConfig(config: ISearchConfig): void; 58 | 59 | /** 60 | * 현재 검색 설정 가져오기 61 | * @returns 현재 검색 설정 62 | */ 63 | getCurrentSearchConfig(): ISearchConfig; 64 | 65 | /** 66 | * 정렬 설정 업데이트 67 | * @param config 정렬 설정 68 | */ 69 | updateSortConfig(config: ISortConfig): void; 70 | 71 | /** 72 | * 현재 정렬 설정 가져오기 73 | * @returns 현재 정렬 설정 74 | */ 75 | getCurrentSortConfig(): ISortConfig; 76 | 77 | /** 78 | * 카드 섹션 업데이트 79 | * @param section 카드 섹션 80 | */ 81 | updateCardSection(section: ICardSection): void; 82 | 83 | /** 84 | * 현재 카드 섹션 가져오기 85 | * @returns 현재 카드 섹션 86 | */ 87 | getCurrentCardSection(): ICardSection; 88 | 89 | /** 90 | * 카드 스타일 업데이트 91 | * @param style 카드 스타일 92 | */ 93 | updateCardStyle(style: ICardStyle): void; 94 | 95 | /** 96 | * 현재 카드 스타일 가져오기 97 | * @returns 현재 카드 스타일 98 | */ 99 | getCurrentCardStyle(): ICardStyle; 100 | 101 | /** 102 | * 레이아웃 설정 업데이트 103 | * @param config 레이아웃 설정 104 | */ 105 | updateLayoutConfig(config: ILayoutConfig): void; 106 | 107 | /** 108 | * 현재 레이아웃 설정 가져오기 109 | * @returns 현재 레이아웃 설정 110 | */ 111 | getCurrentLayoutConfig(): ILayoutConfig; 112 | 113 | /** 114 | * UI 업데이트 115 | */ 116 | updateUI(): void; 117 | } -------------------------------------------------------------------------------- /src/domain/services/application/IViewService.ts: -------------------------------------------------------------------------------- 1 | export interface IViewService { 2 | initializeViews(): void; 3 | activateView(viewId: string): void; 4 | deactivateView(viewId: string): void; 5 | getActiveView(): string | null; 6 | } -------------------------------------------------------------------------------- /src/domain/services/domain/ICacheService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 캐시 서비스 인터페이스 3 | */ 4 | export interface ICacheService { 5 | /** 6 | * 캐시 초기화 7 | */ 8 | initialize(): void; 9 | 10 | /** 11 | * 캐시 정리 12 | */ 13 | cleanup(): void; 14 | 15 | /** 16 | * 캐시 데이터 가져오기 17 | * @param key 캐시 키 18 | */ 19 | get(key: string): T | null; 20 | 21 | /** 22 | * 캐시 데이터 저장 23 | * @param key 캐시 키 24 | * @param value 캐시 값 25 | */ 26 | set(key: string, value: T): void; 27 | 28 | /** 29 | * 캐시 데이터 삭제 30 | * @param key 캐시 키 31 | */ 32 | delete(key: string): void; 33 | 34 | /** 35 | * 캐시 데이터 초기화 36 | */ 37 | clear(): void; 38 | 39 | /** 40 | * 캐시 데이터 존재 여부 확인 41 | * @param key 캐시 키 42 | */ 43 | has(key: string): boolean; 44 | } -------------------------------------------------------------------------------- /src/domain/services/domain/ICardInteractionService.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '../../models/Card'; 2 | import { TFile } from 'obsidian'; 3 | 4 | /** 5 | * 컨텍스트 메뉴 액션 타입 6 | */ 7 | export enum ContextMenuActionType { 8 | /** 링크 복사 */ 9 | COPY_LINK = 'COPY_LINK', 10 | /** 내용 복사 */ 11 | COPY_CONTENT = 'COPY_CONTENT' 12 | } 13 | 14 | /** 15 | * 드래그 앤 드롭 타겟 타입 16 | */ 17 | export enum DragDropTargetType { 18 | /** 편집기 */ 19 | EDITOR = 'EDITOR', 20 | /** 카드 */ 21 | CARD = 'CARD' 22 | } 23 | 24 | /** 25 | * 드래그 앤 드롭 타겟 26 | */ 27 | export interface IDragDropTarget { 28 | /** 타겟 타입 */ 29 | type: DragDropTargetType; 30 | /** 파일 */ 31 | file?: TFile; 32 | } 33 | 34 | /** 35 | * 카드 상호작용 서비스 인터페이스 36 | */ 37 | export interface ICardInteractionService { 38 | /** 39 | * 서비스 초기화 40 | */ 41 | initialize(): void; 42 | 43 | /** 44 | * 서비스 정리 45 | */ 46 | cleanup(): void; 47 | 48 | /** 49 | * 파일 열기 50 | * @param file 파일 51 | */ 52 | openFile(file: TFile): void; 53 | 54 | /** 55 | * 컨텍스트 메뉴 액션 실행 56 | * @param file 파일 57 | * @param action 액션 타입 58 | */ 59 | executeContextMenuAction(file: TFile, action: ContextMenuActionType): void; 60 | 61 | /** 62 | * 드래그 앤 드롭 처리 63 | * @param sourceFile 소스 파일 64 | * @param target 드래그 앤 드롭 타겟 65 | */ 66 | handleDragDrop(sourceFile: TFile, target: IDragDropTarget): void; 67 | 68 | /** 69 | * 인라인 편집 시작 70 | * @param file 파일 71 | */ 72 | startInlineEdit(file: TFile): void; 73 | 74 | /** 75 | * 인라인 편집 종료 76 | */ 77 | endInlineEdit(): void; 78 | 79 | /** 80 | * UI 업데이트 81 | */ 82 | updateUI(): void; 83 | 84 | /** 85 | * 두 카드 간의 링크를 생성합니다. 86 | * @param sourceCard 소스 카드 87 | * @param targetCard 타겟 카드 88 | */ 89 | createLink(sourceCard: ICard, targetCard: ICard): Promise; 90 | } -------------------------------------------------------------------------------- /src/domain/services/domain/ICardSelectionService.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '../../models/Card'; 2 | import { TFile } from 'obsidian'; 3 | 4 | /** 5 | * 선택 타입 6 | */ 7 | export enum SelectionType { 8 | /** 단일 선택 */ 9 | SINGLE = 'SINGLE', 10 | /** 범위 선택 */ 11 | RANGE = 'RANGE', 12 | /** 토글 선택 */ 13 | TOGGLE = 'TOGGLE', 14 | /** 전체 선택 */ 15 | ALL = 'ALL' 16 | } 17 | 18 | /** 19 | * 카드 선택 서비스 인터페이스 20 | */ 21 | export interface ICardSelectionService { 22 | /** 23 | * 서비스 초기화 24 | */ 25 | initialize(): void; 26 | 27 | /** 28 | * 서비스 정리 29 | */ 30 | cleanup(): void; 31 | 32 | /** 33 | * 카드 선택 34 | * @param file 파일 35 | */ 36 | selectCard(file: TFile): void; 37 | 38 | /** 39 | * 카드 범위 선택 40 | * @param file 파일 41 | */ 42 | selectRange(file: TFile): void; 43 | 44 | /** 45 | * 카드 선택 토글 46 | * @param file 파일 47 | */ 48 | toggleCardSelection(file: TFile): void; 49 | 50 | /** 51 | * 모든 카드 선택 52 | */ 53 | selectAllCards(): void; 54 | 55 | /** 56 | * 선택 해제 57 | */ 58 | clearSelection(): void; 59 | 60 | /** 61 | * 선택된 카드 목록 조회 62 | */ 63 | getSelectedCards(): ICard[]; 64 | 65 | /** 66 | * 선택된 파일 목록 조회 67 | */ 68 | getSelectedFiles(): TFile[]; 69 | 70 | /** 71 | * 선택된 카드 수 조회 72 | */ 73 | getSelectedCount(): number; 74 | 75 | /** 76 | * 선택 UI 업데이트 77 | */ 78 | updateSelectionUI(): void; 79 | } -------------------------------------------------------------------------------- /src/domain/services/domain/ICardService.ts: -------------------------------------------------------------------------------- 1 | import { ICard, ICardSection } from '../../models/Card'; 2 | import { TFile } from 'obsidian'; 3 | import { ICardFactory } from '../../factories/ICardFactory'; 4 | 5 | /** 6 | * 카드 서비스 인터페이스 7 | */ 8 | export interface ICardService { 9 | /** 10 | * 초기화 11 | */ 12 | initialize(): void; 13 | 14 | /** 15 | * 정리 16 | */ 17 | cleanup(): void; 18 | 19 | /** 20 | * 카드 팩토리를 설정합니다. 21 | * @param cardFactory 카드 팩토리 22 | */ 23 | setCardFactory(cardFactory: ICardFactory): void; 24 | 25 | /** 26 | * 파일로부터 카드 생성 27 | * @param file 파일 28 | * @param section 카드 섹션 29 | * @returns 생성된 카드 30 | */ 31 | createCardFromFile(file: TFile, section: ICardSection): Promise; 32 | 33 | /** 34 | * 파일로부터 카드를 가져옵니다. 35 | * @param file 파일 36 | * @returns 카드 또는 null 37 | */ 38 | getCardByFile(file: TFile): Promise; 39 | 40 | /** 41 | * 카드 업데이트 42 | * @param card 업데이트할 카드 43 | * @param section 카드 섹션 44 | * @returns 업데이트된 카드 45 | */ 46 | updateCard(card: ICard, section: ICardSection): Promise; 47 | 48 | /** 49 | * 카드 삭제 50 | * @param card 삭제할 카드 51 | */ 52 | deleteCard(card: ICard): Promise; 53 | 54 | /** 55 | * 카드 유효성 검사 56 | * @param card 검사할 카드 57 | * @returns 유효성 여부 58 | */ 59 | validateCard(card: ICard): boolean; 60 | } -------------------------------------------------------------------------------- /src/domain/services/domain/ICardSetService.ts: -------------------------------------------------------------------------------- 1 | import { ICard } from '../../models/Card'; 2 | import { ICardSet, ICardSetConfig, CardSetType, LinkType } from '../../models/CardSet'; 3 | import { TFile } from 'obsidian'; 4 | 5 | /** 6 | * 카드셋 서비스 인터페이스 7 | * 8 | * @remarks 9 | * 카드셋 서비스는 카드셋 생성, 관리, 필터링을 담당합니다. 10 | */ 11 | export interface ICardSetService { 12 | /** 13 | * 초기화 14 | */ 15 | initialize(): void; 16 | 17 | /** 18 | * 정리 19 | */ 20 | cleanup(): void; 21 | 22 | /** 23 | * 카드셋 생성 24 | * @param type 카드셋 타입 25 | * @param config 카드셋 설정 26 | * @returns 생성된 카드셋 27 | */ 28 | createCardSet(type: CardSetType, config: ICardSetConfig): Promise; 29 | 30 | /** 31 | * 카드셋 업데이트 32 | * @param cardSet 업데이트할 카드셋 33 | * @param config 카드셋 설정 34 | * @returns 업데이트된 카드셋 35 | */ 36 | updateCardSet(cardSet: ICardSet, config: ICardSetConfig): Promise; 37 | 38 | /** 39 | * 카드셋에 카드 추가 40 | * @param cardSet 대상 카드셋 41 | * @param file 추가할 파일 42 | */ 43 | addCardToSet(cardSet: ICardSet, file: TFile): Promise; 44 | 45 | /** 46 | * 카드셋에서 카드 제거 47 | * @param cardSet 대상 카드셋 48 | * @param file 제거할 파일 49 | */ 50 | removeCardFromSet(cardSet: ICardSet, file: TFile): Promise; 51 | 52 | /** 53 | * 카드셋 유효성 검사 54 | * @param cardSet 검사할 카드셋 55 | * @returns 유효성 여부 56 | */ 57 | validateCardSet(cardSet: ICardSet): boolean; 58 | 59 | /** 60 | * 카드셋의 카드 필터링 61 | * @param cardSet 카드셋 62 | * @param filter 필터 함수 63 | * @returns 필터링된 카드셋 64 | */ 65 | filterCards(cardSet: ICardSet, filter: (card: ICard) => boolean): Promise; 66 | 67 | /** 68 | * 활성 폴더 카드셋 생성 69 | * @param activeFile 활성 파일 70 | * @returns 생성된 카드셋 71 | */ 72 | createActiveFolderCardSet(activeFile: TFile): Promise; 73 | 74 | /** 75 | * 지정 폴더 카드셋 생성 76 | * @param folderPath 폴더 경로 77 | * @param includeSubfolders 하위 폴더 포함 여부 78 | * @returns 생성된 카드셋 79 | */ 80 | createSpecifiedFolderCardSet(folderPath: string, includeSubfolders: boolean): Promise; 81 | 82 | /** 83 | * 활성 태그 카드셋 생성 84 | * @param activeFile 활성 파일 85 | * @param includeSubtags 하위 태그 포함 여부 86 | * @param caseSensitive 태그 대소문자 구분 여부 87 | * @returns 생성된 카드셋 88 | */ 89 | createActiveTagCardSet(activeFile: TFile, includeSubtags: boolean, caseSensitive: boolean): Promise; 90 | 91 | /** 92 | * 지정 태그 카드셋 생성 93 | * @param tag 태그 94 | * @param includeSubtags 하위 태그 포함 여부 95 | * @param caseSensitive 태그 대소문자 구분 여부 96 | * @returns 생성된 카드셋 97 | */ 98 | createSpecifiedTagCardSet(tag: string, includeSubtags: boolean, caseSensitive: boolean): Promise; 99 | 100 | /** 101 | * 링크 카드셋 생성 102 | * @param filePath 파일 경로 103 | * @param linkType 링크 타입 104 | * @param linkDepth 링크 깊이 105 | * @returns 생성된 카드셋 106 | */ 107 | createLinkCardSet(filePath: string, linkType: LinkType, linkDepth: number): Promise; 108 | 109 | /** 110 | * 폴더 경로 목록 조회 111 | * @returns 폴더 경로 목록 112 | */ 113 | getFolderPaths(): Promise; 114 | 115 | /** 116 | * 태그 목록 조회 117 | * @returns 태그 목록 118 | */ 119 | getTags(): Promise; 120 | 121 | /** 122 | * 카드셋 활성화 123 | * @param cardSetId 카드셋 ID 124 | */ 125 | activateCardSet(cardSetId: string): Promise; 126 | 127 | /** 128 | * 카드셋 비활성화 129 | * @param cardSetId 카드셋 ID 130 | */ 131 | deactivateCardSet(cardSetId: string): Promise; 132 | 133 | /** 134 | * 활성 카드셋 조회 135 | * @returns 활성 카드셋 또는 null 136 | */ 137 | getActiveCardSet(): Promise; 138 | } -------------------------------------------------------------------------------- /src/domain/utils/Debouncer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 디바운스 유틸리티 클래스 3 | * 연속된 호출을 일정 시간 동안 기다린 후 마지막 호출만 실행 4 | */ 5 | export class Debouncer { 6 | private timeoutId: number | null = null; 7 | private lastArgs: T | null = null; 8 | private lastResult: R | null = null; 9 | 10 | constructor( 11 | private readonly fn: (...args: T) => R, 12 | private readonly delay: number 13 | ) {} 14 | 15 | /** 16 | * 디바운스된 함수 호출 17 | * @param args 함수 인자 18 | * @returns 함수 실행 결과 19 | */ 20 | public debounce(...args: T): R { 21 | this.lastArgs = args; 22 | 23 | if (this.timeoutId !== null) { 24 | window.clearTimeout(this.timeoutId); 25 | } 26 | 27 | this.timeoutId = window.setTimeout(() => { 28 | if (this.lastArgs) { 29 | this.lastResult = this.fn(...this.lastArgs); 30 | } 31 | this.timeoutId = null; 32 | }, this.delay); 33 | 34 | return this.lastResult as R; 35 | } 36 | 37 | /** 38 | * 디바운스 취소 39 | */ 40 | public cancel(): void { 41 | if (this.timeoutId !== null) { 42 | window.clearTimeout(this.timeoutId); 43 | this.timeoutId = null; 44 | } 45 | this.lastArgs = null; 46 | this.lastResult = null; 47 | } 48 | 49 | /** 50 | * 디바운스된 함수 즉시 실행 51 | * @param args 함수 인자 52 | * @returns 함수 실행 결과 53 | */ 54 | public execute(...args: T): R { 55 | this.lastArgs = args; 56 | this.lastResult = this.fn(...args); 57 | return this.lastResult; 58 | } 59 | } -------------------------------------------------------------------------------- /src/domain/utils/Throttler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 쓰로틀 유틸리티 클래스 3 | * 일정 시간 내에 함수가 최대 한 번만 실행되도록 보장 4 | */ 5 | export class Throttler { 6 | private lastCallTime: number = 0; 7 | private lastResult: R | null = null; 8 | private pendingCall: boolean = false; 9 | private pendingArgs: T | null = null; 10 | 11 | constructor( 12 | private readonly fn: (...args: T) => R, 13 | private readonly delay: number 14 | ) {} 15 | 16 | /** 17 | * 쓰로틀된 함수 호출 18 | * @param args 함수 인자 19 | * @returns 함수 실행 결과 또는 마지막 실행 결과 20 | */ 21 | public throttle(...args: T): R { 22 | const now = Date.now(); 23 | 24 | // 마지막 호출 이후 지정된 딜레이만큼 시간이 지났는지 확인 25 | if (now - this.lastCallTime >= this.delay) { 26 | // 마지막 호출로부터 충분한 시간이 지났으므로 즉시 실행 27 | return this.executeNow(args); 28 | } 29 | 30 | // 아직 딜레이 내에 있고, 대기 중인 호출이 없으면 다음 딜레이 후에 실행될 호출 예약 31 | if (!this.pendingCall) { 32 | this.pendingCall = true; 33 | this.pendingArgs = args; 34 | 35 | const timeUntilNextCall = this.delay - (now - this.lastCallTime); 36 | setTimeout(() => { 37 | if (this.pendingArgs) { 38 | this.executeNow(this.pendingArgs); 39 | this.pendingCall = false; 40 | this.pendingArgs = null; 41 | } 42 | }, timeUntilNextCall); 43 | } else { 44 | // 이미 대기 중인 호출이 있으면 인자만 새 것으로 업데이트 45 | this.pendingArgs = args; 46 | } 47 | 48 | // 딜레이 내에 있으면 마지막 실행 결과 반환 49 | return this.lastResult as R; 50 | } 51 | 52 | /** 53 | * 쓰로틀 무시하고 함수 즉시 실행 54 | * @param args 함수 인자 55 | * @returns 함수 실행 결과 56 | */ 57 | public executeNow(args: T): R { 58 | this.lastCallTime = Date.now(); 59 | this.lastResult = this.fn(...args); 60 | return this.lastResult; 61 | } 62 | 63 | /** 64 | * 대기 중인 호출 취소 65 | */ 66 | public cancel(): void { 67 | this.pendingCall = false; 68 | this.pendingArgs = null; 69 | } 70 | 71 | /** 72 | * 마지막 실행 결과 가져오기 73 | */ 74 | public getLastResult(): R | null { 75 | return this.lastResult; 76 | } 77 | } -------------------------------------------------------------------------------- /src/domain/utils/fileSystemUtils.ts: -------------------------------------------------------------------------------- 1 | import { TFile, TAbstractFile, TFolder } from 'obsidian'; 2 | 3 | /** 4 | * 파일 시스템 유틸리티 5 | */ 6 | export class FileSystemUtils { 7 | /** 8 | * 파일 경로에서 파일명 추출 9 | */ 10 | static getFileName(filePath: string): string { 11 | return filePath.split('/').pop() || ''; 12 | } 13 | 14 | /** 15 | * 파일 경로에서 확장자 추출 16 | */ 17 | static getFileExtension(filePath: string): string { 18 | const fileName = this.getFileName(filePath); 19 | const extension = fileName.split('.').pop(); 20 | return extension ? `.${extension}` : ''; 21 | } 22 | 23 | /** 24 | * 파일 경로에서 디렉토리 경로 추출 25 | */ 26 | static getDirectoryPath(filePath: string): string { 27 | const parts = filePath.split('/'); 28 | parts.pop(); 29 | return parts.join('/'); 30 | } 31 | 32 | /** 33 | * 파일 경로가 유효한지 확인 34 | */ 35 | static isValidFilePath(filePath: string): boolean { 36 | return /^[a-zA-Z0-9\-_/\.]+$/.test(filePath); 37 | } 38 | 39 | /** 40 | * 파일이 마크다운 파일인지 확인 41 | */ 42 | static isMarkdownFile(file: TAbstractFile): boolean { 43 | return file instanceof TFile && file.extension === 'md'; 44 | } 45 | 46 | /** 47 | * 파일이 폴더인지 확인 48 | */ 49 | static isFolder(file: TAbstractFile): boolean { 50 | return file instanceof TFolder; 51 | } 52 | 53 | /** 54 | * 파일 경로가 특정 폴더 내에 있는지 확인 55 | */ 56 | static isInFolder(filePath: string, folderPath: string): boolean { 57 | return filePath.startsWith(folderPath + '/'); 58 | } 59 | 60 | /** 61 | * 파일 경로의 깊이 계산 62 | */ 63 | static getPathDepth(filePath: string): number { 64 | return filePath.split('/').length - 1; 65 | } 66 | 67 | /** 68 | * 상대 경로를 절대 경로로 변환 69 | */ 70 | static toAbsolutePath(relativePath: string, basePath: string): string { 71 | if (relativePath.startsWith('/')) { 72 | return relativePath; 73 | } 74 | return `${basePath}/${relativePath}`; 75 | } 76 | 77 | /** 78 | * 절대 경로를 상대 경로로 변환 79 | */ 80 | static toRelativePath(absolutePath: string, basePath: string): string { 81 | if (!absolutePath.startsWith(basePath)) { 82 | return absolutePath; 83 | } 84 | return absolutePath.slice(basePath.length + 1); 85 | } 86 | 87 | /** 88 | * 파일 경로 정규화 89 | */ 90 | static normalizePath(path: string): string { 91 | return path.replace(/\\/g, '/').replace(/\/+/g, '/'); 92 | } 93 | 94 | /** 95 | * 파일 경로 결합 96 | */ 97 | static joinPath(...paths: string[]): string { 98 | return paths 99 | .map(path => path.replace(/^\/+|\/+$/g, '')) 100 | .filter(Boolean) 101 | .join('/'); 102 | } 103 | 104 | /** 105 | * 파일 경로 분리 106 | */ 107 | static splitPath(path: string): string[] { 108 | return path.split('/').filter(Boolean); 109 | } 110 | 111 | /** 112 | * 파일 경로의 부모 경로 반환 113 | */ 114 | static getParentPath(path: string): string { 115 | const parts = this.splitPath(path); 116 | parts.pop(); 117 | return parts.join('/'); 118 | } 119 | 120 | /** 121 | * 파일 경로의 마지막 부분 반환 122 | */ 123 | static getBaseName(path: string): string { 124 | const parts = this.splitPath(path); 125 | return parts[parts.length - 1] || ''; 126 | } 127 | 128 | /** 129 | * 마크다운 파일에서 첫 번째 헤더 추출 130 | */ 131 | static extractFirstHeader(content: string): string | null { 132 | const headerMatch = content.match(/^#\s+(.+)$/m); 133 | return headerMatch ? headerMatch[1].trim() : null; 134 | } 135 | 136 | /** 137 | * 마크다운 파일에서 태그 추출 138 | */ 139 | static extractTags(content: string): string[] { 140 | const tagMatches = content.match(/#[^\s#]+/g); 141 | return tagMatches ? tagMatches.map(tag => tag.slice(1)) : []; 142 | } 143 | 144 | /** 145 | * 마크다운 파일에서 프론트매터 속성 추출 146 | */ 147 | static extractProperties(content: string): Record { 148 | const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); 149 | if (!frontmatterMatch) { 150 | return {}; 151 | } 152 | 153 | const properties: Record = {}; 154 | const frontmatter = frontmatterMatch[1]; 155 | const lines = frontmatter.split('\n'); 156 | 157 | for (const line of lines) { 158 | const [key, ...valueParts] = line.split(':'); 159 | if (key && valueParts.length > 0) { 160 | const value = valueParts.join(':').trim(); 161 | try { 162 | properties[key.trim()] = JSON.parse(value); 163 | } catch { 164 | properties[key.trim()] = value; 165 | } 166 | } 167 | } 168 | 169 | return properties; 170 | } 171 | } -------------------------------------------------------------------------------- /src/domain/utils/settingsUtils.ts: -------------------------------------------------------------------------------- 1 | import { IPluginSettings, DEFAULT_PLUGIN_SETTINGS } from '../models/PluginSettings'; 2 | 3 | /** 4 | * 객체의 깊은 복사본 생성 5 | * @param obj 복사할 객체 6 | * @returns 객체의 깊은 복사본 7 | */ 8 | export function deepCopy(obj: T): T { 9 | return JSON.parse(JSON.stringify(obj)); 10 | } 11 | 12 | /** 13 | * 중첩 객체의 특정 경로에 있는 값을 안전하게 업데이트 14 | * @param settings 업데이트할 설정 객체 15 | * @param path 속성 경로 (점으로 구분된 문자열) 16 | * @param value 설정할 새 값 17 | * @returns 업데이트된 설정 객체의 복사본 18 | */ 19 | export function updateNestedSettings( 20 | settings: T, 21 | path: string, 22 | value: any 23 | ): T { 24 | // 원본 객체의 복사본 생성 25 | const result = deepCopy(settings); 26 | 27 | // 경로 분할 28 | const pathParts = path.split('.'); 29 | 30 | // 중첩 객체 탐색 31 | let current: any = result; 32 | for (let i = 0; i < pathParts.length - 1; i++) { 33 | const part = pathParts[i]; 34 | 35 | // 배열 인덱스 처리 36 | if (part.includes('[') && part.includes(']')) { 37 | const arrayName = part.substring(0, part.indexOf('[')); 38 | const index = parseInt(part.substring(part.indexOf('[') + 1, part.indexOf(']'))); 39 | 40 | if (!current[arrayName]) current[arrayName] = []; 41 | if (!current[arrayName][index]) current[arrayName][index] = {}; 42 | 43 | current = current[arrayName][index]; 44 | } else { 45 | if (!current[part]) current[part] = {}; 46 | current = current[part]; 47 | } 48 | } 49 | 50 | // 최종 값 설정 51 | const lastPart = pathParts[pathParts.length - 1]; 52 | 53 | // 배열 인덱스 처리 54 | if (lastPart.includes('[') && lastPart.includes(']')) { 55 | const arrayName = lastPart.substring(0, lastPart.indexOf('[')); 56 | const index = parseInt(lastPart.substring(lastPart.indexOf('[') + 1, lastPart.indexOf(']'))); 57 | 58 | if (!current[arrayName]) current[arrayName] = []; 59 | current[arrayName][index] = value; 60 | } else { 61 | current[lastPart] = value; 62 | } 63 | 64 | return result; 65 | } 66 | 67 | /** 68 | * 플러그인 설정 업데이트 유틸리티 69 | * @param settings 기존 설정 70 | * @param updater 업데이트 함수 71 | * @returns 업데이트된 설정 72 | */ 73 | export function updateSettings( 74 | settings: IPluginSettings, 75 | updater: (draft: IPluginSettings) => void 76 | ): IPluginSettings { 77 | // 설정 복사본 생성 78 | const draft = deepCopy(settings); 79 | 80 | // 업데이트 함수 적용 81 | updater(draft); 82 | 83 | // 업데이트된 설정 반환 84 | return draft; 85 | } 86 | 87 | /** 88 | * 설정 유틸리티 클래스 89 | */ 90 | export class SettingsUtils { 91 | /** 92 | * 기본 설정 생성 93 | * @returns 기본 설정 94 | */ 95 | static createDefaultSettings(): IPluginSettings { 96 | return deepCopy(DEFAULT_PLUGIN_SETTINGS); 97 | } 98 | 99 | /** 100 | * 설정 유효성 검사 101 | * @param settings 설정 102 | * @returns 유효성 검사 결과 103 | */ 104 | static validateSettings(settings: IPluginSettings): boolean { 105 | return !!( 106 | settings && 107 | settings.card && 108 | settings.cardSet && 109 | settings.layout && 110 | settings.sort && 111 | settings.search && 112 | settings.preset 113 | ); 114 | } 115 | 116 | /** 117 | * 설정 병합 118 | * @param base 기본 설정 119 | * @param override 덮어쓸 설정 120 | * @returns 병합된 설정 121 | */ 122 | static mergeSettings(base: IPluginSettings, override: Partial): IPluginSettings { 123 | return { 124 | ...base, 125 | ...override, 126 | }; 127 | } 128 | 129 | public static updateSettings(settings: IPluginSettings, updater: (draft: IPluginSettings) => void): IPluginSettings { 130 | const draft = { ...settings }; 131 | updater(draft); 132 | return draft; 133 | } 134 | 135 | public static updateNestedSettings(settings: T, path: string, value: any): T { 136 | const draft = JSON.parse(JSON.stringify(settings)); 137 | const pathParts = path.split('.'); 138 | let current: any = draft; 139 | for (let i = 0; i < pathParts.length - 1; i++) { 140 | current = current[pathParts[i]]; 141 | } 142 | current[pathParts[pathParts.length - 1]] = value; 143 | return draft; 144 | } 145 | } -------------------------------------------------------------------------------- /src/domain/viewmodels/ICardNavigatorViewModel.ts: -------------------------------------------------------------------------------- 1 | import { ICardNavigatorState } from '@/domain/models/CardNavigatorState'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | export interface ICardNavigatorViewModel { 5 | /** 6 | * 현재 상태 7 | */ 8 | state: BehaviorSubject; 9 | 10 | /** 11 | * 뷰모델 초기화 12 | */ 13 | initialize(): void; 14 | 15 | /** 16 | * 뷰모델 정리 17 | */ 18 | cleanup(): void; 19 | 20 | /** 21 | * 현재 상태 가져오기 22 | */ 23 | getState(): ICardNavigatorState; 24 | 25 | /** 26 | * 상태 구독 27 | * @param callback 상태가 변경될 때마다 호출될 콜백 함수 28 | */ 29 | subscribe(callback: (state: ICardNavigatorState) => void): void; 30 | 31 | /** 32 | * 상태 구독 해제 33 | * @param callback 구독 해제할 콜백 함수 34 | */ 35 | unsubscribe(callback: (state: ICardNavigatorState) => void): void; 36 | } -------------------------------------------------------------------------------- /src/infrastructure/AnalyticsService.ts: -------------------------------------------------------------------------------- 1 | import { IAnalyticsService } from '@/domain/infrastructure/IAnalyticsService'; 2 | import { ILoggingService } from '@/domain/infrastructure/ILoggingService'; 3 | import { Container } from '@/infrastructure/di/Container'; 4 | 5 | interface AnalyticsEvent { 6 | name: string; 7 | timestamp: number; 8 | properties?: Record; 9 | } 10 | 11 | export class AnalyticsService implements IAnalyticsService { 12 | private static instance: AnalyticsService; 13 | private events: AnalyticsEvent[]; 14 | 15 | private constructor( 16 | private readonly loggingService: ILoggingService 17 | ) { 18 | this.events = []; 19 | } 20 | 21 | static getInstance(): AnalyticsService { 22 | if (!AnalyticsService.instance) { 23 | const container = Container.getInstance(); 24 | AnalyticsService.instance = new AnalyticsService( 25 | container.resolve('ILoggingService') 26 | ); 27 | } 28 | return AnalyticsService.instance; 29 | } 30 | 31 | /** 32 | * 이벤트를 기록합니다. 33 | * @param name 이벤트 이름 34 | * @param properties 이벤트 속성 35 | */ 36 | public trackEvent(name: string, properties?: Record): void { 37 | const event: AnalyticsEvent = { 38 | name, 39 | timestamp: Date.now(), 40 | properties, 41 | }; 42 | 43 | this.events.push(event); 44 | this.loggingService.debug(`[Analytics] ${name}`, properties); 45 | } 46 | 47 | /** 48 | * 특정 기간 동안의 이벤트를 조회합니다. 49 | * @param startTime 시작 시간 50 | * @param endTime 종료 시간 51 | * @returns 이벤트 목록 52 | */ 53 | public getEvents( 54 | startTime: number, 55 | endTime: number 56 | ): AnalyticsEvent[] { 57 | return this.events.filter( 58 | (event) => 59 | event.timestamp >= startTime && event.timestamp <= endTime 60 | ); 61 | } 62 | 63 | /** 64 | * 특정 이벤트의 발생 횟수를 반환합니다. 65 | * @param name 이벤트 이름 66 | * @returns 발생 횟수 67 | */ 68 | public getEventCount(name: string): number { 69 | return this.events.filter((event) => event.name === name).length; 70 | } 71 | 72 | /** 73 | * 모든 이벤트 데이터를 초기화합니다. 74 | */ 75 | public reset(): void { 76 | this.events = []; 77 | } 78 | 79 | /** 80 | * 이벤트 데이터를 내보냅니다. 81 | * @returns 이벤트 데이터 JSON 문자열 82 | */ 83 | public exportData(): string { 84 | return JSON.stringify(this.events, null, 2); 85 | } 86 | } -------------------------------------------------------------------------------- /src/infrastructure/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from 'obsidian'; 2 | import { IErrorHandler } from '@/domain/infrastructure/IErrorHandler'; 3 | import { ILoggingService } from '@/domain/infrastructure/ILoggingService'; 4 | import { Container } from '@/infrastructure/di/Container'; 5 | import { ErrorHandlerError } from '@/domain/errors/ErrorHandlerError'; 6 | import { CacheServiceError } from '@/domain/errors/CacheServiceError'; 7 | import { CardServiceError } from '@/domain/errors/CardServiceError'; 8 | import { LayoutServiceError } from '@/domain/errors/LayoutServiceError'; 9 | import { SearchServiceError } from '@/domain/errors/SearchServiceError'; 10 | import { EventError } from '@/domain/errors/EventError'; 11 | import { PresetError } from '@/domain/errors/PresetError'; 12 | import { CardSetError } from '@/domain/errors/CardSetError'; 13 | 14 | /** 15 | * 에러 처리 서비스 16 | * - 전역 에러 처리 및 로깅 담당 17 | */ 18 | export class ErrorHandler implements IErrorHandler { 19 | private static instance: ErrorHandler; 20 | private readonly logger: ILoggingService; 21 | 22 | private constructor() { 23 | const container = Container.getInstance(); 24 | this.logger = container.resolve('ILoggingService'); 25 | } 26 | 27 | /** 28 | * ErrorHandler 인스턴스 가져오기 29 | */ 30 | public static getInstance(): ErrorHandler { 31 | if (!ErrorHandler.instance) { 32 | ErrorHandler.instance = new ErrorHandler(); 33 | } 34 | return ErrorHandler.instance; 35 | } 36 | 37 | /** 38 | * 에러를 처리하고 로깅합니다. 39 | */ 40 | public handleError(error: Error, context: string): void { 41 | try { 42 | this.logger.error(`[${context}] ${error.message}`, error); 43 | 44 | if (error instanceof ErrorHandlerError) { 45 | this.handleErrorHandlerError(error, context); 46 | } else if (error instanceof CacheServiceError) { 47 | this.handleCacheError(error, context); 48 | } else if (error instanceof CardServiceError) { 49 | this.handleCardError(error, context); 50 | } else if (error instanceof LayoutServiceError) { 51 | this.handleLayoutError(error, context); 52 | } else if (error instanceof PresetError) { 53 | this.handlePresetError(error, context); 54 | } else if (error instanceof CardSetError) { 55 | this.handleCardSetError(error, context); 56 | } else if (error instanceof SearchServiceError) { 57 | this.handleSearchError(error, context); 58 | } else if (error instanceof EventError) { 59 | this.handleEventError(error, context); 60 | } else { 61 | this.handleUnknownError(error, context); 62 | } 63 | } catch (e) { 64 | this.handleUnknownError(e as Error, 'ErrorHandler'); 65 | } 66 | } 67 | 68 | private handleErrorHandlerError(error: ErrorHandlerError, context: string): void { 69 | new Notice(`[${context}] 에러 처리 중 오류가 발생했습니다: ${error.message}`); 70 | } 71 | 72 | private handleCacheError(error: CacheServiceError, context: string): void { 73 | new Notice(`[${context}] 캐시 처리 중 오류가 발생했습니다: ${error.message}`); 74 | } 75 | 76 | private handleCardError(error: CardServiceError, context: string): void { 77 | new Notice(`[${context}] 카드 처리 중 오류가 발생했습니다: ${error.message}`); 78 | } 79 | 80 | private handleLayoutError(error: LayoutServiceError, context: string): void { 81 | new Notice(`[${context}] 레이아웃 처리 중 오류가 발생했습니다: ${error.message}`); 82 | } 83 | 84 | private handlePresetError(error: PresetError, context: string): void { 85 | new Notice(`[${context}] 프리셋 처리 중 오류가 발생했습니다: ${error.message}`); 86 | } 87 | 88 | private handleCardSetError(error: CardSetError, context: string): void { 89 | new Notice(`[${context}] 카드셋 처리 중 오류가 발생했습니다: ${error.message}`); 90 | } 91 | 92 | private handleSearchError(error: SearchServiceError, context: string): void { 93 | new Notice(`[${context}] 검색 처리 중 오류가 발생했습니다: ${error.message}`); 94 | } 95 | 96 | private handleEventError(error: EventError, context: string): void { 97 | new Notice(`[${context}] 이벤트 처리 중 오류가 발생했습니다: ${error.message}`); 98 | } 99 | 100 | private handleUnknownError(error: Error, context: string): void { 101 | new Notice(`[${context}] 알 수 없는 오류가 발생했습니다: ${error.message}`); 102 | } 103 | 104 | /** 105 | * Promise 기반 비동기 작업의 에러를 처리합니다. 106 | */ 107 | public async handlePromise( 108 | promise: Promise, 109 | context: string 110 | ): Promise { 111 | try { 112 | return await promise; 113 | } catch (error) { 114 | this.handleError(error as Error, context); 115 | throw error; 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/infrastructure/LoggingService.ts: -------------------------------------------------------------------------------- 1 | import { ILoggingService } from '@/domain/infrastructure/ILoggingService'; 2 | 3 | /** 4 | * 로그 레벨 5 | */ 6 | export enum LogLevel { 7 | DEBUG = 'DEBUG', 8 | INFO = 'INFO', 9 | WARN = 'WARN', 10 | ERROR = 'ERROR' 11 | } 12 | 13 | /** 14 | * 로깅 서비스 구현체 15 | */ 16 | export class LoggingService implements ILoggingService { 17 | private static instance: LoggingService; 18 | private _logLevel: LogLevel = LogLevel.INFO; 19 | private _isDebugEnabled: boolean = false; 20 | private _performanceMetrics: Map = new Map(); 25 | 26 | private constructor() { 27 | // 개발 모드에서는 항상 DEBUG 레벨 설정 28 | this.setLogLevel(LogLevel.DEBUG); 29 | } 30 | 31 | public static getInstance(): LoggingService { 32 | if (!LoggingService.instance) { 33 | LoggingService.instance = new LoggingService(); 34 | } 35 | return LoggingService.instance; 36 | } 37 | 38 | /** 39 | * 디버그 로그 출력 40 | */ 41 | public debug(message: string, ...args: any[]): void { 42 | if (this._isDebugEnabled) { 43 | console.debug(`[DEBUG] ${message}`, ...args); 44 | } 45 | } 46 | 47 | /** 48 | * 정보 로그 출력 49 | */ 50 | public info(message: string, ...args: any[]): void { 51 | if (this._logLevel === LogLevel.INFO || this._logLevel === LogLevel.DEBUG) { 52 | console.info(`[INFO] ${message}`, ...args); 53 | } 54 | } 55 | 56 | /** 57 | * 경고 로그 출력 58 | */ 59 | public warn(message: string, ...args: any[]): void { 60 | if (this._logLevel === LogLevel.WARN || this._logLevel === LogLevel.INFO || this._logLevel === LogLevel.DEBUG) { 61 | console.warn(`[WARN] ${message}`, ...args); 62 | } 63 | } 64 | 65 | /** 66 | * 에러 로그 출력 67 | */ 68 | public error(message: string, ...args: any[]): void { 69 | console.error(`[ERROR] ${message}`, ...args); 70 | } 71 | 72 | /** 73 | * 로그 레벨 설정 74 | */ 75 | public setLogLevel(level: LogLevel): void { 76 | this._logLevel = level; 77 | this._isDebugEnabled = level === LogLevel.DEBUG; 78 | } 79 | 80 | /** 81 | * 디버그 모드 활성화 여부 확인 82 | */ 83 | public isDebugEnabled(): boolean { 84 | return this._isDebugEnabled; 85 | } 86 | 87 | /** 88 | * 성능 측정 시작 89 | */ 90 | public startMeasure(label: string): void { 91 | this._performanceMetrics.set(label, { startTime: performance.now() }); 92 | this.debug(`[Performance] ${label} 측정 시작`); 93 | } 94 | 95 | /** 96 | * 성능 측정 종료 97 | */ 98 | public endMeasure(label: string): void { 99 | const metric = this._performanceMetrics.get(label); 100 | if (metric) { 101 | metric.endTime = performance.now(); 102 | metric.duration = metric.endTime - metric.startTime; 103 | this.debug(`[Performance] ${label}: ${metric.duration.toFixed(2)}ms`); 104 | this._performanceMetrics.delete(label); 105 | } 106 | } 107 | 108 | /** 109 | * 메모리 사용량 로깅 110 | */ 111 | public logMemoryUsage(): void { 112 | const performance = window.performance as any; 113 | if (performance?.memory) { 114 | const memory = performance.memory; 115 | this.debug(`[Memory] 116 | Used: ${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB 117 | Total: ${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)}MB 118 | Limit: ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)}MB 119 | `); 120 | } 121 | } 122 | 123 | /** 124 | * 성능 메트릭 로깅 125 | */ 126 | private _logPerformanceMetrics(): void { 127 | const performance = window.performance as any; 128 | if (performance?.memory) { 129 | const memory = performance.memory; 130 | this.debug('메모리 사용량:', { 131 | usedJSHeapSize: `${Math.round(memory.usedJSHeapSize / 1024 / 1024)}MB`, 132 | totalJSHeapSize: `${Math.round(memory.totalJSHeapSize / 1024 / 1024)}MB` 133 | }); 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /src/infrastructure/PerformanceMonitor.ts: -------------------------------------------------------------------------------- 1 | import { IPerformanceMonitor } from '@/domain/infrastructure/IPerformanceMonitor'; 2 | import { ILoggingService } from '@/domain/infrastructure/ILoggingService'; 3 | import { IErrorHandler } from '@/domain/infrastructure/IErrorHandler'; 4 | import { Container } from './di/Container'; 5 | 6 | /** 7 | * 성능 모니터링 서비스 구현체 8 | */ 9 | export class PerformanceMonitor implements IPerformanceMonitor { 10 | private static instance: PerformanceMonitor; 11 | private measurements: Map = new Map(); 17 | private timers: Map = new Map(); 18 | private logger: ILoggingService; 19 | private errorHandler: IErrorHandler; 20 | 21 | private constructor() { 22 | this.logger = Container.getInstance().resolve('ILoggingService'); 23 | this.errorHandler = Container.getInstance().resolve('IErrorHandler'); 24 | } 25 | 26 | /** 27 | * 싱글톤 인스턴스 반환 28 | */ 29 | public static getInstance(): PerformanceMonitor { 30 | if (!PerformanceMonitor.instance) { 31 | PerformanceMonitor.instance = new PerformanceMonitor(); 32 | } 33 | return PerformanceMonitor.instance; 34 | } 35 | 36 | /** 37 | * 성능 측정 시작 38 | */ 39 | public startTimer(name: string): { stop: () => void } { 40 | const startTime = performance.now(); 41 | this.timers.set(name, startTime); 42 | return { 43 | stop: () => { 44 | const endTime = performance.now(); 45 | const duration = endTime - startTime; 46 | this.recordMeasurement(name, duration); 47 | } 48 | }; 49 | } 50 | 51 | /** 52 | * 성능 측정 53 | */ 54 | public measure(name: string, callback: () => T): T { 55 | const timer = this.startTimer(name); 56 | try { 57 | return callback(); 58 | } finally { 59 | timer.stop(); 60 | } 61 | } 62 | 63 | /** 64 | * 성능 측정 (비동기) 65 | */ 66 | public async measureAsync(name: string, callback: () => Promise): Promise { 67 | const timer = this.startTimer(name); 68 | try { 69 | return await callback(); 70 | } finally { 71 | timer.stop(); 72 | } 73 | } 74 | 75 | /** 76 | * 성능 측정 결과 조회 77 | */ 78 | public getMeasurement(name: string): { 79 | count: number; 80 | average: number; 81 | min: number; 82 | max: number; 83 | total: number; 84 | } { 85 | const measurement = this.measurements.get(name); 86 | if (!measurement) { 87 | return { 88 | count: 0, 89 | average: 0, 90 | min: 0, 91 | max: 0, 92 | total: 0 93 | }; 94 | } 95 | 96 | return { 97 | count: measurement.count, 98 | average: measurement.total / measurement.count, 99 | min: measurement.min, 100 | max: measurement.max, 101 | total: measurement.total 102 | }; 103 | } 104 | 105 | /** 106 | * 모든 성능 측정 결과 조회 107 | */ 108 | public getAllMeasurements(): Record { 115 | const result: Record = {}; 122 | 123 | for (const [name, measurement] of this.measurements) { 124 | result[name] = { 125 | count: measurement.count, 126 | average: measurement.total / measurement.count, 127 | min: measurement.min, 128 | max: measurement.max, 129 | total: measurement.total 130 | }; 131 | } 132 | 133 | return result; 134 | } 135 | 136 | /** 137 | * 성능 측정 결과 초기화 138 | */ 139 | public clearMeasurement(name: string): void { 140 | this.measurements.delete(name); 141 | } 142 | 143 | /** 144 | * 모든 성능 측정 결과 초기화 145 | */ 146 | public clearAllMeasurements(): void { 147 | this.measurements.clear(); 148 | } 149 | 150 | /** 151 | * 메모리 사용량 로깅 152 | */ 153 | public logMemoryUsage(): void { 154 | const performance = window.performance as any; 155 | if (performance?.memory) { 156 | const memory = performance.memory; 157 | this.logger.debug(`[Memory] 158 | Used: ${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB 159 | Total: ${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)}MB 160 | Limit: ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)}MB 161 | `); 162 | } 163 | } 164 | 165 | /** 166 | * 성능 메트릭 초기화 167 | */ 168 | public clearMetrics(): void { 169 | this.measurements.clear(); 170 | this.timers.clear(); 171 | this.logger.debug('[Performance] 메트릭 초기화'); 172 | } 173 | 174 | /** 175 | * 성능 측정 기록 176 | */ 177 | private recordMeasurement(name: string, duration: number): void { 178 | const measurement = this.measurements.get(name) || { 179 | count: 0, 180 | total: 0, 181 | min: Infinity, 182 | max: -Infinity 183 | }; 184 | 185 | measurement.count++; 186 | measurement.total += duration; 187 | measurement.min = Math.min(measurement.min, duration); 188 | measurement.max = Math.max(measurement.max, duration); 189 | 190 | this.measurements.set(name, measurement); 191 | } 192 | } -------------------------------------------------------------------------------- /src/infrastructure/di/Container.ts: -------------------------------------------------------------------------------- 1 | import { ContainerError } from '@/domain/errors/ContainerError'; 2 | 3 | /** 4 | * 서비스 팩토리 타입 5 | */ 6 | type ServiceFactory = () => T; 7 | 8 | /** 9 | * 서비스 등록 정보 10 | */ 11 | interface ServiceRegistration { 12 | factory: ServiceFactory; 13 | singleton: boolean; 14 | instance?: T; 15 | } 16 | 17 | /** 18 | * 의존성 주입 컨테이너 19 | * - 서비스 등록 및 해제 20 | * - 싱글톤 서비스 관리 21 | * - 서비스 인스턴스 생성 22 | */ 23 | export class Container { 24 | private static instance: Container; 25 | private services: Map any> = new Map(); 26 | private instances: Map = new Map(); 27 | private readonly logger: Console; 28 | 29 | private constructor() { 30 | this.logger = console; 31 | } 32 | 33 | /** 34 | * 싱글톤 인스턴스 반환 35 | */ 36 | public static getInstance(): Container { 37 | if (!Container.instance) { 38 | Container.instance = new Container(); 39 | } 40 | return Container.instance; 41 | } 42 | 43 | /** 44 | * 서비스 등록 45 | * @param key 서비스 이름 46 | * @param factory 서비스 팩토리 함수 47 | * @param singleton 싱글톤 여부 48 | */ 49 | public register(key: string, factory: () => T, singleton: boolean = false): void { 50 | if (singleton) { 51 | this.services.set(key, () => { 52 | if (!this.instances.has(key)) { 53 | this.instances.set(key, factory()); 54 | } 55 | return this.instances.get(key); 56 | }); 57 | } else { 58 | this.services.set(key, factory); 59 | } 60 | } 61 | 62 | /** 63 | * 서비스 인스턴스 직접 등록 64 | * @param key 서비스 이름 65 | * @param instance 서비스 인스턴스 66 | */ 67 | public registerInstance(key: string, instance: T): void { 68 | this.services.set(key, () => instance); 69 | this.instances.set(key, instance); 70 | } 71 | 72 | /** 73 | * 서비스 조회 74 | * @param key 서비스 이름 75 | * @returns 서비스 인스턴스 76 | */ 77 | public resolve(key: string): T { 78 | const factory = this.services.get(key); 79 | if (!factory) { 80 | throw new Error(`서비스를 찾을 수 없음: ${key}`); 81 | } 82 | return factory() as T; 83 | } 84 | 85 | /** 86 | * 서비스 조회 (optional) 87 | * @param key 서비스 이름 88 | * @returns 서비스 인스턴스 또는 null 89 | */ 90 | public resolveOptional(key: string): T | null { 91 | try { 92 | return this.resolve(key); 93 | } catch (error) { 94 | return null; 95 | } 96 | } 97 | 98 | /** 99 | * 서비스 해제 100 | */ 101 | public dispose(): void { 102 | this.clear(); 103 | } 104 | 105 | /** 106 | * 서비스 등록 여부 확인 107 | * @param key 서비스 이름 108 | */ 109 | public has(key: string): boolean { 110 | return this.services.has(key); 111 | } 112 | 113 | /** 114 | * 등록된 서비스 목록 조회 115 | */ 116 | public getRegisteredServices(): string[] { 117 | return Array.from(this.services.keys()); 118 | } 119 | 120 | /** 121 | * 서비스 해제 122 | */ 123 | public clear(): void { 124 | this.services.clear(); 125 | this.instances.clear(); 126 | } 127 | } -------------------------------------------------------------------------------- /src/infrastructure/di/register.ts: -------------------------------------------------------------------------------- 1 | import { Container } from './Container'; 2 | import { ErrorHandler } from '../ErrorHandler'; 3 | import { PerformanceMonitor } from '../PerformanceMonitor'; 4 | import { AnalyticsService } from '../AnalyticsService'; 5 | import { LoggingService } from '../LoggingService'; 6 | import { SettingsService } from '../../application/services/application/SettingsService'; 7 | import { CardService } from '../../application/services/domain/CardService'; 8 | import { CardSetService } from '../../application/services/domain/CardSetService'; 9 | import { CardInteractionService } from '../../application/services/domain/CardInteractionService'; 10 | import { SearchService } from '../../application/services/application/SearchService'; 11 | import { SortService } from '../../application/services/application/SortService'; 12 | import { LayoutService } from '../../application/services/application/LayoutService'; 13 | import { ScrollService } from '../../application/services/application/ScrollService'; 14 | import { FileService } from '../../application/services/application/FileService'; 15 | import { ClipboardService } from '../../application/services/application/ClipboardService'; 16 | import { ActiveFileWatcher } from '../../application/services/application/ActiveFileWatcher'; 17 | import { FocusManager } from '../../application/manager/FocusManager'; 18 | import { CardDisplayManager } from '../../application/manager/CardDisplayManager'; 19 | import { CardFactory } from '../../application/factories/CardFactory'; 20 | import { CardSetFactory } from '../../application/factories/CardSetFactory'; 21 | import { PresetManager } from '../../application/manager/PresetManager'; 22 | import { PresetService } from '../../application/services/application/PresetService'; 23 | import { ToolbarService } from '../../application/services/application/ToolbarService'; 24 | import { CardRenderManager } from '../../application/manager/CardRenderManager'; 25 | import { CardManager } from '../../application/manager/CardManager'; 26 | import { CardSelectionService } from '../../application/services/domain/CardSelectionService'; 27 | import { CardFocusService } from '../../application/services/application/CardFocusService'; 28 | import { DomainEventDispatcher } from '../../domain/events/DomainEventDispatcher'; 29 | import { CardNavigatorService } from '../../application/services/application/CardNavigatorService'; 30 | import { EventBus } from '../../domain/events/EventBus'; 31 | 32 | /** 33 | * 서비스 등록 34 | * - 인프라스트럭처 서비스 35 | * - 애플리케이션 서비스 36 | * - 팩토리 37 | */ 38 | export function registerServices(container: Container): void { 39 | // 인프라스트럭처 서비스 등록 (순서 중요) 40 | container.register('IErrorHandler', () => ErrorHandler.getInstance(), true); 41 | container.register('ILoggingService', () => LoggingService.getInstance(), true); 42 | container.register('IPerformanceMonitor', () => PerformanceMonitor.getInstance(), true); 43 | container.register('IAnalyticsService', () => AnalyticsService.getInstance(), true); 44 | container.register('IEventDispatcher', () => EventBus.getInstance(), true); 45 | 46 | // 애플리케이션 서비스 등록 47 | container.register('ISettingsService', () => SettingsService.getInstance(), true); 48 | container.register('ICardService', () => CardService.getInstance(), true); 49 | container.register('ICardSetService', () => CardSetService.getInstance(), true); 50 | container.register('ICardInteractionService', () => CardInteractionService.getInstance(), true); 51 | container.register('ISearchService', () => SearchService.getInstance(), true); 52 | container.register('ISortService', () => SortService.getInstance(), true); 53 | container.register('ILayoutService', () => LayoutService.getInstance(), true); 54 | container.register('IScrollService', () => ScrollService.getInstance(), true); 55 | container.register('IFileService', () => FileService.getInstance(), true); 56 | container.register('IClipboardService', () => ClipboardService.getInstance(), true); 57 | container.register('IActiveFileWatcher', () => ActiveFileWatcher.getInstance(), true); 58 | 59 | // 매니저 등록 60 | container.register('IFocusManager', () => FocusManager.getInstance(), true); 61 | container.register('ICardDisplayManager', () => CardDisplayManager.getInstance(), true); 62 | container.register('ICardFactory', () => CardFactory.getInstance(), true); 63 | container.register('ICardSetFactory', () => CardSetFactory.getInstance(), true); 64 | container.register('IPresetManager', () => PresetManager.getInstance(), true); 65 | container.register('IPresetService', () => PresetService.getInstance(), true); 66 | container.register('IToolbarService', () => ToolbarService.getInstance(), true); 67 | container.register('ICardRenderManager', () => CardRenderManager.getInstance(), true); 68 | container.register('ICardManager', () => CardManager.getInstance(), true); 69 | container.register('ICardSelectionService', () => CardSelectionService.getInstance(), true); 70 | container.register('ICardFocusService', () => CardFocusService.getInstance(), true); 71 | container.register('ICardNavigatorService', () => CardNavigatorService.getInstance(), true); 72 | } 73 | 74 | /** 75 | * 서비스 해제 76 | */ 77 | export function clearServices(container: Container): void { 78 | container.clear(); 79 | } -------------------------------------------------------------------------------- /src/infrastructure/events/EventBus.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent, IEventDispatcher } from '../../domain/events/DomainEvent'; 2 | import { DomainEventType } from '../../domain/events/DomainEventType'; 3 | import { Subject, Subscription } from 'rxjs'; 4 | import { IEventHandler } from '../../domain/infrastructure/IEventDispatcher'; 5 | 6 | /** 7 | * 이벤트 버스 클래스 8 | * - 이벤트 발송 및 구독 관리 9 | */ 10 | export class EventBus implements IEventDispatcher { 11 | private initialized = false; 12 | private subjects: Map>> = new Map(); 13 | private handlers: Map>>> = new Map(); 14 | 15 | /** 16 | * 초기화 17 | */ 18 | initialize(): void { 19 | if (this.initialized) { 20 | return; 21 | } 22 | this.initialized = true; 23 | } 24 | 25 | /** 26 | * 초기화 여부 확인 27 | * @returns 초기화 여부 28 | */ 29 | isInitialized(): boolean { 30 | return this.initialized; 31 | } 32 | 33 | /** 34 | * 정리 35 | */ 36 | cleanup(): void { 37 | this.subjects.forEach(subject => subject.complete()); 38 | this.subjects.clear(); 39 | this.handlers.clear(); 40 | this.initialized = false; 41 | } 42 | 43 | /** 44 | * 이벤트를 발행합니다. 45 | * @param eventName 이벤트 이름 46 | * @param data 이벤트 데이터 47 | */ 48 | publish(eventName: T, data: any): void { 49 | const event = new DomainEvent(eventName, data); 50 | this.dispatch(event); 51 | } 52 | 53 | /** 54 | * 이벤트 구독 55 | * @param eventName 이벤트 이름 56 | * @param callback 콜백 함수 57 | * @returns 구독 객체 58 | */ 59 | subscribe(eventName: T, callback: (event: DomainEvent) => void | Promise): Subscription { 60 | if (!this.subjects.has(eventName)) { 61 | this.subjects.set(eventName, new Subject>()); 62 | } 63 | return this.subjects.get(eventName)?.subscribe(callback) as Subscription; 64 | } 65 | 66 | /** 67 | * 이벤트 구독 해제 68 | * @param eventName 이벤트 이름 69 | * @param callback 콜백 함수 70 | */ 71 | unsubscribe(eventName: T, callback: (event: DomainEvent) => void): void { 72 | const subject = this.subjects.get(eventName); 73 | if (subject) { 74 | subject.unsubscribe(); 75 | this.subjects.delete(eventName); 76 | } 77 | } 78 | 79 | /** 80 | * 이벤트 발송 81 | * @param event 이벤트 객체 82 | */ 83 | dispatch(event: DomainEvent): void { 84 | const subject = this.subjects.get(event.eventName); 85 | if (subject) { 86 | subject.next(event); 87 | } 88 | 89 | const handlers = this.handlers.get(event.eventName); 90 | if (handlers) { 91 | handlers.forEach(handler => handler.handle(event)); 92 | } 93 | } 94 | 95 | /** 96 | * 이벤트 핸들러 등록 97 | * @param eventName 이벤트 이름 98 | * @param handler 이벤트 핸들러 99 | * @returns 구독 객체 100 | */ 101 | registerHandler(eventName: T, handler: IEventHandler>): Subscription { 102 | if (!this.handlers.has(eventName)) { 103 | this.handlers.set(eventName, new Set()); 104 | } 105 | this.handlers.get(eventName)?.add(handler); 106 | 107 | return new Subscription(() => { 108 | this.unregisterHandler(eventName, handler); 109 | }); 110 | } 111 | 112 | /** 113 | * 이벤트 핸들러 해제 114 | * @param eventName 이벤트 이름 115 | * @param handler 이벤트 핸들러 116 | */ 117 | unregisterHandler(eventName: T, handler: IEventHandler>): void { 118 | const handlers = this.handlers.get(eventName); 119 | if (handlers) { 120 | handlers.delete(handler); 121 | if (handlers.size === 0) { 122 | this.handlers.delete(eventName); 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * 이벤트 핸들러 목록 조회 129 | * @param eventName 이벤트 이름 130 | * @returns 이벤트 핸들러 목록 131 | */ 132 | getHandlers(eventName: T): Set>> { 133 | return this.handlers.get(eventName) as Set>> || new Set(); 134 | } 135 | 136 | /** 137 | * 이벤트 핸들러 수 조회 138 | * @param eventName 이벤트 이름 139 | * @returns 이벤트 핸들러 수 140 | */ 141 | getHandlerCount(eventName: string): number { 142 | return this.handlers.get(eventName as DomainEventType)?.size || 0; 143 | } 144 | 145 | /** 146 | * 이벤트 핸들러 존재 여부 확인 147 | * @param eventName 이벤트 이름 148 | * @returns 이벤트 핸들러 존재 여부 149 | */ 150 | hasHandlers(eventName: string): boolean { 151 | return this.getHandlerCount(eventName) > 0; 152 | } 153 | 154 | /** 155 | * 이벤트 핸들러 목록 초기화 156 | * @param eventName 이벤트 이름 157 | */ 158 | clearHandlers(eventName: string): void { 159 | this.handlers.delete(eventName as DomainEventType); 160 | } 161 | 162 | /** 163 | * 모든 이벤트 핸들러 목록 초기화 164 | */ 165 | clearAllHandlers(): void { 166 | this.handlers.clear(); 167 | } 168 | } -------------------------------------------------------------------------------- /src/ui/interfaces/ICardNavigatorView.ts: -------------------------------------------------------------------------------- 1 | import { ICardNavigatorState } from '@/domain/models/CardNavigatorState'; 2 | import { ICardSet } from '@/domain/models/CardSet'; 3 | 4 | /** 5 | * 카드 내비게이터 뷰 인터페이스 6 | */ 7 | export interface ICardNavigatorView { 8 | /** 9 | * 상태 업데이트 10 | * @param state 새로운 상태 11 | */ 12 | updateState(state: ICardNavigatorState): void; 13 | 14 | /** 15 | * 카드셋 업데이트 16 | * @param cardSet 새로운 카드셋 17 | */ 18 | updateCardSet(cardSet: ICardSet): void; 19 | 20 | /** 21 | * 포커스된 카드 업데이트 22 | * @param cardId 포커스된 카드 ID 23 | */ 24 | updateFocusedCard(cardId: string | null): void; 25 | 26 | /** 27 | * 선택된 카드 업데이트 28 | * @param cardIds 선택된 카드 ID 목록 29 | */ 30 | updateSelectedCards(cardIds: Set): void; 31 | 32 | /** 33 | * 활성화된 카드 업데이트 34 | * @param cardId 활성화된 카드 ID 35 | */ 36 | updateActiveCard(cardId: string | null): void; 37 | 38 | /** 39 | * 검색 모드 업데이트 40 | * @param isSearchMode 검색 모드 여부 41 | * @param query 검색어 42 | */ 43 | updateSearchMode(isSearchMode: boolean, query: string): void; 44 | 45 | /** 46 | * 로딩 상태 표시 47 | * @param isLoading 로딩 중 여부 48 | */ 49 | showLoading(isLoading: boolean): void; 50 | 51 | /** 52 | * 에러 메시지 표시 53 | * @param message 에러 메시지 54 | */ 55 | showError(message: string): void; 56 | 57 | /** 58 | * 일반 메시지 표시 59 | * @param message 표시할 메시지 60 | */ 61 | showMessage(message: string): void; 62 | 63 | /** 64 | * 뷰 정리 65 | */ 66 | cleanup(): void; 67 | 68 | /** 69 | * 컨테이너 크기 가져오기 70 | * @returns 컨테이너 크기 71 | */ 72 | getContainerDimensions(): { width: number; height: number }; 73 | 74 | /** 75 | * 카드로 스크롤 76 | * @param cardId 카드 ID 77 | */ 78 | scrollToCard(cardId: string): void; 79 | 80 | /** 81 | * 드래그 타겟 업데이트 82 | * @param cardId 카드 ID 83 | * @param isTarget 드래그 타겟 여부 84 | */ 85 | updateDragTarget(cardId: string, isTarget: boolean): void; 86 | } -------------------------------------------------------------------------------- /src/ui/interfaces/ICardNavigatorViewModel.ts: -------------------------------------------------------------------------------- 1 | import { ICardNavigatorView } from './ICardNavigatorView'; 2 | import { ICardStyle, IRenderConfig } from '@/domain/models/Card'; 3 | import { ICardSet, CardSetType } from '@/domain/models/CardSet'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | import { ICardNavigatorState } from '@/domain/models/CardNavigatorState'; 6 | import { ICard } from '@/domain/models/Card'; 7 | import { ISearchConfig } from '@/domain/models/Search'; 8 | import { ISortConfig } from '@/domain/models/Sort'; 9 | 10 | /** 11 | * 포커스 이동 방향 12 | */ 13 | export type FocusDirection = 'up' | 'down' | 'left' | 'right'; 14 | 15 | /** 16 | * 카드 내비게이터 뷰모델 인터페이스 17 | */ 18 | export interface ICardNavigatorViewModel { 19 | /** 20 | * 상태 관리 21 | */ 22 | readonly state: BehaviorSubject; 23 | 24 | /** 25 | * 뷰 설정 26 | * @param view 뷰 인스턴스 27 | */ 28 | setView(view: ICardNavigatorView): void; 29 | 30 | /** 31 | * 초기화 32 | */ 33 | initialize(): Promise; 34 | 35 | /** 36 | * 정리 37 | */ 38 | cleanup(): Promise; 39 | 40 | /** 41 | * 포커스 이동 42 | * @param direction 이동 방향 43 | */ 44 | moveFocus(direction: FocusDirection): void; 45 | 46 | /** 47 | * 포커스된 카드 열기 48 | */ 49 | openFocusedCard(): void; 50 | 51 | /** 52 | * 포커스 해제 53 | */ 54 | clearFocus(): void; 55 | 56 | /** 57 | * 카드 선택 58 | * @param cardId 카드 ID 59 | */ 60 | selectCard(cardId: string): void; 61 | 62 | /** 63 | * 카드 선택 해제 64 | * @param cardId 카드 ID 65 | */ 66 | deselectCard(cardId: string): void; 67 | 68 | /** 69 | * 카드 범위 선택 70 | * @param cardId 카드 ID 71 | */ 72 | selectCardsInRange(cardId: string): Promise; 73 | 74 | /** 75 | * 카드 선택 토글 76 | * @param cardId 카드 ID 77 | */ 78 | toggleCardSelection(cardId: string): Promise; 79 | 80 | /** 81 | * 카드 포커스 82 | * @param cardId 카드 ID 83 | */ 84 | focusCard(cardId: string): Promise; 85 | 86 | /** 87 | * 카드 활성화 88 | * @param cardId 카드 ID 89 | */ 90 | activateCard(cardId: string): Promise; 91 | 92 | /** 93 | * 카드 비활성화 94 | * @param cardId 카드 ID 95 | */ 96 | deactivateCard(cardId: string): Promise; 97 | 98 | /** 99 | * 카드 컨텍스트 메뉴 표시 100 | * @param cardId 카드 ID 101 | * @param event 마우스 이벤트 102 | */ 103 | showCardContextMenu(cardId: string, event: MouseEvent): void; 104 | 105 | /** 106 | * 카드 클릭 처리 107 | * @param cardId 카드 ID 108 | */ 109 | handleCardClick(cardId: string): Promise; 110 | 111 | /** 112 | * 카드 더블 클릭 처리 113 | * @param cardId 카드 ID 114 | */ 115 | handleCardDoubleClick(cardId: string): Promise; 116 | 117 | /** 118 | * 카드 드래그 시작 119 | * @param cardId 카드 ID 120 | * @param event 드래그 이벤트 121 | */ 122 | handleCardDragStart(cardId: string, event: DragEvent): void; 123 | 124 | /** 125 | * 카드 드래그 종료 126 | * @param cardId 카드 ID 127 | * @param event 드래그 이벤트 128 | */ 129 | handleCardDragEnd(cardId: string, event: DragEvent): void; 130 | 131 | /** 132 | * 카드 드롭 133 | * @param cardId 카드 ID 134 | * @param event 드롭 이벤트 135 | */ 136 | handleCardDrop(cardId: string, event: DragEvent): void; 137 | 138 | /** 139 | * 카드 드래그 오버 140 | * @param cardId 카드 ID 141 | * @param event 드래그 오버 이벤트 142 | */ 143 | handleCardDragOver(cardId: string, event: DragEvent): void; 144 | 145 | /** 146 | * 카드 드래그 엔터 147 | * @param cardId 카드 ID 148 | * @param event 드래그 엔터 이벤트 149 | */ 150 | handleCardDragEnter(cardId: string, event: DragEvent): void; 151 | 152 | /** 153 | * 카드 드래그 리브 154 | * @param cardId 카드 ID 155 | * @param event 드래그 리브 이벤트 156 | */ 157 | handleCardDragLeave(cardId: string, event: DragEvent): void; 158 | 159 | /** 160 | * 카드 간 링크 생성 161 | * @param sourceCardId 소스 카드 ID 162 | * @param targetCardId 타겟 카드 ID 163 | */ 164 | createLinkBetweenCards(sourceCardId: string, targetCardId: string): Promise; 165 | 166 | /** 167 | * 렌더링 설정 가져오기 168 | */ 169 | getRenderConfig(): IRenderConfig; 170 | 171 | /** 172 | * 카드 스타일 가져오기 173 | */ 174 | getCardStyle(): ICardStyle; 175 | 176 | /** 177 | * 현재 카드셋 가져오기 178 | */ 179 | getCurrentCardSet(): ICardSet | null; 180 | 181 | /** 182 | * 카드셋 업데이트 183 | * @param cardSet 카드셋 184 | */ 185 | updateCardSet(cardSet: ICardSet): void; 186 | 187 | /** 188 | * 상태 업데이트 189 | * @param state 상태 190 | */ 191 | updateState(state: ICardNavigatorState): void; 192 | 193 | /** 194 | * 카드셋 변경 195 | * @param cardSetType 카드셋 타입 196 | */ 197 | changeCardSet(cardSetType: CardSetType): Promise; 198 | 199 | /** 200 | * 카드 검색 201 | * @param query 검색어 202 | * @param config 검색 설정 203 | */ 204 | search(query: string, config?: ISearchConfig): Promise; 205 | 206 | /** 207 | * 카드 정렬 208 | * @param config 정렬 설정 209 | */ 210 | sort(config: ISortConfig): Promise; 211 | 212 | /** 213 | * 설정 열기 214 | */ 215 | openSettings(): void; 216 | 217 | /** 218 | * 카드 드래그 시작 219 | * @param cardId 카드 ID 220 | * @param event 드래그 이벤트 221 | */ 222 | startCardDrag(cardId: string, event: DragEvent): void; 223 | } -------------------------------------------------------------------------------- /src/ui/modals/FolderSuggestModal.ts: -------------------------------------------------------------------------------- 1 | import { App, TFolder, SuggestModal } from 'obsidian'; 2 | import { ILoggingService } from '@/domain/infrastructure/ILoggingService'; 3 | import { Container } from '@/infrastructure/di/Container'; 4 | 5 | /** 6 | * 폴더 선택 서제스트 모달 7 | */ 8 | export class FolderSuggestModal extends SuggestModal { 9 | private logger: ILoggingService; 10 | public onChoose: (folder: string) => void; 11 | private folders: string[] = []; 12 | 13 | constructor(app: App) { 14 | super(app); 15 | this.logger = Container.getInstance().resolve('ILoggingService'); 16 | this.setPlaceholder('폴더 경로를 입력하거나 선택하세요'); 17 | this.loadFolders(); 18 | } 19 | 20 | /** 21 | * 볼트 내 모든 폴더 로드 22 | */ 23 | private loadFolders(): void { 24 | try { 25 | this.logger.debug('폴더 목록 로드 시작'); 26 | 27 | this.folders = ['/']; // 루트 폴더 추가 28 | 29 | // 재귀적으로 모든 폴더 탐색 30 | const addFolders = (folder: TFolder) => { 31 | // 숨김 폴더(점으로 시작하는 폴더)는 제외 32 | if (folder.path !== '/' && !folder.name.startsWith('.')) { 33 | this.folders.push(folder.path); 34 | } 35 | 36 | folder.children 37 | .filter(child => child instanceof TFolder) 38 | .forEach(child => addFolders(child as TFolder)); 39 | }; 40 | 41 | const rootFolder = this.app.vault.getRoot(); 42 | addFolders(rootFolder); 43 | 44 | this.logger.debug('폴더 목록 로드 완료', { folderCount: this.folders.length }); 45 | } catch (error) { 46 | this.logger.error('폴더 목록 로드 실패', { error }); 47 | } 48 | } 49 | 50 | /** 51 | * 사용자 입력에 따라 제안 목록 생성 52 | */ 53 | getSuggestions(query: string): string[] { 54 | return this.folders.filter(folder => 55 | folder.toLowerCase().includes(query.toLowerCase()) 56 | ); 57 | } 58 | 59 | /** 60 | * 제안 항목 렌더링 61 | */ 62 | renderSuggestion(folder: string, el: HTMLElement): void { 63 | el.createEl('div', { text: folder }); 64 | } 65 | 66 | /** 67 | * 제안 항목 선택 처리 68 | */ 69 | onChooseSuggestion(folder: string, evt: MouseEvent | KeyboardEvent): void { 70 | try { 71 | this.logger.debug('폴더 선택됨', { folder }); 72 | 73 | // 모달 닫은 후 이벤트 처리 74 | this.close(); 75 | 76 | // 콜백이 있는 경우에만 실행 77 | if (this.onChoose) { 78 | // 모달 내에서의 중복 호출 방지 (마우스 더블 클릭 등에 의한 중복 호출 방지) 79 | setTimeout(() => { 80 | this.onChoose(folder); 81 | }, 50); 82 | } 83 | } catch (error) { 84 | this.logger.error('폴더 선택 처리 중 오류 발생', { error, folder }); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/ui/modals/TagSuggestModal.ts: -------------------------------------------------------------------------------- 1 | import { App, SuggestModal } from 'obsidian'; 2 | import { ILoggingService } from '@/domain/infrastructure/ILoggingService'; 3 | import { Container } from '@/infrastructure/di/Container'; 4 | 5 | /** 6 | * 태그 선택 서제스트 모달 7 | */ 8 | export class TagSuggestModal extends SuggestModal { 9 | private readonly loggingService: ILoggingService; 10 | public onChoose: (tag: string) => void; 11 | private tags: string[] = []; 12 | 13 | constructor(app: App) { 14 | super(app); 15 | this.loggingService = Container.getInstance().resolve('ILoggingService'); 16 | this.setPlaceholder('태그를 입력하거나 선택하세요'); 17 | this.loadTags(); 18 | } 19 | 20 | /** 21 | * 볼트 내 모든 태그 로드 22 | */ 23 | private loadTags(): void { 24 | try { 25 | this.loggingService.debug('태그 목록 로드 시작'); 26 | 27 | const tagSet = new Set(); 28 | 29 | // 마크다운 파일 순회 30 | this.app.vault.getMarkdownFiles().forEach(file => { 31 | const cache = this.app.metadataCache.getFileCache(file); 32 | if (cache?.tags) { 33 | // 캐시에서 태그 추출 34 | cache.tags.forEach(tag => { 35 | tagSet.add(tag.tag); 36 | }); 37 | } 38 | 39 | // 프론트매터에서 태그 추출 40 | if (cache && cache.frontmatter && cache.frontmatter.tags) { 41 | const fmTags = cache.frontmatter.tags; 42 | if (Array.isArray(fmTags)) { 43 | fmTags.forEach(tag => tagSet.add('#' + tag)); 44 | } else if (typeof fmTags === 'string') { 45 | tagSet.add('#' + fmTags); 46 | } 47 | } 48 | }); 49 | 50 | this.tags = Array.from(tagSet).sort(); 51 | this.loggingService.debug('태그 목록 로드 완료', { tagCount: this.tags.length }); 52 | } catch (error) { 53 | this.loggingService.error('태그 목록 로드 실패', { error }); 54 | } 55 | } 56 | 57 | /** 58 | * 사용자 입력에 따라 제안 목록 생성 59 | */ 60 | getSuggestions(query: string): string[] { 61 | return this.tags.filter(tag => 62 | tag.toLowerCase().includes(query.toLowerCase()) 63 | ); 64 | } 65 | 66 | /** 67 | * 제안 항목 렌더링 68 | */ 69 | renderSuggestion(tag: string, el: HTMLElement): void { 70 | el.createEl('div', { text: tag }); 71 | } 72 | 73 | /** 74 | * 제안 항목 선택 처리 75 | */ 76 | onChooseSuggestion(tag: string, evt: MouseEvent | KeyboardEvent): void { 77 | try { 78 | this.loggingService.debug('태그 선택됨', { tag }); 79 | 80 | // 모달 닫은 후 이벤트 처리 81 | this.close(); 82 | 83 | // 콜백이 있는 경우에만 실행 84 | if (this.onChoose) { 85 | // 모달 내에서의 중복 호출 방지 (마우스 더블 클릭 등에 의한 중복 호출 방지) 86 | setTimeout(() => { 87 | this.onChoose(tag); 88 | }, 50); 89 | } 90 | } catch (error) { 91 | this.loggingService.error('태그 선택 처리 중 오류 발생', { error, tag }); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/ui/settings/CardNavigatorSettingTab.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab } from 'obsidian'; 2 | import type CardNavigatorPlugin from '@/main'; 3 | import { CardSettingsSection } from './sections/CardSettingsSection'; 4 | import { CardSetSettingsSection } from './sections/CardSetSettingsSection'; 5 | import { LayoutSettingsSection } from './sections/LayoutSettingsSection'; 6 | import { SearchSettingsSection } from './sections/SearchSettingsSection'; 7 | import { SortSettingsSection } from './sections/SortSettingsSection'; 8 | import { PresetSettingsSection } from './sections/PresetSettingsSection'; 9 | import { Container } from '@/infrastructure/di/Container'; 10 | import { IEventDispatcher } from '@/domain/infrastructure/IEventDispatcher'; 11 | /** 12 | * 카드 내비게이터 설정 탭 13 | */ 14 | export class CardNavigatorSettingTab extends PluginSettingTab { 15 | private cardSettings: CardSettingsSection; 16 | private cardSetSettings: CardSetSettingsSection; 17 | private layoutSettings: LayoutSettingsSection; 18 | private searchSettings: SearchSettingsSection; 19 | private sortSettings: SortSettingsSection; 20 | private presetSettings: PresetSettingsSection; 21 | private eventDispatcher: IEventDispatcher; 22 | 23 | constructor( 24 | app: App, 25 | private plugin: CardNavigatorPlugin 26 | ) { 27 | super(app, plugin); 28 | 29 | // 이벤트 디스패처 가져오기 30 | this.eventDispatcher = Container.getInstance().resolve('IEventDispatcher'); 31 | 32 | // 각 설정 섹션 초기화 33 | this.cardSettings = new CardSettingsSection(plugin, this.eventDispatcher); 34 | this.cardSetSettings = new CardSetSettingsSection(plugin, this.eventDispatcher); 35 | this.layoutSettings = new LayoutSettingsSection(plugin, this.eventDispatcher); 36 | this.searchSettings = new SearchSettingsSection(plugin, this.eventDispatcher); 37 | this.sortSettings = new SortSettingsSection(plugin, this.eventDispatcher); 38 | this.presetSettings = new PresetSettingsSection(plugin, this.eventDispatcher); 39 | } 40 | 41 | /** 42 | * 설정 UI 표시 43 | */ 44 | display(): void { 45 | const { containerEl } = this; 46 | containerEl.empty(); 47 | 48 | // 탭 컨테이너 생성 49 | const tabContainer = containerEl.createDiv('card-navigator-settings-tabs'); 50 | const tabContentContainer = containerEl.createDiv('card-navigator-settings-content'); 51 | 52 | // 탭 목록 생성 53 | const tabs = [ 54 | { id: 'card', name: '카드 설정', section: this.cardSettings }, 55 | { id: 'cardSet', name: '카드셋 설정', section: this.cardSetSettings }, 56 | { id: 'layout', name: '레이아웃 설정', section: this.layoutSettings }, 57 | { id: 'search', name: '검색 설정', section: this.searchSettings }, 58 | { id: 'sort', name: '정렬 설정', section: this.sortSettings }, 59 | { id: 'preset', name: '프리셋 설정', section: this.presetSettings }, 60 | ]; 61 | 62 | // 현재 활성화된 탭 ID 63 | let activeTabId = 'card'; 64 | 65 | // 탭 버튼 생성 66 | tabs.forEach((tab, index) => { 67 | const tabButton = tabContainer.createEl('button', { text: tab.name }); 68 | tabButton.addClass('card-navigator-settings-tab'); 69 | if (index === 0) { 70 | tabButton.addClass('active'); 71 | } 72 | 73 | // 탭 클릭 이벤트 74 | tabButton.addEventListener('click', () => { 75 | // 이미 활성 탭이면 무시 76 | if (activeTabId === tab.id) { 77 | return; 78 | } 79 | 80 | // 활성 탭 변경 81 | activeTabId = tab.id; 82 | 83 | // 활성 탭 변경 84 | tabContainer.querySelectorAll('.card-navigator-settings-tab').forEach(btn => { 85 | btn.removeClass('active'); 86 | }); 87 | tabButton.addClass('active'); 88 | 89 | // 탭 내용 표시 90 | tabContentContainer.empty(); 91 | tab.section.create(tabContentContainer); 92 | 93 | console.log(`${tab.id} 탭으로 전환 완료`); 94 | }); 95 | }); 96 | 97 | // 첫 번째 탭 내용 표시 98 | tabs[0].section.create(tabContentContainer); 99 | } 100 | 101 | /** 102 | * 설정 탭 정리 103 | */ 104 | hide(): void { 105 | // 탭 컨테이너 정리 106 | const { containerEl } = this; 107 | containerEl.empty(); 108 | } 109 | } -------------------------------------------------------------------------------- /src/ui/settings/sections/LayoutSettingsSection.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from 'obsidian'; 2 | import type CardNavigatorPlugin from '@/main'; 3 | import { LayoutType } from '@/domain/models/Layout'; 4 | import { Container } from '@/infrastructure/di/Container'; 5 | import type { ISettingsService } from '@/domain/services/application/ISettingsService'; 6 | import { IEventDispatcher } from '@/domain/infrastructure/IEventDispatcher'; 7 | 8 | /** 9 | * 레이아웃 설정 섹션 10 | */ 11 | export class LayoutSettingsSection { 12 | private settingsService: ISettingsService; 13 | private eventDispatcher: IEventDispatcher; 14 | 15 | constructor(private plugin: CardNavigatorPlugin, eventDispatcher: IEventDispatcher) { 16 | // 설정 서비스 가져오기 17 | this.settingsService = Container.getInstance().resolve('ISettingsService'); 18 | this.eventDispatcher = eventDispatcher; 19 | } 20 | 21 | /** 22 | * 레이아웃 설정 섹션 생성 23 | */ 24 | create(containerEl: HTMLElement): void { 25 | containerEl.createEl('h3', { text: '레이아웃 설정' }); 26 | 27 | const settings = this.settingsService.getSettings(); 28 | 29 | new Setting(containerEl) 30 | .setName('카드 높이 고정') 31 | .setDesc('활성화하면 그리드 레이아웃이 적용되고, 비활성화하면 메이슨리 레이아웃이 적용됩니다.') 32 | .addToggle(toggle => 33 | toggle 34 | .setValue(settings.layout.config.fixedCardHeight) 35 | .onChange(async (value) => { 36 | await this.settingsService.saveSettings({ 37 | ...settings, 38 | layout: { 39 | ...settings.layout, 40 | config: { 41 | ...settings.layout.config, 42 | fixedCardHeight: value, 43 | type: value ? LayoutType.GRID : LayoutType.MASONRY 44 | } 45 | } 46 | }); 47 | })); 48 | 49 | // 레이아웃 타입과 방향 정보 표시 50 | const infoDiv = containerEl.createDiv('layout-info'); 51 | infoDiv.createEl('p', { 52 | text: '레이아웃 타입 및 방향은 카드 높이 고정 여부와 뷰포트 크기에 따라 자동으로 설정됩니다.', 53 | cls: 'setting-item-description' 54 | }); 55 | 56 | new Setting(containerEl) 57 | .setName('카드 최소 너비') 58 | .setDesc('카드의 최소 너비를 설정합니다.') 59 | .addSlider(slider => 60 | slider 61 | .setLimits(200, 800, 10) 62 | .setValue(settings.layout.config.cardThresholdWidth) 63 | .setDynamicTooltip() 64 | .onChange(async (value) => { 65 | await this.settingsService.saveSettings({ 66 | ...settings, 67 | layout: { 68 | ...settings.layout, 69 | config: { 70 | ...settings.layout.config, 71 | cardThresholdWidth: value 72 | } 73 | } 74 | }); 75 | })); 76 | 77 | new Setting(containerEl) 78 | .setName('카드 최소 높이') 79 | .setDesc('카드의 최소 높이를 설정합니다.') 80 | .addSlider(slider => 81 | slider 82 | .setLimits(200, 800, 10) 83 | .setValue(settings.layout.config.cardThresholdHeight) 84 | .setDynamicTooltip() 85 | .onChange(async (value) => { 86 | await this.settingsService.saveSettings({ 87 | ...settings, 88 | layout: { 89 | ...settings.layout, 90 | config: { 91 | ...settings.layout.config, 92 | cardThresholdHeight: value 93 | } 94 | } 95 | }); 96 | })); 97 | 98 | new Setting(containerEl) 99 | .setName('카드 간격') 100 | .setDesc('카드 사이의 간격을 설정합니다.') 101 | .addSlider(slider => 102 | slider 103 | .setLimits(0, 32, 2) 104 | .setValue(settings.layout.config.cardGap) 105 | .setDynamicTooltip() 106 | .onChange(async (value) => { 107 | await this.settingsService.saveSettings({ 108 | ...settings, 109 | layout: { 110 | ...settings.layout, 111 | config: { 112 | ...settings.layout.config, 113 | cardGap: value 114 | } 115 | } 116 | }); 117 | })); 118 | 119 | new Setting(containerEl) 120 | .setName('패딩') 121 | .setDesc('카드 목록의 패딩을 설정합니다.') 122 | .addSlider(slider => 123 | slider 124 | .setLimits(0, 32, 2) 125 | .setValue(settings.layout.config.padding) 126 | .setDynamicTooltip() 127 | .onChange(async (value) => { 128 | await this.settingsService.saveSettings({ 129 | ...settings, 130 | layout: { 131 | ...settings.layout, 132 | config: { 133 | ...settings.layout.config, 134 | padding: value 135 | } 136 | } 137 | }); 138 | })); 139 | } 140 | } -------------------------------------------------------------------------------- /src/ui/settings/sections/SearchSettingsSection.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from 'obsidian'; 2 | import type CardNavigatorPlugin from '@/main'; 3 | import { Container } from '@/infrastructure/di/Container'; 4 | import type { ISettingsService } from '@/domain/services/application/ISettingsService'; 5 | import { IEventDispatcher } from '@/domain/infrastructure/IEventDispatcher'; 6 | 7 | /** 8 | * 검색 설정 섹션 9 | */ 10 | export class SearchSettingsSection { 11 | private settingsService: ISettingsService; 12 | private eventDispatcher: IEventDispatcher; 13 | 14 | constructor(private plugin: CardNavigatorPlugin, eventDispatcher: IEventDispatcher) { 15 | // 설정 서비스 가져오기 16 | this.settingsService = Container.getInstance().resolve('ISettingsService'); 17 | this.eventDispatcher = eventDispatcher; 18 | } 19 | 20 | /** 21 | * 검색 설정 섹션 생성 22 | */ 23 | create(containerEl: HTMLElement): void { 24 | containerEl.createEl('h3', { text: '검색 설정' }); 25 | 26 | const settings = this.settingsService.getSettings(); 27 | 28 | // 검색 범위 29 | new Setting(containerEl) 30 | .setName('검색 범위') 31 | .setDesc('검색할 범위를 선택합니다.') 32 | .addDropdown(dropdown => 33 | dropdown 34 | .addOption('all', '전체') 35 | .addOption('current', '현재 카드셋') 36 | .setValue(settings.search.config.criteria.scope) 37 | .onChange(async (value) => { 38 | await this.settingsService.saveSettings({ 39 | ...settings, 40 | search: { 41 | ...settings.search, 42 | config: { 43 | ...settings.search.config, 44 | criteria: { 45 | ...settings.search.config.criteria, 46 | scope: value as 'all' | 'current' 47 | } 48 | } 49 | } 50 | }); 51 | })); 52 | 53 | // 검색 옵션 섹션 54 | containerEl.createEl('h4', { text: '검색 옵션' }); 55 | 56 | // 대소문자 구분 57 | new Setting(containerEl) 58 | .setName('대소문자 구분') 59 | .setDesc('검색 시 대소문자를 구분합니다.') 60 | .addToggle(toggle => 61 | toggle 62 | .setValue(settings.search.config.criteria.caseSensitive) 63 | .onChange(async (value) => { 64 | await this.settingsService.saveSettings({ 65 | ...settings, 66 | search: { 67 | ...settings.search, 68 | config: { 69 | ...settings.search.config, 70 | criteria: { 71 | ...settings.search.config.criteria, 72 | caseSensitive: value 73 | } 74 | } 75 | } 76 | }); 77 | })); 78 | 79 | // 정규식 사용 80 | new Setting(containerEl) 81 | .setName('정규식 사용') 82 | .setDesc('검색어를 정규식으로 처리합니다.') 83 | .addToggle(toggle => 84 | toggle 85 | .setValue(settings.search.config.criteria.useRegex) 86 | .onChange(async (value) => { 87 | await this.settingsService.saveSettings({ 88 | ...settings, 89 | search: { 90 | ...settings.search, 91 | config: { 92 | ...settings.search.config, 93 | criteria: { 94 | ...settings.search.config.criteria, 95 | useRegex: value 96 | } 97 | } 98 | } 99 | }); 100 | })); 101 | 102 | // 전체 단어 일치 103 | new Setting(containerEl) 104 | .setName('전체 단어 일치') 105 | .setDesc('검색어와 정확히 일치하는 단어만 검색합니다.') 106 | .addToggle(toggle => 107 | toggle 108 | .setValue(settings.search.config.criteria.wholeWord) 109 | .onChange(async (value) => { 110 | await this.settingsService.saveSettings({ 111 | ...settings, 112 | search: { 113 | ...settings.search, 114 | config: { 115 | ...settings.search.config, 116 | criteria: { 117 | ...settings.search.config.criteria, 118 | wholeWord: value 119 | } 120 | } 121 | } 122 | }); 123 | })); 124 | 125 | // 검색 히스토리 섹션 126 | containerEl.createEl('h4', { text: '검색 히스토리' }); 127 | 128 | // 최대 히스토리 수 129 | new Setting(containerEl) 130 | .setName('최대 히스토리 수') 131 | .setDesc('저장할 검색 히스토리의 최대 개수를 설정합니다.') 132 | .addText(text => 133 | text 134 | .setValue(settings.search.config.maxHistory.toString()) 135 | .onChange(async (value) => { 136 | const limit = parseInt(value); 137 | if (!isNaN(limit) && limit > 0) { 138 | await this.settingsService.saveSettings({ 139 | ...settings, 140 | search: { 141 | ...settings.search, 142 | config: { 143 | ...settings.search.config, 144 | maxHistory: limit 145 | } 146 | } 147 | }); 148 | } 149 | })); 150 | } 151 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "module": "ESNext", 10 | "target": "ES6", 11 | "allowJs": true, 12 | "noImplicitAny": true, 13 | "moduleResolution": "node", 14 | "importHelpers": true, 15 | "isolatedModules": true, 16 | "strictNullChecks": true, 17 | "allowSyntheticDefaultImports": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "lib": [ 21 | "DOM", 22 | "ES5", 23 | "ES6", 24 | "ES7" 25 | ] 26 | }, 27 | "include": [ 28 | "src/**/*.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0" 3 | } 4 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | environment: 'jsdom', 8 | include: ['**/*.{test,spec}.{js,ts}'], 9 | setupFiles: ['./src/__tests__/setup.ts'], 10 | coverage: { 11 | reporter: ['text', 'json', 'html'], 12 | exclude: [ 13 | 'node_modules/', 14 | 'src/__tests__/', 15 | ], 16 | }, 17 | }, 18 | resolve: { 19 | alias: { 20 | '@': path.resolve(__dirname, './src'), 21 | }, 22 | }, 23 | }); --------------------------------------------------------------------------------