Skip to content

채팅 연속 전송 비활성화#474

Open
Seong-Myeong wants to merge 1 commit intomainfrom
feature/#473-chatpage-multiple-question-send
Open

채팅 연속 전송 비활성화#474
Seong-Myeong wants to merge 1 commit intomainfrom
feature/#473-chatpage-multiple-question-send

Conversation

@Seong-Myeong
Copy link
Copy Markdown
Contributor

@Seong-Myeong Seong-Myeong commented Apr 9, 2026

관련 이슈

PR 설명

채팅 페이지에서 답변 수신 중 연속 전송이 가능하던 문제를 수정했습니다.

  • src/app/(route)/chat/[id]/ChatPage.tsx

    • 답변 대기 상태(isAwaitingResponse)를 추가했습니다.
    • 답변 생성 중에는 추가 질문을 보내지 못하도록 제어했습니다.
    • 응답 대기 중 답변을 생성하고 있어요. 로딩 UI가 보이도록 추가했습니다.
    • 홈에서 첫 질문 후 진입할 때 router.replace 대신 history.replaceState를 사용하도록 바꿔 화면이 다시 로드되는 느낌을 줄였습니다.
    • 소켓 종료 신호가 명확하지 않은 경우를 대비해 마지막 응답 이후 0.5초 동안 추가 응답이 없으면 다시 전송 가능 상태가 되도록 처리했습니다.
  • src/apis/chatSocket.ts

    • 소켓 응답 payload에서 isEnd 값을 파싱하도록 수정했습니다.
    • 채팅 페이지가 응답 완료 여부를 더 정확하게 판단할 수 있도록 타입과 파싱 로직을 보완했습니다.
  • src/components/wrappers/QueryBox.tsx

    • QueryBox의 비활성화 동작을 분리했습니다.
    • 입력창 전체를 막는 방식이 아니라, 전송 버튼만 비활성화할 수 있도록 disabledinputDisabled 역할을 나눴습니다.
  • src/app/(route)/chat/_components/ChatQueryBox.tsx

    • QueryBox 변경에 맞춰 inputDisabled를 받을 수 있도록 수정했습니다.
    • 채팅 페이지에서는 입력은 가능하고 전송 버튼만 막히는 동작을 적용할 수 있게 연결했습니다.
  • src/app/(route)/home/HomePage.tsx

    • 홈에서 첫 질문 전송 후 대기 화면에도 첫 질문 복사 버튼이 바로 보이도록 수정했습니다.
    • 홈에서 채팅방 생성 중일 때도 입력창은 유지하고, 전송 버튼만 비활성화되도록 동작을 맞췄습니다.

@Seong-Myeong Seong-Myeong linked an issue Apr 9, 2026 that may be closed by this pull request
@Seong-Myeong Seong-Myeong self-assigned this Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 860b1920-3eb7-4e20-b892-e4232a3d201b

📥 Commits

Reviewing files that changed from the base of the PR and between 68ef526 and 461c805.

📒 Files selected for processing (5)
  • src/apis/chatSocket.ts
  • src/app/(route)/chat/[id]/ChatPage.tsx
  • src/app/(route)/chat/_components/ChatQueryBox.tsx
  • src/app/(route)/home/HomePage.tsx
  • src/components/wrappers/QueryBox.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/apis/chatSocket.ts
  • src/app/(route)/home/HomePage.tsx
  • src/app/(route)/chat/_components/ChatQueryBox.tsx

Walkthrough

이 PR은 소켓으로 수신되는 메시지에 isEnd: boolean 필드를 추가하고, 채팅 페이지에 isAwaitingResponse 상태와 자동 해제 타이머를 도입하여 응답이 완료되기 전 연속 제출을 차단하도록 변경합니다. 또한 입력 비활성화(inputDisabled)와 제출 비활성화(disabled)를 분리해 ChatQueryBoxQueryBox에 전달하고, UI에서 응답 대기 시 스피너와 관련 비활성화 동작을 반영하도록 수정했습니다.

Possibly related PRs

  • 채팅페이지 querybox 고정 #470: ChatPage와 ChatQueryBox 사용/배치 변경을 다루며, 본 PR의 QueryBox/ChatQueryBox prop 확장과 직접적인 연관이 있음.
  • QueryBox 컴포넌트 구현 #230: QueryBox의 공개 props에 disabled/inputDisabled를 추가하는 변경을 포함해, 본 PR의 QueryBox 변경과 코드 수준에서 직접 연결됨.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 '응답 대기 중 연속 전송 방지'를 명확하고 간결하게 설명합니다.
Description check ✅ Passed PR 설명이 템플릿을 따르고 있으며, 관련 이슈 번호, 변경사항 상세 설명이 포함되어 있습니다.
Linked Issues check ✅ Passed 모든 코드 변경사항이 이슈 #473의 목표(응답 생성 중 추가 질문 방지)를 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 연속 전송 방지라는 명확한 범위 내에 있으며, 관련 없는 변경사항은 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#473-chatpage-multiple-question-send

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(route)/chat/[id]/ChatPage.tsx (1)

81-108: ⚠️ Potential issue | 🟠 Major

상태값만으로는 초고속 연타를 완전히 막지 못합니다.

setIsAwaitingResponse(true)는 다음 렌더에서 반영되므로, 같은 렌더 사이클에서 다시 들어온 submit은 아직 false인 값을 봅니다. 빠른 Enter 연타나 이중 클릭이면 같은 이벤트 루프 내에서 여러 번 send()가 호출될 수 있어서, UI state와 별도로 useRef 기반 동기 락을 두는 편이 안전합니다.

제안된 awaitingResponseRef를 이용한 lockAwaitingResponse() / unlockAwaitingResponse() 방식이 적절합니다. 동기적으로 ref를 업데이트하면 같은 이벤트 사이클 내 후속 호출들을 즉시 차단할 수 있습니다.

제안 코드
   const [messages, setMessages] = useState<ChatMessage[]>([]);
   const [isAwaitingResponse, setIsAwaitingResponse] = useState(false);
@@
   const reactionRequestSeqRef = useRef<Record<string, number>>({});
   const responseUnlockTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const awaitingResponseRef = useRef(false);
 
+  const unlockAwaitingResponse = useCallback(() => {
+    awaitingResponseRef.current = false;
+    setIsAwaitingResponse(false);
+  }, []);
+
+  const lockAwaitingResponse = useCallback(() => {
+    awaitingResponseRef.current = true;
+    setIsAwaitingResponse(true);
+  }, []);
+
   const clearResponseUnlockTimer = useCallback(() => {
     if (!responseUnlockTimerRef.current) return;
     clearTimeout(responseUnlockTimerRef.current);
     responseUnlockTimerRef.current = null;
   }, []);
@@
   const scheduleResponseUnlock = useCallback(() => {
     clearResponseUnlockTimer();
     responseUnlockTimerRef.current = setTimeout(() => {
-      setIsAwaitingResponse(false);
+      unlockAwaitingResponse();
       responseUnlockTimerRef.current = null;
     }, RESPONSE_IDLE_UNLOCK_MS);
-  }, [clearResponseUnlockTimer]);
+  }, [clearResponseUnlockTimer, unlockAwaitingResponse]);
@@
   const handleSubmit = (value: string) => {
@@
-    if (isAwaitingResponse) return;
+    if (awaitingResponseRef.current) return;
@@
-    setIsAwaitingResponse(true);
+    lockAwaitingResponse();
     clearResponseUnlockTimer();
@@
     try {
       send(trimmedValue);
     } catch (err) {
       clearResponseUnlockTimer();
-      setIsAwaitingResponse(false);
+      unlockAwaitingResponse();
       setStreamError((err as Error).message ?? '메시지 전송에 실패했습니다.');
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(route)/chat/[id]/ChatPage.tsx around lines 81 - 108, The UI state
is racy because setIsAwaitingResponse updates on next render, so add a
synchronous ref-based lock: create awaitingResponseRef = useRef(false) and
implement lockAwaitingResponse() that returns false if already true, otherwise
sets it true and returns true, plus unlockAwaitingResponse() that clears it and
calls clearResponseUnlockTimer()/scheduleResponseUnlock() as appropriate; update
the send()/submit flow to call lockAwaitingResponse() at start (and bail if it
returns false), call setIsAwaitingResponse(true) for UI, and ensure
unlockAwaitingResponse() is called on completion or error (and clear the ref in
clearResponseUnlockTimer/scheduleResponseUnlock interactions) so rapid repeated
events are blocked immediately.
🧹 Nitpick comments (1)
src/components/wrappers/QueryBox.tsx (1)

14-35: disabled가 버튼 클릭에만 적용되고 Enter 제출 경로는 그대로 열려 있습니다.

지금 구현은 SendButton만 막고 TextAreaonSubmit은 그대로 넘겨서, 호출처가 별도 가드를 빼먹으면 Enter로는 계속 제출됩니다. 공용 컴포넌트인 만큼 QueryBox 내부에서 제출 조건을 한 번만 계산해서 버튼/키보드 경로를 같이 막는 편이 안전합니다.

제안 코드
 export default function QueryBox({
   value,
   onChange,
   onSubmit,
   disabled = false,
   inputDisabled = false,
 }: Props) {
+  const canSubmit = !disabled && !!value.trim();
+
+  const handleSubmit = () => {
+    if (!canSubmit) return;
+    onSubmit();
+  };
+
   return (
     <div className="relative w-full">
       <TextArea
         heightLines={2}
         maxHeightLines={6}
@@
         showMax
         value={value}
         onChange={e => onChange(e.target.value)}
-        onSubmit={onSubmit}
+        onSubmit={handleSubmit}
         disabled={inputDisabled}
         className="shadow-[0_2px_4px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.04)]"
       />
-      <SendButton disabled={disabled || !value.trim()} onClick={onSubmit} />
+      <SendButton disabled={!canSubmit} onClick={handleSubmit} />
     </div>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/wrappers/QueryBox.tsx` around lines 14 - 35, The
component-level submission guard is missing: QueryBox currently only uses the
disabled prop to disable SendButton while passing onSubmit directly into
TextArea, allowing Enter submissions when disabled; change QueryBox to compute a
single isDisabled = disabled || inputDisabled || !value?.trim() and pass that to
both SendButton (disabled) and TextArea (prevent calling onSubmit when
isDisabled is true), i.e., keep onChange as-is but wrap or gate calls to
onSubmit inside QueryBox (use the computed isDisabled to early-return) so both
button and keyboard paths honor the same condition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(route)/chat/[id]/ChatPage.tsx:
- Around line 188-196: The current flow calls appendAiMessage(payload) before
checking control frames, which causes an empty AI bubble when payload.isEnd
arrives with no content; change the logic in the success branch to first inspect
payload.isEnd and, if true, run the unlock logic (call
clearResponseUnlockTimer(), setIsAwaitingResponse(false)) and
setStreamError(null) as needed without calling appendAiMessage, then return;
only when payload.isEnd is false should you call appendAiMessage(payload) and
scheduleResponseUnlock(); update the block that references appendAiMessage,
payload, isEnd, clearResponseUnlockTimer, scheduleResponseUnlock,
setIsAwaitingResponse, and setStreamError accordingly.

---

Outside diff comments:
In `@src/app/`(route)/chat/[id]/ChatPage.tsx:
- Around line 81-108: The UI state is racy because setIsAwaitingResponse updates
on next render, so add a synchronous ref-based lock: create awaitingResponseRef
= useRef(false) and implement lockAwaitingResponse() that returns false if
already true, otherwise sets it true and returns true, plus
unlockAwaitingResponse() that clears it and calls
clearResponseUnlockTimer()/scheduleResponseUnlock() as appropriate; update the
send()/submit flow to call lockAwaitingResponse() at start (and bail if it
returns false), call setIsAwaitingResponse(true) for UI, and ensure
unlockAwaitingResponse() is called on completion or error (and clear the ref in
clearResponseUnlockTimer/scheduleResponseUnlock interactions) so rapid repeated
events are blocked immediately.

---

Nitpick comments:
In `@src/components/wrappers/QueryBox.tsx`:
- Around line 14-35: The component-level submission guard is missing: QueryBox
currently only uses the disabled prop to disable SendButton while passing
onSubmit directly into TextArea, allowing Enter submissions when disabled;
change QueryBox to compute a single isDisabled = disabled || inputDisabled ||
!value?.trim() and pass that to both SendButton (disabled) and TextArea (prevent
calling onSubmit when isDisabled is true), i.e., keep onChange as-is but wrap or
gate calls to onSubmit inside QueryBox (use the computed isDisabled to
early-return) so both button and keyboard paths honor the same condition.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: aad1928d-bf18-4f43-ac65-511117786dd7

📥 Commits

Reviewing files that changed from the base of the PR and between 731884b and 68ef526.

📒 Files selected for processing (5)
  • src/apis/chatSocket.ts
  • src/app/(route)/chat/[id]/ChatPage.tsx
  • src/app/(route)/chat/_components/ChatQueryBox.tsx
  • src/app/(route)/home/HomePage.tsx
  • src/components/wrappers/QueryBox.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

채팅페이지 질문 연속 가능

1 participant