Skip to content

feat: 강의실 화이트보드 — 그리기/도형/텍스트/선택·편집/레이어#71

Open
congsoony wants to merge 11 commits into
developfrom
feat/#58-classroom_whiteboard
Open

feat: 강의실 화이트보드 — 그리기/도형/텍스트/선택·편집/레이어#71
congsoony wants to merge 11 commits into
developfrom
feat/#58-classroom_whiteboard

Conversation

@congsoony

Copy link
Copy Markdown
Collaborator

요약

강의실 화이트보드(로컬 필기) 프론트엔드 구현. 객체(retained) 기반 <canvas>로 그리기·선택·편집·레이어 관리를 지원합니다. (실시간 공유는 후속 작업)

도구

  • 펜 / 형광펜(반투명 multiply), 직선 / 곡선(클릭 점 추가→더블클릭 완료)
  • 도형: 사각형 / 원 / 삼각형 / 다각형(↑↓로 각수 실시간 조절, 말풍선 표시)
  • 텍스트: 멀티라인, 글꼴·크기·굵기, 한글 IME 조합 처리
  • 지우개(원형 범위 미리보기, 굵기 연동) / 전체 지우기
  • 좌측 툴바는 그룹화(펜·선·도형) — 길게 눌러 하위 도구 플라이아웃

선택·편집

  • 다중 선택: 빈 곳 드래그(마퀴) / Shift+클릭 토글
  • 이동, 크기변경(모서리 핸들), 회전(상단 핸들 + 각도 말풍선·직접 입력)
  • 모디파이어: Shift(45° 스냅·비율 유지·수직수평 이동), Ctrl+이동(복사)
  • 도형 그릴 때: Ctrl=중심 기준, Shift=정비율(정사각형/원/정다각형)
  • 커서: 이동/크기조절(회전각 반영)/회전 — PPT·포토샵식

레이어 패널

  • 도형별 모양 아이콘, 클릭 선택, 드래그 정렬, 표시/숨김, 수정/삭제
  • 헤더 드래그로 패널 이동(보드 영역 내 제한), 고정 높이 + 스크롤

색상·속성

  • 프리셋 색 + 임의 색 선택(컬러피커), 선굵기·투명도 슬라이더(선택 도형에도 적용)

구조

  • src/pages/classroom/Whiteboard.jsx — 상태·포인터 핸들러 오케스트레이터
  • whiteboard/constants.js · geometry.js(기하·히트테스트) · painting.js(렌더) · OptionsBar.jsx · LayersPanel.jsx 로 관심사 분리

참고

  • 입장 게이팅 중 StrictMode에서 멈추던 회귀(mountedRef) 수정 포함
  • vite build 통과. 로컬 전용이며 다른 참가자와의 실시간 동기화는 후속.

