Skip to content
Merged
86 changes: 59 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,82 +3,105 @@
## 핵심 기능 정의

---
### ☑️ 메모 저장 기능

### ✅메모 저장 기능

- 메모를 저장할 수 있어야 한다.
- 메모의 저장 데이터는 다음과 같다.
- `int id (not null)`: 메모의 고유한 아이디(PK)
- `String content (nullable)`: 사용자가 입력하는 메모의 내용
- `LocalDateTime createdAt (not null)`: 메모가 생성된 시간
- `LocalDateTime updatedAt (not null)`: 메모가 수정된 시간
- `String commitHash (nullable)`: 현재 커밋의 해시값
- `String filePath (nullable)`: 파일 경로
- `String selectedCodeSnippet (nullable)`: 선택된 코드
- `long selectionEnd (nullable)`: 문서에서 선택한 정확한 종료 위치
- `long selectionStart (nullable)`: 문서에서 선택한 정확한 시작 위치
- `int visibleEnd (nullable)`: 선택한 종료 줄
- `int visibleStart (nullable)`: 선택한 시작 줄
- `int id (not null)`: 메모의 고유한 아이디(PK)
- `String content (not null)`: 사용자가 입력하는 메모의 내용
- `LocalDateTime createdAt (not null)`: 메모가 생성된 시간
- `LocalDateTime updatedAt (not null)`: 메모가 수정된 시간
- `String commitHash (nullable)`: 현재 커밋의 해시값
- `String filePath (nullable)`: 파일 경로
- `String selectedCodeSnippet (nullable)`: 선택된 코드
- `long selectionEnd (nullable)`: 문서에서 선택한 정확한 종료 위치
- `long selectionStart (nullable)`: 문서에서 선택한 정확한 시작 위치
- `int visibleEnd (nullable)`: 선택한 종료 줄
- `int visibleStart (nullable)`: 선택한 시작 줄
- 저장은 시간 순서대로 저장이 되어야 한다.

### ⚠️ 메모 저장 기능 예외 상황
### ⚠️ 메모 저장 기능 제약 상황

- 메모의 `content`가 blank라면 저장이 되어선 안된다.
- `selectionStart`가 `selectionEnd`보다 앞에 있어야 한다.
- `visibleStart`가 `visibleEnd`보다 앞에 있어야 한다.
- `createdAt`은 자동으로 생성 되어야 한다.

---

### ☑️ 메모 조회 기능

- 메모의 id를 이용해서 데이터를 조회할 수 있어야 한다.
- 전체 메모를 날짜순으로 조회할 수 있어야 한다.
-

### ⚠️ 메모 조회 기능 예외 상황

---

### ☑️ 메모 삭제 기능

- 저장된 메모를 삭제할 수 있어야 한다.
- 메모를 한 번에 여러개 삭제할 수 있어야 한다.

### ⚠️ 메모 삭제 기능 예외 상황

---

### ☑️ 메모 수정 기능

- 저장된 메모의 content를 수정할 수 있어야 한다.

### ⚠️ 메모 수정 기능 예외 상황

---

### ☑️ 메모 추출 기능

- 저장된 메모를 한 개 이상 선택하여 txt 파일로 추출할 수 있어야 한다.
- 추출한 단위 메모의 구성 내용은 다음과 같다.
- 메모 순서(시간순)
- `LocalDateTime timestamp`: 메모가 생성된 시간
- `String content`: 사용자가 입력하는 메모의 내용
- `String filePath`: 파일 경로
- `String commitHash`: 현재 커밋의 해시값
- `int visibleStart`: 선택한 시작 줄
- `int visibleEnd`: 선택한 종료 줄
- 메모 순서(시간순)
- `LocalDateTime timestamp`: 메모가 생성된 시간
- `String content`: 사용자가 입력하는 메모의 내용
- `String filePath`: 파일 경로
- `String commitHash`: 현재 커밋의 해시값
- `int visibleStart`: 선택한 시작 줄
- `int visibleEnd`: 선택한 종료 줄
- txt 파일 상단에 프로젝트명, 내보낸 시각, 메모의 개수가 있어야 한다.
- txt 파일의 이름은 `devlog-{프로젝트명}-{내보낸날짜}-{내보낸시각}.txt` 이다.
- 추출할 메모가 없으면 빈 txt 파일을 반환한다.

