[Feat] answer + citations 응답 조합 - #77
Conversation
llmProvider/llmModelName이 각각 무엇을 저장하는 필드인지 명시한다.
void -> List<SearchResult>. 저장된 SearchResult의 id를 호출한 쪽이 받을 수 있게 해서, RagFacade가 response_citations.search_result_id를 채울 수 있게 한다 (Issue 4에서 예고한 변경).
…tions 필드 추가 SearchOutcome: SearchFacade가 SearchController에게 candidates/savedResults를 함께 넘기기 위한 내부 전달용 객체 (API로 노출되지 않음). CitationResponse: 최종 응답에 노출되는 출처 1건. RagFacade(rag 도메인)가 이미 search 도메인에 의존하고 있어, 순환 참조를 피하기 위해 이 타입을 rag가 아닌 search 도메인에 둔다. SearchResponse는 answer/citations 필드와 병합용 withAnswer()를 추가하고, 기존 results 필드는 하위 호환을 위해 그대로 유지한다.
반환 타입을 SearchResponse -> SearchOutcome으로 변경. 검색 로직 자체는 변경 없고, candidates/savedResults를 함께 실어 반환하도록 두 반환 지점만 수정한다.
검색 결과가 0건이라 LLM 호출을 생략한 경우, citation 없이 고정 안내 문구로 status=SUCCESS 기록을 남긴다. createFailed()의 고정 문구 패턴과 대칭되게 구현한다.
saveAll()에 List<SearchResult> 파라미터를 추가해 Issue 4에서 비워뒀던 search_result_id를 채운다. candidates와 searchResults는 SearchResultCommandService.saveAll()이 동일한 순서로 만든 것이라는 전제로 인덱스 기반 1:1 매핑한다.
Issue 1~4에서 만든 PromptBuilder/OllamaClient/RagResponseCommandService/ ResponseCitationCommandService를 순서대로 호출하는 조율자. SearchFacade와 별도 트랜잭션으로 분리해, Ollama HTTP 호출이 검색 DB 작업과 같은 커넥션을 오래 물고 있지 않게 한다. candidates가 비어있으면(NO_CONTEXT) LLM 호출을 생략하고, 실패 시 createFailed()로 기록 후 예외를 재전파해 GlobalExceptionHandler가 503으로 변환하게 한다.
searchFacade.search() 완료 후 ragFacade.generate()를 순차 호출하고, SearchResponse.withAnswer()로 병합해 반환한다. RAG 블록은 자기만의 엔드포인트를 갖지 않고 기존 POST /search를 확장하는 구조다.
SearchFacadeTest 3개 테스트를 SearchOutcome 접근 경로(outcome.response())로 수정하고, SearchResultCommandServiceTest에 saveAll() 반환값 검증을 추가한다.
createNoContext 테스트 추가, ResponseCitationCommandServiceTest를 변경된 saveAll() 시그니처(searchResults 인자)에 맞춰 수정하고 search_result_id가 실제로 연결되는지 검증한다. RagFacadeTest는 NO_CONTEXT/정상/실패 3가지 흐름을 검증한다.
|
Warning Review limit reached
Next review available in: 39 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthrough검색 단계가 저장된 결과와 후보를 ChangesRAG 검색 응답 통합
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant SearchController
participant SearchFacade
participant SearchResultRepository
participant RagFacade
participant OllamaClient
participant ResponseCitationCommandService
SearchController->>SearchFacade: search(userId, request)
SearchFacade->>SearchResultRepository: saveAll(search results)
SearchFacade-->>SearchController: SearchOutcome
SearchController->>RagFacade: generate(queryId, queryText, candidates, savedResults)
RagFacade->>OllamaClient: generate(prompt)
RagFacade->>ResponseCitationCommandService: saveAll(response, candidates, savedResults)
RagFacade-->>SearchController: RagAnswer
SearchController-->>SearchController: withAnswer(answer, citations)
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/test/java/com/opensource/docgrid/domain/search/service/command/SearchResultCommandServiceTest.java (1)
55-65: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winrepository의 실제 반환값 전파를 검증하도록 stub을 분리하세요.
현재
saveAllstub은 입력 리스트를 그대로 반환하므로, 서비스가 repository 결과 대신 입력 리스트를 반환해도 테스트가 통과합니다. 입력 리스트와 다른persisted리스트를 반환하도록 설정하고 동일 인스턴스인지 검증해야 변경된 계약을 제대로 보호할 수 있습니다.+ List<SearchResult> persisted = List.of(mock(SearchResult.class), mock(SearchResult.class)); - given(searchResultRepository.saveAll(any())).willAnswer(i -> i.getArgument(0)); + given(searchResultRepository.saveAll(any())).willReturn(persisted); List<SearchResult> returned = searchResultCommandService.saveAll(query, List.of(c1, c2)); + assertThat(returned).isSameAs(persisted);테스트 경로 지침의 “테스트 커버리지 ... 확인” 요구에 따른 수정입니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/opensource/docgrid/domain/search/service/command/SearchResultCommandServiceTest.java` around lines 55 - 65, SearchResultCommandServiceTest의 saveAll 테스트에서 searchResultRepository.saveAll stub이 입력 리스트와 다른 persisted 리스트를 반환하도록 분리하세요. 서비스 호출 후 returned가 persisted와 동일한 인스턴스인지 검증하고, 입력값 검증은 기존 captor 검증으로 유지해 repository 실제 반환값 전파 계약을 보호하세요.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/java/com/opensource/docgrid/domain/rag/entity/RagResponse.java`:
- Around line 60-64: RagResponse의 llmProvider 및 인접한 모델 필드 설명 주석에서 “Ollama”와
“qwen2.5:3b” 같은 특정 값을 제거하고, 각각 LLM 제공자 이름과 LLM 모델 이름이라는 역할만 설명하도록 수정하세요.
In `@src/main/java/com/opensource/docgrid/domain/rag/service/RagFacade.java`:
- Line 23: RagFacade의 클래스 Javadoc에서 내부 순번 라벨 “F-RAG-05”를 제거하고, 해당 위치에는 실제 이슈 번호나
RAG 답변 생성 흐름을 조율한다는 설명형 문구만 남기세요.
In
`@src/main/java/com/opensource/docgrid/domain/search/dto/response/CitationResponse.java`:
- Around line 7-14: CitationResponse record에 클래스 수준 주석을 추가해 역할과 책임, API 응답 경계를
설명하세요. 기존 필드와 Schema 설명은 변경하지 말고, record 선언 바로 앞에 프로젝트의 Javadoc 스타일로 작성하세요.
In
`@src/main/java/com/opensource/docgrid/domain/search/service/SearchFacade.java`:
- Around line 107-113: Update the SearchFacade flow and ResponseCitation mapping
so RAG citations retain each saved SearchResult reference after the transaction.
Pass searchResultId values from savedResults, resolve them with
EntityManager.getReference(SearchResult.class, ...) like the existing chunkId
mapping, and support nullable searchResult fields if required. Add unit and
integration coverage verifying search_result_id is persisted.
In
`@src/test/java/com/opensource/docgrid/domain/search/service/SearchFacadeTest.java`:
- Around line 76-82: Update the SearchFacadeTest success-path setup and
assertions around searchFacade.search so searchResultCommandService.saveAll
returns a concrete saved result, then assert SearchOutcome.savedResults()
contains and preserves that exact value. Keep the existing candidate and
response assertions, ensuring the test fails if saved results are omitted or
altered.
---
Outside diff comments:
In
`@src/test/java/com/opensource/docgrid/domain/search/service/command/SearchResultCommandServiceTest.java`:
- Around line 55-65: SearchResultCommandServiceTest의 saveAll 테스트에서
searchResultRepository.saveAll stub이 입력 리스트와 다른 persisted 리스트를 반환하도록 분리하세요. 서비스
호출 후 returned가 persisted와 동일한 인스턴스인지 검증하고, 입력값 검증은 기존 captor 검증으로 유지해 repository
실제 반환값 전파 계약을 보호하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 034b9ce6-ac09-48ee-9ef9-2fa536304a4b
📒 Files selected for processing (17)
docs/design/kangcheolung-#75-rag-facade-integration.mdsrc/main/java/com/opensource/docgrid/domain/rag/dto/RagAnswer.javasrc/main/java/com/opensource/docgrid/domain/rag/entity/RagResponse.javasrc/main/java/com/opensource/docgrid/domain/rag/service/RagFacade.javasrc/main/java/com/opensource/docgrid/domain/rag/service/command/RagResponseCommandService.javasrc/main/java/com/opensource/docgrid/domain/rag/service/command/ResponseCitationCommandService.javasrc/main/java/com/opensource/docgrid/domain/search/controller/SearchController.javasrc/main/java/com/opensource/docgrid/domain/search/dto/SearchOutcome.javasrc/main/java/com/opensource/docgrid/domain/search/dto/response/CitationResponse.javasrc/main/java/com/opensource/docgrid/domain/search/dto/response/SearchResponse.javasrc/main/java/com/opensource/docgrid/domain/search/service/SearchFacade.javasrc/main/java/com/opensource/docgrid/domain/search/service/command/SearchResultCommandService.javasrc/test/java/com/opensource/docgrid/domain/rag/service/RagFacadeTest.javasrc/test/java/com/opensource/docgrid/domain/rag/service/command/RagResponseCommandServiceTest.javasrc/test/java/com/opensource/docgrid/domain/rag/service/command/ResponseCitationCommandServiceTest.javasrc/test/java/com/opensource/docgrid/domain/search/service/SearchFacadeTest.javasrc/test/java/com/opensource/docgrid/domain/search/service/command/SearchResultCommandServiceTest.java
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| /** | ||
| * RAG 답변 생성 전체 흐름을 조율하는 Facade (F-RAG-05). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
내부 번호 라벨을 설명형 문구로 바꾸세요.
F-RAG-05 형식의 내부 순번 라벨이 코드 Javadoc에 노출됩니다. 실제 이슈 번호 또는 기능 설명만 남기세요.
수정 예시
- * RAG 답변 생성 전체 흐름을 조율하는 Facade (F-RAG-05).
+ * RAG 답변 생성·저장 전체 흐름을 조율하는 Facade.As per coding guidelines, “Never expose private numbered PR sequence labels in … code, or comments.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| * RAG 답변 생성 전체 흐름을 조율하는 Facade (F-RAG-05). | |
| * RAG 답변 생성·저장 전체 흐름을 조율하는 Facade. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/opensource/docgrid/domain/rag/service/RagFacade.java` at
line 23, RagFacade의 클래스 Javadoc에서 내부 순번 라벨 “F-RAG-05”를 제거하고, 해당 위치에는 실제 이슈 번호나
RAG 답변 생성 흐름을 조율한다는 설명형 문구만 남기세요.
Source: Coding guidelines
| public record CitationResponse( | ||
| @Schema(description = "인용 라벨") String label, | ||
| @Schema(description = "출처 문서 ID") Long documentId, | ||
| @Schema(description = "출처 문서 제목") String documentTitle, | ||
| @Schema(description = "근거 chunk ID") Long chunkId, | ||
| @Schema(description = "원본 문서 페이지 번호, 페이지 개념이 없는 형식은 null") Integer pageNo, | ||
| @Schema(description = "인용된 텍스트") String quotedText | ||
| ) { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
신규 DTO에 클래스 수준 주석을 추가해 주세요.
CitationResponse는 새로 생성된 record지만 역할, 책임, API 경계를 설명하는 class-level comment가 없습니다.
제안
+/**
+ * RAG 답변에 포함되는 검색 출처 1건을 표현하는 읽기 전용 응답 DTO.
+ *
+ * <p>검색 후보의 문서·청크 메타데이터를 API 응답 형태로 변환한다.
+ */
public record CitationResponse(코딩 가이드라인의 “새로 생성된 class, interface, record에는 역할·책임·경계를 설명하는 class-level comment가 있어야 한다” 규칙에 따른 수정입니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public record CitationResponse( | |
| @Schema(description = "인용 라벨") String label, | |
| @Schema(description = "출처 문서 ID") Long documentId, | |
| @Schema(description = "출처 문서 제목") String documentTitle, | |
| @Schema(description = "근거 chunk ID") Long chunkId, | |
| @Schema(description = "원본 문서 페이지 번호, 페이지 개념이 없는 형식은 null") Integer pageNo, | |
| @Schema(description = "인용된 텍스트") String quotedText | |
| ) { | |
| /** | |
| * RAG 답변에 포함되는 검색 출처 1건을 표현하는 읽기 전용 응답 DTO. | |
| * | |
| * <p>검색 후보의 문서·청크 메타데이터를 API 응답 형태로 변환한다. | |
| */ | |
| public record CitationResponse( | |
| `@Schema`(description = "인용 라벨") String label, | |
| `@Schema`(description = "출처 문서 ID") Long documentId, | |
| `@Schema`(description = "출처 문서 제목") String documentTitle, | |
| `@Schema`(description = "근거 chunk ID") Long chunkId, | |
| `@Schema`(description = "원본 문서 페이지 번호, 페이지 개념이 없는 형식은 null") Integer pageNo, | |
| `@Schema`(description = "인용된 텍스트") String quotedText | |
| ) { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/opensource/docgrid/domain/search/dto/response/CitationResponse.java`
around lines 7 - 14, CitationResponse record에 클래스 수준 주석을 추가해 역할과 책임, API 응답 경계를
설명하세요. 기존 필드와 Schema 설명은 변경하지 말고, record 선언 바로 앞에 프로젝트의 Javadoc 스타일로 작성하세요.
Source: Coding guidelines
| List<SearchResult> savedResults = searchResultCommandService.saveAll(searchQuery, verified); | ||
|
|
||
| int latency = latencyMs(start); | ||
| searchQueryCommandService.markSuccess(searchQuery, latency); | ||
| log.info("[SEARCH] done queryId={} results={} latencyMs={}", searchQuery.getId(), verified.size(), latency); | ||
|
|
||
| return SearchResponse.of(searchQuery.getId(), verified); | ||
| return new SearchOutcome(SearchResponse.of(searchQuery.getId(), verified), verified, savedResults); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n -C 6 'class ResponseCitation|searchResult|`@ManyToOne`|cascade' src/main/javaRepository: DocGrid/backend
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
git ls-files | rg 'SearchOutcome|SearchQuery\.java|SearchResultCommandService\.java|ResponseCitationCommandService\.java|RagFacade\.java|ResponseCitation\.java|SearchResult\.java|SearchQueryRepository\.java|SearchResultRepository\.java|ResponseCitationRepository\.java' || true
echo
echo "== SearchOutcome =="
rg -n -C 8 'record SearchOutcome|class SearchOutcome|SearchOutcome' src/main/java || true
echo
echo "== SearchQueryCommandService =="
fd -a 'SearchQueryCommandService\.java$' . | while read -r f; do
echo "--- ${f#$(pwd)/} ---"
wc -l "$f"
sed -n '1,180p' "$f"
done
echo
echo "== ResponseCitationCommandService =="
fd -a 'ResponseCitationCommandService\.java$' . | while read -r f; do
echo "--- ${f#$(pwd)/} ---"
wc -l "$f"
sed -n '1,80p' "$f"
done
echo
echo "== ResponseCitation entity/repository =="
fd -a 'ResponseCitation.*\.java$' . | while read -r f; do
if echo "$f" | rg '(entity|repository|Repository)' >/dev/null; then
echo "--- ${f#$(pwd)/} ---"
wc -l "$f"
sed -n '1,180p' "$f"
fi
done
echo
echo "== search result repositories =="
fd -a 'SearchQueryRepository\.java$|SearchResultRepository\.java$' . | while read -r f; do
echo "--- ${f#$(pwd)/} ---"
wc -l "$f"
sed -n '1,220p' "$f"
done
echo
echo "== SearchQuery entity relevant mapping =="
fd -a 'SearchQuery\.java$' . | while read -r f; do
echo "--- ${f#$(pwd)/} ---"
sed -n '1,180p' "$f"
doneRepository: DocGrid/backend
Length of output: 31127
🏁 Script executed:
rg -n -C 6 'class ResponseCitation|searchResult|`@ManyToOne`|cascade' src/main/javaRepository: DocGrid/backend
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ResponseCitation searchResult builder/setter excerpts =="
rg -n -C 4 'searchResult|search_result_id' src/main/java/com/opensource/docgrid/domain/rag/entity src/main/java/com/opensource/docgrid/domain/rag/service
echo
echo "== ResponseCitation full builder annotation block =="
sed -n '90,180p' src/main/java/com/opensource/docgrid/domain/rag/entity/ResponseCitation.java
echo
echo "== Search facade transaction outline =="
sed -n '1,28p' src/main/java/com/opensource/docgrid/domain/search/service/SearchFacade.java
sed -n '35,120p' src/main/java/com/opensource/docgrid/domain/search/service/SearchFacade.java
echo
echo "== RAG facade relevant transaction outline =="
sed -n '1,75p' src/main/java/com/opensource/docgrid/domain/rag/service/RagFacade.java
echo
echo "== all `@Transactional` declarations around Search/RAG controllers/services =="
rg -n -C 3 '`@Transactional`' src/main/java/com/opensource/docgrid/domain/search src/main/java/com/opensource/docgrid/domain/ragRepository: DocGrid/backend
Length of output: 29961
RAG 트랜잭션에서 SearchResult를 참조할 수 있게 해 주세요.
savedResults는 검색 트랜잭션 종료가후 detached 상태라서 ResponseCitation.searchResult(build)로 직접 연결하면 search_result_id가 저장에서 누락됩니다. 기존 chunkId처럼 searchResultId만 넘겨 entityManager.getReference(SearchResult.class, ...)로 연결하거나, searchResult 필드를 nullable로 두고 ID 기반 매핑을 적용해 주세요. 함께 save_result_id 저장을 단가/통합 테스트로 검증해 주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/opensource/docgrid/domain/search/service/SearchFacade.java`
around lines 107 - 113, Update the SearchFacade flow and ResponseCitation
mapping so RAG citations retain each saved SearchResult reference after the
transaction. Pass searchResultId values from savedResults, resolve them with
EntityManager.getReference(SearchResult.class, ...) like the existing chunkId
mapping, and support nullable searchResult fields if required. Add unit and
integration coverage verifying search_result_id is persisted.
ResponseCitationCommandService: SearchFacade 트랜잭션에서 넘어온(detached) SearchResult 엔티티를 그대로 FK에 대입하던 것을, chunk 필드와 동일하게 entityManager.getReference(SearchResult.class, id)로 통일한다. RagResponse: llmModelName은 하드코딩이 아니라 OllamaGenerateResult.model()에서 동적으로 채워지는 값이라, 필드 주석에서 특정 모델명 예시를 제거한다.
ResponseCitationCommandServiceTest: getReference(SearchResult.class, id) 방식으로 바뀐 구현에 맞춰 mocking을 수정한다. SearchFacadeTest: saveAll()이 빈 리스트를 반환하도록 stub되어 있어 SearchOutcome.savedResults() 전달 여부를 검증하지 못하던 것을 보강한다.
CodeRabbit 코멘트 5건의 처리 내역과, entityManager.getReference()가 성능 목적/트랜잭션 안전성 목적 두 가지로 쓰이는 이유를 정리한다.
🔍️ 작업 내용
✨ 상세 설명
RAG 블록(F-RAG-01
05) 마지막 이슈입니다. Issue 14에서 만든 PromptBuilder/OllamaClient/RagResponseCommandService/ResponseCitationCommandService를 RagFacade로 묶어 기존 POST /search에 연결하고, 최종 응답에 answer/citations를 포함시킵니다.RagFacade신설 — SearchFacade와 별도 트랜잭션으로 분리 (LLM HTTP 호출이 검색 DB 작업과 같은 커넥션을 오래 물고 있지 않도록)SearchController에서searchFacade.search()커밋 후ragFacade.generate()순차 호출,SearchResponse.withAnswer()로 병합RagResponseCommandService.createNoContext()— status=SUCCESS, 고정 문구, LLM 호출 생략, citations 빈 배열SearchResponse에answer/citations필드 추가 (기존results필드는 하위 호환을 위해 유지)SearchResultCommandService.saveAll()을List<SearchResult>반환으로 변경하고,ResponseCitationCommandService.saveAll()에 전달해response_citations.search_result_id를 채움 (Issue 4에서 미룬 부분)SearchOutcome(search 도메인),RagAnswer(rag 도메인) — 각 Facade가 Controller에게 넘기는 내부 전달용 DTOCitationResponse는 rag가 아닌 search 도메인에 배치 — RagFacade가 이미 search 도메인에 의존하고 있어 순환 참조를 피하기 위함상세 설계 배경은
docs/design/kangcheolung-#75-rag-facade-integration.md참고해주세요.🛠️ 추후 리팩토링 및 고도화 계획
📸 스크린샷 (선택)
N/A
💬 리뷰 요구사항
Summary by CodeRabbit
새로운 기능
오류 처리
테스트