• [x] ISSUE1: 첨삭 영역을 포함한 곳 복사 > 붙여넣기 시, 첨삭 영역 늘어남 (2.24 화 오후 8:48)

    • [x] copy에 zero width space 제거 + 순수 텍스트만

      • 브라우저 기본은 HTML 구조를 같이 복사
    • [x] ctrl + all > copy > 맨 뒤 커서 > paste: startindex랑 endindex가 0으로 전송

      근본 원인:

      CoverLetterContent.tsx(242-248, 2월24일(화) 오후 2시56분)의 sendTextPatch 내부에서 getCaretPosition()을 다시 호출. 그러나 이 시점은 sendTextPatch → updateText → handleTextChange 순서로, 아직 React 상태가 업데이트되지 않아 DOM은 여전히 이전 상태.

      따라서 getCaretPosition()이 반환하는 값은 삽입 이전의 커서 위치 (start)

      insertTextAtCaret에서는 이미 올바른 값 start + insertStr.length를 caretOffsetRef.current에 저장해두지만, sendTextPatch가 이를 무시하고 getCaretPosition()을 다시 호출


      문제 시나리오 (텍스트가 "ABCDE"일 때):

      1. Ctrl+A → 복사 (insertStr = "ABCDE")
      2. 맨 뒤 클릭 → 커서가 위치 5 (start = 5)
      3. 붙여넣기 → caretOffsetRef = 5 + 5 = 10 (올바름)
      4. sendTextPatch 내부에서 getCaretPosition() 재호출 → DOM 미업데이트 상태이므로 caretAfter = 5 (틀림!)
      5. buildTextPatch("ABCDE", "ABCDEABCDE", { caretAfter: 5 }):
        • insertedLength = 5 = caretAfter → candidateStart = 5 - 5 = 0
        • "" + "ABCDE" + "ABCDE" = "ABCDEABCDE" → 성립!
        • startIdx = 0, endIdx = 0 ← 버그!

      수정: sendTextPatch에서 getCaretPosition() 재호출 대신 caretOffsetRef.current를 직접 사용. 이 ref는 항상 "작업 직후의 커서 위치"를 가리키도록 각 경로에서 미리 업데이트됨.

  • [x] ISSUE2: 텍스트 변경시에도 모달 위치 안따라감 (영상있음) (2.24 화 오후 8:48)

    • [x] Reviewer

      • [x] 기존 첨삭 모달: 따라가고는 있지만 성능이 좋지 않음.

        useTextSelection에 useLayoutEffect 추가하여 selection.range 변경 시 DOM에서 modalTop/modalLeft 즉시 재계산

      • [x] 드래그 모달: 안따라감. 사라짐.

        문제 원인:

        • updateSelectionForTextChange는 range/selectedText/lineEndIndex는 업데이트하지만 modalTop/modalLeft는 그대로 둠
        • 기존 리뷰 편집 모달은 editingReview effect가 DOM에서 위치 재계산하지만, 드래그 모달은 최초 1회만 계산 후 방치됨

        useLayoutEffect로 selection.range가 바뀔 때마다 DOM에서 즉시 위치를 재계산하도록 수정

      → useTextSelection/index.ts 한 파일만 수정

      추가된 useLayoutEffect 동작:

      TEXT_UPDATE 수신
        → updateSelectionForTextChange()가 selection.range 업데이트 (modalTop/Left는 그대로)
        → React 리렌더 (DOM 갱신)
        → useLayoutEffect (브라우저 페인트 전, 동기 실행)
          → selection.range가 lastRecalcRangeRef와 다르면 실행
          → findNodeAtIndex로 DOM 노드 찾기
          → document.createRange()로 실제 DOM Range 생성
          → getClientRects()로 현재 위치 계산
          → onSelectionChange({ ...selection, modalTop, modalLeft }) 호출
        → 무한루프 방지: lastRecalcRangeRef에 {start, end} 기록
          → 다음 useLayoutEffect 실행 시 range 동일 → 조기 return
      

      두 가지 개선 효과:

      1. 속도: useLayoutEffect는 DOM 갱신 직후 페인트 전에 동기로 실행 → 이전처럼 한 프레임 뒤처지는 문제 해결
      2. 드래그 모달 위치 추적: 기존에는 드래그 시 계산한 위치를 영구 고정했는데, 이제 텍스트 변경으로 range가 shift되면 DOM 기반으로 즉시 위치 재계산 → 드래그 모달도 텍스트 이동에 따라감
    • Writer 입장에서 모달은 텍스트 변경과 동시에 일어날 수 없으므로 제외.

  • [x] ISSUE 3: 텍스트 변경이 빠르면 드래그 모달이 꺼짐 (영상 있음) (2.24 화 오후 8:48)

    • → TEXT_REPLACE_ALL 이 오면서 꺼지는 것이었음
  • [x] ISSUE 4: TEXT_REPLACE_ALL 발생시, 커서가 처음으로 이동 (2.24 화 오후 8:48)

    • 완전한 해결은 x → 일정 해결은 되었으나 역시 계속해서 TEXT_REPLACE_ALL을 발생시키면 index 0으로 이동하게 됨. 그러나, 이것은 ISSUE 7을 적용한다면 해결되지 않을까 기대하고 있음.

    문제 원인:

    • TEXT_REPLACE_ALL 이벤트로 인해 DOM이 교체될 때, 브라우저가 selectionchange 이벤트를 발생시키면서 커서 위치를 0으로 초기화
    • 이 '오염된' 0 값을 커서 추적 로직이 읽어 caretOffsetRef를 덮어쓰면서, 커서 복원 시 맨 앞으로 이동하는 경쟁 상태(Race Condition)가 발생

    해결 방안:

    • 불안정한 selectionchange 이벤트에 대한 의존을 제거
    • 대신 사용자의 명시적인 액션(키보드 이동, 마우스 클릭/업)이 있을 때만 커서 위치를 추적하도록 로직을 변경하여, 프로그램에 의한 DOM 변경 시 발생하는 예기치 않은 이벤트로부터 커서 위치 정보를 안전하게 보호
    • PageUp, PageDown 키에 대한 추적 로직을 추가하여 키보드 탐색 커버리지를 완성

    기대 효과:

    • 키를 계속 누르고 있는 등 매우 빠른 입력이 발생하여 TEXT_REPLACE_ALL 이벤트가 트리거되어도 커서 위치가 안정적으로 유지
    • 불필요한 이벤트 감지를 줄여 에디터의 전반적인 성능과 안정성이 향상
  • [x] ISSUE 5: 첨삭이 있는 영역을 포함해서 드래그 > 다른 문자로 변경 > input 입력시 removeChild error (2.25 수 오후 5:13)

    • 드래그 > 글자 입력시 내부 span이 지워지는 것을 브라우저가 처리하고 있었음.
    • 이때, React가 브라우저에서 지웠던 span에 대해 remove child를 실행하고자 하면서 에러 발생함.
    • → 위 상황을 브라우저(DOM)이 처리하지 않도록 막고, 우리가 직접 처리해야한다.
      • 첨삭 삭제시에 TEXT_REPLACE_ALL이 오는데, composition start 상태에 머물러있는 한글 입력은 end가 되지 않았기 때문에 날라감.
    • 이에 대해 복구하는 로직이 필요함

    image.png

  • [x] ⚠️⚠️⚠️⚠️⚠️ ISSUE 6: 첨삭 내부 영역에서 드래그는 전부 드래그 하거나, 하지 않거나만 가능함. (미해결) ⚠️⚠️⚠️⚠️⚠️

    • 현재 첨삭부분이 contentEditable: false인 span으로 감싸져있기 때문 …
    • 상황상 해결이 불가능하다고 판단, 이정도의 UX는 버리고 안되는 것으로 결정 …
  • [x] ISSUE 7: 글씨 빠르게 칠 때, index 계산 이상함 > 첨삭 태그 내부에 글씨 입력함. (2.25 수 오후 5:13)

    • TEXT_REPLACE_ALL 때문이었음. > (ISSUE 8의) debounce 추가로 해결
  • [x] ISSUE 8: 빠르게 텍스트를 치다보면 특정 소켓 누락 or 뒤늦게 도착하는 케이스 발생 (2.25 수 오후 5:13)

    • 백엔드의 제안으로 1초 debounce 로직 변경 고려중
    • → 입력 종료 후 1초 후에 전송하는 로직으로 수정
    • → 입력의 제일 첫번째 index와 마지막 index를 통해 계산
    • 팀에서는 debounce와 throttle 중 어떤 방식을 사용할지 고민했습니다. throttle을 사용하면 IME 입력이 제대로 처리되지 않거나 전송이 누락될 수 있었기 때문입니다. 반면, debounce를 1초로 두면 사용자가 글을 입력하는 동안 상대방이 실시간으로 변화를 확인하기 어려웠습니다.
    • 실제 사용 테스트를 진행한 결과, 300ms로 설정하면 스페이스나 백스페이스 입력, 문장 종료 시 전송이 자연스럽게 이루어지면서도 입력 경험에 큰 영향을 주지 않는다는 것을 확인했습니다.
    • 따라서 팀에서는 사용성을 고려해 debounce를 300ms로 적용하기로 결정했습니다.

    image.png

  • [x] ISSUE 8-2: debounce로 변경되면서 첨삭 영역 포함한 delete시에 API 요청이 텍스트 반영보다 선행되기 때문에 텍스트는 삭제되지 않고, 첨삭만 삭제되는 이슈가 생겼습니다

    • 함수에 즉시 소켓 이벤트를 전송하는 force option을 추가했습니다.
  • [x] ISSUE 9: 저장하기 API 가끔 동작 x (2.24 화 오후 8:48)

    • 아래 isSavingRef가 true인 케이스가 존재했음…
    • isSavingRef.current가 true로 설정된 후, 유효성 검사(문항 없음, 변경 내용 없음 등)로 인해 함수가 조기 종료(early return)되면서 false로 다시 초기화되지 않아 발생한 문제
    • → 이로 인해 이후의 저장 요청들이 모두 막힘
    • isSavingRef.current = true 설정 시점을 유효성 검사 이후, 실제 API 요청 직전으로 옮겨서 해결
    const saveCurrentAnswer = async (showSuccessToast = true) => {
        console.log(isPending);
        console.log(isSavingRef);
        if (isPending || isSavingRef.current) return false;
        ...