Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@

---

### ☑️ 메모 추출 기능
### 🏁 메모 추출 기능

- 저장된 메모를 한 개 이상 선택하여 txt 파일로 추출할 수 있어야 한다.
- 추출한 단위 메모의 구성 내용은 다음과 같다.
Expand Down
22 changes: 22 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
Expand Up @@ -2,6 +2,7 @@ package com.github.yeoli.devlog.domain.memo.domain

import com.github.yeoli.devlog.domain.memo.repository.MemoState
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

class Memo(
val id: Long,
Expand Down Expand Up @@ -93,4 +94,25 @@ class Memo(
visibleEnd = this.visibleEnd
)
}

fun buildMemoBlock(index: Int): String {
return """

# Memo $index
📅 생성 시간 : ${this.createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}
📅 수정 시간 : ${this.updatedAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}

📌 Content
${this.content}

🔗 Metadata
- Commit: ${this.commitHash ?: "N/A"}
- File Path: ${this.filePath ?: "N/A"}
- Visible Lines: ${this.visibleStart ?: "?"} ~ ${this.visibleEnd ?: "?"}
- Selected Code :
${this.selectedCodeSnippet ?: "(no selected code)"}

---------------------------------------
""".trimIndent()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import com.github.yeoli.devlog.domain.memo.domain.Memo
import com.github.yeoli.devlog.domain.memo.repository.MemoRepository
import com.ibm.icu.impl.IllegalIcuArgumentException
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import git4idea.repo.GitRepositoryManager
import java.awt.Point
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

@Service(Service.Level.PROJECT)
class MemoService(private val project: Project) {
Expand Down Expand Up @@ -119,4 +123,48 @@
logger.warn("[updateMemo] 메모 수정 중 알 수 없는 에러가 발생했습니다. ${e.message}", e)
}
}

fun buildHeader(): String {
val projectName = project.name
val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))

return """
========== DEV LOG ==========

# 요약 정보
💻 프로젝트 명: $projectName
⏰ 추출 시간: $now

---------------------------------------
""".trimIndent()
}

fun buildExportText(selectedMemos: List<Memo>): String {
val header = buildHeader()

if (selectedMemos.isEmpty()) {
return header + "\n(내보낼 메모가 없습니다.)"

Check notice on line 146 in src/main/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

String concatenation that can be converted to string template

'String' concatenation can be converted to a template
}

val body = selectedMemos
.mapIndexed { index, memo ->
memo.buildMemoBlock(index + 1)
}
.joinToString(separator = "\n")

return header + "\n\n" + body
}