congsoony added 10 commits June 11, 2026 17:53
- 객체(retained) 기반 캔버스로 전환: 선택/이동/크기변경/삭제/수정
- 도구: 선택·펜·형광펜·직선·사각형·원·텍스트·지우개(부분)·전체지우기
- 선굵기/투명도 슬라이더(임의값), 임의 색 선택(컬러피커), 텍스트 글꼴/크기/굵기
- 지우개 원형 미리보기(굵기 연동)
- ClassroomPage: mountedRef를 마운트 시 true로 리셋(StrictMode에서 입장 멈춤 회귀 수정)
- 회전 핸들 + 각도 말풍선(실시간 표시·직접 입력)
- 커서: 이동(move)/크기조절(회전각 반영 nwse·nesw·ns·ew)/회전(원형 화살표)
- Shift: 회전 45° 스냅, 크기조절 비율 유지, 이동 수직/수평 고정
- Ctrl+이동: 복사본 드래그(원본 유지)
- 다중선택(마퀴 드래그/Shift), 다중 이동·삭제·속성적용
- 곡선 도구(클릭 점 추가→더블클릭 완료)
- 삼각형·다각형 추가, 다각형 각수 ↑↓ 실시간(그리는 중 draft 포함)·말풍선
- 레이어 패널: 도형별 모양 아이콘, 헤더 드래그 이동(보드 내 제한), 고정높이+스크롤, 표시/숨김·정렬·수정·삭제
- 좌측 툴바 그룹화(펜/형광펜, 직선/곡선, 도형) 길게눌러 플라이아웃
- whiteboard/constants.js: 상수·id 생성기
- whiteboard/geometry.js: 기하·히트테스트 순수함수(bbox/center/hitTest/handleAt/mapShape 등)
- whiteboard/painting.js: 캔버스 도형 렌더링(paintShape/paintPath)
- whiteboard/OptionsBar.jsx: 상단 옵션바 UI
- whiteboard/LayersPanel.jsx: 레이어 패널 UI
- Whiteboard.jsx: 상태+포인터 핸들러 오케스트레이터(548→약 300줄)
동작 동일, 빌드 통과
- 텍스트 editor: textarea(멀티라인, Enter완료/Shift+Enter줄바꿈), key remount+autoFocus
- 멀티라인 렌더/측정(painting/geometry)
- 좌측 도구 그룹 선택을 onPointerUp→onClick 기반으로(탭 안정성)
- handleDown에 진단 console.log + tool prop 폴백 (임시)
- [임시] 보드 좌하단 디버그 배지
- 사진(이미지) 불러오기: 여러 장, 좌측 사이드바 버튼(forwardRef로 호출), 이동/크기/회전/Ctrl/Shift/레이어 기존 도형과 동일
- 옵션 표시 정리: 지우개=투명도 숨김(굵기는 '지우개 크기'로 표시), 텍스트/사진=굵기 숨김
- 텍스트 크기 드롭다운→직접 입력(최대 100px 클램프)
- 전체 지우기 아이콘을 지우개 모양 SVG로 교체
- 레이어 패널에 사진 아이콘/라벨 추가

@leejy1019 leejy1019 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

FE PR #71 코드 리뷰 — 강의실 화이트보드 (그리기/도형/텍스트/선택·편집/레이어)

잘 된 부분

  • 관심사 분리가 모범적입니다. Whiteboard.jsx(상태·포인터 오케스트레이터) / constants.js / geometry.js(순수 기하·히트테스트) / painting.js(렌더) / OptionsBar·LayersPanel(UI)로 깔끔하게 쪼갰습니다. 838줄 규모인데도 각 파일 책임이 명확해 읽기 쉽습니다
  • retained-mode 설계가 정석적입니다. 회전 반영 toLocal/screenAABB 히트테스트, 모서리 anchor 기반 리사이즈, 마퀴 교차 판정, DPR 보정 캔버스(setTransform(dpr,...))까지 — 캔버스 에디터에서 흔히 빠뜨리는 디테일을 잘 챙김
  • 한글 IME 조합 처리 (composingRef + e.nativeEvent.isComposing)를 정확히 구현 — 조합 중 Enter/Escape 오작동을 막는, 자주 놓치는 부분입니다
  • StrictMode 재마운트 회귀 수정 (mountedRef를 마운트 effect에서 true로 복구) — #56에서 지적됐던 "입장 중…" 멈춤을 정확히 해결
  • 곡선 미드포인트 2차 베지어 스무딩, 회전/리사이즈 커서 각도 반영, Shift/Ctrl 모디파이어, 포인터 캡처 + touchAction:none 등 인터랙션 완성도가 높음

🟠 전역 keydown 핸들러가 다른 입력창(채팅 등) 입력을 가로챔

Whiteboard.jsxonKey (window keydown)

const onKey = (e) => {
  if (editing) return                      // ← 화이트보드 자체 텍스트 편집만 체크
  if (toolRef.current === 'polygon' && (ArrowUp/Down)) { e.preventDefault(); ... }
  if ((Delete || Backspace) && selRef.current.length) { /* 선택 도형 삭제 */ }
}
window.addEventListener('keydown', onKey)

editing화이트보드 내부 텍스트 편집 상태만 가리킵니다. 같은 강의실 화면의 채팅 입력창에 포커스가 있어도 이 핸들러는 동작합니다. 그래서:

  • 채팅 입력 중 Backspace → 도형이 선택돼 있으면 그 도형이 삭제됨 (글자 지우려다 도형이 날아감)
  • polygon 도구가 켜진 채 채팅에서 ↑/↓preventDefault로 커서 이동이 막힘

