diff --git a/README.md b/README.md index 8e75a9a..bc3bce7 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ --- -### ☑️ 메모 추출 기능 +### 🏁 메모 추출 기능 - 저장된 메모를 한 개 이상 선택하여 txt 파일로 추출할 수 있어야 한다. - 추출한 단위 메모의 구성 내용은 다음과 같다. diff --git a/src/main/kotlin/com/github/yeoli/devlog/domain/memo/domain/Memo.kt b/src/main/kotlin/com/github/yeoli/devlog/domain/memo/domain/Memo.kt index 1bb1b4f..da69aab 100644 --- a/src/main/kotlin/com/github/yeoli/devlog/domain/memo/domain/Memo.kt +++ b/src/main/kotlin/com/github/yeoli/devlog/domain/memo/domain/Memo.kt @@ -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, @@ -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() + } } diff --git a/src/main/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoService.kt b/src/main/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoService.kt index 0874bd2..2be41b3 100644 --- a/src/main/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoService.kt +++ b/src/main/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoService.kt @@ -3,14 +3,18 @@ package com.github.yeoli.devlog.domain.memo.service 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) { @@ -119,4 +123,48 @@ class MemoService(private val project: Project) { 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): String { + val header = buildHeader() + + if (selectedMemos.isEmpty()) { + return header + "\n(내보낼 메모가 없습니다.)" + } + + 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 + } } \ No newline at end of file diff --git a/src/test/kotlin/com/github/yeoli/devlog/domain/memo/domain/MemoTest.kt b/src/test/kotlin/com/github/yeoli/devlog/domain/memo/domain/MemoTest.kt index 4533a23..38df2f5 100644 --- a/src/test/kotlin/com/github/yeoli/devlog/domain/memo/domain/MemoTest.kt +++ b/src/test/kotlin/com/github/yeoli/devlog/domain/memo/domain/MemoTest.kt @@ -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 { @@ -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)")) + } } diff --git a/src/test/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoServiceTest.kt b/src/test/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoServiceTest.kt index 5c776cb..d86b68b 100644 --- a/src/test/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoServiceTest.kt +++ b/src/test/kotlin/com/github/yeoli/devlog/domain/memo/service/MemoServiceTest.kt @@ -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) + } + }