fun exportToTxt(text: String, directory: VirtualFile): VirtualFile? {
val date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"))
val fileName = "devlog-${project.name}-$date.txt"

var file: VirtualFile? = null
WriteCommandAction.runWriteCommandAction(project) {
file = directory.createChildData(this, fileName)
file!!.setBinaryContent(text.toByteArray(Charsets.UTF_8))
}

return file
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.github.yeoli.devlog.domain.memo.domain

import com.github.yeoli.devlog.domain.memo.repository.MemoState
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.test.*

class MemoTest {
Expand Down Expand Up @@ -99,4 +101,93 @@ class MemoTest {
assertNotNull(memoState.createdAt)
assertNotNull(memoState.updatedAt)
}

@Test
fun `test buildMemoBlock - 정상적으로 문자열 생성`() {
val created = LocalDateTime.of(2025, 11, 22, 13, 11, 10)
val updated = LocalDateTime.of(2025, 11, 22, 13, 12, 0)

val memo = Memo(
id = 1L,
createdAt = created,
updatedAt = updated,
content = "메모 내용입니다.",
commitHash = "abc123",
filePath = "/path/to/file",
selectedCodeSnippet = "println(\"Hello\")",
selectionStart = 0,
selectionEnd = 5,
visibleStart = 1,
visibleEnd = 10
)

val block = memo.buildMemoBlock(1)

assertTrue(block.contains("# Memo 1"))
assertTrue(block.contains("📅 생성 시간 : ${created.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}"))
assertTrue(block.contains("📅 수정 시간 : ${updated.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}"))
assertTrue(block.contains("📌 Content"))
assertTrue(block.contains("메모 내용입니다."))
assertTrue(block.contains("Commit: abc123"))
assertTrue(block.contains("File Path: /path/to/file"))
assertTrue(block.contains("Visible Lines: 1 ~ 10"))
assertTrue(block.contains("println(\"Hello\")"))
}

@Test
fun `test buildMemoBlock - 모든 값이 있을 때 정상적으로 표시되는지`() {
val created = LocalDateTime.of(2025, 11, 22, 13, 11, 10)
val updated = LocalDateTime.of(2025, 11, 22, 13, 15, 30)

val memo = Memo(
id = 2L,
createdAt = created,
updatedAt = updated,
content = "전체 필드 테스트",
commitHash = "ff12aa",
filePath = "/full/path/file.kt",
selectedCodeSnippet = "val x = 10",
selectionStart = 3,
selectionEnd = 9,
visibleStart = 2,
visibleEnd = 12
)

val block = memo.buildMemoBlock(2)

assertTrue(block.contains("# Memo 2"))
assertTrue(block.contains("📅 생성 시간 : ${created.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}"))
assertTrue(block.contains("📅 수정 시간 : ${updated.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}"))
assertTrue(block.contains("전체 필드 테스트"))
assertTrue(block.contains("Commit: ff12aa"))
assertTrue(block.contains("File Path: /full/path/file.kt"))
assertTrue(block.contains("Visible Lines: 2 ~ 12"))
assertTrue(block.contains("val x = 10"))
}

@Test
fun `test buildMemoBlock - null 값들이 기본값으로 표시되는지`() {
val created = LocalDateTime.of(2025, 11, 22, 13, 11, 10)

val memo = Memo(
id = 1L,
createdAt = created,
updatedAt = created,
content = "내용",
commitHash = null,
filePath = null,
selectedCodeSnippet = null,
selectionStart = null,
selectionEnd = null,
visibleStart = null,
visibleEnd = null
)

val block = memo.buildMemoBlock(0)

assertTrue(block.contains("Commit: N/A"))
assertTrue(block.contains("File Path: N/A"))
assertTrue(block.contains("Visible Lines: ? ~ ?"))
assertTrue(block.contains("(no selected code)"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -396,4 +396,169 @@ class MemoServiceTest : BasePlatformTestCase() {
})
}

// ========= 메모 추출 기능 =========

fun `test buildHeader - 프로젝트명과 시간 포함`() {
// given
val service = MemoService(project)

// when
val header = service.buildHeader()

// then
assertTrue(header.contains("========== DEV LOG =========="))
assertTrue(header.contains("💻 프로젝트 명: ${project.name}"))
assertTrue(header.contains("⏰ 추출 시간:"))
val regex = Regex("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")
assertTrue(regex.containsMatchIn(header), "날짜 포맷이 잘못되었습니다.")
assertTrue(header.contains("---------------------------------------"))
}

fun `test buildExportText - 빈 메모 리스트`() {
// given
val service = MemoService(project)

// when
val export = service.buildExportText(emptyList())

// then
assertTrue(export.contains("========== DEV LOG =========="))
assertTrue(export.contains("(내보낼 메모가 없습니다.)"))
}

fun `test buildExportText - 단일 메모 포함`() {
val now = LocalDateTime.of(2025, 1, 1, 10, 20, 30)

val memo = Memo(
id = 1L,
createdAt = now,
updatedAt = now,
content = "hello",
commitHash = "abc123",
filePath = "/path/file",
selectedCodeSnippet = "val a = 1",
selectionStart = 0,
selectionEnd = 10,
visibleStart = 3,
visibleEnd = 5
)

val service = MemoService(project)

// when
val export = service.buildExportText(listOf(memo))

// then: 헤더 포함
assertTrue(export.contains("========== DEV LOG =========="))
assertTrue(export.contains("💻 프로젝트 명: ${project.name}"))

// 메모 블록 정보 포함 확인
assertTrue(export.contains("# Memo 1"))
assertTrue(export.contains("📌 Content"))
assertTrue(export.contains("hello"))
assertTrue(export.contains("abc123"))
assertTrue(export.contains("/path/file"))
assertTrue(export.contains("val a = 1"))
assertTrue(export.contains("3 ~ 5"))
}

fun `test buildExportText - 여러 메모 순서대로 출력`() {
val now = LocalDateTime.of(2025, 1, 1, 9, 0, 0)

val memo1 = Memo(
id = 1L,
createdAt = now,
updatedAt = now,
content = "first",
commitHash = null,
filePath = "/f1",
selectedCodeSnippet = null,
selectionStart = 0,
selectionEnd = 0,
visibleStart = 1,
visibleEnd = 1
)

val memo2 = Memo(
id = 2L,
createdAt = now.plusHours(1),
updatedAt = now.plusHours(1),
content = "second",
commitHash = null,
filePath = "/f2",
selectedCodeSnippet = null,
selectionStart = 10,
selectionEnd = 10,
visibleStart = 2,
visibleEnd = 2
)

val service = MemoService(project)

// when
val export = service.buildExportText(listOf(memo1, memo2))

// then
assertTrue(export.contains("# Memo 1"))
assertTrue(export.contains("# Memo 2"))

// 순서 보장
val index1 = export.indexOf("# Memo 1")
val index2 = export.indexOf("# Memo 2")
assertTrue(index1 < index2, "Memo 1이 Memo 2보다 먼저 출력되어야 합니다.")
}

fun `test buildExportText - memoBlock 필드 검증`() {
val now = LocalDateTime.of(2025, 1, 1, 12, 34, 56)

val memo = Memo(
id = 10L,
createdAt = now,
updatedAt = now,
content = "내용입니다",
commitHash = null,
filePath = null,
selectedCodeSnippet = null,
selectionStart = 100,
selectionEnd = 200,
visibleStart = 3,
visibleEnd = 8
)

val service = MemoService(project)

// when
val export = service.buildExportText(listOf(memo))

// then
// 날짜 체크
assertTrue(export.contains("2025-01-01 12:34:56"))
// content
assertTrue(export.contains("내용입니다"))
// null 매핑
assertTrue(export.contains("- Commit: N/A"))
assertTrue(export.contains("- File Path: N/A"))
assertTrue(export.contains("(no selected code)"))
// visible range
assertTrue(export.contains("3 ~ 8"))
}

fun `test exportToTxt - 파일 생성 및 내용 저장`() {
// given
val service = MemoService(project)
val text = "Hello DevLog Export Test"
val exportDir = myFixture.tempDirFixture.findOrCreateDir("exports")

// when
val exported = service.exportToTxt(text, exportDir)

// then
assertNotNull(exported)
assertTrue(exported!!.exists())
assertTrue(exported.name.startsWith("devlog-${project.name}-"))

val content = String(exported.contentsToByteArray(), Charsets.UTF_8)
assertEquals(text, content)
}

}
Loading