### ⚠️ 메모 추출 기능 예외 상황

---

### ☑️ 노트 수정/저장 기능

- 노트를 저장할 수 있어야 한다.
- 노트의 수정/저장 데이터는 다음과 같다.
- `String content`: 노트의 내용
- `LocalDateTime savedAt`: 저장된 시각
- `String content`: 노트의 내용
- `LocalDateTime savedAt`: 저장된 시각

### ⚠️ 노트 수정/저장 기능 예외 상황

---

## 화면 요구 사항

---

### ☑️ 플러그인 기본 화면

- 화면의 최상단엔 기본 조작용 버튼이 있어야 한다.
- 기본 조작용 버튼은 아래와 같다.
- 메모목록/노트 화면 전환 버튼
- 메모 전체 선택/선택해제 버튼
- 선택된 메모 추출 버튼
- 선택된 메모 삭제 버튼
- 메모목록/노트 화면 전환 버튼
- 메모 전체 선택/선택해제 버튼
- 선택된 메모 추출 버튼
- 선택된 메모 삭제 버튼
- 메인 컨텐츠를 표시하는 화면이 있어야 한다.

### ☑️ 메모 목록 출력 화면

- 저장된 메모 전체가 최근순 정렬되어 화면에 보여야 한다.
- 각 메모 좌측에는 메모를 선택할 수 있는 체크박스가 있어야 한다.
- 화면 하단에는 새 메모를 적을 수 있는 텍스트 입력창이 있어야 한다.
Expand All @@ -89,7 +112,16 @@
### ⚠️ 메모 목록 출력 화면 예외 상황

---

### ☑️ 노트 출력 화면

- 저장된 노트의 모든 내용이 출력 되어야 한다.
- 노트의 변경 내용은 자동 저장 되어야 한다.
- 화면의 크기를 벗어나지 않게 내용을 출력해야 한다.

### ⚠️ 노트 출력 화면 예외 상황

<!-- Plugin description -->
DevLog Plugin is a memo/note tracking plugin that automatically captures your code selection,
file path, commit hash, and editor state, storing it for later use.
<!-- Plugin description end -->
14 changes: 12 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,18 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.opentest4j)

testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0")

// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
intellijPlatform {
create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion"))
create(
providers.gradleProperty("platformType"),
providers.gradleProperty("platformVersion")
)

bundledPlugins("Git4Idea")

// Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins.
bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') })
Expand Down Expand Up @@ -99,7 +108,8 @@ intellijPlatform {
// The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3
// Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more:
// https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel
channels = providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) }
channels = providers.gradleProperty("pluginVersion")
.map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) }
}

pluginVerification {
Expand Down
118 changes: 118 additions & 0 deletions com/intellij/testFramework/ServiceContainerUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("ServiceContainerUtil")
package com.intellij.testFramework

import com.intellij.ide.plugins.IdeaPluginDescriptorImpl
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.Application
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.components.ServiceDescriptor
import com.intellij.openapi.extensions.BaseExtensionPointName
import com.intellij.openapi.extensions.DefaultPluginDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.serviceContainer.ComponentManagerImpl
import com.intellij.util.messages.ListenerDescriptor
import com.intellij.util.messages.MessageBusOwner
import org.jetbrains.annotations.TestOnly

private val testDescriptor by lazy { DefaultPluginDescriptor("test") }

@TestOnly
fun <T : Any> ComponentManager.registerServiceInstance(serviceInterface: Class<T>, instance: T) {
(this as ComponentManagerImpl).registerServiceInstance(serviceInterface, instance, testDescriptor)
}

/**
* Unregister service specified by [serviceInterface] if it was registered;
* throws [IllegalStateException] if the service was not registered.
*/
@TestOnly
fun ComponentManager.unregisterService(serviceInterface: Class<*>) {
(this as ComponentManagerImpl).unregisterService(serviceInterface)
}

/**
* Register a new service or replace an existing service with a specified instance for testing purposes.
* Registration will be rolled back when parentDisposable is disposed. In most of the cases,
* [com.intellij.testFramework.UsefulTestCase.getTestRootDisposable] should be specified.
*/
@TestOnly
fun <T : Any> ComponentManager.registerOrReplaceServiceInstance(serviceInterface: Class<T>, instance: T, parentDisposable: Disposable) {
val previous = this.getService(serviceInterface)
if (previous != null) {
replaceService(serviceInterface, instance, parentDisposable)
}
else {
(this as ComponentManagerImpl).registerServiceInstance(serviceInterface, instance, testDescriptor)
if (instance is Disposable) {
Disposer.register(parentDisposable, instance)
}
else {
Disposer.register(parentDisposable) {
this.unregisterComponent(serviceInterface)
}
}
}
}

@TestOnly
fun <T : Any> ComponentManager.replaceService(serviceInterface: Class<T>, instance: T, parentDisposable: Disposable) {
(this as ComponentManagerImpl).replaceServiceInstance(serviceInterface, instance, parentDisposable)
}

@TestOnly
fun <T : Any> ComponentManager.registerComponentInstance(componentInterface: Class<T>, instance: T, parentDisposable: Disposable?) {
(this as ComponentManagerImpl).replaceComponentInstance(componentInterface, instance, parentDisposable)
}

@TestOnly
@JvmOverloads
fun ComponentManager.registerComponentImplementation(key: Class<*>, implementation: Class<*>, shouldBeRegistered: Boolean = false) {
(this as ComponentManagerImpl).registerComponentImplementation(key, implementation, shouldBeRegistered)
}

@TestOnly
fun <T : Any> ComponentManager.registerExtension(name: BaseExtensionPointName<*>, instance: T, parentDisposable: Disposable) {
extensionArea.getExtensionPoint<T>(name.name).registerExtension(instance, parentDisposable)
}

@TestOnly
fun ComponentManager.getServiceImplementationClassNames(prefix: String): List<String> {
val result = ArrayList<String>()
processAllServiceDescriptors(this) { serviceDescriptor ->
val implementation = serviceDescriptor.implementation ?: return@processAllServiceDescriptors
if (implementation.startsWith(prefix)) {
result.add(implementation)
}
}
return result
}

fun processAllServiceDescriptors(componentManager: ComponentManager, consumer: (ServiceDescriptor) -> Unit) {
for (plugin in PluginManagerCore.loadedPlugins) {
val pluginDescriptor = plugin as IdeaPluginDescriptorImpl
val containerDescriptor = when (componentManager) {
is Application -> pluginDescriptor.appContainerDescriptor
is Project -> pluginDescriptor.projectContainerDescriptor
else -> pluginDescriptor.moduleContainerDescriptor
}
containerDescriptor.services.forEach {
if ((componentManager as? ComponentManagerImpl)?.isServiceSuitable(it) != false &&
(it.os == null || componentManager.isSuitableForOs(it.os))) {
consumer(it)
}
}
}
}

fun createSimpleMessageBusOwner(owner: String): MessageBusOwner {
return object : MessageBusOwner {
override fun createListener(descriptor: ListenerDescriptor) = throw UnsupportedOperationException()

override fun isDisposed() = false

override fun toString() = owner
}
}
43 changes: 43 additions & 0 deletions src/main/kotlin/com/github/yeoli/devlog/domain/memo/domain/Memo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.github.yeoli.devlog.domain.memo.domain

import java.time.LocalDateTime

class Memo(
val content: String,

val commitHash: String? = null,
val filePath: String? = null,
val selectedCodeSnippet: String? = null,

val selectionStart: Int? = null,
val selectionEnd: Int? = null,

val visibleStart: Int? = null,
val visibleEnd: Int? = null
) {
val id: Long = generateId()
val createdAt: LocalDateTime = LocalDateTime.now()
val updatedAt: LocalDateTime = LocalDateTime.now()

init {
validate()
}

private fun validate() {
if (selectionStart != null && selectionEnd != null) {
require(selectionStart <= selectionEnd) {
"selectionStart는 selectionEnd 보다 작아야합니다."
}
}

if (visibleStart != null && visibleEnd != null) {
require(visibleStart <= visibleEnd) {
"visibleStart는 visibleEnd 보다 작아야합니다."
}
}
}

private fun generateId(): Long {
return System.currentTimeMillis()
}
}
Loading
Loading