e.target이 폼 요소면 무시하도록 가드를 추가하면 해결됩니다.

const el = e.target
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable) return

강의실은 채팅과 화이트보드가 한 화면에 공존하므로 실제로 부딪칠 수 있는 시나리오입니다.


🟡 이미지 Object URL이 해제되지 않음 — 메모리 누수

Whiteboard.jsxaddImages

const url = URL.createObjectURL(file)
const img = new Image()
img.onload = () => { ... setShapes(prev => [...prev, { ..., src: url, _img: img }]) }
img.onerror = () => URL.revokeObjectURL(url)   // 실패 시에만 revoke

성공 경로에서 URL.revokeObjectURL이 호출되지 않습니다. 캔버스는 디코드된 _img로 그리므로 load 직후 revoke해도 안전합니다. 또 도형 삭제·"전체 지우기" 시에도 해당 이미지의 Object URL이 남습니다. 큰 사진을 여러 장 넣다 보면 누적됩니다. load 직후 revoke하거나, 삭제/clear 시 해당 shape의 url을 정리해 주세요.


🟡 성능 — 포인터 이동마다 전체 캔버스 재그리기

펜으로 그리는 동안 매 pointermovesetDraft → 리렌더 → redraw()(전체 도형 순회)를 유발합니다. 도형이 수백 개 쌓이면 드로잉이 무거워질 수 있습니다. MVP·로컬 사용엔 충분하지만, 장기적으로 requestAnimationFrame으로 redraw를 코얼레싱하거나 "확정된 도형 레이어 + 드로잉 중 도형만 다시 그리는" 더블 레이어 구조를 고려할 만합니다. (낮은 우선순위)


🟡 실시간/영속화 단계에서 직렬화 불가 — 후속 작업용 메모

PR 설명대로 실시간 공유가 후속이라 지금은 무관하지만, 미리 인지하면 좋습니다:

  • image 도형의 _img(Image 객체)·src(blob URL)는 다른 클라이언트로 전송 불가 — 서버 업로드 URL로 바꿔야 함
  • nextId가 모듈 전역 카운터(s1, s2…)라 여러 클라이언트에서 ID 충돌 — UUID나 clientId:seq 형태 필요

🟡 기존 텍스트 편집 시 도형의 원래 글꼴이 아닌 현재 툴바 값으로 표시

텍스트를 더블클릭해 편집하면 textarea가 현재 툴바의 fontFamily/fontSize/bold로 렌더되고, commitTexttext만 갱신합니다. serif로 만든 텍스트를 sans-serif 상태에서 편집하면 편집 중 글꼴이 어긋나 보입니다. 편집 진입 시 해당 도형의 글꼴 값을 툴바/에디터에 로드하면 일관됩니다. (낮은 우선순위)


참고 — 툴바 키보드 접근성

좌측 도구·색상이 <div onClick>이라 키보드 포커스/Enter로 조작이 안 됩니다(기존 패턴 유지). 캔버스 도구 특성상 우선순위는 낮지만, 추후 <button> + aria-pressed로 바꾸면 접근성이 좋아집니다.


총평: 이 규모의 캔버스 에디터를 회전 히트테스트·IME·DPR 보정까지 갖춰 깔끔한 모듈 구조로 구현한, 완성도 높은 PR입니다. 머지 전 🟠 전역 keydown 가드(채팅과 충돌)만 꼭 처리해 주세요 — 같은 화면에 채팅이 있어 실사용에서 부딪칩니다. 🟡 이미지 URL 누수 정도는 가볍게 같이 고치면 좋고, 나머지는 실시간 연동 후속 때 챙기면 됩니다.

- 각 페이지가 자신의 shapes(도형=레이어) 보관, 전환 시 해당 페이지 내용·레이어 표시
- 하단 페이지 바: 현재/총 페이지 표시(예 5 / 12) + ◀ ▶ 이동 + '+ new page' 추가
- 페이지 전환 시 선택/그리기 중 상태 초기화(페이지별 독립)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants