Conversation
Summary by CodeRabbit릴리스 노트
워크스루광범위한 패키지 재구조화와 서비스 분해 리팩토링으로, 변경사항
예상 코드 리뷰 노력🎯 4 (Complex) | ⏱️ ~45분 관련 PR
추천 리뷰어
축하 시
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 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 |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces significant architectural improvements to the Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Code refactored, clean and bright, Old facade gone, new services take flight. Domain and tool, now clearly defined, A better structure, for all to find. Footnotes
|
Test Results 48 files 48 suites 1m 14s ⏱️ Results for commit 3ae2032. |
There was a problem hiding this comment.
Code Review
이번 PR은 코드 구조를 개선하기 위한 대규모 리팩토링을 포함하고 있습니다. 특히 interview 서비스가 도메인 중심의 하위 패키지(core, question, resume, social, infra)로 잘 분리되었고, CQS 원칙에 따라 InterviewStartFacadeService, InterviewProceedFacadeService, InterviewQueryService로 역할이 명확하게 나뉜 점이 인상적입니다. 전반적으로 코드의 구조가 명확해지고 유지보수성이 향상되었습니다.
다만, 리팩토링 과정에서 발생한 것으로 보이는 심각한 버그를 하나 발견했습니다. 비동기 처리와 AOP를 함께 사용하면서 발생한 레이스 컨디션으로 인해 메트릭이 올바르게 집계되지 않을 수 있습니다. 또한, InterviewQueryService의 이름과 실제 역할이 완전히 일치하지 않는 점에 대한 개선 제안을 포함했습니다. 자세한 내용은 각 파일의 리뷰 코멘트를 참고해주세요.
Note: Security Review did not run due to the size of the PR.
|
|
||
| @RequiredArgsConstructor | ||
| @Service | ||
| public class InterviewQueryService { |
There was a problem hiding this comment.
서비스 이름이 InterviewQueryService이지만, 상태를 변경하는 likeInterview, unlikeInterview와 같은 커맨드(command) 성격의 메서드를 포함하고 있습니다. 이는 커맨드와 쿼리를 분리하는 CQS(Command-Query Separation) 원칙에 위배될 수 있어 코드 구조의 명확성을 해칠 수 있습니다.
이 서비스의 책임을 명확히 하기 위해 다음 두 가지 방법 중 하나를 고려해볼 수 있습니다:
- 서비스 이름을
InterviewSocialFacadeService또는InterviewPublicFacadeService와 같이 읽기 및 쓰기 작업을 모두 포함하는 역할이 드러나도록 변경합니다. likeInterview,unlikeInterview와 같은 쓰기 작업을 별도의 서비스(예:InterviewSocialCommandService)로 분리하고,InterviewQueryService는 순수 읽기 전용으로 유지합니다.
코드 구조 개선의 일환으로 서비스의 역할을 이름에 명확하게 반영하는 것이 유지보수성에 도움이 될 것입니다.
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/main/java/com/samhap/kokomen/interview/tool/GptSystemMessageConstant.java (2)
33-33:⚠️ Potential issue | 🟡 Minor랭크 기준 문구에 불필요한
\s텍스트가 포함되어 있습니다.Line [33], Line [71]의
0점\s는 의도된 설명이 아니라면 프롬프트 노이즈입니다.0점으로 정리해 주세요.수정 예시
- - F : 0점\s + - F : 0점Also applies to: 71-71
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/samhap/kokomen/interview/tool/GptSystemMessageConstant.java` at line 33, In GptSystemMessageConstant, remove the stray backslash-s from the rank description strings that currently contain "0점\s" (found where the class defines the rank criteria strings, including the occurrences at lines shown) and replace them with "0점"; update all occurrences (e.g., the two places that currently read "0점\s") so the prompt text contains plain "0점" without the extraneous "\s".
77-77:⚠️ Potential issue | 🟠 MajorEND 프롬프트의 출력 필드 지시가 서로 충돌합니다.
Line [77]에서
rank, feedback, total_feedback를 요구한 뒤 “다음 질문” 존댓말 조건이 남아 있어, 모델이next_question을 추가 생성할 여지가 있습니다. 응답 스키마 계약(JSON 파싱) 안정성을 위해 문구를total_feedback기준으로 정정해 주세요.수정 예시
- - 응답 포맷 : JSON으로 응답해줘. 필드는 위에서 설명한대로 rank, feedback, total_feedback 이야. ObjectMapper로 파싱해야 하니까 ` 같은 건 붙이지 말아줘. 이때 피드백과 다음 질문은 존댓말로 응답해야해. + - 응답 포맷 : JSON으로 응답해줘. 필드는 위에서 설명한대로 rank, feedback, total_feedback 이야. ObjectMapper로 파싱해야 하니까 ` 같은 건 붙이지 말아줘. 이때 피드백은 존댓말로 응답해야해.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/samhap/kokomen/interview/tool/GptSystemMessageConstant.java` at line 77, Update the END prompt string in GptSystemMessageConstant so the JSON output contract ends explicitly at the total_feedback field and remove any language that could prompt generation of a "next_question" field; ensure the instruction states to return only the JSON object with fields "rank", "feedback", and "total_feedback" (parsed by ObjectMapper) and that only the feedback field must be in 존댓말 (polite form), avoiding any trailing clause that asks for a next question or additional text outside the JSON.src/main/java/com/samhap/kokomen/interview/tool/QuestionAndAnswers.java (1)
22-22: 🧹 Nitpick | 🔵 TrivialTODO 주석 확인
힙 메모리 관련 실험 TODO가 남아있습니다. 이 PR 범위는 아니지만, 추후 확인이 필요합니다.
이 TODO를 추적하기 위한 이슈를 생성해 드릴까요?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/samhap/kokomen/interview/tool/QuestionAndAnswers.java` at line 22, A leftover TODO comment in the QuestionAndAnswers class mentions an experimental task about constructor exceptions and heap retention; remove the TODO from the constructor (or replace it with a concise // NOTE linking to a newly created issue ID) and create a tracker issue describing the experiment to investigate whether an object remains on the heap if its constructor throws; ensure the comment references the issue number and the unique symbol QuestionAndAnswers (constructor) so the intent is preserved without cluttering the code.src/main/java/com/samhap/kokomen/interview/external/dto/request/InterviewInvokeFlowRequestFactory.java (1)
15-15:⚠️ Potential issue | 🟡 Minor오타 수정 필요:
IN_PORGRESS→IN_PROGRESS상수명에 오타가 있습니다. 일관성을 위해 수정이 필요합니다.
✏️ 제안된 수정
- private static final String IN_PORGRESS_INTERVIEW_PROCEED_FLOW_ALIAS_ID = "BUY0SE9CTX"; + private static final String IN_PROGRESS_INTERVIEW_PROCEED_FLOW_ALIAS_ID = "BUY0SE9CTX";Line 43도 함께 수정해야 합니다:
- .flowAliasIdentifier(IN_PORGRESS_INTERVIEW_PROCEED_FLOW_ALIAS_ID) + .flowAliasIdentifier(IN_PROGRESS_INTERVIEW_PROCEED_FLOW_ALIAS_ID)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/samhap/kokomen/interview/external/dto/request/InterviewInvokeFlowRequestFactory.java` at line 15, There is a typo in the constant name IN_PORGRESS_INTERVIEW_PROCEED_FLOW_ALIAS_ID in InterviewInvokeFlowRequestFactory; rename it to IN_PROGRESS_INTERVIEW_PROCEED_FLOW_ALIAS_ID and update every usage/occurrence (including the other occurrence referenced in the file) to the corrected identifier so all references compile and remain consistent.src/main/java/com/samhap/kokomen/interview/service/dto/proceedstate/InterviewProceedStateVoiceModeResponse.java (1)
31-34: 🧹 Nitpick | 🔵 Trivial
proceedState()메서드 중복 제거 고려record는 기본적으로 컴포넌트에 대한 접근자 메서드를 생성합니다. 인터페이스의
proceedState()메서드와 시그니처가 동일하므로 이 오버라이드는 제거 가능합니다.♻️ 제안된 수정
- `@Override` - public InterviewProceedState proceedState() { - return proceedState; - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/samhap/kokomen/interview/service/dto/proceedstate/InterviewProceedStateVoiceModeResponse.java` around lines 31 - 34, The explicit override of proceedState() in the InterviewProceedStateVoiceModeResponse record is redundant because Java records already generate an accessor with the same name; remove the proceedState() method from the InterviewProceedStateVoiceModeResponse record so the synthesized accessor is used and the class still implements the interface's proceedState() contract.src/main/java/com/samhap/kokomen/global/aop/RootQuestionMetricAspect.java (1)
55-57:⚠️ Potential issue | 🟡 Minor신규 비동기 메트릭 경로의 테스트 TODO는 머지 전에 해소가 필요합니다.
현재 TODO 상태면 메트릭 회귀를 놓치기 쉽습니다. 최소 종료 카운트/답변 랭크 카운트 각각 1개 이상 검증 테스트를 추가해 주세요.
원하시면
InterviewProceedBedrockFlowAsyncService경유 시나리오 기준으로 테스트 뼈대를 바로 작성해드리겠습니다.Also applies to: 74-77
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/samhap/kokomen/global/aop/RootQuestionMetricAspect.java` around lines 55 - 57, Add tests that assert the new asynchronous metric paths in RootQuestionMetricAspect are covered: create tests that exercise increaseRootQuestionInterviewEndCountByAsyncProceed and the corresponding async handler around lines 74-77 by invoking the async flow through InterviewProceedBedrockFlowAsyncService, then verify that the "interview end" metric and the "answer rank" metric each increase by at least 1; specifically, call the async service scenario, await completion (or flush the async executor), and assert metric counters updated for the root question IDs observed by RootQuestionMetricAspect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLAUDE.md`:
- Line 92: Several Markdown headings (e.g., the "Cross-Cutting Patterns" heading
and the other headings referenced at lines 99, 132, 141, 148) are missing
required blank lines around them causing MD022 lint warnings; edit those
headings to ensure there is one blank line both immediately before and
immediately after each heading (i.e., insert a blank line above and below
"Cross-Cutting Patterns" and the other referenced section headings) so the file
conforms to the MD022 rule.
- Around line 46-47: 해당 섹션의 마크다운은 헤딩 주변 공백(MD022/MD031)과 코드펜스 언어 태그(MD040) 규칙을
위반하니, 각 헤딩(예: "### Domain Packages", "### Domain Package Convention", "###
Interview Service Sub-Packages") 앞뒤에 빈 줄을 추가하고 모든 코드 펜스 블록(``` ... ```)에 언어 태그를
명시(예: ```text)하며 펜스 전후에도 빈 줄을 넣어 문서 린트 규칙을 충족하도록 수정하세요.
- Around line 149-152: Clarify the apparent conflict by explicitly stating the
exception: update the README bullets so BaseTest remains the default (real MySQL
container + MySQLDatabaseCleaner) and DocsTest is an intentional exception that
uses H2 for lightweight REST Docs generation only; mention both class names
(BaseTest and DocsTest) and add a short sentence like "Exception: DocsTest uses
H2 in-memory DB solely for lightweight REST Docs generation and is excluded from
the 'use real MySQL for tests' rule."
In
`@src/main/java/com/samhap/kokomen/interview/service/infra/InterviewProceedBedrockFlowAsyncService.java`:
- Around line 68-70: The infra class InterviewProceedBedrockFlowAsyncService is
calling facade static helpers
InterviewProceedFacadeService.createInterviewProceedLockKey and
createInterviewProceedStateKey, which inverts layer dependencies; extract the
key-generation logic into a new shared utility (e.g., InterviewProceedKeyUtil or
InterviewProceedKeys) in a common module/package accessible to both service and
infra, move the implementations there, update
InterviewProceedBedrockFlowAsyncService to call
InterviewProceedKeyUtil.createInterviewProceedLockKey and
InterviewProceedKeyUtil.createInterviewProceedStateKey, and refactor the other
occurrences (the similar calls around the 83-85 region) to use the same utility
so both service and infra depend on the shared util instead of the facade.
In
`@src/main/java/com/samhap/kokomen/interview/service/InterviewProceedFacadeService.java`:
- Around line 63-70: The error logs in InterviewProceedFacadeService are using
formatted placeholders with the exception (e and ex) which omits stacktraces;
update the two catch blocks so log.error is called with the throwable as the
last argument (e.g., log.error("Bedrock API 호출 실패, GPT 폴백에시 기록", e) and
log.error("Gpt API 호출 실패", ex)), leaving the descriptive message string only and
passing the exception object as the final parameter; keep the surrounding calls
(interviewProceedBedrockFlowAsyncService.proceedInterviewByGptFlowAsync and
redisService.releaseLockSafely) unchanged.
In
`@src/main/java/com/samhap/kokomen/interview/service/InterviewQueryService.java`:
- Around line 72-75: Rename the method getRootQuestionsByCategory to
findRootQuestionsByCategory in InterviewQueryService and update all callers
accordingly; keep the return type List<RootQuestionResponse> and the body that
maps
rootQuestionService.findAllRootQuestionByCategory(category).stream().map(RootQuestionResponse::from).toList(),
and update any tests, interfaces, or classes that reference
getRootQuestionsByCategory to the new name to preserve behavior and compilation.
In
`@src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.java`:
- Around line 106-109: The call to questionService.createAndUploadQuestionVoice
is performing external I/O inside the transactional flow in
InterviewStartFacadeService; remove that synchronous call from the transaction
and instead trigger voice generation after the interview/question transaction
commits (e.g., publish an event or register an after-commit callback) so the DB
commit completes quickly and I/O happens outside the transaction; update the
method that currently returns InterviewStartVoiceModeResponse to either return a
response without voiceUrl immediately (or a placeholder/processing state) and
implement a TransactionSynchronization/TransactionalEventListener handler or
async service that invokes questionService.createAndUploadQuestionVoice and then
updates the question/voiceUrl or notifies the client when complete.
In `@src/main/java/com/samhap/kokomen/token/service/TokenFacadeService.java`:
- Around line 160-164: The grantPurchasedTokens method in TokenFacadeService
updates a member's token balance but lacks the member-level distributed lock
used by other write operations; wrap this method with the same `@DistributedLock`
(or equivalent) annotation/locking strategy applied to other write methods in
the class so concurrent webhook or token operations for the same member
serialize; specifically, add the member-scoped lock annotation to
grantPurchasedTokens (which calls tokenPurchaseService.saveTokenPurchase and
tokenService.addPaidTokens) using the same lock key expression/strategy used
elsewhere in the class to lock on tokenPurchase.getMemberId() (or the memberId
parameter).
In
`@src/test/java/com/samhap/kokomen/interview/service/InterviewQueryServiceTest.java`:
- Around line 49-64: In InterviewQueryServiceTest remove the `@Disabled`
annotation from the test method 이미_좋아요를_누른_인터뷰에_좋아요를_요청하면_예외가_발생한다 so the
duplicate-like validation is exercised in CI; ensure the test still sets up a
Member, RootQuestion and Interview, invokes
interviewQueryService.likeInterview(interview.getId(), new
MemberAuth(member.getId())) once to create the like and then asserts the second
call to interviewQueryService.likeInterview(...) throws BadRequestException with
the expected message, leaving the rest of the method unchanged.
---
Outside diff comments:
In `@src/main/java/com/samhap/kokomen/global/aop/RootQuestionMetricAspect.java`:
- Around line 55-57: Add tests that assert the new asynchronous metric paths in
RootQuestionMetricAspect are covered: create tests that exercise
increaseRootQuestionInterviewEndCountByAsyncProceed and the corresponding async
handler around lines 74-77 by invoking the async flow through
InterviewProceedBedrockFlowAsyncService, then verify that the "interview end"
metric and the "answer rank" metric each increase by at least 1; specifically,
call the async service scenario, await completion (or flush the async executor),
and assert metric counters updated for the root question IDs observed by
RootQuestionMetricAspect.
In
`@src/main/java/com/samhap/kokomen/interview/external/dto/request/InterviewInvokeFlowRequestFactory.java`:
- Line 15: There is a typo in the constant name
IN_PORGRESS_INTERVIEW_PROCEED_FLOW_ALIAS_ID in
InterviewInvokeFlowRequestFactory; rename it to
IN_PROGRESS_INTERVIEW_PROCEED_FLOW_ALIAS_ID and update every usage/occurrence
(including the other occurrence referenced in the file) to the corrected
identifier so all references compile and remain consistent.
In
`@src/main/java/com/samhap/kokomen/interview/service/dto/proceedstate/InterviewProceedStateVoiceModeResponse.java`:
- Around line 31-34: The explicit override of proceedState() in the
InterviewProceedStateVoiceModeResponse record is redundant because Java records
already generate an accessor with the same name; remove the proceedState()
method from the InterviewProceedStateVoiceModeResponse record so the synthesized
accessor is used and the class still implements the interface's proceedState()
contract.
In
`@src/main/java/com/samhap/kokomen/interview/tool/GptSystemMessageConstant.java`:
- Line 33: In GptSystemMessageConstant, remove the stray backslash-s from the
rank description strings that currently contain "0점\s" (found where the class
defines the rank criteria strings, including the occurrences at lines shown) and
replace them with "0점"; update all occurrences (e.g., the two places that
currently read "0점\s") so the prompt text contains plain "0점" without the
extraneous "\s".
- Line 77: Update the END prompt string in GptSystemMessageConstant so the JSON
output contract ends explicitly at the total_feedback field and remove any
language that could prompt generation of a "next_question" field; ensure the
instruction states to return only the JSON object with fields "rank",
"feedback", and "total_feedback" (parsed by ObjectMapper) and that only the
feedback field must be in 존댓말 (polite form), avoiding any trailing clause that
asks for a next question or additional text outside the JSON.
In `@src/main/java/com/samhap/kokomen/interview/tool/QuestionAndAnswers.java`:
- Line 22: A leftover TODO comment in the QuestionAndAnswers class mentions an
experimental task about constructor exceptions and heap retention; remove the
TODO from the constructor (or replace it with a concise // NOTE linking to a
newly created issue ID) and create a tracker issue describing the experiment to
investigate whether an object remains on the heap if its constructor throws;
ensure the comment references the issue number and the unique symbol
QuestionAndAnswers (constructor) so the intent is preserved without cluttering
the code.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (114)
CLAUDE.mdsrc/main/java/com/samhap/kokomen/admin/service/AdminService.javasrc/main/java/com/samhap/kokomen/answer/domain/Answer.javasrc/main/java/com/samhap/kokomen/answer/repository/AnswerMemoRepository.javasrc/main/java/com/samhap/kokomen/answer/repository/AnswerRepository.javasrc/main/java/com/samhap/kokomen/answer/service/AnswerService.javasrc/main/java/com/samhap/kokomen/global/TestDataInitializer.javasrc/main/java/com/samhap/kokomen/global/aop/RootQuestionMetricAspect.javasrc/main/java/com/samhap/kokomen/interview/controller/InterviewController.javasrc/main/java/com/samhap/kokomen/interview/controller/InterviewControllerV2.javasrc/main/java/com/samhap/kokomen/interview/controller/InterviewControllerV3.javasrc/main/java/com/samhap/kokomen/interview/controller/ResumeBasedInterviewController.javasrc/main/java/com/samhap/kokomen/interview/domain/GeneratedQuestion.javasrc/main/java/com/samhap/kokomen/interview/domain/Interview.javasrc/main/java/com/samhap/kokomen/interview/domain/InterviewLike.javasrc/main/java/com/samhap/kokomen/interview/domain/InterviewMode.javasrc/main/java/com/samhap/kokomen/interview/domain/InterviewState.javasrc/main/java/com/samhap/kokomen/interview/domain/InterviewType.javasrc/main/java/com/samhap/kokomen/interview/domain/Question.javasrc/main/java/com/samhap/kokomen/interview/domain/ResumeQuestionGeneration.javasrc/main/java/com/samhap/kokomen/interview/domain/ResumeQuestionGenerationState.javasrc/main/java/com/samhap/kokomen/interview/domain/RootQuestion.javasrc/main/java/com/samhap/kokomen/interview/domain/RootQuestionState.javasrc/main/java/com/samhap/kokomen/interview/external/InterviewProceedGptClient.javasrc/main/java/com/samhap/kokomen/interview/external/dto/request/InterviewInvokeFlowRequestFactory.javasrc/main/java/com/samhap/kokomen/interview/external/dto/response/InterviewSummaryResponses.javasrc/main/java/com/samhap/kokomen/interview/repository/GeneratedQuestionRepository.javasrc/main/java/com/samhap/kokomen/interview/repository/InterviewLikeRepository.javasrc/main/java/com/samhap/kokomen/interview/repository/InterviewRepository.javasrc/main/java/com/samhap/kokomen/interview/repository/QuestionRepository.javasrc/main/java/com/samhap/kokomen/interview/repository/ResumeQuestionGenerationRepository.javasrc/main/java/com/samhap/kokomen/interview/repository/RootQuestionRepository.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewFacadeService.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewProceedFacadeService.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewQueryService.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.javasrc/main/java/com/samhap/kokomen/interview/service/core/InterviewProceedService.javasrc/main/java/com/samhap/kokomen/interview/service/core/InterviewService.javasrc/main/java/com/samhap/kokomen/interview/service/dto/AnswerRequestV2.javasrc/main/java/com/samhap/kokomen/interview/service/dto/InterviewProceedResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/InterviewRequest.javasrc/main/java/com/samhap/kokomen/interview/service/dto/InterviewResultResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/InterviewSummaryResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/RootQuestionCustomInterviewRequest.javasrc/main/java/com/samhap/kokomen/interview/service/dto/RootQuestionResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/check/InterviewCheckResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/check/InterviewCheckTextModeResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/check/InterviewCheckVoiceModeResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/check/InterviewFinishedCheckResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/proceedstate/InterviewProceedStateResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/proceedstate/InterviewProceedStateTextModeResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/proceedstate/InterviewProceedStateVoiceModeResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/resumebased/GeneratedQuestionsResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/resumebased/QuestionGenerationStateResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/resumebased/ResumeBasedInterviewStartRequest.javasrc/main/java/com/samhap/kokomen/interview/service/dto/resumebased/ResumeQuestionGenerationResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/start/InterviewStartTextModeResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/start/InterviewStartVoiceModeResponse.javasrc/main/java/com/samhap/kokomen/interview/service/infra/InterviewProceedBedrockFlowAsyncService.javasrc/main/java/com/samhap/kokomen/interview/service/infra/InterviewSchedulerService.javasrc/main/java/com/samhap/kokomen/interview/service/question/QuestionGenerationAsyncService.javasrc/main/java/com/samhap/kokomen/interview/service/question/QuestionGenerationStateService.javasrc/main/java/com/samhap/kokomen/interview/service/question/QuestionService.javasrc/main/java/com/samhap/kokomen/interview/service/question/RootQuestionService.javasrc/main/java/com/samhap/kokomen/interview/service/resume/ResumeBasedInterviewService.javasrc/main/java/com/samhap/kokomen/interview/service/resume/ResumeContentService.javasrc/main/java/com/samhap/kokomen/interview/service/social/InterviewLikeService.javasrc/main/java/com/samhap/kokomen/interview/service/social/InterviewViewCountService.javasrc/main/java/com/samhap/kokomen/interview/tool/GptSystemMessageConstant.javasrc/main/java/com/samhap/kokomen/interview/tool/InterviewMessagesFactory.javasrc/main/java/com/samhap/kokomen/interview/tool/InterviewProceedResult.javasrc/main/java/com/samhap/kokomen/interview/tool/InterviewProceedState.javasrc/main/java/com/samhap/kokomen/interview/tool/QuestionAndAnswers.javasrc/main/java/com/samhap/kokomen/interview/tool/QuestionVoicePathResolver.javasrc/main/java/com/samhap/kokomen/member/service/MemberService.javasrc/main/java/com/samhap/kokomen/payment/service/WebhookService.javasrc/main/java/com/samhap/kokomen/token/service/TokenFacadeService.javasrc/test/java/com/samhap/kokomen/admin/controller/AdminControllerTest.javasrc/test/java/com/samhap/kokomen/answer/controller/AnswerControllerTest.javasrc/test/java/com/samhap/kokomen/answer/repository/AnswerLikeRepositoryTest.javasrc/test/java/com/samhap/kokomen/answer/repository/AnswerMemoRepositoryTest.javasrc/test/java/com/samhap/kokomen/answer/service/AnswerFacadeServiceTest.javasrc/test/java/com/samhap/kokomen/global/BaseTest.javasrc/test/java/com/samhap/kokomen/global/fixture/answer/AnswerFixtureBuilder.javasrc/test/java/com/samhap/kokomen/global/fixture/interview/InterviewFixtureBuilder.javasrc/test/java/com/samhap/kokomen/global/fixture/interview/InterviewLikeFixtureBuilder.javasrc/test/java/com/samhap/kokomen/global/fixture/interview/QuestionFixtureBuilder.javasrc/test/java/com/samhap/kokomen/global/fixture/interview/ResumeQuestionGenerationFixtureBuilder.javasrc/test/java/com/samhap/kokomen/global/fixture/interview/RootQuestionFixtureBuilder.javasrc/test/java/com/samhap/kokomen/interview/controller/InterviewControllerTest.javasrc/test/java/com/samhap/kokomen/interview/controller/InterviewControllerV2Test.javasrc/test/java/com/samhap/kokomen/interview/controller/InterviewControllerV3Test.javasrc/test/java/com/samhap/kokomen/interview/controller/ResumeBasedInterviewControllerTest.javasrc/test/java/com/samhap/kokomen/interview/docs/InterviewDocsTest.javasrc/test/java/com/samhap/kokomen/interview/docs/InterviewDocsV2Test.javasrc/test/java/com/samhap/kokomen/interview/repository/AnswerRepositoryTest.javasrc/test/java/com/samhap/kokomen/interview/repository/InterviewBatchRepositoryTest.javasrc/test/java/com/samhap/kokomen/interview/repository/InterviewLikeRepositoryTest.javasrc/test/java/com/samhap/kokomen/interview/repository/InterviewRepositoryTest.javasrc/test/java/com/samhap/kokomen/interview/repository/QuestionRepositoryTest.javasrc/test/java/com/samhap/kokomen/interview/repository/RootQuestionRepositoryTest.javasrc/test/java/com/samhap/kokomen/interview/service/InterviewProceedFacadeServiceTest.javasrc/test/java/com/samhap/kokomen/interview/service/InterviewQueryServiceTest.javasrc/test/java/com/samhap/kokomen/interview/service/core/InterviewServiceTest.javasrc/test/java/com/samhap/kokomen/interview/service/infra/InterviewSchedulerServiceTest.javasrc/test/java/com/samhap/kokomen/interview/service/question/RootQuestionServiceTest.javasrc/test/java/com/samhap/kokomen/interview/service/resume/ResumeBasedInterviewServiceTest.javasrc/test/java/com/samhap/kokomen/interview/service/social/InterviewViewCountServiceTest.javasrc/test/java/com/samhap/kokomen/interview/tool/InterviewMessagesFactoryTest.javasrc/test/java/com/samhap/kokomen/interview/tool/InterviewTest.javasrc/test/java/com/samhap/kokomen/interview/tool/QuestionAndAnswersTest.javasrc/test/java/com/samhap/kokomen/member/controller/MemberControllerTest.javasrc/test/java/com/samhap/kokomen/member/repository/MemberRepositoryTest.javasrc/test/java/com/samhap/kokomen/member/service/MemberServiceTest.java
💤 Files with no reviewable changes (1)
- src/main/java/com/samhap/kokomen/interview/service/InterviewFacadeService.java
| ### Domain Packages | ||
| ``` |
There was a problem hiding this comment.
헤딩/코드펜스 마크다운 규칙 위반을 한 번에 정리해 주세요.
해당 구간은 MD022/MD031/MD040(헤딩 주변 공백, 펜스 주변 공백, fence language 지정) 경고가 발생합니다. 문서 lint를 통과하도록 공백 줄과 언어 태그를 추가해 주세요.
수정 예시
### Domain Packages
+
-```
+```text
src/main/java/com/samhap/kokomen/
...Domain Package Convention
- +text
{domain}/
...
### Interview Service Sub-Packages
The interview domain uses sub-packages to organize its service layer. Facade services and InterviewQueryService remain at the root for controller access:
-```
+
+```text
interview/service/
...
</details>
Also applies to: 63-64, 77-79
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>
[warning] 46-46: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
---
[warning] 47-47: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
---
[warning] 47-47: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against the current code and only fix it if needed.
In @CLAUDE.md around lines 46 - 47, 해당 섹션의 마크다운은 헤딩 주변 공백(MD022/MD031)과 코드펜스 언어
태그(MD040) 규칙을 위반하니, 각 헤딩(예: "### Domain Packages", "### Domain Package
Convention", "### Interview Service Sub-Packages") 앞뒤에 빈 줄을 추가하고 모든 코드 펜스 블록(...)에 언어 태그를 명시(예: ```text)하며 펜스 전후에도 빈 줄을 넣어 문서 린트 규칙을 충족하도록 수정하세요.
</details>
<!-- fingerprinting:phantom:triton:hawk -->
<!-- This is an auto-generated comment by CodeRabbit -->
| └── dto/ | ||
| ``` | ||
|
|
||
| ### Cross-Cutting Patterns |
There was a problem hiding this comment.
헤딩 주변 빈 줄 규칙(MD022)도 동일하게 맞춰 주세요.
여러 섹션 헤딩 앞/뒤 빈 줄이 누락되어 lint 경고가 반복됩니다. 동일 규칙으로 일괄 정리하는 편이 유지보수에 좋습니다.
Also applies to: 99-99, 132-132, 141-141, 148-148
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 92-92: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CLAUDE.md` at line 92, Several Markdown headings (e.g., the "Cross-Cutting
Patterns" heading and the other headings referenced at lines 99, 132, 141, 148)
are missing required blank lines around them causing MD022 lint warnings; edit
those headings to ensure there is one blank line both immediately before and
immediately after each heading (i.e., insert a blank line above and below
"Cross-Cutting Patterns" and the other referenced section headings) so the file
conforms to the MD022 rule.
| - **`BaseTest`**: `@SpringBootTest` with `@ActiveProfiles("test")`, mocks external services (GPT, S3, Supertone, Tosspayments, OAuth clients, Bedrock), spies on Redis. Uses real MySQL container + `MySQLDatabaseCleaner`. | ||
| - **`BaseControllerTest`**: Extends BaseTest, adds MockMvc with RestDocs configuration. | ||
| - **`DocsTest`**: `@ActiveProfiles("docs")`, uses H2 in-memory DB with `@Transactional`. For lightweight REST Docs generation without Docker. | ||
|
|
There was a problem hiding this comment.
테스트 인프라 원칙이 충돌로 읽힙니다—예외 범위를 명시해 주세요.
BaseTest는 실 MySQL 컨테이너를 전제로 하지만 DocsTest는 H2를 사용한다고 되어 있어 “모든 테스트는 MySQL” 원칙과 상충될 수 있습니다. DocsTest는 REST Docs 전용 예외 같은 문구를 바로 옆에 명시해 해석 여지를 없애는 게 좋습니다.
Based on learnings: Use real MySQL container (not H2) for all tests.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CLAUDE.md` around lines 149 - 152, Clarify the apparent conflict by
explicitly stating the exception: update the README bullets so BaseTest remains
the default (real MySQL container + MySQLDatabaseCleaner) and DocsTest is an
intentional exception that uses H2 for lightweight REST Docs generation only;
mention both class names (BaseTest and DocsTest) and add a short sentence like
"Exception: DocsTest uses H2 in-memory DB solely for lightweight REST Docs
generation and is excluded from the 'use real MySQL for tests' rule."
| String lockKey = InterviewProceedFacadeService.createInterviewProceedLockKey(memberId); | ||
| String interviewProceedStateKey = InterviewProceedFacadeService.createInterviewProceedStateKey(interviewId, | ||
| questionAndAnswers.readCurQuestion().getId()); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
infra 계층이 facade 계층 정적 메서드에 의존하고 있습니다.
키 생성 로직은 InterviewProceedFacadeService가 아니라 공통 유틸/툴 클래스로 분리해서 service와 infra가 함께 참조하도록 정리하는 편이 계층 의존성 방향을 더 안정적으로 유지합니다.
Also applies to: 83-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/samhap/kokomen/interview/service/infra/InterviewProceedBedrockFlowAsyncService.java`
around lines 68 - 70, The infra class InterviewProceedBedrockFlowAsyncService is
calling facade static helpers
InterviewProceedFacadeService.createInterviewProceedLockKey and
createInterviewProceedStateKey, which inverts layer dependencies; extract the
key-generation logic into a new shared utility (e.g., InterviewProceedKeyUtil or
InterviewProceedKeys) in a common module/package accessible to both service and
infra, move the implementations there, update
InterviewProceedBedrockFlowAsyncService to call
InterviewProceedKeyUtil.createInterviewProceedLockKey and
InterviewProceedKeyUtil.createInterviewProceedStateKey, and refactor the other
occurrences (the similar calls around the 83-85 region) to use the same utility
so both service and infra depend on the shared util instead of the facade.
| log.info("Gpt API 호출 시도 - interviewId: {}, curQuestionId: {}, memberId: {}", | ||
| interviewId, curQuestionId, memberAuth.memberId()); | ||
| log.error("Bedrock API 호출 실패, GPT 폴백에시 기록 - {}", e); | ||
| interviewProceedBedrockFlowAsyncService.proceedInterviewByGptFlowAsync(memberAuth.memberId(), | ||
| questionAndAnswers, interviewId, lockValue); | ||
| } catch (Exception ex) { | ||
| log.error("Gpt API 호출 실패 - {}", ex); | ||
| redisService.releaseLockSafely(lockKey, lockValue); |
There was a problem hiding this comment.
예외 로그가 스택트레이스를 남기지 못하고 있습니다.
log.error("... {}", e) 형태는 스택트레이스가 누락됩니다. 운영 장애 분석을 위해 Throwable 인자를 마지막 파라미터로 직접 전달해 주세요.
🔧 로그 보완 예시
- log.error("Bedrock API 호출 실패, GPT 폴백에시 기록 - {}", e);
+ log.error("Bedrock API 호출 실패, GPT 폴백 시도 기록", e);
...
- log.error("Gpt API 호출 실패 - {}", ex);
+ log.error("Gpt API 호출 실패", ex);📝 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.
| log.info("Gpt API 호출 시도 - interviewId: {}, curQuestionId: {}, memberId: {}", | |
| interviewId, curQuestionId, memberAuth.memberId()); | |
| log.error("Bedrock API 호출 실패, GPT 폴백에시 기록 - {}", e); | |
| interviewProceedBedrockFlowAsyncService.proceedInterviewByGptFlowAsync(memberAuth.memberId(), | |
| questionAndAnswers, interviewId, lockValue); | |
| } catch (Exception ex) { | |
| log.error("Gpt API 호출 실패 - {}", ex); | |
| redisService.releaseLockSafely(lockKey, lockValue); | |
| log.info("Gpt API 호출 시도 - interviewId: {}, curQuestionId: {}, memberId: {}", | |
| interviewId, curQuestionId, memberAuth.memberId()); | |
| log.error("Bedrock API 호출 실패, GPT 폴백 시도 기록", e); | |
| interviewProceedBedrockFlowAsyncService.proceedInterviewByGptFlowAsync(memberAuth.memberId(), | |
| questionAndAnswers, interviewId, lockValue); | |
| } catch (Exception ex) { | |
| log.error("Gpt API 호출 실패", ex); | |
| redisService.releaseLockSafely(lockKey, lockValue); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/samhap/kokomen/interview/service/InterviewProceedFacadeService.java`
around lines 63 - 70, The error logs in InterviewProceedFacadeService are using
formatted placeholders with the exception (e and ex) which omits stacktraces;
update the two catch blocks so log.error is called with the throwable as the
last argument (e.g., log.error("Bedrock API 호출 실패, GPT 폴백에시 기록", e) and
log.error("Gpt API 호출 실패", ex)), leaving the descriptive message string only and
passing the exception object as the final parameter; keep the surrounding calls
(interviewProceedBedrockFlowAsyncService.proceedInterviewByGptFlowAsync and
redisService.releaseLockSafely) unchanged.
| public List<RootQuestionResponse> getRootQuestionsByCategory(Category category) { | ||
| return rootQuestionService.findAllRootQuestionByCategory(category).stream() | ||
| .map(RootQuestionResponse::from) | ||
| .toList(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
getRootQuestionsByCategory 메서드명은 find- 계열로 바꿔주세요.
빈 결과가 가능한 컬렉션 조회이므로 get-보다 findRootQuestionsByCategory가 가이드와 의미에 맞습니다.
As per coding guidelines 'Use 'find-' prefix for methods where values may not exist, returning Optional or empty list' 및 'Avoid using 'get-' prefix for non-getter methods in Java'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/samhap/kokomen/interview/service/InterviewQueryService.java`
around lines 72 - 75, Rename the method getRootQuestionsByCategory to
findRootQuestionsByCategory in InterviewQueryService and update all callers
accordingly; keep the return type List<RootQuestionResponse> and the body that
maps
rootQuestionService.findAllRootQuestionByCategory(category).stream().map(RootQuestionResponse::from).toList(),
and update any tests, interfaces, or classes that reference
getRootQuestionsByCategory to the new name to preserve behavior and compilation.
| if (interviewMode == InterviewMode.VOICE) { | ||
| String voiceUrl = questionService.createAndUploadQuestionVoice(question); | ||
| return new InterviewStartVoiceModeResponse(interview, question, voiceUrl); | ||
| } |
There was a problem hiding this comment.
트랜잭션 내부에서 음성 생성/업로드를 수행하지 않는 것이 안전합니다.
createAndUploadQuestionVoice 같은 외부 I/O를 트랜잭션 안에서 실행하면 트랜잭션 점유 시간이 길어지고 실패 시 롤백/지연 영향 범위가 커집니다. 인터뷰/질문 저장 트랜잭션과 음성 생성 단계를 분리(예: after-commit 처리)해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.java`
around lines 106 - 109, The call to questionService.createAndUploadQuestionVoice
is performing external I/O inside the transactional flow in
InterviewStartFacadeService; remove that synchronous call from the transaction
and instead trigger voice generation after the interview/question transaction
commits (e.g., publish an event or register an after-commit callback) so the DB
commit completes quickly and I/O happens outside the transaction; update the
method that currently returns InterviewStartVoiceModeResponse to either return a
response without voiceUrl immediately (or a placeholder/processing state) and
implement a TransactionSynchronization/TransactionalEventListener handler or
async service that invokes questionService.createAndUploadQuestionVoice and then
updates the question/voiceUrl or notifies the client when complete.
| @Transactional | ||
| public void grantPurchasedTokens(TokenPurchase tokenPurchase, int tokenCount) { | ||
| tokenPurchaseService.saveTokenPurchase(tokenPurchase); | ||
| tokenService.addPaidTokens(tokenPurchase.getMemberId(), tokenCount); | ||
| } |
There was a problem hiding this comment.
토큰 적립 공용 경로에 멤버 단위 락이 빠져 있습니다.
grantPurchasedTokens는 토큰 잔액을 변경하는 쓰기 메서드인데 @DistributedLock이 없어, 웹훅 경로에서 동일 멤버의 다른 토큰 연산과 경합할 수 있습니다. 클래스 내 다른 쓰기 메서드와 락 전략을 맞춰 주세요.
수정 제안
- `@Transactional`
+ `@DistributedLock`(prefix = "token", key = "#tokenPurchase.memberId")
+ `@Transactional`(propagation = Propagation.REQUIRES_NEW)
public void grantPurchasedTokens(TokenPurchase tokenPurchase, int tokenCount) {
tokenPurchaseService.saveTokenPurchase(tokenPurchase);
tokenService.addPaidTokens(tokenPurchase.getMemberId(), tokenCount);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/samhap/kokomen/token/service/TokenFacadeService.java`
around lines 160 - 164, The grantPurchasedTokens method in TokenFacadeService
updates a member's token balance but lacks the member-level distributed lock
used by other write operations; wrap this method with the same `@DistributedLock`
(or equivalent) annotation/locking strategy applied to other write methods in
the class so concurrent webhook or token operations for the same member
serialize; specifically, add the member-scoped lock annotation to
grantPurchasedTokens (which calls tokenPurchaseService.saveTokenPurchase and
tokenService.addPaidTokens) using the same lock key expression/strategy used
elsewhere in the class to lock on tokenPurchase.getMemberId() (or the memberId
parameter).
| @Disabled | ||
| @Test | ||
| void 이미_좋아요를_누른_인터뷰에_좋아요를_요청하면_예외가_발생한다() { | ||
| // given | ||
| Member member = memberRepository.save(MemberFixtureBuilder.builder().build()); | ||
| RootQuestion rootQuestion = rootQuestionRepository.save(RootQuestionFixtureBuilder.builder().build()); | ||
| Interview interview = interviewRepository.save( | ||
| InterviewFixtureBuilder.builder().member(member).rootQuestion(rootQuestion).likeCount(0L).build()); | ||
| interviewQueryService.likeInterview(interview.getId(), new MemberAuth(member.getId())); | ||
|
|
||
| // when & then | ||
| assertThatThrownBy( | ||
| () -> interviewQueryService.likeInterview(interview.getId(), new MemberAuth(member.getId()))) | ||
| .isInstanceOf(BadRequestException.class) | ||
| .hasMessageContaining("이미 좋아요를 누른 인터뷰입니다."); | ||
| } |
There was a problem hiding this comment.
중복 좋아요 예외 테스트를 비활성화하면 핵심 회귀를 놓칩니다.
@Disabled로 인해 "이미 좋아요를 누른 인터뷰" 차단 규칙이 CI에서 검증되지 않습니다. 이 시나리오는 활성화 상태로 유지되어야 합니다.
수정 예시
- `@Disabled`
`@Test`
void 이미_좋아요를_누른_인터뷰에_좋아요를_요청하면_예외가_발생한다() {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/test/java/com/samhap/kokomen/interview/service/InterviewQueryServiceTest.java`
around lines 49 - 64, In InterviewQueryServiceTest remove the `@Disabled`
annotation from the test method 이미_좋아요를_누른_인터뷰에_좋아요를_요청하면_예외가_발생한다 so the
duplicate-like validation is exercised in CI; ensure the test still sets up a
Member, RootQuestion and Interview, invokes
interviewQueryService.likeInterview(interview.getId(), new
MemberAuth(member.getId())) once to create the like and then asserts the second
call to interviewQueryService.likeInterview(...) throws BadRequestException with
the expected message, leaving the rest of the method unchanged.
| void 인터뷰에_좋아요를_누르면_최신_좋아요_수로_이벤트가_발행된다() { | ||
| // given | ||
| Member member = memberRepository.save(MemberFixtureBuilder.builder().build()); | ||
| RootQuestion rootQuestion = rootQuestionRepository.save(RootQuestionFixtureBuilder.builder().build()); | ||
| Interview interview = interviewRepository.save( | ||
| InterviewFixtureBuilder.builder().member(member).rootQuestion(rootQuestion).likeCount(0L).build()); | ||
| MemberAuth memberAuth = new MemberAuth(member.getId()); | ||
| Long beforeLikeCount = interview.getLikeCount(); | ||
|
|
||
| // when | ||
| interviewQueryService.likeInterview(interview.getId(), memberAuth); | ||
|
|
||
| // then | ||
| Interview updatedInterview = interviewRepository.findById(interview.getId()).get(); | ||
| assertThat(updatedInterview.getLikeCount()).isEqualTo(beforeLikeCount + 1); | ||
| } |
There was a problem hiding this comment.
테스트 이름과 실제 검증 대상이 다릅니다.
현재는 이벤트 발행을 검증하지 않고 likeCount 증가만 확인합니다. 테스트명을 실제 검증 내용에 맞추거나, 이벤트 발행 검증을 추가해야 합니다.
수정 예시(이름 정합성)
- void 인터뷰에_좋아요를_누르면_최신_좋아요_수로_이벤트가_발행된다() {
+ void 인터뷰에_좋아요를_누르면_좋아요_수가_증가한다() {
closed #333
작업 내용
스크린샷
참고 사항