Skip to content

Conversation

@tjsdn052
Copy link
Collaborator

@tjsdn052 tjsdn052 commented Jan 7, 2026

🎯 이슈 번호


✅ 작업 내용

캔버스

  • 마우스 휠로 줌인/아웃, 드래그로 캔버스 이동
  • 요소 선택 및 변형
  • Delete/Backspace로 선택된 요소 삭제

텍스트

  • 사이드바에서 텍스트 버튼 클릭 시 화면 중앙에 생성
  • 더블클릭으로 편집 모드 진입 및 텍스트 수정
  • 좌우 핸들로 너비 조절, 드래그로 위치 이동, 회전

화살표

  • 사이드바 도형 - 삼각형 버튼 클릭 시 화면 중앙에 화살표 생성
  • 화살표 더블클릭 시 클릭 위치에 가장 가까운 지점에 제어 가능한 핸들 추가
  • 시작점/끝점 핸들로 화살표 방향 조절
  • 중간점 핸들로 화살표 경로 수정
  • 중간점 선택 후 Delete/Backspace로 제거

새로운 요소 추가 시 아래와 같은 방식으로 진행해 주시면 될 거 같습니다.

타입 정의 (types/whiteboard.ts)

export interface NewItem extends BaseItem {
  type: 'new';
  x: number;
  y: number;
  // 기타 속성...
}
export type WhiteboardItem = TextItem | ArrowItem | NewItem;

스토어 액션 추가 (store/useCanvasStore.ts)

interface CanvasState {
  addNew: (payload?: Partial<Omit<NewItem, 'id' | 'type'>>) => void;
}

addNew: (payload) =>
  set((state) => {
    const id = uuidv4();
    const newItem: NewItem = {
      id,
      type: 'new',
      x: payload?.x ?? state.canvasWidth / 2,
      y: payload?.y ?? state.canvasHeight / 2,
      // 기본값 설정...
    };
    return { items: [...state.items, newItem] };
  })

렌더링 로직 (items/RenderItem.tsx)

if (item.type === 'new') {
  return <Shape {...item} draggable onDragEnd={...} onTransformEnd={...} />
}

버튼 추가 및 좌표 변환 (sidebar/panels/*.tsx)

const addNew = useCanvasStore((state) => state.addNew);
const stagePos = useCanvasStore((state) => state.stagePos);
const stageScale = useCanvasStore((state) => state.stageScale);

const handleAddNew = () => {
  if (typeof window === 'undefined') return;
  
  const centerX = window.innerWidth / 2;
  const centerY = window.innerHeight / 2;
  const worldPos = screenToWorld(centerX, centerY, stagePos, stageScale);
  
  addNew({ x: worldPos.x, y: worldPos.y });
};

🤔 리뷰 요구사항

  • 상단 툴바는 변경 예정이라 추가로 변경하지 않았습니다.
  • 사이드바 도형 메뉴의 삼각형 버튼에는 임시로 화살표 생성 로직을 연결해 두었습니다.
  • /whiteboard 에서 확인 가능합니다.
  • 현재 화이트보드 도메인 내부에서만 사용되는 유틸 함수를 app/utils/에 위치시켜 두었는데, 도메인 한정 유틸의 경우 app/utils/app/src/components/whiteboard/utils/ 중 어느 위치가 더 적절한지에 대한 의견을 구하고 싶습니다. 마찬가지 도메인/컴포넌트 전용 hook / store는 어디에 위치하면 좋을지도 의견이 궁금합니다.

📸 스크린샷 (선택)

image

- 15000x9000 드래그/줌 가능 캔버스 생성
- Zustand 상태 관리
- 마우스 휠 줌 인/아웃
- 현재 스케일 수준 표시
- 줌인 줌아웃 버튼 동작
- 캔버스 조작 및 윈도우 사이즈 파악 로직 훅으로 분리
- 캔버스 사이즈 변경(20000x20000)
- 상수 분리
- 텍스트, 화살표 타입 정의
- 캔버스 상태 관리 (zoom, position)
- 텍스트, 화살표 추가 및 아이템 제거
- 선택/편집 상태 관리
- 레이어 순서 조정
- 크기 조절 및 회전
- 텍스트/도형별 앵커 설정
- 스케일 보정
- 더블클릭으로 편집 모드 진입
- ESC/외부 클릭으로 편집 종료
- 스타일 동기화 (폰트, 색상, 정렬
- 패널 버튼으로 화면 중앙에 생성
- 요소 렌더링 및 선택
- 중간 point 추출, point, 선분 거리 계산
- 양 끝 핸들로 위치 조작
- 중간 지점 더블클릭시 위치 이동 가능한 핸들 생성 및 클린하여 선택 후 백스페이스로 제거
@tjsdn052 tjsdn052 linked an issue Jan 7, 2026 that may be closed by this pull request
Copy link
Collaborator

@chamny20 chamny20 left a comment

Choose a reason for hiding this comment

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

고생많으셨습니다 ~!! 잘 동작하네요.
캔버스 로직 깔끔하게 설명해주셔서 보는데 큰 도움이 되었습니다. konva의 달인이 되셨군요..ㅎㅎ

현재 화이트보드 도메인 내부에서만 사용되는 유틸 함수를 app/utils/에 위치시켜 두었는데, 도메인 한정 유틸의 경우 app/utils/ 와 app/src/components/whiteboard/utils/ 중 어느 위치가 더 적절한지에 대한 의견을 구하고 싶습니다. 마찬가지 도메인/컴포넌트 전용 hook / store는 어디에 위치하면 좋을지도 의견이 궁금합니다.

저는 작업중인 브랜치에선 app/utils/meeting/...ts => 이렇게 도메인별로 구분을 하긴 했습니다! hooks도 마찬가지로 /hooks/meeting/useHooks.ts 이런식으로 생각했어요.
개인적으로 src/utils/가 이미 있다면 이 안에 위치시키는 게 바람직하지 않나 생각이 들어요. 이것도 따로 논의한 적이 없어서 같이 얘기해봐도 좋을 것 같아요.

Comment on lines +11 to +14
import RenderItem from '@/components/whiteboard/items/RenderItem';
import TextArea from '@/components/whiteboard/items/text/TextArea';
import ItemTransformer from '@/components/whiteboard/controls/ItemTransformer';
import ArrowHandles from '@/components/whiteboard/items/arrow/ArrowHandles';
Copy link
Collaborator

Choose a reason for hiding this comment

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

p5: 추후에 /items나 /controls에서 가져올 컴포넌트가 더 많아진다면 import line을 줄이기 위해 각 컴포넌트별로 export 모아두는 것도 좋을 것 같아요!

Comment on lines +30 to +40
if (selectedId && !isArrowSelected) {
const selectedNode = stage.findOne('#' + selectedId);
if (selectedNode) {
transformerRef.current.nodes([selectedNode]);
transformerRef.current.getLayer()?.batchDraw();
} else {
transformerRef.current.nodes([]);
}
} else {
transformerRef.current.nodes([]);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4: early-return 패턴을 시도해봐도 좋을 것 같아요!

Comment on lines +84 to +86
if (node.getClassName() === 'Text') {
node.width(node.width() * scaleX);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Text만 보정한 이유가 텍스트 배율에 따라 찌그러져서 그런 걸까요!? 일반 도형아이템은 해당 로직 없이도 잘 보정되나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

네 맞아요 리시가 언급한 대로 요소 박스(Transformer)를 늘려 크기 변화시 각 요쇼의 width, height가 변경되는게 아니라 그만큼의 scaleX / scaleY 확대 수준이 변화하게 되는데 텍스트의 경우는 박스를 늘려도 텍스트 자체 크기를 유지하도록 보정 로직을 추가하였습니다.

일반 도형 아이템은 해도 안해도 보이는 결과는 같긴하지만 높이 너비만 변화시키는게 바람직해보여 추후에 요소 추가시 보정 로직도 추가하면 좋지 않을까 생각하고 있어요.

Comment on lines +108 to +121
onDragEnd={(e) => {
const pos = e.target.position();
const newPoints = arrowItem.points.map((p, i) =>
i % 2 === 0 ? p + pos.x : p + pos.y
);

e.target.position({ x: 0, y: 0 });

onChange({
points: newPoints,
});

onDragEnd?.();
}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

p4: 길어지는 로직은 함수로 따로 빼도 좋을 것 같아요~

Comment on lines +17 to +24
export default function ArrowHandles({
arrow,
selectedHandleIndex,
onHandleClick,
onStartDrag,
onControlPointDrag,
onEndDrag,
}: ArrowHandlesProps) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

<ArrowHandles />안에서 useArrowHandles()를 불러 가져오지 않고, props를 받아 활용하는 방식을 선택하신 이유가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

일단 관련 상태나 로직을 Canvas에서 관리하고 있고 Canvas 내의 다른 기능(키보드 Delete 등)에서 접근해야 하는데 내부에서 훅을 호출하면 접근이 안돼서 props로 넘기는 방식을 사용했습니다.

Copy link
Collaborator

@seorang42 seorang42 left a comment

Choose a reason for hiding this comment

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

직접 사용해봤는데 생각보다 완성도 높게 구현되어 있네요!

사용해보며 느꼈던 몇 가지 의견 및 질문 드려요.

  1. 화살표의 클릭 범위를 조금 넓혀도 좋을 것 같아요. 화살표의 몸통 너비만큼만 클릭 범위로 설정되다보니, 화이트보드를 축소했을 경우 화살표를 클릭하여 옮기는게 조금 어려웠던 것 같아요. 투명한 범위를 설정해서 살짝 주변을 클릭해도 선택되게 하면 좋을 것 같아요.
  2. 캔버스 요소에 대한 복사/붙여넣기/잘라내기는 이 PR에서는 고려되지 않을 예정인지 궁금합니다!
  3. 커서 / 펜 / 지우개 등 사용자 커서 타입에 대한 활성화 여부 또한 이번 PR에서는 고려되지 않는 것인지 궁금합니다!

고생 많으셨습니다!

Comment on lines +22 to +26
// 텍스트 렌더링
if (item.type === 'text') {
const textItem = item as TextItem;
return (
<Text
Copy link
Collaborator

Choose a reason for hiding this comment

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

P5: 나중에 타입이 많아지면 if문이 길어지지 않을까 생각이 드는데,
이후 함수로 분리 후, 객체를 사용해서 팩토리 패턴으로 컴포넌트를 렌더링해도 좋을 것 같아요!

// 시작점과 끝점을 제외한 중간점들만 추출
export function getControlPoints(points: number[]) {
const controlPoints = [];
for (let i = 2; i < points.length - 2; i += 2) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

P4: i = 2, i += 2가 어떤 의미를 가지는지 명확하지 않을 수 있겠다 생각이 들아요
의미를 가지는 상수로 정의해도 좋을 것 같다는 의견 드려요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵 해당 부분 상수로 분리하였습니다!

- 렌더시 hitStrokeWidth={30} 추가
- COORDS_PER_POINT: 각 점을 표현하는데 필요한 좌표수 이차원이므로 (x,y) 두 개
- START_POINT_OFFSET: 건너뛸 시작점  인덱스 기준 0,1 건너 뜀
- END_POINT_OFFSET 건너뛸 끝 점 , 인덱스 기준 -1, -2 포함 안함
@tjsdn052
Copy link
Collaborator Author

tjsdn052 commented Jan 8, 2026

@seorang42

  1. 클릭 범위의 경우 관련해서 각 노드에 줄 수 있는 속성(hitStrokeWidth)이 있어 화살표의 경우 범위를 조금 늘렸습니다.
  2. 키보드 단축키가 들어가다 보니 범위가 조금 애매해졌는데 해당 부분의 경우 사용자 편의 부분으로 분리하여 진행하여하였습니다.
  3. 커서 타입의 경우 먼저 요소 추가(도형, 이미지 등)이 이루어진 다음 진행하려했는데 조금 앞당겨도 될거같아요

결론적으로 2,3 번의 경우 지금 pr 범위에서는 고려하지 않을 부분이었습니다!!

@tjsdn052 tjsdn052 merged commit fcfdda5 into dev Jan 8, 2026
2 checks passed
@tjsdn052 tjsdn052 deleted the feature/#166/workspace-canvas branch January 16, 2026 02:01
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.

[FE] 워크스페이스 캔버스 구현

4 participants