diff --git a/README.md b/README.md index bc3bce7..305d039 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ - 저장된 메모를 삭제할 수 있어야 한다. - 메모를 한 번에 여러개 삭제할 수 있어야 한다. -### ⚠️ 메모 삭제 기능 예외 상황 +### ⚠️ 메모 삭제 기능 고려 사항 - 삭제할 메모가 없으면 아무 수행도 해선 안된다. @@ -55,13 +55,13 @@ - 저장된 메모의 content를 수정할 수 있어야 한다. -### ⚠️ 메모 수정 기능 예외 상황 +### ⚠️ 메모 수정 기능 고려 사항 - 수정할 메모가 없다면 아무 수행도 해선 안된다. --- -### 🏁 메모 추출 기능 +### ✅ 메모 추출 기능 - 저장된 메모를 한 개 이상 선택하여 txt 파일로 추출할 수 있어야 한다. - 추출한 단위 메모의 구성 내용은 다음과 같다. @@ -76,18 +76,22 @@ - txt 파일의 이름은 `devlog-{프로젝트명}-{내보낸날짜}-{내보낸시각}.txt` 이다. - 추출할 메모가 없으면 빈 txt 파일을 반환한다. -### ⚠️ 메모 추출 기능 예외 상황 +### ⚠️ 메모 추출 기능 고려 사항 + +- 메모 추출 기능에서 고려 사항은 없습니다. --- -### ☑️ 노트 수정/저장 기능 +### ✅ 노트 수정/저장 기능 - 노트를 저장할 수 있어야 한다. - 노트의 수정/저장 데이터는 다음과 같다. - `String content`: 노트의 내용 - `LocalDateTime savedAt`: 저장된 시각 -### ⚠️ 노트 수정/저장 기능 예외 상황 +### ⚠️ 노트 수정/저장 기능 고려 사항 + +- 노트가 수정할 내용이 없다면 아무 수행도 해선 안된다. --- diff --git a/src/main/kotlin/com/github/yeoli/devlog/domain/note/domain/Note.kt b/src/main/kotlin/com/github/yeoli/devlog/domain/note/domain/Note.kt new file mode 100644 index 0000000..fc51fe0 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/domain/note/domain/Note.kt @@ -0,0 +1,25 @@ +package com.github.yeoli.devlog.domain.note.domain + +import com.github.yeoli.devlog.domain.note.repository.NoteState +import java.time.LocalDateTime + +class Note( + val content: String, + val updatedAt: LocalDateTime +) { + + constructor(content: String) : this(content, LocalDateTime.now()) + + fun update(content: String): Note { + return Note( + content = content + ) + } + + fun toState(): NoteState { + return NoteState( + content = this.content, + updatedAt = this.updatedAt.toString() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteRepository.kt b/src/main/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteRepository.kt new file mode 100644 index 0000000..12fa924 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteRepository.kt @@ -0,0 +1,40 @@ +package com.github.yeoli.devlog.domain.note.repository + +import com.github.yeoli.devlog.domain.note.domain.Note +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import java.time.LocalDateTime + +@State( + name = "DevLogNoteStorage", + storages = [Storage("devlog-note.xml")] +) +@Service(Service.Level.PROJECT) +class NoteRepository : PersistentStateComponent { + + private var state: NoteState? = NoteState( + content = "", + updatedAt = LocalDateTime.now().toString() + ) + + override fun getState(): NoteState? = state + + override fun loadState(state: NoteState) { + this.state = state + } + + fun getNote(): Note { + if (state == null) { + state = Note( + content = "" + ).toState() + } + return state!!.toDomain() + } + + fun updateNote(updatedNote: Note) { + this.state = updatedNote.toState() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteState.kt b/src/main/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteState.kt new file mode 100644 index 0000000..29aaf25 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteState.kt @@ -0,0 +1,14 @@ +package com.github.yeoli.devlog.domain.note.repository + +import com.github.yeoli.devlog.domain.note.domain.Note +import java.time.LocalDateTime + +data class NoteState( + val content: String, + val updatedAt: String +) { + + fun toDomain(): Note { + return Note(content, updatedAt = this.updatedAt.let { LocalDateTime.parse(it) }) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/domain/note/service/NoteService.kt b/src/main/kotlin/com/github/yeoli/devlog/domain/note/service/NoteService.kt new file mode 100644 index 0000000..9173405 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/domain/note/service/NoteService.kt @@ -0,0 +1,27 @@ +package com.github.yeoli.devlog.domain.note.service + +import com.github.yeoli.devlog.domain.note.domain.Note +import com.github.yeoli.devlog.domain.note.repository.NoteRepository +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project + +@Service(Service.Level.PROJECT) +class NoteService(private val project: Project) { + + private val noteRepository = + project.getService(NoteRepository::class.java) + + fun getNote(): Note { + return noteRepository.getNote() + } + + fun updateNote(content: String) { + val note: Note = getNote() + if (note.content == content) return + + val updatedNote = note.update(content) + + noteRepository.updateNote(updatedNote) + } +} + diff --git a/src/test/kotlin/com/github/yeoli/devlog/domain/note/domain/NoteTest.kt b/src/test/kotlin/com/github/yeoli/devlog/domain/note/domain/NoteTest.kt new file mode 100644 index 0000000..c8333f6 --- /dev/null +++ b/src/test/kotlin/com/github/yeoli/devlog/domain/note/domain/NoteTest.kt @@ -0,0 +1,83 @@ +package com.github.yeoli.devlog.domain.note.domain + +import org.junit.jupiter.api.Assertions.* +import java.time.LocalDateTime +import kotlin.test.Test + +class NoteTest { + + @Test + fun `test 업데이트 시 새로운 인스턴스를 반환해야 한다`() { + val original = Note( + content = "hello" + ) + + val updated = original.update("new content") + + assertNotSame(original, updated) + } + + @Test + fun `test 업데이트 시 콘텐츠가 변경되어야 한다`() { + val original = Note( + content = "old content" + ) + + val updated = original.update("new content") + + assertEquals("new content", updated.content) + assertNotEquals(original.content, updated.content) + } + + @Test + fun `test 빈 문자열로 업데이트할 때 정상적으로 처리되어야 한다`() { + val original = Note( + content = "something" + ) + + val updated = original.update("") + + assertEquals("", updated.content) + } + + // =========== toState 테스트 =========== + @Test + fun `test toState - content와 updatedAt이 올바르게 변환된다`() { + // given + val now = LocalDateTime.of(2025, 1, 1, 12, 0) + val note = Note("Hello", now) + + // when + val state = note.toState() + + // then + assertEquals("Hello", state.content) + assertEquals(now.toString(), state.updatedAt) + } + + @Test + fun `test toState - empty content도 정상 변환된다`() { + // given + val note = Note("") + + // when + val state = note.toState() + + // then + assertEquals("", state.content) + assertNotNull(state.updatedAt) + } + + @Test + fun `test toState - updatedAt이 현재 시간이 아닐 수 있다`() { + // given + val customTime = LocalDateTime.of(2024, 12, 31, 23, 59) + val note = Note("Time test", customTime) + + // when + val state = note.toState() + + // then + assertEquals(customTime.toString(), state.updatedAt) + } +} diff --git a/src/test/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteStateTest.kt b/src/test/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteStateTest.kt new file mode 100644 index 0000000..dfcd291 --- /dev/null +++ b/src/test/kotlin/com/github/yeoli/devlog/domain/note/repository/NoteStateTest.kt @@ -0,0 +1,54 @@ +package com.github.yeoli.devlog.domain.note.repository + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertEquals +import java.time.LocalDateTime +import kotlin.test.Test + +class NoteStateTest { + @Test + fun `test toDomain - content와 updatedAt이 올바르게 변환된다`() { + // given + val nowString = "2025-01-01T12:00:00" + val state = NoteState( + content = "Hello Note", + updatedAt = nowString + ) + + // when + val note = state.toDomain() + + // then + assertEquals("Hello Note", note.content) + assertEquals(LocalDateTime.parse(nowString), note.updatedAt) + } + + @Test + fun `test toDomain - 빈 content도 정상적으로 변환된다`() { + // given + val nowString = LocalDateTime.now().toString() + val state = NoteState( + content = "", + updatedAt = nowString + ) + + // when + val note = state.toDomain() + + // then + assertEquals("", note.content) + assertEquals(LocalDateTime.parse(nowString), note.updatedAt) + } + + @Test + fun `test toDomain - updatedAt 문자열 포맷이 LocalDateTime으로 파싱 가능해야 한다`() { + // given + val formattedTime = "2024-12-31T23:59:00" + val state = NoteState("TimeCheck", formattedTime) + + // when & then + assertDoesNotThrow { + state.toDomain() + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/yeoli/devlog/domain/note/service/NoteServiceTest.kt b/src/test/kotlin/com/github/yeoli/devlog/domain/note/service/NoteServiceTest.kt new file mode 100644 index 0000000..3d78a15 --- /dev/null +++ b/src/test/kotlin/com/github/yeoli/devlog/domain/note/service/NoteServiceTest.kt @@ -0,0 +1,100 @@ +package com.github.yeoli.devlog.domain.note.service + +import com.github.yeoli.devlog.domain.note.domain.Note +import com.github.yeoli.devlog.domain.note.repository.NoteRepository +import com.intellij.openapi.project.Project +import org.mockito.Mockito.mock +import org.mockito.kotlin.* +import java.time.LocalDateTime +import kotlin.test.Test +import kotlin.test.assertEquals + +class NoteServiceTest { + + private val project: Project = mock(Project::class.java) + private val noteRepository: NoteRepository = mock(NoteRepository::class.java) + + private fun createService(): NoteService { + val service = NoteService(project) + val field = service.javaClass.getDeclaredField("noteRepository") + field.isAccessible = true + field.set(service, noteRepository) + return service + } + + @Test + fun `test 레포지토리와 동일한 노트를 반환한다`() { + // given + val now = LocalDateTime.now() + val expectedNote = Note("Hello Test", now) + + whenever(noteRepository.getNote()).thenReturn(expectedNote) + + val service = NoteService(project) + val noteServiceField = service.javaClass.getDeclaredField("noteRepository") + noteServiceField.isAccessible = true + noteServiceField.set(service, noteRepository) + + // when + val actual = service.getNote() + + // then + assertEquals(expectedNote, actual) + } + + @Test + fun `test 기본 빈 노트를 반환한다`() { + // given + val defaultNote = Note("") + whenever(noteRepository.getNote()).thenReturn(defaultNote) + + val service = NoteService(project) + val noteServiceField = service.javaClass.getDeclaredField("noteRepository") + noteServiceField.isAccessible = true + noteServiceField.set(service, noteRepository) + + // when + val actual = service.getNote() + + // then + assertEquals(defaultNote, actual) + } + + // ========= updateNote 테스트 ========= + + @Test + fun `test 내용이 변경되면 업데이트한다`() { + // given + val oldNote = Note("old", LocalDateTime.now()) + whenever(noteRepository.getNote()).thenReturn(oldNote) + + val service = createService() + + val newContent = "new" + + // when + service.updateNote(newContent) + + // then + verify(noteRepository, times(1)).updateNote( + check { updated -> + assertEquals(newContent, updated.content) + } + ) + } + + @Test + fun `test 내용이 동일하면 업데이트하지 않는다`() { + // given + val sameNote = Note("same", LocalDateTime.now()) + whenever(noteRepository.getNote()).thenReturn(sameNote) + + val service = createService() + + // when + service.updateNote("same") + + // then + verify(noteRepository, never()).updateNote(any()) + } +}