ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • FE Agent 작업 완료 리포트 (Claude Code: Opus 4.5, Cycle Mode)
    이코에코(Eco²)/Agent 2026. 1. 23. 10:00

    Chat Agent SSE FE-BE 안정화 완료, 이코에코 캐릭터를 주입받은 나노 바나나 Pro가 생성한 이미지들
    Cycle Mode 예시(1.Location, 2.Web-agent): plan 생성 -> 스텝 진행(구현, 테스트, skills 코드 리뷰 루프) -> 완료까지 Cycle Mode로 운용 -> 개발자 검증

    Claude Code Cycle Mode, shift + tap

    https://code.claude.com/docs/en/common-workflows

     

    Common workflows - Claude Code Docs

    Step-by-step guides for exploring codebases, fixing bugs, refactoring, testing, and other everyday tasks with Claude Code.

    code.claude.com

     

    Tap 1: FE Agent, Chat Agent SSE 안정화

    Tap 2: BE 주요 Chat Agents 기능 안정화(완료) -> 도메인별 에러 핸들링 보강

    Tap 3: Tool callling-Web Search Agent 보강

    Tap 4: 분산 트레이싱(OTEL, Jaeger) 점검 및 보강

    Tap 5: Location 도메인 고도화, BE-FE 병행

    Tap 6: 포트폴리오 업데이트

    FE 에이전트 작업 요약

    항목 수량
    총 PR 29개
    Feature/Fix PR 19개
    Deploy PR 10개
    전체 머지 완료 O
    수정 파일 수 20+

    1. FE Agent 채팅 데이터 무결성

    #92 refactor(agent): IndexedDB v3 스키마 계층화 및 데이터 무결성 개선

    배경: IndexedDB 스키마가 백엔드 계층 구조와 불일치하여 세션 관리, 메시지 격리에 문제 발생

    변경 내용:

    • chat_idsession_id 명명 변경 (Backend chat_conversations.id와 매칭)
    • User isolation을 위한 by-user-session-created 복합 인덱스 추가
    • v1 → v2 → v3 자동 마이그레이션 로직 구현
    • SSE keepalive 이벤트 타임아웃 리셋 처리
    • Session cleanup: 채팅 전환 시 SSE 연결 정리
    • Image upload 400 에러 수정 (image_url 빈 문자열 → undefined)
    • Race condition 방지: isSendingRef flag

    수정 파일: src/db/messageDB.ts, src/db/schema.ts, src/hooks/agent/useAgentChat.ts, src/hooks/agent/useAgentSSE.ts, src/api/services/agent/agent.type.ts, src/components/agent/AgentContainer.tsx, AgentInputBar.tsx


    #93 feat(agent): Skills v2 - 데이터 무결성 & Vercel Best Practices 통합

    변경 내용:

    • docs/reports/.claude/skills/ 재구성 (6개 카테고리)
    • 각 skill: SKILL.md (개요/의사결정 트리) + references/ (상세 문서)
    • Vercel React Best Practices 통합 (40+ 규칙, 8개 카테고리)

    수정 파일: .claude/skills/ 하위 13개 파일


    #94 [Release] Agent Feature v2 (develop → main)


    2. Agent UI/UX 개선

    #95 fix: Agent 이미지 인식 및 중복 메시지 버그 수정

    원인 1 (이미지 인식 실패): AgentInputBar에서 이미지 업로드 후 stale closure로 selectedImageonSend에 전달되지 않음

    해결: 컴포넌트에서 업로드 후 CDN URL을 직접 전달

    원인 2 (중복 메시지): handleSSEComplete에서 updateMessageStatus 호출 시 server_id 전달 누락 → reconcile 중복 제거 실패

    해결: server_id 설정으로 정상 dedup

    // Before
    updateMessageStatus(msg, 'committed')
    // After
    updateMessageStatus(msg, 'committed', userServerId)

    수정 파일: src/components/agent/AgentInputBar.tsx, src/hooks/agent/useAgentChat.ts


    #96 feat(toast): 채팅 삭제 시 로딩/완료 토스트 추가

    변경 내용: toast.loading(message) API + AnimatedDots (. → .. → ... 순환)

    수정 파일: src/components/Toast/ToastContainer.tsx, toast.ts, AgentContainer.tsx


    #97 feat(sidebar): 스와이프하여 삭제 기능 구현

    변경 내용: 터치 이벤트 기반 좌측 스와이프 제스처 (임계값 60px, 수직 스크롤 충돌 방지)

    수정 파일: src/components/agent/sidebar/AgentSidebarItem.tsx


    #102 style: 모델 선택 버튼 위치 수정

    변경 내용: 갤러리 버튼 items-center, 모델 버튼 self-end + ml-6

    수정 파일: src/components/agent/AgentInputBar.tsx


    #105 feat(agent): 이미지만 전송 시 기본 분류 프롬프트 추가

    코드베이스 (useAgentChat.ts:453-457):

    // 이미지만 전송 시 기본 프롬프트 추가
    const effectiveMessage =
      !message.trim() && finalImageUrl
        ? '이 이미지 분류해줘'
        : message;

    수정 파일: src/hooks/agent/useAgentChat.ts


    3. SSE 스트리밍 안정화 (iOS Safari / PWA)

    #98 fix: Safari SSE 안정성 및 iOS 이미지 선택 버그 수정

    원인 (SSE): Safari 백그라운드 전환 시 EventSource 연결 끊어짐 + 복구 로직 부재

    코드베이스 (useAgentSSE.ts:401-428 — visibility change 핸들러):

    useEffect(() => {
      const handleVisibilityChange = () => {
        if (document.visibilityState === 'visible') {
          if (currentJobIdRef.current && eventSourceRef.current && !isManualDisconnectRef.current) {
            const readyState = eventSourceRef.current.readyState;
            // EventSource가 CLOSED면 자동 재연결 실패한 것 → 새로 생성
            if (readyState === EventSource.CLOSED) {
              createEventSourceFn(currentJobIdRef.current);
            }
          }
        }
      };
      document.addEventListener('visibilitychange', handleVisibilityChange);
      return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
    }, [createEventSourceFn]);

    원인 (iOS 이미지): 키보드 활성화 상태에서 파일 선택기 열릴 때 viewport 깨짐

    해결: 키보드를 먼저 닫고 100ms 후 파일 선택 열기

    수정 파일: src/hooks/agent/useAgentSSE.ts, src/components/agent/AgentInputBar.tsx


    #104 fix(pwa): SSE 무한 대기 및 사이드바 비율 수정

    원인: EventSource API가 iOS PWA의 Service Worker를 통과할 때 이벤트 스트리밍 차단

    해결: fetch() + ReadableStream으로 교체 (후에 #107에서 EventSource로 복원)

    수정 파일: src/hooks/agent/useAgentSSE.ts, src/components/agent/AgentContainer.tsx


    #107 fix(sse): EventSource 복원 + polling fallback 추가

    원인: PR #104의 fetch+ReadableStream이 iOS Safari/크롬에서 cross-origin 스트리밍 미지원

    해결: EventSource 복원 + onStale 콜백으로 polling fallback 트리거

    코드베이스 (useAgentSSE.ts:64-70 — onStale 인터페이스):

    interface UseAgentSSEOptions {
      onToken?: (token: string) => void;
      onProgress?: (stage: CurrentStage) => void;
      onComplete?: (result: DoneEvent['result']) => void;
      onError?: (error: Error) => void;
      /** SSE 연결은 되었지만 meaningful 이벤트를 받지 못할 때 콜백 */
      onStale?: (jobId: string) => void;
    }

    수정 파일: src/hooks/agent/useAgentSSE.ts, src/hooks/agent/useAgentChat.ts


    #109 fix(reconcile): SSE 실패 후 메시지 중복 버그 수정

    원인: SSE 실패 후 복귀 시 로컬 failed 메시지와 서버 메시지의 ID 불일치로 중복 표시

    코드베이스 (message.ts:130-168 — content 기반 매칭):

    // 서버 메시지 content 기반 매칭 (role + content → timestamp)
    const CONTENT_MATCH_WINDOW_MS = 120000; // 2분 이내 같은 내용이면 동일 메시지로 판단
    const serverContentIndex = new Map<string, number>();
    serverMessages.forEach((m) => {
      const key = `${m.role}:${m.content}`;
      const ts = new Date(m.created_at).getTime();
      const existing = serverContentIndex.get(key);
      if (!existing || ts > existing) {
        serverContentIndex.set(key, ts);
      }
    });
    
    // 로컬 메시지: content 매칭으로 중복 확인
    if (!local.server_id) {
      const contentKey = `${local.role}:${local.content}`;
      const serverTs = serverContentIndex.get(contentKey);
      if (serverTs !== undefined) {
        const localTs = new Date(local.created_at).getTime();
        if (Math.abs(localTs - serverTs) < CONTENT_MATCH_WINDOW_MS) {
          return false; // 서버에 동일 메시지 존재 → 로컬 드롭
        }
      }
    }

    수정 파일: src/utils/message.ts


    #110 feat(sse): 네이티브 Last-Event-ID 기반 복구

    변경 내용: 수동 ?last_seq=X → SSE 표준 Last-Event-ID 헤더 (브라우저 자동 재연결 활용)

    코드베이스 (useAgentSSE.ts:82-94 — 상수 정의):

    // 네이티브 EventSource 자동 재연결 시 연속 에러 최대 허용 횟수
    const MAX_CONSECUTIVE_ERRORS = 5;
    
    // 타임아웃 설정
    const DEFAULT_EVENT_TIMEOUT = 60000; // 60초
    const IMAGE_GENERATION_TIMEOUT = 180000; // 3분
    
    // 연결 상태 확인 주기
    const HEALTH_CHECK_INTERVAL = 10000; // 10초
    
    // Stale 감지: meaningful 이벤트 없이 이 시간이 지나면 onStale 호출
    const STALE_THRESHOLD = 3000; // 3초

    수정 파일: src/hooks/agent/useAgentSSE.ts


    #112 fix(streaming): PWA 토큰 스트리밍 화면 깜빡임 수정

    원인: 3개의 스크롤 effect 충돌 + smooth/instant 경합 + overflow-anchor 충돌 + Streamdown components 매번 재생성

    해결:

    이전 현재
    useEffect 3개 통합 1개 (RAF 기반)
    scrollToBottom('smooth'/'instant') scrollTop = scrollHeight 직접 설정
    streamingText 직접 렌더 useDeferredValue(streamingText) 배칭
    components = {...} 매번 생성 useMemo(() => ({...}), [])

    수정 파일: src/components/agent/AgentMarkdownRenderer.tsx, AgentMessageList.tsx


    #116 fix(sse,layout): SSE 스트리밍 복구 전략 개선 및 채팅 스크롤 고정

    원인 (SSE 1/3 확률 실패):

    • Gateway Pub/Sub 구독 완료 전 Event Router가 publish → 이벤트 유실
    • State KV 없으면 catch-up 불가
    • 기존 25s stale + keepalive reset → stale 미발동

    코드베이스 (useAgentChat.ts:279-358 — 3단계 복구 전략):

    const handleSSEStale = useCallback(
      (jobId: string) => {
        const chatId = pendingChatIdRef.current;
        if (!chatId) return;
    
        // 최대 3회 SSE 재연결 시도 (gateway State catch-up 기대)
        const MAX_SSE_RECONNECTS = 3;
        if (sseReconnectAttemptRef.current < MAX_SSE_RECONNECTS) {
          sseReconnectAttemptRef.current += 1;
          console.log(`[SSE] Stale - reconnecting (attempt ${sseReconnectAttemptRef.current}/${MAX_SSE_RECONNECTS})`);
          connectSSERef.current?.(jobId);
          return;
        }
    
        // 재연결 모두 실패: polling fallback (3초 간격, getChatDetail)
        pollingIntervalRef.current = setInterval(async () => {
          const response = await AgentService.getChatDetail(chatId, { limit: 5 });
          const lastMsg = response.messages[response.messages.length - 1];
          if (lastMsg && lastMsg.role === 'assistant') {
            stopPolling();
            stopGenerationRef.current?.();
            // handleSSEComplete과 동일한 처리...
          }
        }, 3000);
    
        // 최대 120초 후 폴링 중단
        setTimeout(() => { if (pollingIntervalRef.current) stopPolling(); }, 120000);
      },
      [messages.length, userId, stopPolling],
    );

    Stale 감지 (useAgentSSE.ts:207-214):

    // Stale 감지 타이머 (meaningful 이벤트 없으면 polling fallback 트리거)
    if (!receivedMeaningfulEventRef.current) {
      staleTimeoutRef.current = setTimeout(() => {
        if (!receivedMeaningfulEventRef.current && !isManualDisconnectRef.current) {
          onStaleRef.current?.(jobId);
        }
      }, STALE_THRESHOLD); // 3초
    }

    SSE ref 패턴 (circular dependency 방지, useAgentChat.ts:361-382):

    // handleSSEStale가 connectSSE를 호출해야 하지만, connectSSE는 useAgentSSE에서 반환
    // → ref로 우회
    const stopGenerationRef = useRef<(() => void) | null>(null);
    const connectSSERef = useRef<((jobId: string) => void) | null>(null);
    
    const { connect: connectSSE, disconnect: stopGeneration } = useAgentSSE({
      onComplete: handleSSEComplete,
      onError: handleSSEError,
      onStale: handleSSEStale,
    });
    
    useEffect(() => {
      stopGenerationRef.current = stopGeneration;
      connectSSERef.current = connectSSE;
    }, [stopGeneration, connectSSE]);

    스크롤 고정 (AppLayout.tsx:12-16, 44-45):

    // 자체 스크롤을 관리하는 페이지 (외부 스크롤 비활성화)
    const selfScrollPaths = ['/agent', '/chat'];
    const isSelfScroll = selfScrollPaths.some((path) => pathname.startsWith(path));
    
    // 콘텐츠 영역
    <div className={`absolute right-0 left-0 ${isSelfScroll ? 'overflow-hidden' : 'overflow-y-auto'}`}>

    수정 파일: src/hooks/agent/useAgentSSE.ts, useAgentChat.ts, src/pages/App/AppLayout.tsx


    4. 메시지 정합성 (Reconcile / 세션 관리)

    #103 fix: 세션 전환 시 메시지 이동 및 이미지 소실 버그 수정

    원인: SSE 스트리밍 중 채팅 전환 시 handleSSEComplete가 현재 세션에 저장

    코드베이스 (useAgentChat.ts:104-106, 167-171):

    // 메시지 전송 시점의 chatId 추적 (세션 전환 시 올바른 채팅에 저장)
    const pendingChatIdRef = useRef<string | null>(null);
    
    // SSE 완료 시 세션 비교
    const originalChatId = pendingChatIdRef.current;
    const currentChatId = currentChatRef.current?.id;
    const isSameSession = originalChatId === currentChatId;
    // isSameSession이 false면 UI 업데이트 스킵 (IndexedDB에는 원래 chatId로 저장)

    수정 파일: src/hooks/agent/useAgentChat.ts, src/utils/message.ts


    #114 fix(chat): 세션 간 메시지 오염, 순서 역전, 이미지 소실 수정

    수정 사항 (5건):

    1. Cross-session 오염: setMessages([])로 채팅 전환 시 즉시 클리어
    2. User/Assistant 순서 역전: assistant created_at을 user +1ms로 보정
    3. 페이지네이션 드롭: append + dedup 전략
    4. 이미지 소실: localImageMap 기반 복원

    코드베이스 (message.ts:108-125 — localImageMap):

    // 로컬 image_url 보존 맵 (서버가 image_url을 반환하지 않을 때 대비)
    const localImageMap = new Map<string, string>();
    localMessages.forEach((m) => {
      if (m.image_url) {
        if (m.server_id) localImageMap.set(m.server_id, m.image_url);
        localImageMap.set(m.client_id, m.image_url);
      }
    });
    
    // 서버 메시지 변환 (image_url 없으면 로컬에서 복원)
    const serverConverted = serverMessages.map((sm) => {
      const converted = serverToClientMessage(sm);
      if (!converted.image_url) {
        const localImage = localImageMap.get(sm.id);
        if (localImage) converted.image_url = localImage;
      }
      return converted;
    });
    1. Reconcile committed+server_id 무조건 유지 로직 제거: 오염 메시지 영구 보존 원인

    수정 파일: src/hooks/agent/useAgentChat.ts, src/utils/message.ts


    5. iOS PWA 스크롤 / 키보드

    #99 fix: 스트리밍 중 스크롤 튕김 현상 수정

    해결: RAF 기반 스크롤 + instant behavior + force 파라미터 + isAtBottom 체크

    수정 파일: src/components/agent/AgentMessageList.tsx, src/hooks/agent/useScrollToBottom.ts


    #100 fix: iOS 키보드 닫힘 타이밍 및 스크롤 튕김 수정

    해결: visualViewport API로 키보드 감지, 열림 상태면 350ms 대기 후 파일 선택

    수정 파일: src/components/agent/AgentInputBar.tsx


    #101 fix: Agent UI/UX 개선 및 Safari 안정성 강화

    변경 내용: PR #99 + #100 통합 + 모델 선택 버튼 위치 조정


    6. 이미지 관련

    #118 fix(image): iOS PWA 이미지 다운로드 후 레이아웃 깨짐 수정

    원인: <a download>가 iOS PWA에서 네비게이션 → 풀사이즈 이미지 + 히스토리 오염

    코드베이스 (AgentImage.tsx:10-13 — iOS 판별):

    const isIOS = () =>
      /iPad|iPhone|iPod/.test(navigator.userAgent) ||
      (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);

    코드베이스 (AgentImage.tsx:114-149 — blob 다운로드 핸들러):

    const handleDownload = useCallback(
      async (e: React.MouseEvent) => {
        e.preventDefault();
        e.stopPropagation();
        if (!src) return;
    
        try {
          if (isIOS() && navigator.share) {
            // iOS: Web Share API 사용 (사진 앱 저장 가능)
            const res = await fetch(src);
            const blob = await res.blob();
            const file = new File([blob], alt || 'image.png', { type: blob.type });
            await navigator.share({ files: [file] });
          } else {
            // 기타: Blob 다운로드
            const res = await fetch(src);
            const blob = await res.blob();
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = alt || 'image';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
          }
        } catch {
          window.open(src, '_blank');
        }
        closeModal();
      },
      [src, alt, closeModal],
    );

    코드베이스 (AgentImage.tsx:34-54 — 히스토리 정리):

    const closeModal = useCallback(() => {
      if (isClosingRef.current) return;
      isClosingRef.current = true;
    
      setIsExpanded(false);
      setShowDownload(false);
    
      // pushState로 추가한 히스토리 엔트리 제거
      if (window.history.state?.imageModal) {
        window.history.back();
      }
    
      setTimeout(() => { isClosingRef.current = false; }, 0);
    }, []);

    수정 파일: src/components/agent/AgentImage.tsx


    7. Scan (카메라 분류)

    #119 fix(scan): step 전진을 completed 이벤트 기준으로 변경

    원인: started/completed 구분 없이 step 전진 → 실제 완료 전에 체크마크 표시

    백엔드 Scan Worker 이벤트 스키마:

    seq = STAGE_ORDER[stage] * 10 + (1 if status == "completed" else 0)
    
    vision:started  (seq=10) → UI: spinner
    vision:completed (seq=11) → UI: ✓
    rule:started    (seq=20) → UI: spinner
    rule:completed  (seq=21) → UI: ✓
    answer:started  (seq=30) → UI: spinner
    answer:completed (seq=31) → UI: ✓
    done:completed  (seq=51) → UI: Answer 페이지 이동

    코드베이스 (useScanSSE.ts:140-158 — completed 기준 핸들러):

    const handleEvent = (data: ScanSSEEvent) => {
      console.log(`[Scan SSE] ${data.stage}:${data.status} seq=${data.seq}`);
    
      // completed 이벤트만 step 전진
      if (data.status === 'completed') {
        const step = STAGE_TO_STEP[data.stage] ?? 0;
        setCurrentStep((prev) => Math.max(prev, step));
      }
    
      // done 완료 → 결과 조회
      if (data.stage === 'done' && data.status === 'completed') {
        disconnect();
        ScanService.getScanResult(jobId).then((scanResult) => {
          setIsComplete(true);
          setResult(scanResult);
          options?.onComplete?.(scanResult);
        });
      }
    };

    코드베이스 (scan.type.ts:29-40 — 업데이트된 타입):

    export type ScanSSEEvent = {
      job_id: string;
      stage: ScanSSEStage;
      status: 'started' | 'completed' | 'failed';
      seq: number;
      ts: string;
      progress?: number;
      result?: Record<string, unknown>;
      trace_id?: string;
      span_id?: string;
      traceparent?: string;
    };

    수정 파일: src/hooks/useScanSSE.ts, src/api/services/scan/scan.type.ts


    8. Deploy (develop → main)

    PR 제목 포함 내용
    #94 [Release] Agent Feature v2 #92, #93
    #106 release: develop → main 배포 #104, #105
    #108 deploy: SSE EventSource 복원 + polling fallback #107
    #111 deploy: Last-Event-ID + 메시지 중복 수정 #109, #110
    #113 deploy: PWA 스트리밍 안정화 #112
    #115 Release: 메시지 정합성 및 이미지 소실 수정 #114
    #117 deploy: SSE 스트리밍 안정화 및 채팅 스크롤 고정 #116
    #120 deploy: 스캔 step 완료 기준 변경 + iOS 이미지 #118, #119

    아키텍처 다이어그램

    SSE 스트리밍 흐름 (Agent Chat)

    ┌─────────┐    ┌──────────────┐    ┌──────────────┐    ┌───────────┐    ┌────────┐
    │  Worker │───▶│ Redis Streams │───▶│ Event Router │───▶│ State KV  │    │ Client │
    │         │    │ (XADD)       │    │ (XREADGROUP) │    │ + Pub/Sub │    │        │
    └─────────┘    └──────────────┘    └──────────────┘    └─────┬─────┘    └───┬────┘
                                                                 │              │
                                                                 ▼              │
                                                           ┌───────────┐       │
                                                           │SSE Gateway│───────┘
                                                           │(subscribe)│  EventSource
                                                           └───────────┘

    복구 전략:

    T+0s   SSE 연결 + Pub/Sub 구독
    T+3s   Stale 감지 → 재연결 1 (State 존재 시 catch-up 성공)
    T+6s   Stale 감지 → 재연결 2
    T+9s   Stale 감지 → 재연결 3
    T+12s  포기 → Polling fallback (getChatDetail 3초 간격, 최대 120초)

    메시지 Reconcile 흐름

    ┌──────────────┐     ┌──────────────┐
    │ Local State  │     │ Server State │
    │ (messages[]) │     │ (GET /chat)  │
    └──────┬───────┘     └──────┬───────┘
           │                     │
           ▼                     ▼
    ┌────────────────────────────────────┐
    │         reconcileMessages()        │
    │                                    │
    │ 1. localImageMap 생성 (image 보존) │
    │ 2. serverConverted 변환 + 이미지복원│
    │ 3. serverIdMap 중복 체크           │
    │ 4. content 매칭 (2분 윈도우)       │
    │ 5. pending/streaming 유지          │
    │ 6. committed retention (30초)      │
    │ 7. 병합 + 시간순 정렬              │
    └────────────────────────────────────┘

    Scan Worker 이벤트 흐름

    ┌─────────────┐    ┌──────────────┐    ┌──────────────┐    ┌────────┐
    │ Scan Worker │───▶│ Redis Streams│───▶│ Event Router │───▶│ Client │
    │             │    │ (Lua script) │    │              │    │        │
    └─────────────┘    └──────────────┘    └──────────────┘    └────────┘
    
    이벤트:  queued → vision → rule → answer → reward → done
    Status:  started ────────────────────────────────▶ completed
    UI Step: spinner ────────────────────────────────▶ ✓ 체크마크

    정합성 체크 결과

    발견된 불일치 (수정 완료)

    위치 문제 수정
    useAgentChat.ts:150 주석 "1회 재연결 후 polling" 코드는 MAX_SSE_RECONNECTS=3 → 주석을 "3회"로 수정

    검증 완료 항목

    항목 PR 설명 코드베이스 일치
    STALE_THRESHOLD 3초 3000 (L94)
    MAX_SSE_RECONNECTS 3회 3 (L285)
    MAX_CONSECUTIVE_ERRORS 5회 5 (L83)
    DEFAULT_EVENT_TIMEOUT 60초 60000 (L86)
    IMAGE_GENERATION_TIMEOUT 3분 180000 (L87)
    HEALTH_CHECK_INTERVAL 10초 10000 (L90)
    CONTENT_MATCH_WINDOW_MS 2분 120000 (L132)
    committedRetentionMs 30초 30000 (L106)
    Polling interval (agent) 3초 3000 (L348)
    Polling max duration 120초 120000 (L352)
    Polling interval (scan) 2초 2000 (L20)
    MAX_POLLING_ATTEMPTS (scan) 60회 60 (L21)
    LONG_PRESS_DURATION 500ms 500 (L20)
    image_url 빈 문자열 방지 || undefined L468 finalImageUrl || undefined
    이미지 기본 프롬프트 "이 이미지 분류해줘" L456
    selfScrollPaths /agent, /chat AppLayout L13
    ScanSSEEvent.seq number 타입 scan.type.ts L33
    Scan step completed만 전진 data.status === 'completed' useScanSSE L145
    iOS 감지 userAgent + maxTouchPoints AgentImage L11-13
    히스토리 정리 imageModal state check AgentImage L47
    popstate 재진입 방지 isClosingRef AgentImage L31,35-36

    PR 파일 vs 코드베이스 교차 검증

    PR 설명 파일 실제 변경 일치
    #116 useAgentSSE, useAgentChat, AppLayout 3개 파일 모두 변경 확인
    #118 AgentImage.tsx <button> + handleDownload + isClosingRef 확인
    #119 useScanSSE, scan.type.ts handleEvent + ScanSSEEvent 타입 확인
    #114 useAgentChat, message.ts pendingChatIdRef + localImageMap 확인
    #109 message.ts serverContentIndex + CONTENT_MATCH_WINDOW_MS 확인

    SSE 스트리밍 변천사 (시간순)

    PR 방식 문제점 결과
    #92 EventSource + last_seq Safari 백그라운드 끊김 초기 구현
    #98 + visibility change + health check iOS 키보드 viewport 안정성 개선
    #104 fetch + ReadableStream iOS PWA에서 SW 차단 cross-origin 미지원
    #107 EventSource 복원 + onStale 25s stale → 폴링 CORS 정상
    #110 Last-Event-ID 네이티브 수동 재연결 제거 브라우저 자동 복구
    #112 useDeferredValue + RAF 스크롤 깜빡임 렌더링 최적화
    #116 3s stale × 3회 재연결 Pub/Sub 타이밍 race 최종 안정화

    수정 파일 전체 목록

    파일 관련 PR 변경 내용
    src/hooks/agent/useAgentSSE.ts #92,#98,#104,#107,#110,#116 SSE 스트리밍 아키텍처 (EventSource, stale, health check)
    src/hooks/agent/useAgentChat.ts #92,#95,#103,#105,#107,#114,#116 세션 관리, 재연결, polling fallback, 이미지 프롬프트
    src/utils/message.ts #103,#109,#112,#114 reconcile, content 매칭, localImageMap, sort 최적화
    src/components/agent/AgentInputBar.tsx #95,#98,#100,#101,#102 이미지 업로드, 키보드 처리, 버튼 위치
    src/components/agent/AgentImage.tsx #92,#118 blob 다운로드, 히스토리 정리, iOS Share API
    src/components/agent/AgentMessageList.tsx #99,#112 RAF 스크롤, useDeferredValue
    src/components/agent/AgentMarkdownRenderer.tsx #112 useMemo components
    src/components/agent/AgentContainer.tsx #92,#96,#104 사이드바 absolute, 토스트 연동
    src/components/agent/sidebar/AgentSidebarItem.tsx #97 스와이프 삭제
    src/hooks/agent/useScrollToBottom.ts #99 RAF + instant + force
    src/hooks/useScanSSE.ts #119 completed 기준 step, handleEvent 통합
    src/api/services/scan/scan.type.ts #119 ScanSSEEvent (seq, ts, traceparent)
    src/pages/App/AppLayout.tsx #92,#116 selfScrollPaths overflow-hidden
    src/db/messageDB.ts #92 IndexedDB v3, by-user-session-created
    src/db/schema.ts #92 v3 스키마 정의
    src/components/Toast/ToastContainer.tsx #96 AnimatedDots
    src/components/Toast/toast.ts #96 toast.loading API
    .claude/skills/** #93 Skills v2.0 (6개 카테고리)

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango