@deepnoid/canvas 0.1.84 → 0.1.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,197 @@
1
+ # 컴포넌트 API 레퍼런스
2
+
3
+ ## AnnotationEditor
4
+
5
+ 이미지 위에 어노테이션을 생성하고 편집할 수 있는 컴포넌트.
6
+
7
+ ### Import
8
+
9
+ ```tsx
10
+ import { AnnotationEditor } from '@deepnoid/canvas';
11
+ import type { AnnotationCanvasHandle } from '@deepnoid/canvas';
12
+ ```
13
+
14
+ ### Props
15
+
16
+ | Prop | Type | Required | Default | 설명 |
17
+ |------|------|----------|---------|------|
18
+ | `image` | `string` | ✅ | — | 이미지 URL |
19
+ | `drawing` | `AnnotationCanvasDrawing` | ✅ | — | 그리기 모드 및 스타일 설정 |
20
+ | `events` | `AnnotationCanvasEvents` | ✅ | — | 이미지 로드 이벤트 핸들러 (빈 객체 가능) |
21
+ | `annotations` | `Annotation[]` | — | `[]` | 어노테이션 목록 |
22
+ | `setAnnotations` | `Dispatch<SetStateAction<Annotation[] \| undefined>>` | — | — | 어노테이션 변경 콜백 |
23
+ | `options` | `AnnotationCanvasOptions` | — | — | 줌/팬 및 기타 옵션 |
24
+ | `enableHotkeys` | `boolean` | — | `true` | 단축키 활성화 여부 |
25
+ | `keepCrossOnImageChange` | `boolean` | — | `false` | 이미지 변경 시 가이드 십자선 유지 |
26
+
27
+ ### ref (AnnotationCanvasHandle)
28
+
29
+ ```tsx
30
+ const canvasRef = useRef<AnnotationCanvasHandle>(null);
31
+ <AnnotationEditor ref={canvasRef} ... />
32
+ ```
33
+
34
+ | Method | 설명 |
35
+ |--------|------|
36
+ | `resetImageSize()` | 캔버스 크기 및 줌을 초기 상태로 리셋 |
37
+
38
+ ### 전체 사용 예시
39
+
40
+ ```tsx
41
+ import { useState, useRef } from 'react';
42
+ import { AnnotationEditor, DrawMode } from '@deepnoid/canvas';
43
+ import type { Annotation, ApplyAnnotationStyle, AnnotationCanvasHandle } from '@deepnoid/canvas';
44
+
45
+ const applyStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
46
+ if (variant === 'drawRect') {
47
+ context.strokeStyle = annotation.selected ? '#00FF00' : '#FF4136';
48
+ context.lineWidth = drawLineSize / zoom;
49
+ if (annotation.type === DrawMode.RECTANGLE) {
50
+ context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
51
+ } else if (annotation.type === DrawMode.POLYGON) {
52
+ context.beginPath();
53
+ annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
54
+ context.closePath();
55
+ context.stroke();
56
+ }
57
+ }
58
+ if (variant === 'drawText') {
59
+ if (annotation.label) {
60
+ context.fillStyle = '#FF4136';
61
+ context.font = `bold ${14 / zoom}px sans-serif`;
62
+ const tx = annotation.type === DrawMode.RECTANGLE ? annotation.x : Math.min(...annotation.points.map(p => p.x));
63
+ const ty = (annotation.type === DrawMode.RECTANGLE ? annotation.y : Math.min(...annotation.points.map(p => p.y))) - 5;
64
+ context.fillText(annotation.label.name, tx, ty);
65
+ }
66
+ }
67
+ };
68
+
69
+ function MyEditor() {
70
+ const canvasRef = useRef<AnnotationCanvasHandle>(null);
71
+ const [annotations, setAnnotations] = useState<Annotation[]>([]);
72
+ const [mode, setMode] = useState(DrawMode.RECTANGLE);
73
+
74
+ return (
75
+ <div style={{ width: '100%', height: '100vh' }}>
76
+ <AnnotationEditor
77
+ ref={canvasRef}
78
+ image="https://example.com/image.jpg"
79
+ annotations={annotations}
80
+ setAnnotations={setAnnotations}
81
+ drawing={{
82
+ mode,
83
+ color: '#FF4136',
84
+ lineSize: 2,
85
+ applyStyle,
86
+ label: { id: 1, name: 'defect', type: 'bbox' },
87
+ }}
88
+ events={{
89
+ onImageLoadSuccess: () => console.log('loaded'),
90
+ onImageLoadError: (err) => console.error(err),
91
+ }}
92
+ options={{
93
+ panZoomEnabled: true,
94
+ zoom: { min: 0.5, max: 4, step: 0.9 },
95
+ }}
96
+ enableHotkeys
97
+ />
98
+ </div>
99
+ );
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## AnnotationViewer
106
+
107
+ 이미지 위에 어노테이션을 읽기 전용으로 표시하는 컴포넌트.
108
+
109
+ ### Import
110
+
111
+ ```tsx
112
+ import { AnnotationViewer } from '@deepnoid/canvas';
113
+ ```
114
+
115
+ ### Props
116
+
117
+ | Prop | Type | Required | Default | 설명 |
118
+ |------|------|----------|---------|------|
119
+ | `image` | `string` | ✅ | — | 이미지 URL |
120
+ | `drawing` | `Pick<AnnotationCanvasDrawing, 'lineSize' \| 'applyStyle'>` | ✅ | — | 렌더링 스타일 설정 (`mode`, `color`, `label` 불필요) |
121
+ | `events` | `Pick<AnnotationCanvasEvents, 'onImageLoadSuccess' \| 'onImageLoadError'>` | ✅ | — | 이미지 로드 이벤트 핸들러 (빈 객체 가능) |
122
+ | `annotations` | `Annotation[]` | — | `[]` | 표시할 어노테이션 목록 |
123
+ | `options` | `AnnotationCanvasOptions` | — | — | 줌/팬 및 기타 옵션 |
124
+ | `enableHotkeys` | `boolean` | — | `true` | 단축키 활성화 여부 |
125
+
126
+ > **AnnotationEditor와의 차이**: `drawing`에서 `mode`, `color`, `label`이 제외됩니다. `setAnnotations`도 불필요합니다. `keepCrossOnImageChange`도 없습니다.
127
+
128
+ ### 전체 사용 예시
129
+
130
+ ```tsx
131
+ import { AnnotationViewer } from '@deepnoid/canvas';
132
+
133
+ function MyViewer({ annotations }) {
134
+ return (
135
+ <div style={{ width: '100%', height: 600 }}>
136
+ <AnnotationViewer
137
+ image="https://example.com/image.jpg"
138
+ annotations={annotations}
139
+ drawing={{
140
+ lineSize: 2,
141
+ applyStyle, // Editor와 동일한 함수 재사용 가능
142
+ }}
143
+ events={{
144
+ onImageLoadSuccess: () => console.log('loaded'),
145
+ }}
146
+ options={{
147
+ panZoomEnabled: true,
148
+ zoom: { min: 0.5, max: 4 },
149
+ }}
150
+ />
151
+ </div>
152
+ );
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Props 상세 타입
159
+
160
+ ### AnnotationCanvasDrawing
161
+
162
+ ```typescript
163
+ type AnnotationCanvasDrawing = {
164
+ lineSize: number; // 선 두께 (필수)
165
+ applyStyle: ApplyAnnotationStyle; // 커스텀 렌더링 함수 (필수)
166
+ label?: Label; // 새 어노테이션에 부여할 라벨 (Editor 전용)
167
+ mode?: DrawMode; // 그리기 모드 (Editor 전용)
168
+ color?: string; // HEX 색상, 새 어노테이션 그리기 시 사용 (Editor 전용)
169
+ };
170
+ ```
171
+
172
+ ### AnnotationCanvasOptions
173
+
174
+ ```typescript
175
+ type AnnotationCanvasOptions = {
176
+ panZoomEnabled?: boolean; // 줌/팬 활성화 (기본: false)
177
+ zoom?: {
178
+ min?: number; // 최소 줌 (기본: 0.1)
179
+ max?: number; // 최대 줌 (기본: Infinity)
180
+ step?: number; // 줌 단계 (기본: 0.9)
181
+ };
182
+ ZoomButton?: ComponentType<{ // 커스텀 줌 버튼
183
+ onClick: () => void;
184
+ children: ReactNode;
185
+ }>;
186
+ resetOnImageChange?: boolean; // 이미지 변경 시 줌 리셋 (기본: false)
187
+ };
188
+ ```
189
+
190
+ ### AnnotationCanvasEvents
191
+
192
+ ```typescript
193
+ type AnnotationCanvasEvents = {
194
+ onImageLoadSuccess?: () => void;
195
+ onImageLoadError?: (error: Error) => void;
196
+ };
197
+ ```
@@ -0,0 +1,231 @@
1
+ # 타입 API 레퍼런스
2
+
3
+ 모든 public 타입은 `@deepnoid/canvas`에서 직접 import합니다.
4
+
5
+ ## Import
6
+
7
+ ```tsx
8
+ import { DrawMode } from '@deepnoid/canvas';
9
+ import type {
10
+ Annotation,
11
+ Rectangle,
12
+ Label,
13
+ ApplyAnnotationStyle,
14
+ AnnotationCanvasHandle,
15
+ } from '@deepnoid/canvas';
16
+ ```
17
+
18
+ ---
19
+
20
+ ## DrawMode (Enum)
21
+
22
+ 어노테이션 그리기 모드를 지정하는 enum.
23
+
24
+ ```typescript
25
+ enum DrawMode {
26
+ RECTANGLE = 'RECTANGLE',
27
+ POLYGON = 'POLYGON',
28
+ }
29
+ ```
30
+
31
+ ### 사용 예시
32
+
33
+ ```tsx
34
+ import { DrawMode } from '@deepnoid/canvas';
35
+
36
+ // drawing.mode에 사용
37
+ <AnnotationEditor drawing={{ mode: DrawMode.RECTANGLE, ... }} />
38
+
39
+ // 어노테이션 타입 판별
40
+ if (annotation.type === DrawMode.RECTANGLE) {
41
+ // RectangleAnnotation으로 좁혀짐
42
+ console.log(annotation.x, annotation.y, annotation.width, annotation.height);
43
+ } else if (annotation.type === DrawMode.POLYGON) {
44
+ // PolygonAnnotation으로 좁혀짐
45
+ console.log(annotation.points);
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Annotation (Union Type)
52
+
53
+ ```typescript
54
+ type Annotation = RectangleAnnotation | PolygonAnnotation;
55
+ ```
56
+
57
+ `type` 필드로 discriminated union 판별.
58
+
59
+ ### RectangleAnnotation
60
+
61
+ ```typescript
62
+ type RectangleAnnotation = {
63
+ type: DrawMode.RECTANGLE;
64
+ x: number; // 좌상단 X (원본 이미지 px)
65
+ y: number; // 좌상단 Y (원본 이미지 px)
66
+ width: number; // 너비 (원본 이미지 px)
67
+ height: number; // 높이 (원본 이미지 px)
68
+ label?: Label; // 라벨 정보
69
+ color?: 'success' | 'warning' | 'danger'; // 상태 색상
70
+ selected?: boolean; // 선택 상태 (엔진 내부 관리)
71
+ showOnlySelected?: boolean;
72
+ };
73
+ ```
74
+
75
+ ### PolygonAnnotation
76
+
77
+ ```typescript
78
+ type PolygonAnnotation = {
79
+ type: DrawMode.POLYGON;
80
+ points: { x: number; y: number }[]; // 꼭짓점 배열 (원본 이미지 px)
81
+ label?: Label;
82
+ color?: 'success' | 'warning' | 'danger';
83
+ selected?: boolean;
84
+ showOnlySelected?: boolean;
85
+ };
86
+ ```
87
+
88
+ ### 데이터 예시
89
+
90
+ ```typescript
91
+ // RectangleAnnotation
92
+ const rect: Annotation = {
93
+ type: DrawMode.RECTANGLE,
94
+ x: 100,
95
+ y: 50,
96
+ width: 200,
97
+ height: 150,
98
+ label: { id: 1, name: 'defect', type: 'bbox' },
99
+ color: 'danger',
100
+ };
101
+
102
+ // PolygonAnnotation
103
+ const polygon: Annotation = {
104
+ type: DrawMode.POLYGON,
105
+ points: [
106
+ { x: 100, y: 100 },
107
+ { x: 250, y: 80 },
108
+ { x: 300, y: 200 },
109
+ { x: 150, y: 250 },
110
+ ],
111
+ label: { id: 2, name: 'crack', type: 'polygon' },
112
+ color: 'warning',
113
+ };
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Rectangle (Type)
119
+
120
+ 사각형의 기하 정보만 담는 순수 타입. 어노테이션이 아닌 좌표 계산 등에 활용.
121
+
122
+ ```typescript
123
+ type Rectangle = {
124
+ x: number;
125
+ y: number;
126
+ width: number;
127
+ height: number;
128
+ };
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Label (Type)
134
+
135
+ 어노테이션에 부여하는 라벨 정보.
136
+
137
+ ```typescript
138
+ type Label = {
139
+ id: number; // 라벨 고유 ID
140
+ name: string; // 표시 이름 (applyStyle의 drawText에서 사용)
141
+ type: string; // 라벨 분류 (자유 문자열, 예: 'bbox', 'polygon', 'seed')
142
+ };
143
+ ```
144
+
145
+ ### 사용 예시
146
+
147
+ ```tsx
148
+ const labels: Label[] = [
149
+ { id: 1, name: '결함', type: 'defect' },
150
+ { id: 2, name: '균열', type: 'crack' },
151
+ { id: 3, name: '정상', type: 'normal' },
152
+ ];
153
+
154
+ // drawing.label에 설정하면 새로 그리는 어노테이션에 자동 부여
155
+ <AnnotationEditor
156
+ drawing={{
157
+ mode: DrawMode.RECTANGLE,
158
+ label: labels[0],
159
+ lineSize: 2,
160
+ applyStyle,
161
+ }}
162
+ ...
163
+ />
164
+ ```
165
+
166
+ ---
167
+
168
+ ## ApplyAnnotationStyle (Type)
169
+
170
+ 어노테이션 렌더링 스타일을 완전 제어하는 콜백 함수 타입.
171
+
172
+ ```typescript
173
+ type ApplyAnnotationStyle = (params: ApplyAnnotationStyleParams) => void;
174
+
175
+ type ApplyAnnotationStyleParams = {
176
+ variant: 'drawRect' | 'drawText'; // 현재 렌더링 단계
177
+ context: CanvasRenderingContext2D; // Canvas 2D 컨텍스트
178
+ annotation: Annotation; // 렌더링 대상 어노테이션
179
+ drawLineSize: number; // 설정된 선 두께 (현재 2px 고정)
180
+ zoom: number; // 현재 줌 배율
181
+ };
182
+ ```
183
+
184
+ ### variant 설명
185
+
186
+ | variant | 호출 시점 | 설정해야 할 Canvas 속성 |
187
+ |---------|----------|------------------------|
188
+ | `'drawRect'` | 도형 렌더링 | `strokeStyle`, `fillStyle`, `lineWidth`, `setLineDash` + 실제 도형 그리기 (`strokeRect`, `beginPath`/`stroke` 등) |
189
+ | `'drawText'` | 라벨 텍스트 렌더링 | `fillStyle`, `font`, `textBaseline` + 실제 텍스트 그리기 (`fillText`) |
190
+
191
+ 상세 예제는 [apply-style.md](apply-style.md) 참조.
192
+
193
+ ---
194
+
195
+ ## AnnotationCanvasHandle (Interface)
196
+
197
+ `ref`를 통해 캔버스를 프로그래밍 방식으로 제어.
198
+
199
+ ```typescript
200
+ interface AnnotationCanvasHandle {
201
+ resetImageSize(): void; // 캔버스 크기 및 줌을 초기 상태로 리셋
202
+ }
203
+ ```
204
+
205
+ ### 사용 예시
206
+
207
+ ```tsx
208
+ import { useRef } from 'react';
209
+ import type { AnnotationCanvasHandle } from '@deepnoid/canvas';
210
+
211
+ const ref = useRef<AnnotationCanvasHandle>(null);
212
+
213
+ // 줌 리셋 버튼
214
+ <button onClick={() => ref.current?.resetImageSize()}>초기화</button>
215
+ <AnnotationEditor ref={ref} ... />
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 전체 Export 요약
221
+
222
+ | Export | Kind | import 방식 |
223
+ |--------|------|------------|
224
+ | `AnnotationEditor` | Component | `import { AnnotationEditor } from '@deepnoid/canvas'` |
225
+ | `AnnotationViewer` | Component | `import { AnnotationViewer } from '@deepnoid/canvas'` |
226
+ | `DrawMode` | Enum (value) | `import { DrawMode } from '@deepnoid/canvas'` |
227
+ | `Annotation` | Type | `import type { Annotation } from '@deepnoid/canvas'` |
228
+ | `Rectangle` | Type | `import type { Rectangle } from '@deepnoid/canvas'` |
229
+ | `Label` | Type | `import type { Label } from '@deepnoid/canvas'` |
230
+ | `ApplyAnnotationStyle` | Type | `import type { ApplyAnnotationStyle } from '@deepnoid/canvas'` |
231
+ | `AnnotationCanvasHandle` | Interface | `import type { AnnotationCanvasHandle } from '@deepnoid/canvas'` |
@@ -0,0 +1,152 @@
1
+ # 어노테이션 데이터 규칙
2
+
3
+ ## 규칙 1: 모든 어노테이션에 type 필드가 필수다
4
+
5
+ `Annotation` 타입은 discriminated union이므로, `type` 필드가 없으면 타입 추론이 동작하지 않습니다.
6
+
7
+ ```tsx
8
+ // ✅ 올바른 사용 — type 필드 포함
9
+ const rect: Annotation = {
10
+ type: DrawMode.RECTANGLE,
11
+ x: 100,
12
+ y: 50,
13
+ width: 200,
14
+ height: 150,
15
+ };
16
+
17
+ const polygon: Annotation = {
18
+ type: DrawMode.POLYGON,
19
+ points: [
20
+ { x: 100, y: 100 },
21
+ { x: 200, y: 100 },
22
+ { x: 150, y: 200 },
23
+ ],
24
+ };
25
+
26
+ // ❌ 잘못된 사용 — type 누락
27
+ const rect = { x: 100, y: 50, width: 200, height: 150 };
28
+ ```
29
+
30
+ ## 규칙 2: DrawMode enum을 사용해야 한다
31
+
32
+ 문자열 리터럴 대신 반드시 `DrawMode` enum을 사용합니다.
33
+
34
+ ```tsx
35
+ import { DrawMode } from '@deepnoid/canvas';
36
+
37
+ // ✅ 올바른 사용
38
+ { type: DrawMode.RECTANGLE, ... }
39
+ { type: DrawMode.POLYGON, ... }
40
+
41
+ // ❌ 잘못된 사용 — 문자열 직접 사용 (타입 에러)
42
+ { type: 'RECTANGLE', ... }
43
+ { type: 'rectangle', ... }
44
+ ```
45
+
46
+ ## 규칙 3: 좌표는 원본 이미지 기준 픽셀 좌표다
47
+
48
+ 캔버스에 표시된 좌표가 아니라, 원본 이미지의 픽셀 좌표를 사용합니다. 줌/팬 변환은 엔진이 자동 처리합니다.
49
+
50
+ ```tsx
51
+ // ✅ 올바른 사용 — 원본 이미지 좌표
52
+ // 원본 이미지가 1920x1080일 때:
53
+ { type: DrawMode.RECTANGLE, x: 500, y: 300, width: 200, height: 150 }
54
+
55
+ // ❌ 잘못된 사용 — 화면 표시 좌표를 그대로 사용
56
+ // 캔버스가 50%로 축소되어 있어도 원본 좌표를 사용해야 함
57
+ ```
58
+
59
+ ## 규칙 4: RectangleAnnotation의 width/height는 양수여야 한다
60
+
61
+ 사용자가 역방향으로 드래그하면 엔진이 자동 정규화하지만, 외부에서 직접 생성 시 양수 값이어야 합니다.
62
+
63
+ ```tsx
64
+ // ✅ 올바른 사용
65
+ { type: DrawMode.RECTANGLE, x: 100, y: 100, width: 200, height: 150 }
66
+
67
+ // ❌ 잘못된 사용 — 음수 크기
68
+ { type: DrawMode.RECTANGLE, x: 300, y: 250, width: -200, height: -150 }
69
+ ```
70
+
71
+ ## 규칙 5: PolygonAnnotation의 points는 최소 3개여야 한다
72
+
73
+ 유효한 폴리곤을 형성하려면 최소 3개의 점이 필요합니다.
74
+
75
+ ```tsx
76
+ // ✅ 올바른 사용 — 3개 이상의 점
77
+ {
78
+ type: DrawMode.POLYGON,
79
+ points: [
80
+ { x: 100, y: 100 },
81
+ { x: 200, y: 100 },
82
+ { x: 150, y: 200 },
83
+ ],
84
+ }
85
+
86
+ // ❌ 잘못된 사용 — 2개 이하의 점
87
+ {
88
+ type: DrawMode.POLYGON,
89
+ points: [
90
+ { x: 100, y: 100 },
91
+ { x: 200, y: 100 },
92
+ ],
93
+ }
94
+ ```
95
+
96
+ ## 규칙 6: annotations 상태 관리는 React state로 한다
97
+
98
+ `setAnnotations` 콜백으로 어노테이션이 변경될 때 외부 상태와 동기화됩니다.
99
+
100
+ ```tsx
101
+ // ✅ 올바른 사용 — React state 관리
102
+ const [annotations, setAnnotations] = useState<Annotation[]>([]);
103
+
104
+ <AnnotationEditor
105
+ annotations={annotations}
106
+ setAnnotations={setAnnotations}
107
+ ...
108
+ />
109
+
110
+ // ❌ 잘못된 사용 — ref나 외부 변수로 관리
111
+ const annotationsRef = useRef<Annotation[]>([]);
112
+ <AnnotationEditor
113
+ annotations={annotationsRef.current}
114
+ setAnnotations={(a) => { annotationsRef.current = a; }}
115
+ ...
116
+ />
117
+ // → 리렌더링이 발생하지 않아 UI가 동기화되지 않음
118
+ ```
119
+
120
+ ## 규칙 7: selected 필드를 외부에서 직접 조작하지 않는다
121
+
122
+ 어노테이션 선택 상태는 엔진이 내부적으로 관리합니다. 외부에서 `selected: true`를 설정해도 동작하지만, 엔진과 충돌할 수 있습니다.
123
+
124
+ ```tsx
125
+ // ✅ 올바른 사용 — 선택은 캔버스 내 클릭으로
126
+ // 사용자가 캔버스에서 어노테이션을 클릭하면 자동으로 selected 처리됨
127
+
128
+ // ⚠️ 주의 — 외부에서 selected 설정
129
+ setAnnotations(prev => prev.map(a =>
130
+ a === target ? { ...a, selected: true } : { ...a, selected: false }
131
+ ));
132
+ // 가능하지만, setAnnotations가 다시 엔진에 전달되면서 불필요한 리렌더링 발생 가능
133
+ ```
134
+
135
+ ## 규칙 8: label은 선택적이지만, 텍스트 표시에 필요하다
136
+
137
+ `label` 필드가 없으면 `drawText` variant에서 표시할 텍스트가 없습니다.
138
+
139
+ ```tsx
140
+ // ✅ 텍스트 라벨이 필요한 경우
141
+ {
142
+ type: DrawMode.RECTANGLE,
143
+ x: 100, y: 100, width: 200, height: 150,
144
+ label: { id: 1, name: 'defect', type: 'bbox' },
145
+ }
146
+
147
+ // ✅ 텍스트 라벨이 불필요한 경우 — label 생략 가능
148
+ {
149
+ type: DrawMode.RECTANGLE,
150
+ x: 100, y: 100, width: 200, height: 150,
151
+ }
152
+ ```
@@ -0,0 +1,124 @@
1
+ # 이벤트 및 단축키 규칙
2
+
3
+ ## 규칙 1: annotationSelected 이벤트로 DrawMode 동기화
4
+
5
+ 캔버스에서 어노테이션을 클릭하면 `window`에 `annotationSelected` CustomEvent가 발생합니다.
6
+ 이를 통해 외부 UI의 그리기 모드를 자동 전환할 수 있습니다.
7
+
8
+ ```tsx
9
+ // ✅ 올바른 사용 — annotationSelected 이벤트 리스닝
10
+ useEffect(() => {
11
+ const handler = (e: CustomEvent) => {
12
+ if (e.detail?.type) {
13
+ setDrawMode(e.detail.type); // DrawMode.RECTANGLE 또는 DrawMode.POLYGON
14
+ }
15
+ };
16
+ window.addEventListener('annotationSelected', handler);
17
+ return () => window.removeEventListener('annotationSelected', handler);
18
+ }, []);
19
+
20
+ // ❌ 잘못된 사용 — 이벤트 클린업 누락
21
+ useEffect(() => {
22
+ window.addEventListener('annotationSelected', handler);
23
+ // return 없음 → 메모리 누수
24
+ }, []);
25
+ ```
26
+
27
+ ## 규칙 2: enableHotkeys를 명시적으로 설정한다
28
+
29
+ 단축키는 기본적으로 비활성화되어 있습니다 (`enableHotkeys` 기본값: `true` in Editor, 단 명시적 설정 권장).
30
+
31
+ ```tsx
32
+ // ✅ 올바른 사용 — 명시적 설정
33
+ <AnnotationEditor enableHotkeys {...otherProps} />
34
+
35
+ // ✅ 단축키 충돌이 있는 경우 비활성화
36
+ <AnnotationEditor enableHotkeys={false} {...otherProps} />
37
+ ```
38
+
39
+ ### 지원 단축키
40
+
41
+ | 키 | 기능 | 조건 |
42
+ |---|---|---|
43
+ | `Delete` | 선택된 어노테이션 삭제 | 어노테이션 선택 상태일 때 |
44
+ | `Ctrl+Z` (Mac: `Cmd+Z`) | Undo | 히스토리가 있을 때 |
45
+ | `Ctrl+Shift+Z` (Mac: `Cmd+Shift+Z`) | Redo | undo 후 히스토리가 있을 때 |
46
+ | `X` | 선택 어노테이션만 보기 토글 | 언제든 |
47
+ | `Escape` | 진행 중인 그리기 취소 | 폴리곤 점 찍기 중 등 |
48
+
49
+ ## 규칙 3: 이미지 로드 이벤트를 활용한다
50
+
51
+ `events` prop으로 이미지 로드 성공/실패를 감지할 수 있습니다.
52
+
53
+ ```tsx
54
+ // ✅ 올바른 사용 — 로드 상태 관리
55
+ const [imageLoaded, setImageLoaded] = useState(false);
56
+
57
+ <AnnotationEditor
58
+ image={imageUrl}
59
+ events={{
60
+ onImageLoadSuccess: () => setImageLoaded(true),
61
+ onImageLoadError: (error) => {
62
+ console.error('이미지 로드 실패:', error);
63
+ setImageLoaded(false);
64
+ },
65
+ }}
66
+ ...
67
+ />
68
+
69
+ // ❌ 잘못된 사용 — events를 undefined로 전달
70
+ <AnnotationEditor
71
+ events={undefined} // 타입 에러: events는 필수 prop
72
+ ...
73
+ />
74
+
75
+ // ✅ 이벤트가 필요 없으면 빈 객체 전달
76
+ <AnnotationEditor events={{}} ... />
77
+ ```
78
+
79
+ ## 규칙 4: 단축키와 외부 키 이벤트의 충돌에 주의한다
80
+
81
+ `enableHotkeys`가 켜져 있으면 `window` 레벨에서 키 이벤트를 감지합니다.
82
+ 외부에서 같은 키(Delete, X, Ctrl+Z)를 사용하는 경우 충돌이 발생할 수 있습니다.
83
+
84
+ ```tsx
85
+ // ✅ 충돌 방지 — 캔버스 영역에서만 단축키 활성화
86
+ // enableHotkeys를 조건부로 제어
87
+ const [canvasFocused, setCanvasFocused] = useState(false);
88
+
89
+ <div
90
+ onFocus={() => setCanvasFocused(true)}
91
+ onBlur={() => setCanvasFocused(false)}
92
+ >
93
+ <AnnotationEditor
94
+ enableHotkeys={canvasFocused}
95
+ ...
96
+ />
97
+ </div>
98
+
99
+ // ❌ 잘못된 사용 — 폼 입력이 있는 페이지에서 항상 enableHotkeys
100
+ // → Delete 키로 텍스트 삭제 시 어노테이션도 삭제됨
101
+ ```
102
+
103
+ ## 규칙 5: setAnnotations 콜백은 비동기적으로 호출된다
104
+
105
+ 엔진 내부에서 어노테이션이 변경되면 `setAnnotations` 콜백이 호출됩니다.
106
+ 이 콜백 내에서 무거운 로직을 실행하면 성능에 영향을 줄 수 있습니다.
107
+
108
+ ```tsx
109
+ // ✅ 올바른 사용 — 가벼운 상태 업데이트만
110
+ <AnnotationEditor
111
+ setAnnotations={setAnnotations}
112
+ ...
113
+ />
114
+
115
+ // ⚠️ 주의 — 콜백 내 무거운 로직
116
+ <AnnotationEditor
117
+ setAnnotations={(annotations) => {
118
+ setAnnotations(annotations);
119
+ // 매 마우스 이동마다 호출될 수 있어 성능 이슈
120
+ saveToServer(annotations); // ← debounce 권장
121
+ }}
122
+ ...
123
+ />
124
+ ```