Repository navigation
Expand file tree
/
Copy pathscript.js
More file actions
1932 lines (1681 loc) · 91.1 KB
/
Copy pathscript.js
File metadata and controls
1932 lines (1681 loc) · 91.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ============================================
// InMyRoom - 방 인테리어 시뮬레이터 애플리케이션
// 사용자가 웹 브라우저에서 가구를 배치하고 방을 꾸밀 수 있는 애플리케이션
// ============================================
// 상수 정의 - 애플리케이션 전체에서 사용할 고정값들을 한 곳에 모아 관리
// 이렇게 상수를 분리하면 나중에 값을 변경할 때 한 곳에서만 수정하면 됨
const CONSTANTS = {
FLOOR_HEIGHT: 120, // 바닥의 높이(픽셀) - 가구 배치 시 바닥 영역 계산에 사용
MIN_FURNITURE_SIZE: 20, // 가구의 최소 크기(픽셀) - 가구가 너무 작아져서 보이지 않는 것을 방지
MAX_FURNITURE_SIZE: 300, // 가구의 최대 크기(픽셀) - 가구가 너무 커져서 화면을 벗어나는 것을 방지
ROTATION_STEP: 15, // 회전 시 각도 단위(도) - 가구 회전 시 15도씩 회전하여 정확한 배치 가능
MOVEMENT_STEP: 1, // 일반 이동 시 픽셀 단위 - 키보드로 가구를 미세 조정할 때 사용
FAST_MOVEMENT_STEP: 10, // 빠른 이동 시 픽셀 단위 - Shift 키와 함께 사용할 때 빠른 이동
RESIZE_STEP: 10, // 크기 조절 시 픽셀 단위 - 가구 크기를 늘리거나 줄일 때의 변화량
CANVAS_PADDING: 5, // 캔버스 가장자리 여백 - 가구가 경계에 너무 붙지 않도록 하는 안전 영역
FLOOR_MARGIN: 5, // 바닥과의 여백 - 가구가 바닥에 너무 붙지 않도록 하는 공간
MAX_Z_INDEX: 10000 // 최대 z-index 값 - 가구의 앞뒤 배치 순서 관리의 시작점
};
// 바닥 스타일 정의
const FLOOR_STYLES = {
// 기본 나무 바닥 스타일
default: {
backgroundColor: '#deb887', // 연한 갈색 배경
borderTop: '4px solid #bfa16a' // 상단에 어두운 갈색 테두리로 입체감 표현
},
// 어두운 나무 바닥 스타일
dark_wood: {
backgroundColor: '#8b4513', // 진한 갈색 배경
borderTop: '4px solid #654321' // 더 어두운 갈색 테두리로 깊이감 표현
},
// 타일 바닥 스타일
tile: {
backgroundColor: '#e0e0e0', // 밝은 회색 배경
borderTop: '4px solid #b0b0b0' // 어두운 회색 테두리로 타일 경계 표현
},
// 카펫 바닥 스타일
carpet: {
backgroundColor: '#8b0000', // 짙은 빨간색
borderTop: '4px solid #6b0000' // 더 어두운 빨간색 테두리
},
// 대리석 바닥 스타일
marble: {
backgroundColor: '#f8f8f8', // 거의 흰
borderTop: '4px solid #d0d0d0' // 연한 회색 테두리로 경계 표현
}
};
// 에러 핸들링 유틸리티 클래스 - 애플리케이션 전체의 오류를 일관성 있게 처리
// 정적 클래스로 구현하여 어디서든 쉽게 오류 처리 가능
class ErrorHandler {
// 오류 발생 시 호출되는 정적 메서드 - 개발자와 사용자 모두를 위한 오류 처리
static handle(error, context = 'Unknown') {
// 콘솔에 오류 정보를 출력 (개발자를 위한 디버깅 정보)
// context를 포함하여 어느 부분에서 오류가 발생했는지 명확히 표시
console.error(`[${context}] 오류:`, error);
// 사용자에게 친화적인 오류 메시지 표시 (기술적 세부사항 숨김)
this.showUserError(`${context}에서 오류가 발생했습니다: ${error.message}`);
}
// 사용자에게 오류 메시지를 표시하는 메서드
// 접근성을 고려하여 스크린 리더 사용자도 오류를 인지할 수 있도록 구현
static showUserError(message) {
// 스크린 리더 사용자를 위한 접근성 영역에 메시지 전달
// aria-live 속성을 가진 요소에 텍스트를 넣으면 자동으로 읽어줌
const liveRegion = document.getElementById('a11y-live');
if (liveRegion) {
liveRegion.textContent = message;
}
// 콘솔에도 오류 메시지 출력 (개발자가 브라우저 콘솔에서 확인 가능)
console.error(message);
}
// 성공 메시지를 표시하는 메서드 - 사용자 액션이 성공했을 때 피드백 제공
static showSuccess(message) {
// 접근성 영역을 통해 성공 메시지 전달 (스크린 리더가 읽어줌)
const liveRegion = document.getElementById('a11y-live');
if (liveRegion) {
liveRegion.textContent = message;
}
}
}
// 상태 관리 클래스 - 애플리케이션의 모든 상태 정보를 중앙에서 관리
// MVC 패턴의 Model 역할을 하며, 데이터의 일관성을 보장
class AppState {
constructor() {
// Map을 사용하여 가구 정보를 효율적으로 저장 (ID를 키로 사용)
// Array 대신 Map을 사용하는 이유: ID로 빠른 검색이 가능하고, 삭제/추가가 효율적
this.furniture = new Map();
// 현재 선택된 가구 참조 (null이면 선택된 가구 없음)
// 하나의 가구만 선택 가능하도록 제한
this.selectedFurniture = null;
// 현재 드래그 중인지 여부를 나타내는 플래그
// 드래그 중일 때 다른 상호작용을 방지하기 위해 사용
this.isDragging = false;
// 드래그 모드 ('new': 새 가구 배치, 'move': 기존 가구 이동)
// 드래그 동작을 구분하여 적절한 처리를 하기 위함
this.dragMode = null;
// 가구 생성 시 고유 ID 생성을 위한 카운터
// 가구가 삭제되어도 ID 중복을 방지하기 위해 계속 증가
this.furnitureCounter = 0;
// 현재 최대 z-index 값 (가구의 앞뒤 순서 관리)
// 새 가구를 항상 가장 앞에 배치하기 위해 추적
this.maxZIndex = CONSTANTS.MAX_Z_INDEX;
// 배경 설정 정보 저장 객체
this.background = {
type: '', // 배경 타입 (색상 코드 또는 'upload')
imageData: null // 업로드된 이미지 데이터 (Base64 인코딩된 문자열)
};
// 바닥 설정 정보 저장 객체
this.floor = {
type: 'default', // 바닥 타입 (미리 정의된 스타일 또는 'upload')
imageData: null // 업로드된 바닥 이미지 데이터 (Base64 인코딩된 문자열)
};
}
// 새 가구를 Map에 추가하는 메서드
// ID와 가구 DOM 요소를 연결하여 추후 관리가 용이하도록 함
addFurniture(id, furniture) {
this.furniture.set(id, furniture);
}
// 가구를 Map에서 제거하는 메서드
// 가구 삭제 시 메모리에서도 완전히 제거하여 메모리 누수 방지
removeFurniture(id) {
this.furniture.delete(id);
}
// ID로 특정 가구를 조회하는 메서드
// 빠른 검색을 위해 Map의 get 메서드 활용
getFurniture(id) {
return this.furniture.get(id);
}
// 모든 가구를 배열로 반환하는 메서드
// 전체 가구에 대한 일괄 처리가 필요할 때 사용 (예: 저장, 삭제 등)
getAllFurniture() {
return Array.from(this.furniture.values());
}
// 선택된 가구를 설정하는 메서드
// 가구 선택 상태를 중앙에서 관리하여 일관성 보장
setSelectedFurniture(furniture) {
this.selectedFurniture = furniture;
}
// 현재 선택된 가구를 반환하는 메서드
// 다양한 컴포넌트에서 현재 선택 상태를 확인할 때 사용
getSelectedFurniture() {
return this.selectedFurniture;
}
// 새 가구의 고유 ID를 생성하는 메서드
// 'furniture-숫자' 형태로 생성하여 가독성과 고유성을 동시에 확보
getNextFurnitureId() {
return `furniture-${++this.furnitureCounter}`;
}
// 다음 z-index 값을 반환하는 메서드 (가구를 맨 앞으로 가져올 때 사용)
// 새로 생성되거나 선택된 가구를 다른 가구들보다 앞에 표시하기 위함
getNextZIndex() {
return ++this.maxZIndex;
}
}
// 바닥 관리 클래스 - 방의 바닥 스타일과 이미지를 관리
// 바닥은 시각적으로 공간감을 주는 중요한 요소로, 별도 클래스로 관리
class FloorManager {
constructor(state) {
this.state = state; // AppState 인스턴스 참조로 바닥 상태 정보 접근
this.floorElement = null; // DOM의 바닥 요소 참조 (캐싱으로 성능 향상)
}
// 바닥 관리자 초기화 - DOM 요소 찾기 및 기본 스타일 적용
init() {
// CSS 선택자로 바닥 요소 찾기 (.floor 클래스를 가진 요소)
this.floorElement = document.querySelector('.floor');
if (!this.floorElement) {
// 바닥 요소가 없으면 오류 발생 (필수 요소이므로 앱이 동작할 수 없음)
throw new Error('바닥 요소를 찾을 수 없습니다');
}
// 현재 상태의 바닥 타입으로 스타일 적용 (초기화 시 기본값 적용)
this.applyFloorStyle(this.state.floor.type);
}
// 바닥 스타일을 적용하는 핵심 메서드
// 다양한 바닥 타입을 지원하며, 이미지와 색상 모두 처리 가능
applyFloorStyle(floorType) {
if (!this.floorElement) return; // 바닥 요소가 없으면 조기 반환
try {
// 바닥의 기본 CSS 속성들을 설정 (위치, 크기, 레이어 등)
// Object.assign을 사용하여 여러 속성을 한 번에 설정 (성능상 유리)
Object.assign(this.floorElement.style, {
position: 'absolute', // 절대 위치 지정으로 정확한 배치
left: '0', // 왼쪽 끝에 배치
right: '0', // 오른쪽 끝에 배치 (width: 100%와 동일 효과)
bottom: '0', // 바닥에 배치 (방의 아래쪽)
width: '100%', // 전체 너비 사용
height: `${CONSTANTS.FLOOR_HEIGHT}px`, // 미리 정의된 높이 사용
borderRadius: '0 0 12px 12px', // 하단 모서리만 둥글게 (자연스러운 느낌)
zIndex: '2', // 가구보다는 뒤에, 배경보다는 앞에 배치
display: 'block', // 블록 요소로 표시
visibility: 'visible', // 화면에 표시
opacity: '1', // 완전 불투명
pointerEvents: 'none' // 마우스 이벤트 차단 (가구 선택 방해 방지)
});
// 이전 배경 스타일들을 모두 초기화 (깨끗한 상태에서 새 스타일 적용)
// 이전 설정이 새 설정과 충돌하는 것을 방지
this.floorElement.style.background = '';
this.floorElement.style.backgroundColor = '';
this.floorElement.style.backgroundImage = '';
// 바닥 타입에 따라 적절한 스타일 적용
if (floorType === 'upload' && this.state.floor.imageData) {
// 사용자가 업로드한 이미지 사용
this.applyImageFloor();
} else if (FLOOR_STYLES[floorType]) {
// 미리 정의된 스타일 사용 (default, dark_wood 등)
Object.assign(this.floorElement.style, FLOOR_STYLES[floorType]);
} else {
// 정의되지 않은 타입이면 기본 스타일 사용 (안전장치)
Object.assign(this.floorElement.style, FLOOR_STYLES.default);
}
// 바닥 요소에 타입 정보를 데이터 속성으로 저장 (CSS나 디버깅에서 활용)
this.floorElement.dataset.floorType = floorType;
// 상태 객체에도 바닥 타입 저장 (일관성 유지)
this.state.floor.type = floorType;
console.log(`바닥 스타일 적용 완료: ${floorType}`);
} catch (error) {
// 오류 발생 시 일관된 오류 처리
ErrorHandler.handle(error, '바닥 스타일 적용');
}
}
// 업로드된 이미지를 바닥에 적용하는 메서드
// 이미지 배경 처리는 별도 메서드로 분리하여 코드 가독성 향상
applyImageFloor() {
if (!this.state.floor.imageData) return; // 이미지 데이터가 없으면 조기 반환
// 이미지를 배경으로 설정하는 CSS 속성들
Object.assign(this.floorElement.style, {
backgroundImage: `url('${this.state.floor.imageData}')`, // Base64 이미지 데이터 사용
backgroundSize: 'cover', // 이미지가 요소 전체를 덮도록 (비율 유지하며 크기 조정)
backgroundPosition: 'center', // 이미지를 중앙에 배치
backgroundRepeat: 'no-repeat', // 이미지 반복 없음 (하나의 이미지만 표시)
borderTop: '4px solid #bfa16a' // 상단 테두리 (입체감 표현)
});
}
// 바닥 변경 요청을 처리하는 메서드
// 사용자가 UI에서 바닥을 변경할 때 호출되는 진입점
changeFloor(floorType) {
if (floorType === 'upload') {
// 'upload' 선택 시 파일 선택 다이얼로그 열기
// 숨겨진 파일 입력 요소를 프로그래밍적으로 클릭
document.getElementById('floorImageInput').click();
return; // 파일 선택 후 별도 처리되므로 여기서 종료
}
// 일반 바닥 타입이면 바로 스타일 적용
this.applyFloorStyle(floorType);
// UI 드롭다운도 함께 업데이트 (사용자 인터페이스 일관성 유지)
this.updateFloorSelect(floorType);
}
// 바닥 이미지 업로드를 처리하는 메서드
// 비동기 파일 읽기를 통해 이미지를 Base64로 변환
handleFloorImageUpload(file) {
if (!file) return; // 파일이 선택되지 않으면 조기 반환
// FileReader를 사용하여 파일을 Base64로 변환
// Base64를 사용하는 이유: 별도 서버 없이 브라우저에서 이미지 처리 가능
const reader = new FileReader();
reader.onload = (e) => {
try {
// 변환된 이미지 데이터를 상태에 저장
this.state.floor.imageData = e.target.result; // Base64 인코딩된 이미지 데이터
this.state.floor.type = 'upload';
// 업로드 타입으로 스타일 적용
this.applyFloorStyle('upload');
// 사용자에게 업로드 완료 피드백 제공
// 파일명을 표시하여 어떤 이미지가 업로드되었는지 알려줌
const feedbackSpan = document.getElementById('floorImageName');
if (feedbackSpan) {
feedbackSpan.textContent = `업로드됨: ${file.name}`;
}
// 접근성을 위한 성공 메시지 (스크린 리더 사용자 포함)
ErrorHandler.showSuccess('바닥 이미지가 업로드되었습니다');
} catch (error) {
ErrorHandler.handle(error, '바닥 이미지 업로드');
}
};
// 파일 읽기 실패 시 오류 처리
reader.onerror = () => {
ErrorHandler.handle(new Error('이미지 파일을 읽을 수 없습니다'), '바닥 이미지 업로드');
};
// 파일을 Data URL(Base64) 형태로 읽기 시작
reader.readAsDataURL(file);
}
// UI의 바닥 선택 드롭다운을 업데이트하는 메서드
// 프로그래밍적으로 바닥이 변경될 때 UI 동기화를 위해 필요
updateFloorSelect(value) {
const floorSelect = document.getElementById('floorSelect');
// 드롭다운이 존재하고 현재 값과 다를 때만 업데이트 (무한 루프 방지)
if (floorSelect && floorSelect.value !== value) {
floorSelect.value = value;
}
}
}
// 배경 관리 클래스 - 방의 배경색이나 배경 이미지를 관리
class BackgroundManager {
constructor(state) {
this.state = state; // AppState 인스턴스 참조
this.canvasElement = null; // DOM의 캔버스 요소 참조 (배경이 적용될 요소)
}
// 배경 관리자 초기화 - 캔버스 요소 찾기
init() {
this.canvasElement = document.getElementById('roomCanvas');
if (!this.canvasElement) {
// 캔버스 요소는 배경과 가구 배치의 기반이 되므로 필수
throw new Error('캔버스 요소를 찾을 수 없습니다');
}
}
// 배경 변경을 처리하는 메서드
// 색상 배경과 이미지 배경을 모두 지원
changeBackground(value) {
try {
console.log('changeBackground 호출됨:', value); // 디버깅 로그
console.log('canvasElement:', this.canvasElement); // 디버깅 로그
// 기존 배경 스타일들을 모두 초기화 (새로운 배경과의 충돌 방지)
// background 속성 전체를 덮어써야 CSS 우선순위 문제 해결
this.canvasElement.style.setProperty('background', '', 'important');
this.clearBackgroundFeedback();
this.state.background.type = value;
this.state.background.imageData = null; // 새 배경 설정 시 이전 이미지 데이터 제거
if (value === 'upload') {
// 이미지 업로드 선택 시 파일 다이얼로그 열기
console.log('파일 업로드 다이얼로그 열기'); // 디버깅 로그
document.getElementById('bgImageInput').click();
} else if (value === '') {
// 기본 선택 시 - 그라데이션 배경 복구
const gradientValue = 'linear-gradient(135deg, #F0F8FF 0%, #E6F3FF 100%)';
this.canvasElement.style.setProperty('background', gradientValue, 'important');
console.log('기본 배경으로 설정됨 (그라데이션 복구):', gradientValue); // 디버깅 로그
} else if (value.startsWith('#')) {
// 색상 코드인 경우 (예: #ffffff) 배경색으로 설정
this.canvasElement.style.setProperty('background', value, 'important');
console.log(`배경색 설정: ${value}`); // 디버깅 로그
} else {
// 그 외의 경우 기본 흰색 배경 (안전장치)
this.canvasElement.style.setProperty('background', '#ffffff', 'important');
console.log('기본 흰색 배경 설정'); // 디버깅 로그
}
} catch (error) {
ErrorHandler.handle(error, '배경 변경');
}
}
// 배경 이미지 업로드를 처리하는 메서드
// 바닥 이미지 업로드와 유사하지만 배경에 특화된 처리
handleBackgroundImageUpload(file) {
console.log('handleBackgroundImageUpload 호출됨:', file); // 디버깅 로그
if (!file) return; // 파일이 선택되지 않으면 조기 반환
const reader = new FileReader();
reader.onload = (e) => {
try {
console.log('파일 읽기 완료, 이미지 데이터 길이:', e.target.result.length); // 디버깅 로그
// 변환된 이미지 데이터를 상태에 저장
this.state.background.imageData = e.target.result;
this.state.background.type = 'upload';
// 캔버스 배경으로 이미지 설정 - background 속성 전체 사용
const backgroundValue = `url('${e.target.result}') center/cover no-repeat`;
this.canvasElement.style.setProperty('background', backgroundValue, 'important');
console.log('배경 이미지 설정 완료:', backgroundValue.substring(0, 50) + '...'); // 디버깅 로그
// 사용자에게 업로드 완료 피드백
const feedbackSpan = document.getElementById('bgImageName');
if (feedbackSpan) {
feedbackSpan.textContent = `업로드됨: ${file.name}`;
}
ErrorHandler.showSuccess('배경 이미지가 업로드되었습니다');
} catch (error) {
ErrorHandler.handle(error, '배경 이미지 업로드');
}
};
// 파일 읽기 실패 시 오류 처리
reader.onerror = () => {
ErrorHandler.handle(new Error('이미지 파일을 읽을 수 없습니다'), '배경 이미지 업로드');
};
// 파일을 Data URL로 읽기 시작
reader.readAsDataURL(file);
}
// 배경 업로드 피드백을 지우는 메서드
clearBackgroundFeedback() {
const feedbackSpan = document.getElementById('bgImageName');
if (feedbackSpan) {
feedbackSpan.textContent = '';
}
}
}
// 가구 관리 클래스 - 가구의 생성, 선택, 조작, 삭제를 담당
// 애플리케이션의 핵심 기능으로, 사용자가 가장 많이 상호작용하는 부분
class FurnitureManager {
constructor(state) {
this.state = state; // AppState 인스턴스 참조
this.roomCanvas = null; // 가구가 배치될 캔버스 요소
this.dragOffset = { x: 0, y: 0 }; // 드래그 시 마우스와 가구의 상대 위치 저장
}
// 가구 관리자 초기화 - 필수 DOM 요소 찾기
init() {
this.roomCanvas = document.getElementById('roomCanvas');
if (!this.roomCanvas) {
// 룸 캔버스는 가구 배치의 기본 영역이므로 필수
throw new Error('룸 캔버스 요소를 찾을 수 없습니다');
}
// 초기 상태에서 컨트롤 패널 비활성화 (가구가 선택되지 않은 상태)
this.disableControlPanel();
}
// 새 가구를 생성하는 메서드 - 가구 객체의 전체 생성 과정 담당
createFurniture(type, width, height, x = 0, y = 0) {
try {
// 고유한 가구 ID 생성 (중복 방지를 위해 카운터 사용)
const furnitureId = this.state.getNextFurnitureId();
// 가구를 나타낼 div 요소 생성 (DOM 조작의 기본 단위)
const furniture = document.createElement('div');
// 가구 요소의 클래스와 데이터 속성 설정
furniture.className = 'placed-furniture'; // CSS 스타일링을 위한 클래스명
furniture.dataset.type = type; // 가구 타입을 데이터 속성으로 저장
furniture.dataset.id = furnitureId; // 고유 ID를 데이터 속성으로 저장
// 접근성을 위한 ARIA 속성들 설정 (스크린 리더 지원)
furniture.setAttribute('role', 'button'); // 스크린 리더에게 버튼으로 인식시킴
furniture.setAttribute('aria-label', `${this.getFurnitureTypeLabel(type)} 가구`);
furniture.setAttribute('tabindex', '0'); // 키보드로 포커스 가능하게 설정
// 가구의 위치와 크기, 레이어 설정 (CSS 인라인 스타일 사용)
Object.assign(furniture.style, {
width: width + 'px', // 가구 너비 설정
height: height + 'px', // 가구 높이 설정
left: x + 'px', // X 좌표 (캔버스 내 절대 위치)
top: y + 'px', // Y 좌표 (캔버스 내 절대 위치)
zIndex: this.state.getNextZIndex(), // 가장 앞 레이어로 설정 (새 가구는 항상 앞에)
position: 'absolute' // 절대 위치 지정으로 자유로운 배치
});
// 가구 이미지를 표시할 img 요소 생성
const img = document.createElement('img');
img.src = `image/${type}.png`; // 가구 타입에 따른 이미지 파일 경로
img.className = 'furniture-preview-img'; // CSS 스타일링용 클래스명
img.alt = this.getFurnitureTypeLabel(type); // 접근성을 위한 대체 텍스트
furniture.appendChild(img); // 가구 요소에 이미지 자식으로 추가
// 가구 클릭 시 선택되도록 이벤트 리스너 추가
furniture.addEventListener('click', (e) => {
e.stopPropagation(); // 이벤트 버블링 방지 (부모 요소로 전파 차단)
this.selectFurniture(furniture); // 클릭된 가구를 선택 상태로 변경
});
// 메모리 누수 방지를 위한 정리 함수를 요소에 저장
// 가구 삭제 시 이벤트 리스너도 함께 제거하기 위함
furniture._cleanup = () => {
furniture.removeEventListener('click', (e) => {
e.stopPropagation();
this.selectFurniture(furniture);
});
};
// 생성된 가구를 상태 관리 객체에 추가
this.state.addFurniture(furnitureId, furniture);
return furniture; // 생성된 가구 요소 반환
} catch (error) {
ErrorHandler.handle(error, '가구 생성');
return null; // 오류 발생 시 null 반환
}
}
// 가구를 삭제하는 메서드 - 완전한 정리 작업 포함
deleteFurniture(furniture) {
if (!furniture) return; // 가구가 없으면 조기 반환
try {
const furnitureId = furniture.dataset.id;
// 이벤트 리스너 정리 (메모리 누수 방지)
// 가구 생성 시 추가된 cleanup 함수 실행
if (furniture._cleanup) {
furniture._cleanup();
}
// 상태 관리 객체에서 가구 정보 제거
this.state.removeFurniture(furnitureId);
// 삭제될 가구가 현재 선택된 가구라면 선택 해제
if (this.state.getSelectedFurniture() === furniture) {
this.deselectFurniture();
}
// DOM에서 가구 요소 완전 제거
furniture.remove();
// 사용자에게 삭제 완료 피드백
ErrorHandler.showSuccess('가구가 삭제되었습니다');
} catch (error) {
ErrorHandler.handle(error, '가구 삭제');
}
}
// 가구를 선택하는 메서드 - 선택 상태 관리와 UI 업데이트
selectFurniture(furniture) {
try {
// 기존 선택 해제
this.deselectFurniture();
// 새 가구 선택
this.state.setSelectedFurniture(furniture);
furniture.classList.add('selected');
// 선택 정보 업데이트
this.updateSelectionInfo();
// 선택 패널 표시
this.showSelectionPanel();
// 컨트롤 패널 활성화
this.enableControlPanel();
} catch (error) {
ErrorHandler.handle(error, '가구 선택');
}
}
// 가구 선택 해제 시 호출되는 메서드
deselectFurniture() {
try {
// 기존 선택된 가구의 선택 표시 제거
const selectedFurniture = this.state.getSelectedFurniture();
if (selectedFurniture) {
selectedFurniture.classList.remove('selected');
}
// 상태에서 선택된 가구 제거
this.state.setSelectedFurniture(null);
// 선택 정보 초기화
this.updateSelectionInfo();
// 선택 패널 숨기기
this.hideSelectionPanel();
// 컨트롤 패널 비활성화
this.disableControlPanel();
} catch (error) {
ErrorHandler.handle(error, '가구 선택 해제');
}
}
// 선택된 가구의 정보를 UI에 업데이트하는 메서드
// 사용자가 현재 선택된 가구의 상태를 확인할 수 있도록 함
updateSelectionInfo() {
const furniture = this.state.getSelectedFurniture();
if (!furniture) return; // 선택된 가구가 없으면 조기 반환
try {
// 가구의 현재 속성값들 읽기 (CSS 스타일에서 추출)
const type = furniture.dataset.type || 'unknown'; // 가구 타입
const left = parseInt(furniture.style.left) || 0; // X 좌표
const top = parseInt(furniture.style.top) || 0; // Y 좌표
const width = parseInt(furniture.style.width) || 0; // 너비
const height = parseInt(furniture.style.height) || 0; // 높이
const transform = furniture.style.transform || 'rotate(0deg)'; // 회전 상태
const zIndex = furniture.style.zIndex || 'auto'; // 레이어 순서
// UI 정보 표시 요소들을 한 번에 찾기 (성능 최적화)
const elements = {
selectedType: document.getElementById('selectedType'),
selectedPosition: document.getElementById('selectedPosition'),
selectedSize: document.getElementById('selectedSize'),
selectedRotation: document.getElementById('selectedRotation'),
selectedLayer: document.getElementById('selectedLayer')
};
// 각 UI 요소에 가구 정보 표시 (요소가 존재할 때만 업데이트)
if (elements.selectedType) elements.selectedType.textContent = this.getFurnitureTypeLabel(type);
if (elements.selectedPosition) elements.selectedPosition.textContent = `(${left}, ${top})`;
if (elements.selectedSize) elements.selectedSize.textContent = `${width} × ${height}`;
if (elements.selectedRotation) elements.selectedRotation.textContent = transform;
if (elements.selectedLayer) elements.selectedLayer.textContent = zIndex;
} catch (error) {
ErrorHandler.handle(error, '선택 정보 업데이트');
}
}
// 가구 조작 패널을 화면에 표시하는 메서드
showSelectionPanel() {
const panel = document.getElementById('selectionInfo');
if (panel) {
panel.classList.remove('hidden'); // 숨김 클래스 제거로 패널 표시
}
}
// 가구 조작 패널을 화면에서 숨기는 메서드
hideSelectionPanel() {
const panel = document.getElementById('selectionInfo');
if (panel) {
panel.classList.add('hidden'); // 숨김 클래스 추가로 패널 숨김
}
}
// 가구 타입을 한국어로 변환하는 메서드
// 사용자 친화적인 인터페이스를 위해 영어 타입명을 한국어로 변환
getFurnitureTypeLabel(type) {
// 가구 타입별 한국어 라벨 매핑 객체
const labels = {
chair: '의자',
chair_2: '의자2',
chair_3: '의자3',
table: '테이블',
bed: '침대',
sofa: '소파',
window: '창문',
window_2: '창문2',
window_3: '창문3',
window_4: '창문4',
door: '문',
door_2: '문2',
door_3: '문3',
door_4: '문4',
picture: '그림',
plant: '식물',
basil: '바질',
lamp: '램프'
};
// 매핑된 라벨이 있으면 반환, 없으면 원본 타입 반환 (안전장치)
return labels[type] || type;
}
// ============================================
// 가구 조작 메서드들 - 선택된 가구를 조작하는 기능들
// 사용자가 가구를 세밀하게 조정할 수 있는 기능 제공
// ============================================
// 선택된 가구를 앞으로 가져오는 메서드 (다른 가구 위에 표시)
bringForward() {
const furniture = this.state.getSelectedFurniture();
if (!furniture) return; // 선택된 가구가 없으면 조기 반환
try {
// 현재 z-index 값 가져오기 (없으면 현재 최대값 사용)
const currentZ = parseInt(furniture.style.zIndex) || this.state.maxZIndex;
// z-index를 1 증가시켜 다른 가구들보다 앞으로 가져오기
const newZ = currentZ + 1;
furniture.style.zIndex = newZ;
// 전체 최대 z-index 값 업데이트 (다음 가구를 위해)
this.state.maxZIndex = Math.max(this.state.maxZIndex, newZ);
// UI에 변경된 정보 즉시 반영
this.updateSelectionInfo();
} catch (error) {
ErrorHandler.handle(error, '가구 앞으로 이동');
}
}
// 선택된 가구를 뒤로 보내는 메서드 (다른 가구 아래에 표시)
sendBackward() {
const furniture = this.state.getSelectedFurniture();
if (!furniture) return;
try {
const currentZ = parseInt(furniture.style.zIndex) || this.state.maxZIndex;
// z-index가 10보다 클 때만 감소 (너무 뒤로 가서 보이지 않는 것 방지)
if (currentZ > 10) {
furniture.style.zIndex = currentZ - 1;
this.updateSelectionInfo();
}
} catch (error) {
ErrorHandler.handle(error, '가구 뒤로 이동');
}
}
// 선택된 가구를 왼쪽으로 회전시키는 메서드
rotateLeft() {
this.rotateFurniture(-CONSTANTS.ROTATION_STEP); // 음수로 반시계방향 회전
}
// 선택된 가구를 오른쪽으로 회전시키는 메서드
rotateRight() {
this.rotateFurniture(CONSTANTS.ROTATION_STEP); // 양수로 시계방향 회전
}
// 가구를 지정된 각도만큼 회전시키는 메서드
// CSS transform의 rotate 함수를 사용하여 부드러운 회전 효과
rotateFurniture(degrees) {
const furniture = this.state.getSelectedFurniture();
if (!furniture) return;
try {
// 현재 회전 상태를 transform 스타일에서 추출
const currentRotation = furniture.style.transform || 'rotate(0deg)';
// 정규식으로 현재 각도 값 파싱 (예: "rotate(45deg)" → 45)
const match = currentRotation.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
const currentAngle = match ? parseFloat(match[1]) : 0;
// 새로운 각도 계산 (기존 각도 + 회전할 각도)
const newAngle = currentAngle + degrees;
// 새로운 회전 변환 적용
furniture.style.transform = `rotate(${newAngle}deg)`;
this.updateSelectionInfo(); // UI 정보 업데이트
} catch (error) {
ErrorHandler.handle(error, '가구 회전');
}
}
// 선택된 가구를 작게 만드는 메서드
resizeSmaller() {
this.resizeFurniture(-CONSTANTS.RESIZE_STEP); // 음수로 크기 감소
}
// 선택된 가구를 크게 만드는 메서드
resizeBigger() {
this.resizeFurniture(CONSTANTS.RESIZE_STEP); // 양수로 크기 증가
}
// 가구 크기를 조절하는 메서드
// 최소/최대 크기 제한을 두어 적절한 범위 내에서만 크기 조절 가능
resizeFurniture(sizeChange) {
const furniture = this.state.getSelectedFurniture();
if (!furniture) return;
try {
// 현재 가구의 너비와 높이 가져오기
const currentWidth = parseInt(furniture.style.width) || 60; // 기본값 60px
const currentHeight = parseInt(furniture.style.height) || 60; // 기본값 60px
// 새로운 크기 계산 (최소/최대 크기 제한 적용)
// Math.max와 Math.min을 조합하여 범위 제한
const newWidth = Math.max(CONSTANTS.MIN_FURNITURE_SIZE,
Math.min(CONSTANTS.MAX_FURNITURE_SIZE, currentWidth + sizeChange));
const newHeight = Math.max(CONSTANTS.MIN_FURNITURE_SIZE,
Math.min(CONSTANTS.MAX_FURNITURE_SIZE, currentHeight + sizeChange));
// 크기가 실제로 변경되었을 때만 적용 (불필요한 DOM 조작 방지)
if (newWidth !== currentWidth || newHeight !== currentHeight) {
furniture.style.width = newWidth + 'px';
furniture.style.height = newHeight + 'px';
this.updateSelectionInfo();
}
} catch (error) {
ErrorHandler.handle(error, '가구 크기 조절');
}
}
// 선택된 가구를 지정된 거리만큼 이동시키는 메서드
// 키보드 화살표 키나 조작 버튼으로 정밀한 이동 가능
moveFurniture(deltaX, deltaY) {
const furniture = this.state.getSelectedFurniture();
if (!furniture) return;
try {
// 현재 가구의 위치와 크기 정보 가져오기
const currentLeft = parseInt(furniture.style.left) || 0;
const currentTop = parseInt(furniture.style.top) || 0;
const width = parseInt(furniture.style.width) || 60;
const height = parseInt(furniture.style.height) || 60;
// 캔버스의 경계 계산 (가구가 화면 밖으로 나가지 않도록)
const canvasRect = this.roomCanvas.getBoundingClientRect();
const maxX = canvasRect.width - width - CONSTANTS.CANVAS_PADDING;
const maxY = canvasRect.height - height - CONSTANTS.CANVAS_PADDING;
// 새로운 위치 계산 (경계 내에서만 이동 가능)
// Math.max와 Math.min을 사용하여 범위 제한
const newLeft = Math.max(CONSTANTS.CANVAS_PADDING,
Math.min(currentLeft + deltaX, maxX));
const newTop = Math.max(CONSTANTS.CANVAS_PADDING,
Math.min(currentTop + deltaY, maxY));
// 새로운 위치 적용
furniture.style.left = newLeft + 'px';
furniture.style.top = newTop + 'px';
this.updateSelectionInfo(); // UI 위치 정보 업데이트
} catch (error) {
ErrorHandler.handle(error, '가구 이동');
}
}
// 가구가 선택되지 않았을 때 사용자에게 안내 메시지 표시
showNoSelectionMessage() {
ErrorHandler.showUserError('가구를 먼저 선택해주세요.');
}
// 컨트롤 패널 활성화 메서드 - 가구 선택했을 때
enableControlPanel() {
const controlButtons = [
'bringForward', 'sendBackward',
'rotateLeft', 'rotateRight',
'resizeSmaller', 'resizeBigger',
'alignLeft', 'alignCenter', 'alignRight',
'alignTop', 'alignMiddle', 'alignBottom',
'deleteSelected'
];
controlButtons.forEach(buttonId => {
const button = document.getElementById(buttonId);
if (button) {
button.disabled = false;
button.classList.remove('disabled');
}
});
}
// 컨트롤 패널 비활성화 메서드 - 가구 선택하지 않았을 때
disableControlPanel() {
const controlButtons = [
'bringForward', 'sendBackward',
'rotateLeft', 'rotateRight',
'resizeSmaller', 'resizeBigger',
'alignLeft', 'alignCenter', 'alignRight',
'alignTop', 'alignMiddle', 'alignBottom',
'deleteSelected'
];
controlButtons.forEach(buttonId => {
const button = document.getElementById(buttonId);
if (button) {
button.disabled = true;
button.classList.add('disabled');
}
});
}
}
// 드래그 앤 드롭 관리 클래스 - 마우스로 가구를 끌어서 배치하고 이동하는 기능
// 직관적인 사용자 인터페이스의 핵심 기능으로, 자연스러운 조작감 제공
class DragDropManager {
constructor(state, furnitureManager) {
this.state = state; // AppState 인스턴스 참조
this.furnitureManager = furnitureManager; // FurnitureManager 인스턴스 참조
this.roomCanvas = null; // 가구가 배치될 캔버스 요소
this.sidebar = null; // 가구 목록이 있는 사이드바 요소
this.dragOffset = { x: 0, y: 0 }; // 드래그 시 마우스와 가구의 상대 위치
// 이벤트 핸들러를 미리 바인딩 (메모리 효율성과 이벤트 제거를 위해)
// 화살표 함수로 this 바인딩을 보장하고, 나중에 removeEventListener에서 사용
this._boundOnGlobalDrag = (e) => this.onGlobalDrag(e);
this._boundOnGlobalDragEnd = (e) => this.onGlobalDragEnd(e);
}
// 드래그 앤 드롭 관리자 초기화
init() {
this.roomCanvas = document.getElementById('roomCanvas');
this.sidebar = document.querySelector('.sidebar');
if (!this.roomCanvas || !this.sidebar) {
throw new Error('필수 DOM 요소를 찾을 수 없습니다');
}
this.setupEventListeners(); // 이벤트 리스너 설정
}
// 드래그 앤 드롭 관련 이벤트 리스너들을 설정하는 메서드
// 이벤트 위임 방식을 사용하여 동적으로 생성되는 요소도 처리 가능
setupEventListeners() {
// 사이드바에서 새 가구 드래그 시작 (이벤트 위임 방식 사용)
// 상위 요소에 이벤트를 등록하여 하위 요소들의 이벤트를 한 번에 처리
this.sidebar.addEventListener('mousedown', (e) => {
// 클릭된 요소가 가구 아이템인지 확인 (closest로 상위 요소까지 탐색)
const item = e.target.closest('.furniture-item');
if (item) {
this.startDragNewFurniture(e, item);
}
});
// 캔버스에서 배치된 가구 드래그 시작 (이벤트 위임 방식 사용)
this.roomCanvas.addEventListener('mousedown', (e) => {
// 클릭된 요소가 배치된 가구인지 확인
const target = e.target.closest('.placed-furniture');
if (target) {
e.preventDefault(); // 기본 동작 방지 (텍스트 선택, 이미지 드래그 등)
this.startDragPlacedFurniture(e, target);
}
});
// 캔버스 빈 공간 클릭 시 가구 선택 해제
this.roomCanvas.addEventListener('click', (e) => this.handleCanvasClick(e));
}
// 사이드바에서 새 가구를 드래그하기 시작할 때 호출되는 메서드
// 새 가구를 생성하면서 동시에 드래그 상태로 전환
startDragNewFurniture(e, furnitureItem) {
if (this.state.isDragging) return; // 이미 드래그 중이면 무시 (중복 방지)
try {
e.preventDefault(); // 기본 동작 방지
// 가구 아이템에서 타입과 크기 정보 가져오기
const type = furnitureItem.dataset.type;
const width = parseInt(furnitureItem.dataset.width) || 60; // 기본값 60px
const height = parseInt(furnitureItem.dataset.height) || 60; // 기본값 60px
// 마우스 위치를 기준으로 가구를 생성할 위치 계산
const rect = this.roomCanvas.getBoundingClientRect();
// 가구 중앙이 마우스 위치에 오도록 조정하고, 경계 내에 배치
const x = Math.max(CONSTANTS.CANVAS_PADDING,
Math.min(e.clientX - rect.left - width / 2,
rect.width - width - CONSTANTS.CANVAS_PADDING));
const y = Math.max(CONSTANTS.CANVAS_PADDING,
Math.min(e.clientY - rect.top - height / 2,
rect.height - height - CONSTANTS.CANVAS_PADDING));
// 새 가구 생성
const newFurniture = this.furnitureManager.createFurniture(type, width, height, x, y);
if (!newFurniture) return; // 생성 실패 시 조기 반환
// 드래그 상태 설정
this.state.isDragging = true;
this.state.dragMode = 'new'; // 새 가구 배치 모드
newFurniture.classList.add('dragging'); // 드래그 중임을 나타내는 CSS 클래스
// 생성된 가구를 캔버스에 추가하고 선택
this.roomCanvas.appendChild(newFurniture);
this.furnitureManager.selectFurniture(newFurniture);
// 전역 마우스 이벤트 리스너 추가 (문서 전체에서 드래그 추적)
// document에 등록하여 마우스가 캔버스를 벗어나도 드래그 계속 추적
document.addEventListener('mousemove', this._boundOnGlobalDrag);
document.addEventListener('mouseup', this._boundOnGlobalDragEnd);
} catch (error) {
ErrorHandler.handle(error, '새 가구 드래그 시작');
}
}
// 이미 배치된 가구를 드래그하기 시작할 때 호출되는 메서드
// 기존 가구의 위치를 변경하는 기능
startDragPlacedFurniture(e, furniture) {
if (this.state.isDragging) return; // 이미 드래그 중이면 무시
try {
e.preventDefault();
e.stopPropagation(); // 이벤트 전파 중단 (다른 클릭 이벤트와 충돌 방지)
// 드래그 상태 설정
this.state.isDragging = true;
this.state.dragMode = 'move'; // 기존 가구 이동 모드
this.furnitureManager.selectFurniture(furniture); // 드래그할 가구 선택
furniture.classList.add('dragging'); // 드래그 중 시각적 표시
// 마우스와 가구의 상대적 위치 계산 (드래그 시 가구가 마우스를 따라 자연스럽게 움직이도록)
// 가구를 클릭한 지점을 기준으로 상대 위치를 저장하여, 드래그 중에도 같은 지점이 마우스를 따라가도록 함
const furnitureRect = furniture.getBoundingClientRect();
this.dragOffset = {
x: e.clientX - furnitureRect.left, // 마우스 X 좌표 - 가구 왼쪽 경계
y: e.clientY - furnitureRect.top // 마우스 Y 좌표 - 가구 상단 경계
};
// 전역 마우스 이벤트 리스너 추가 (문서 전체에서 드래그 추적)
document.addEventListener('mousemove', this._boundOnGlobalDrag);
document.addEventListener('mouseup', this._boundOnGlobalDragEnd);
} catch (error) {
ErrorHandler.handle(error, '가구 드래그 시작');
}
}
// 마우스가 움직일 때마다 호출되는 드래그 처리 메서드
// 실시간으로 가구 위치를 업데이트하여 부드러운 드래그 효과 제공
onGlobalDrag(e) {
if (!this.state.isDragging || !this.state.selectedFurniture) return; // 드래그 중이 아니거나 선택된 가구가 없으면 무시
try {
e.preventDefault(); // 기본 동작 방지
const rect = this.roomCanvas.getBoundingClientRect(); // 캔버스의 현재 위치와 크기
const furniture = this.state.selectedFurniture;
const width = parseInt(furniture.style.width) || 60; // 가구 너비
const height = parseInt(furniture.style.height) || 60; // 가구 높이
let x, y; // 가구의 새로운 위치