diff --git a/.claude/hooks/post-edit-check.py b/.claude/hooks/post-edit-check.py new file mode 100755 index 00000000..0b5c1b14 --- /dev/null +++ b/.claude/hooks/post-edit-check.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import json +import sys +import re + +data = json.load(sys.stdin) +file_path = data.get("tool_input", {}).get("file_path", "") + +if not file_path.endswith(".java") or not file_path: + sys.exit(0) + +try: + with open(file_path) as f: + content = f.read() + lines = content.split("\n") +except Exception: + sys.exit(0) + +warnings = [] + +# 1. 와일드카드 import 체크 +for i, line in enumerate(lines, 1): + if re.match(r"\s*import\s+.*\.\*;", line): + warnings.append(f"L{i}: 와일드카드 import 발견 -> 명시적 import 필요") + +# 2. 파일 끝 줄바꿈 체크 +if content and not content.endswith("\n"): + warnings.append("파일 끝 줄바꿈 누락") + +# 3. Entity 클래스의 @Column 체크 +if "@Entity" in content: + field_pattern = re.compile(r"^\s+private\s+\w+(?:<[^>]+>)?\s+\w+;") + relation_annotations = { + "@Column", "@Id", "@ManyToOne", "@OneToMany", + "@JoinColumn", "@OneToOne", "@ManyToMany", + "@Transient", "@Version", "@Embedded", "@EmbeddedId", + } + for i, line in enumerate(lines): + if field_pattern.match(line): + preceding = "\n".join(lines[max(0, i - 5):i]) + has_annotation = any(ann in preceding for ann in relation_annotations) + if not has_annotation: + warnings.append(f"L{i + 1}: Entity 필드에 @Column 누락 가능성: {line.strip()}") + +if warnings: + print(f"[컨벤션 체크 - {file_path.split('/')[-1]}]") + for w in warnings: + print(f" - {w}") diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..86e70b68 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,29 @@ +{ + "env": { + "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" + }, + "hooks": { + "Notification": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "osascript -e 'display notification \"Awaiting your input\" with title \"Claude Code\"'" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/post-edit-check.py" + } + ] + } + ] + } +} diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md new file mode 100644 index 00000000..c2312257 --- /dev/null +++ b/.claude/skills/test/SKILL.md @@ -0,0 +1,247 @@ +--- +name: test +description: 테스트 코드를 작성하거나 수정할 때 이 프로젝트의 테스트 컨벤션과 패턴을 참고합니다 +--- + +# 테스트 코드 작성 가이드 + +## 테스트 기본 설정 + +모든 통합 테스트는 `@TestContainerSpringBootTest` 어노테이션을 사용합니다. + +```java +@TestContainerSpringBootTest +@DisplayName("채팅 서비스 테스트") +class ChatServiceTest { + // 테스트 코드 +} +``` + +**제공 기능:** +- MySQL, Redis 자동 실행 +- Spring Boot 컨텍스트 로드 +- 테스트 후 자동 DB 초기화 +- JUnit 5 기반 + +## Fixture 패턴 + +테스트 데이터는 Fixture로 생성합니다 (FixtureBuilder + Fixture 패턴). + +**위치:** `src/test/java/com/example/solidconnection/[domain]/fixture/` + +``` +fixture/ +├── [Entity]FixtureBuilder.java # Builder 패턴 구현 +└── [Entity]Fixture.java # 편의 메서드 제공 +``` + +### 예제: ChatRoomFixtureBuilder + +```java +@TestComponent +@RequiredArgsConstructor +public class ChatRoomFixtureBuilder { + + private final ChatRoomRepository chatRoomRepository; + + private boolean isGroup; + private Long mentoringId; + + public ChatRoomFixtureBuilder chatRoom() { + return new ChatRoomFixtureBuilder(chatRoomRepository); + } + + public ChatRoomFixtureBuilder isGroup(boolean isGroup) { + this.isGroup = isGroup; + return this; + } + + public ChatRoomFixtureBuilder mentoringId(long mentoringId) { + this.mentoringId = mentoringId; + return this; + } + + public ChatRoom create() { + ChatRoom chatRoom = new ChatRoom(mentoringId, isGroup); + return chatRoomRepository.save(chatRoom); // DB 저장 + } +} +``` + +### 예제: ChatRoomFixture + +```java +@TestComponent +@RequiredArgsConstructor +public class ChatRoomFixture { + + private final ChatRoomFixtureBuilder chatRoomFixtureBuilder; + + // 편의 메서드: 기본값으로 생성 + public ChatRoom 채팅방(boolean isGroup) { + return chatRoomFixtureBuilder.chatRoom() + .isGroup(isGroup) + .create(); + } + + public ChatRoom 멘토링_채팅방(long mentoringId) { + return chatRoomFixtureBuilder.chatRoom() + .mentoringId(mentoringId) + .isGroup(false) + .create(); + } +} +``` + +**편의 메서드 작성 팁:** + +- 한국어 메서드명 사용 (가독성) +- 자주 사용되는 기본값 조합만 제공 +- Builder를 조합하여 필요한 데이터 설정 + +### 테스트에서 사용 + +```java +@TestContainerSpringBootTest +class ChatServiceTest { + + @Autowired + private ChatRoomFixture chatRoomFixture; + + @Test + void 채팅방을_생성할_수_있다() { + // 편의 메서드 사용 + ChatRoom room = chatRoomFixture.채팅방(false); + + // Builder 직접 사용 + ChatRoom customRoom = chatRoomFixture.chatRoomFixtureBuilder.chatRoom() + .isGroup(true) + .mentoringId(100L) + .create(); + } +} +``` + +## 테스트 네이밍 컨벤션 + +### 테스트 메서드 네이밍 규칙 + +테스트 메서드명은 **한국어로 명확하게** 작성하며, 다음 패턴을 따릅니다: + +#### 1. 정상 동작 테스트 + +```java +// 패턴: 어떤_것을_하면_어떤_결과가_나온다 +@Test +void 채팅방이_없으면_빈_목록을_반환한다() { ... } + +@Test +void 최신_메시지_순으로_정렬되어_조회한다() { ... } + +@Test +void 참여자는_메시지를_전송할_수_있다() { ... } + +@Test +void 페이징이_정상_작동한다() { ... } +``` + +#### 2. 예외 테스트 + +```java +// 패턴: 어떤_것을_하면_예외_응답을_반환한다 +@Test +void 참여하지_않은_채팅방에_접근하면_예외_응답을_반환한다() { ... } + +@Test +void 존재하지_않는_사용자로_메시지를_전송하면_예외_응답을_반환한다() { ... } + +@Test +void 권한이_없으면_예외_응답을_반환한다() { ... } + +@Test +void 필수_파라미터가_없으면_예외_응답을_반환한다() { ... } +``` + +## BDD 테스트 작성 + +테스트는 Given-When-Then 구조로 작성합니다. + +```java +@Test +@DisplayName("최신 메시지순으로 채팅방 목록을 조회한다") +void 최신_메시지_순으로_조회한다() { + // Given: 테스트 사전 조건 + SiteUser user = siteUserFixture.사용자(); + ChatRoom room1 = chatRoomFixture.채팅방(false); + ChatRoom room2 = chatRoomFixture.채팅방(false); + chatMessageFixture.메시지("오래된 메시지", user.getId(), room1); + chatMessageFixture.메시지("최신 메시지", user.getId(), room2); + + // When: 실제 동작 + ChatRoomListResponse response = chatService.getChatRooms(user.getId()); + + // Then: 결과 검증 + assertAll( + () -> assertThat(response.chatRooms()).hasSize(2), + () -> assertThat(response.chatRooms().get(0).id()).isEqualTo(room2.getId()) + ); +} +``` + +## 테스트 그룹화 (@Nested) + +기능별로 테스트를 그룹화합니다. + +```java +@TestContainerSpringBootTest +class ChatServiceTest { + + @Nested + @DisplayName("채팅방 목록 조회") + class 채팅방_목록을_조회한다 { + + @Test + void 빈_목록을_반환한다() { ... } + + @Test + void 최신_메시지_순으로_조회한다() { ... } + } + + @Nested + @DisplayName("채팅 메시지 전송") + class 채팅_메시지를_전송한다 { + + @BeforeEach + void setUp() { + // 이 그룹에만 적용되는 초기 설정 + } + + @Test + void 참여자는_메시지를_전송할_수_있다() { ... } + } +} +``` + +## 자주 사용하는 Assertion + +```java +// 기본 검증 +assertThat(value).isEqualTo(expected); +assertThat(value).isNotNull(); + +// 컬렉션 +assertThat(list).hasSize(3); +assertThat(list).isEmpty(); +assertThat(list).contains(item); + +// 예외 검증 +assertThatCode(() -> method()) + .isInstanceOf(CustomException.class) + .hasMessage("error message"); + +// 복수 검증 +assertAll( + () -> assertThat(a).isEqualTo(1), + () -> assertThat(b).isEqualTo(2) +); +``` diff --git a/.gitignore b/.gitignore index d5df4047..7a58382b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ out/ ### VS Code ### .vscode/ +### Claude Code ### +.claude/settings.local.json + ### YML ### application-secret.yml application-prod.yml diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 00000000..14d86ad6 --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000..2f196bdb --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,112 @@ +# the name by which the project can be referenced within Serena +project_name: "solid-connect-server" + + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# powershell python python_jedi r rego +# ruby ruby_solargraph rust scala swift +# terraform toml typescript typescript_vts vue +# yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- java + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore in all projects +# same syntax as gitignore, so you can use * and ** +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" diff --git a/claude.md b/claude.md index ddd2cb9b..af2e96a4 100644 --- a/claude.md +++ b/claude.md @@ -196,248 +196,11 @@ public class UserCreateResponse { ... } ## 테스트 코드 작성 -### 테스트 기본 설정 +테스트 작성 시 `/test` skill을 참고하세요. (테스트 관련 작업 시 자동으로 로드됩니다) -모든 통합 테스트는 `@TestContainerSpringBootTest` 어노테이션을 사용합니다. - -```java -@TestContainerSpringBootTest -@DisplayName("채팅 서비스 테스트") -class ChatServiceTest { - // 테스트 코드 -} -``` - -**제공 기능:** -- MySQL, Redis 자동 실행 -- Spring Boot 컨텍스트 로드 -- 테스트 후 자동 DB 초기화 -- JUnit 5 기반 - -### Fixture 패턴 - -테스트 데이터는 Fixture로 생성합니다 (FixtureBuilder + Fixture 패턴). - -**위치:** `src/test/java/com/example/solidconnection/[domain]/fixture/` - -``` -fixture/ -├── [Entity]FixtureBuilder.java # Builder 패턴 구현 -└── [Entity]Fixture.java # 편의 메서드 제공 -``` - -#### 예제: ChatRoomFixtureBuilder - -```java -@TestComponent -@RequiredArgsConstructor -public class ChatRoomFixtureBuilder { - - private final ChatRoomRepository chatRoomRepository; - - private boolean isGroup; - private Long mentoringId; - - public ChatRoomFixtureBuilder chatRoom() { - return new ChatRoomFixtureBuilder(chatRoomRepository); - } - - public ChatRoomFixtureBuilder isGroup(boolean isGroup) { - this.isGroup = isGroup; - return this; - } - - public ChatRoomFixtureBuilder mentoringId(long mentoringId) { - this.mentoringId = mentoringId; - return this; - } - - public ChatRoom create() { - ChatRoom chatRoom = new ChatRoom(mentoringId, isGroup); - return chatRoomRepository.save(chatRoom); // DB 저장 - } -} -``` - -#### 예제: ChatRoomFixture - -```java -@TestComponent -@RequiredArgsConstructor -public class ChatRoomFixture { - - private final ChatRoomFixtureBuilder chatRoomFixtureBuilder; - - // 편의 메서드: 기본값으로 생성 - public ChatRoom 채팅방(boolean isGroup) { - return chatRoomFixtureBuilder.chatRoom() - .isGroup(isGroup) - .create(); - } - - public ChatRoom 멘토링_채팅방(long mentoringId) { - return chatRoomFixtureBuilder.chatRoom() - .mentoringId(mentoringId) - .isGroup(false) - .create(); - } -} -``` - -**편의 메서드 작성 팁:** - -- 한국어 메서드명 사용 (가독성) -- 자주 사용되는 기본값 조합만 제공 -- Builder를 조합하여 필요한 데이터 설정 - - -#### 테스트에서 사용 - -```java -@TestContainerSpringBootTest -class ChatServiceTest { - - @Autowired - private ChatRoomFixture chatRoomFixture; - - @Test - void 채팅방을_생성할_수_있다() { - // 편의 메서드 사용 - ChatRoom room = chatRoomFixture.채팅방(false); - - // Builder 직접 사용 - ChatRoom customRoom = chatRoomFixture.chatRoomFixtureBuilder.chatRoom() - .isGroup(true) - .mentoringId(100L) - .create(); - } -} -``` - -## 테스트 네이밍 컨벤션 - -### 테스트 메서드 네이밍 규칙 - -테스트 메서드명은 **한국어로 명확하게** 작성하며, 다음 패턴을 따릅니다: - -#### 1. 정상 동작 테스트 - -```java -// 패턴: 어떤_것을_하면_어떤_결과가_나온다 -@Test -void 채팅방이_없으면_빈_목록을_반환한다() { ... } - -@Test -void 최신_메시지_순으로_정렬되어_조회한다() { ... } - -@Test -void 참여자는_메시지를_전송할_수_있다() { ... } - -@Test -void 페이징이_정상_작동한다() { ... } -``` - -#### 2. 예외 테스트 - -```java -// 패턴: 어떤_것을_하면_예외_응답을_반환한다 -@Test -void 참여하지_않은_채팅방에_접근하면_예외_응답을_반환한다() { ... } - -@Test -void 존재하지_않는_사용자로_메시지를_전송하면_예외_응답을_반환한다() { ... } - -@Test -void 권한이_없으면_예외_응답을_반환한다() { ... } - -@Test -void 필수_파라미터가_없으면_예외_응답을_반환한다() { ... } -``` - - -### BDD 테스트 작성 - -테스트는 Given-When-Then 구조로 작성합니다. - -```java -@Test -@DisplayName("최신 메시지순으로 채팅방 목록을 조회한다") -void 최신_메시지_순으로_조회한다() { - // Given: 테스트 사전 조건 - SiteUser user = siteUserFixture.사용자(); - ChatRoom room1 = chatRoomFixture.채팅방(false); - ChatRoom room2 = chatRoomFixture.채팅방(false); - chatMessageFixture.메시지("오래된 메시지", user.getId(), room1); - chatMessageFixture.메시지("최신 메시지", user.getId(), room2); - - // When: 실제 동작 - ChatRoomListResponse response = chatService.getChatRooms(user.getId()); - - // Then: 결과 검증 - assertAll( - () -> assertThat(response.chatRooms()).hasSize(2), - () -> assertThat(response.chatRooms().get(0).id()).isEqualTo(room2.getId()) - ); -} -``` - -### 테스트 그룹화 (@Nested) - -기능별로 테스트를 그룹화합니다. - -```java -@TestContainerSpringBootTest -class ChatServiceTest { - - @Nested - @DisplayName("채팅방 목록 조회") - class 채팅방_목록을_조회한다 { - - @Test - void 빈_목록을_반환한다() { ... } - - @Test - void 최신_메시지_순으로_조회한다() { ... } - } - - @Nested - @DisplayName("채팅 메시지 전송") - class 채팅_메시지를_전송한다 { - - @BeforeEach - void setUp() { - // 이 그룹에만 적용되는 초기 설정 - } - - @Test - void 참여자는_메시지를_전송할_수_있다() { ... } - } -} -``` - -### 자주 사용하는 Assertion - -```java -// 기본 검증 -assertThat(value).isEqualTo(expected); -assertThat(value).isNotNull(); - -// 컬렉션 -assertThat(list).hasSize(3); -assertThat(list).isEmpty(); -assertThat(list).contains(item); - -// 예외 검증 -assertThatCode(() -> method()) - .isInstanceOf(CustomException.class) - .hasMessage("error message"); - -// 복수 검증 -assertAll( - () -> assertThat(a).isEqualTo(1), - () -> assertThat(b).isEqualTo(2) -); -``` +- `@TestContainerSpringBootTest` 기반 통합 테스트 +- FixtureBuilder + Fixture 패턴으로 테스트 데이터 생성 +- 한국어 메서드명, Given-When-Then 구조, @Nested 그룹화 ---