Files
sam-kd/guiderail/draw.php
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

1265 lines
52 KiB
PHP

<?php
/**
* 그리기 기능 컴포넌트
*
* 사용법:
* 1. CSS 파일 포함: <link href="css/style.css" rel="stylesheet">
* 2. 이 파일 포함: <?php include 'draw.php'; ?>
* 3. 그리기 버튼 추가: <button type="button" id="drawBtn" class="btn btn-primary">그리기</button>
* 4. 미리보기 컨테이너 추가: <div id="previewContainer" class="text-center mb-3"></div>
*/
// 필수 요소들이 있는지 확인
if (!isset($previewContainerId)) {
$previewContainerId = 'previewContainer';
}
if (!isset($drawBtnId)) {
$drawBtnId = 'drawBtn';
}
?>
<!-- 그리기 도구 HTML -->
<div class="drawing-tools" style="display: none;">
<div class="row drawing-row">
<div class="col-sm-12">
<div class="drawing-container">
<div class="drawing-canvas-area" id="drawingCanvasArea">
<!-- 캔버스가 여기에 생성됩니다 -->
</div>
<div class="drawing-controls">
<h6 class="mb-3">그리기 도구</h6>
<div class="mb-3">
<label class="form-label">그리기 모드:</label>
<select id="drawMode" class="form-select form-select-sm">
<option value="polyline">점연결</option>
<option value="line">직선</option>
<option value="free">자유선</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">색상:</label>
<input type="color" id="drawColor" class="form-control form-control-sm" value="#000000">
</div>
<div class="mb-3">
<button type="button" id="lineBtn" class="btn btn-outline-primary btn-sm me-2">선</button>
<button type="button" id="textBtn" class="btn btn-outline-primary btn-sm me-2">텍스트</button>
<button type="button" id="eraserBtn" class="btn btn-outline-warning btn-sm">지우개</button>
</div>
<div class="mb-3" id="eraserSizeContainer" style="display: none;">
<label class="form-label">지우개 크기:</label>
<input type="range" id="eraserSize" class="form-range" min="5" max="50" value="15">
<small class="text-muted" id="eraserSizeLabel">15px</small>
</div>
<div class="mb-3" id="straightModeContainer" style="display: block;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="straightToggle">
<label class="form-check-label" for="straightToggle" id="straightToggleLabel">
직각모드
</label>
</div>
</div>
<div class="mb-3">
<button type="button" id="saveDrawingBtn" class="btn btn-success btn-sm me-2">저장</button>
<button type="button" id="clearDrawingBtn" class="btn btn-danger btn-sm">초기화</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 그리기 기능 JavaScript
(function() {
'use strict';
// 전역 변수
let canvas = null;
let ctx = null;
let polyPrev = null;
let drawing = false;
let startX = 0;
let startY = 0;
let previewLine = null;
let drawingData = [];
let originalImage = null;
let eraserRadius = 15;
let currentPath = [];
let textMode = false;
let isEraser = false;
let drawMode = 'polyline';
let drawColor = '#000000';
let handlers, onPolyClick, onPolyKeydown;
// 전역에 이미지 객체를 둡니다.
let erasedImageObj = null;
// 그리기 기능 초기화 함수
function initializeDrawingFeatures() {
// 이미 초기화된 경우 중복 실행 방지
if ($('#<?= $drawBtnId ?>').data('initialized')) {
console.log('이미 그리기 기능이 초기화됨');
return;
}
$('#<?= $drawBtnId ?>').data('initialized', true);
console.log('initializeDrawingFeatures 함수 호출됨');
// 그리기 관련 요소들
const preview = document.getElementById('<?= $previewContainerId ?>');
const drawBtn = document.getElementById('<?= $drawBtnId ?>');
const textBtn = document.getElementById('textBtn');
const saveDrawingBtn = document.getElementById('saveDrawingBtn');
const clearBtn = document.getElementById('clearDrawingBtn');
const modeSelect = document.getElementById('drawMode');
const colorPicker = document.getElementById('drawColor');
const lineBtn = document.getElementById('lineBtn');
const eraserBtn = document.getElementById('eraserBtn');
const eraserSize = document.getElementById('eraserSize');
const eraserSizeLabel = document.getElementById('eraserSizeLabel');
// 그리기 버튼 이벤트
if (drawBtn) {
drawBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('그리기 버튼 클릭 이벤트 발생');
drawBtnClickHandler();
});
}
// 그리기 컨트롤 이벤트 핸들러 추가
attachDrawingControlEvents();
}
// 그리기 버튼 클릭 핸들러 함수
function drawBtnClickHandler() {
const drawBtn = document.getElementById('<?= $drawBtnId ?>');
console.log('그리기 버튼 클릭됨, 현재 상태:', drawBtn.classList.contains('active'));
if (drawBtn.classList.contains('active')) {
// 그리기 중지: 모든 상태 원복
console.log('그리기 중지 모드 - 원상태로 복원');
// 1. 캔버스 완전 제거
if (canvas) {
try {
['mousedown','mousemove','mouseup','mouseout'].forEach(evt =>
canvas.removeEventListener(evt, handlers[evt])
);
canvas.removeEventListener('click', onPolyClick);
document.removeEventListener('keydown', onPolyKeydown);
canvas.remove();
} catch (e) {
console.log('canvas 제거 중 오류:', e);
}
canvas = null;
ctx = null;
console.log('캔버스 제거 완료');
}
// 기존 drawingCanvas도 제거
const existingCanvas = document.getElementById('drawingCanvas');
if (existingCanvas) {
console.log('기존 drawingCanvas 제거');
existingCanvas.remove();
}
// 2. 버튼 상태 변경
drawBtn.classList.remove('active');
drawBtn.textContent = '그리기';
// 3. 그리기 row 제거
let drawingRow = document.querySelector('.drawing-row');
if (drawingRow) {
drawingRow.remove();
console.log('그리기 row 제거됨');
}
// 4. 원본 이미지 다시 표시 및 미러링 제거
const preview = document.getElementById('<?= $previewContainerId ?>');
const originalImgElement = preview.querySelector('img:not(.mirror-image)');
const mirrorImg = preview.querySelector('.mirror-image');
if (originalImgElement) {
originalImgElement.style.display = '';
console.log('원본 이미지 다시 표시');
}
if (mirrorImg) {
mirrorImg.remove();
console.log('미러링 이미지 제거');
}
// 그리기 데이터가 없으면 "아직 등록된 이미지가 없습니다" 메시지 표시
if (drawingData.length === 0) {
showNoImageMessage();
}
// 지우개 가상 커서 제거
removeEraserCursor();
// previewContainer에서 drawing-mode 클래스 제거
if (preview) {
preview.classList.remove('drawing-mode');
console.log('previewContainer에서 drawing-mode 클래스 제거');
}
// drawBtn의 initialized 플래그를 false로 돌려줌
$('#<?= $drawBtnId ?>').data('initialized', false);
console.log('그리기 중지 완료 - 모든 상태 원복됨');
} else {
// 그리기 시작: 새로운 row 생성
console.log('그리기 시작 모드 - 새로운 row 생성');
// 1. 버튼 상태 변경
drawBtn.classList.add('active');
drawBtn.textContent = '그리기 중지';
// 2. "아직 등록된 이미지가 없습니다" 메시지 즉시 숨기기
hideNoImageMessage();
// 3. 기존 미러링 이미지 제거
const preview = document.getElementById('<?= $previewContainerId ?>');
if (preview) {
const mirrorImg = preview.querySelector('.mirror-image');
if (mirrorImg) {
mirrorImg.remove();
console.log('기존 미러링 이미지 제거');
}
}
// 4. 그리기 도구 표시
const drawingTools = document.querySelector('.drawing-tools');
if (drawingTools) {
drawingTools.style.display = 'block';
}
// 5. 캔버스 생성 및 설정
setTimeout(() => {
createAndShowCanvas();
// 6. 직각모드 기본 체크 설정
setTimeout(() => {
const straightToggle = document.getElementById('straightToggle');
if (straightToggle) {
straightToggle.checked = true;
console.log('직각모드 기본 체크 설정 완료');
}
// previewContainer에 drawing-mode 클래스 추가
if (preview) {
preview.classList.add('drawing-mode');
console.log('previewContainer에 drawing-mode 클래스 추가');
}
// 그리기 시작 시 "아직 등록된 이미지가 없습니다" 메시지 즉시 숨기기
hideNoImageMessage();
}, 100);
console.log('그리기 시작 완료 - 새로운 row 생성됨');
}, 200);
}
}
// 그리기 컨트롤 이벤트 핸들러 추가
function attachDrawingControlEvents() {
console.log('그리기 컨트롤 이벤트 핸들러 추가 시작');
// 그리기 관련 요소들
const modeSelect = document.getElementById('drawMode');
const colorPicker = document.getElementById('drawColor');
const lineBtn = document.getElementById('lineBtn');
const textBtn = document.getElementById('textBtn');
const eraserBtn = document.getElementById('eraserBtn');
const eraserSize = document.getElementById('eraserSize');
const eraserSizeLabel = document.getElementById('eraserSizeLabel');
// 그리기 모드 선택
if (modeSelect) {
modeSelect.addEventListener('change', function() {
drawMode = this.value;
// 지우개 크기 컨테이너 숨기기 (선택된 모드가 지우개가 아닌 경우)
const eraserSizeContainer = document.getElementById('eraserSizeContainer');
if (eraserSizeContainer) {
eraserSizeContainer.style.display = 'none';
console.log('지우개 크기 컨테이너 숨김 (모드 변경)');
}
// 직각모드 컨테이너 숨기기 (모드 변경 시)
const straightModeContainer = document.getElementById('straightModeContainer');
if (straightModeContainer) {
straightModeContainer.style.display = 'none';
console.log('직각모드 컨테이너 숨김 (모드 변경)');
}
console.log('그리기 모드 변경:', drawMode);
});
}
// 색상 선택
if (colorPicker) {
colorPicker.addEventListener('input', function() {
drawColor = this.value;
console.log('색상 변경:', drawColor);
});
}
// 선 버튼
if (lineBtn) {
lineBtn.addEventListener('click', function() {
isEraser = false;
if (ctx) ctx.globalCompositeOperation = 'source-over';
textMode = false;
drawMode = 'polyline';
if (modeSelect) modeSelect.value = 'polyline';
lineBtn.classList.add('active');
textBtn.classList.remove('active');
eraserBtn.classList.remove('active');
// 지우개 크기 컨테이너 숨기기
const eraserSizeContainer = document.getElementById('eraserSizeContainer');
if (eraserSizeContainer) {
eraserSizeContainer.style.display = 'none';
console.log('지우개 크기 컨테이너 숨김');
}
// 직각모드 컨테이너 표시
const straightModeContainer = document.getElementById('straightModeContainer');
if (straightModeContainer) {
straightModeContainer.style.display = 'block';
console.log('직각모드 컨테이너 표시됨');
}
// 지우개 가상 커서 제거
removeEraserCursor();
console.log('선 모드 활성화');
});
}
// 텍스트 버튼
if (textBtn) {
textBtn.addEventListener('click', function() {
isEraser = false;
if (ctx) ctx.globalCompositeOperation = 'source-over';
textMode = true;
drawMode = null;
textBtn.classList.add('active');
lineBtn.classList.remove('active');
eraserBtn.classList.remove('active');
// 지우개 크기 컨테이너 숨기기
const eraserSizeContainer = document.getElementById('eraserSizeContainer');
if (eraserSizeContainer) {
eraserSizeContainer.style.display = 'none';
console.log('지우개 크기 컨테이너 숨김');
}
// 직각모드 컨테이너 숨기기
const straightModeContainer = document.getElementById('straightModeContainer');
if (straightModeContainer) {
straightModeContainer.style.display = 'none';
console.log('직각모드 컨테이너 숨김');
}
// 지우개 가상 커서 제거
removeEraserCursor();
console.log('텍스트 모드 활성화');
});
}
// 지우개 버튼
if (eraserBtn) {
eraserBtn.addEventListener('click', function() {
isEraser = true;
textMode = false;
drawMode = null;
eraserBtn.classList.add('active');
lineBtn.classList.remove('active');
textBtn.classList.remove('active');
// 지우개 크기 컨테이너 표시
const eraserSizeContainer = document.getElementById('eraserSizeContainer');
if (eraserSizeContainer) {
eraserSizeContainer.style.display = 'block';
console.log('지우개 크기 컨테이너 표시됨');
}
// 직각모드 컨테이너 숨기기
const straightModeContainer = document.getElementById('straightModeContainer');
if (straightModeContainer) {
straightModeContainer.style.display = 'none';
console.log('직각모드 컨테이너 숨김');
}
// 지우개 가상 커서 생성
updateEraserCursor();
console.log('지우개 모드 활성화');
});
}
// 지우개 크기 조절
if (eraserSize && eraserSizeLabel) {
eraserSize.addEventListener('input', function() {
eraserRadius = parseInt(this.value);
eraserSizeLabel.textContent = eraserRadius + 'px';
// 가상 커서 크기 업데이트
updateEraserCursor();
});
}
// 초기화 버튼
const clearBtn = document.getElementById('clearDrawingBtn');
if (clearBtn) {
clearBtn.addEventListener('click', function() {
if (canvas && ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (originalImage) {
ctx.drawImage(originalImage, 0, 0, canvas.width, canvas.height);
} else {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
drawingData = [];
currentPath = [];
console.log('캔버스 초기화 완료');
// 미러링 업데이트
updatePreviewMirror();
// 그리기 데이터가 없으면 "아직 등록된 이미지가 없습니다" 메시지 표시
if (drawingData.length === 0) {
showNoImageMessage();
}
}
});
}
console.log('그리기 컨트롤 이벤트 핸들러 추가 완료');
}
// 캔버스 겹치기 생성 함수
function createCanvasOverlay() {
console.log('createCanvasOverlay 함수 시작');
const cv = document.createElement('canvas');
cv.id = 'drawingCanvas';
cv.classList.add('drawing-canvas');
// 기준 크기 설정 (320px x 240px)
const STANDARD_WIDTH = 320;
const STANDARD_HEIGHT = 240;
cv.width = STANDARD_WIDTH;
cv.height = STANDARD_HEIGHT;
// 캔버스 스타일 설정 - 기준 크기로 고정
cv.style.width = STANDARD_WIDTH + 'px';
cv.style.height = STANDARD_HEIGHT + 'px';
cv.style.maxWidth = '100%';
cv.style.maxHeight = '100%';
cv.style.objectFit = 'contain';
cv.style.display = 'block';
cv.style.margin = '0 auto';
const cctx = cv.getContext('2d');
console.log('캔버스 크기 설정 (1:1):', cv.width, 'x', cv.height);
// 원본 이미지 저장 및 즉시 그리기
const preview = document.getElementById('<?= $previewContainerId ?>');
const img = preview ? preview.querySelector('img') : null;
console.log('preview 내부의 img 요소:', img);
if (img) {
console.log('이미지가 존재합니다. originalImage 설정');
originalImage = new Image();
originalImage.src = img.src;
originalImage.onload = function() {
console.log('originalImage 로드 완료');
cctx.drawImage(originalImage, 0, 0, cv.width, cv.height);
// 미러링 업데이트 (이미지가 있을 때만)
updatePreviewMirror();
};
cctx.drawImage(img, 0, 0, cv.width, cv.height);
console.log('이미지를 캔버스에 그렸습니다.');
// 이미지가 있으면 "아직 등록된 이미지가 없습니다" 메시지 숨기기
hideNoImageMessage();
} else {
console.log('이미지가 없습니다. 흰색 배경으로 채웁니다.');
// 이미지가 없으면 흰색 배경 채우기
cctx.fillStyle = '#ffffff';
cctx.fillRect(0, 0, cv.width, cv.height);
// 그리기 모드에서는 메시지를 표시하지 않음 (나중에 그리기 시작 시 숨김)
// showNoImageMessage(); // 이 줄을 제거
}
console.log('createCanvasOverlay 함수 완료, 캔버스 반환:', cv);
return cv;
}
// "아직 등록된 이미지가 없습니다" 메시지 표시 함수
function showNoImageMessage() {
const preview = document.getElementById('<?= $previewContainerId ?>');
if (preview) {
// 기존 메시지가 있는지 확인
let noImageMsg = preview.querySelector('.no-image-message');
if (!noImageMsg) {
noImageMsg = document.createElement('div');
noImageMsg.className = 'no-image-message text-center text-muted mt-3';
noImageMsg.innerHTML = '<i class="fas fa-image"></i> 아직 등록된 이미지가 없습니다.';
preview.appendChild(noImageMsg);
}
noImageMsg.style.display = 'block';
console.log('"아직 등록된 이미지가 없습니다" 메시지 표시');
}
}
// "아직 등록된 이미지가 없습니다" 메시지 숨기기 함수
function hideNoImageMessage() {
const preview = document.getElementById('<?= $previewContainerId ?>');
if (preview) {
const noImageMsg = preview.querySelector('.no-image-message');
if (noImageMsg) {
noImageMsg.style.display = 'none';
console.log('"아직 등록된 이미지가 없습니다" 메시지 숨김');
}
}
}
// 지우개 가상 커서 생성/업데이트 함수
function updateEraserCursor() {
const canvas = document.getElementById('drawingCanvas');
if (!canvas || !isEraser) return;
// 기존 커서 제거
removeEraserCursor();
// 새로운 커서 생성
const cursor = document.createElement('div');
cursor.id = 'eraserCursor';
cursor.style.position = 'absolute';
cursor.style.width = (eraserRadius * 2) + 'px';
cursor.style.height = (eraserRadius * 2) + 'px';
cursor.style.border = '2px solid #ff4444';
cursor.style.borderRadius = '50%';
cursor.style.backgroundColor = 'rgba(255, 68, 68, 0.2)';
cursor.style.pointerEvents = 'none';
cursor.style.zIndex = '1000';
cursor.style.display = 'none';
// 캔버스 컨테이너에 추가
const canvasArea = document.getElementById('drawingCanvasArea');
if (canvasArea) {
canvasArea.style.position = 'relative';
canvasArea.appendChild(cursor);
}
}
// 지우개 가상 커서 제거 함수
function removeEraserCursor() {
const cursor = document.getElementById('eraserCursor');
if (cursor) {
cursor.remove();
}
}
// 지우개 가상 커서 위치 업데이트 함수
function updateEraserCursorPosition(e) {
const cursor = document.getElementById('eraserCursor');
const canvas = document.getElementById('drawingCanvas');
if (!cursor || !canvas || !isEraser) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
cursor.style.left = (x - eraserRadius) + 'px';
cursor.style.top = (y - eraserRadius) + 'px';
cursor.style.display = 'block';
}
// 미러링 업데이트 함수
function updatePreviewMirror() {
const canvas = document.getElementById('drawingCanvas');
const preview = document.getElementById('<?= $previewContainerId ?>');
if (canvas && preview) {
// 그리기 데이터가 있을 때만 미러링 표시
if (drawingData.length > 0) {
// 캔버스의 현재 상태를 이미지로 변환
const dataURL = canvas.toDataURL('image/png');
// previewContainer에 미러링 이미지 표시
let mirrorImg = preview.querySelector('.mirror-image');
if (!mirrorImg) {
mirrorImg = document.createElement('img');
mirrorImg.className = 'mirror-image';
// 기준 크기로 설정 (320px x 240px)
mirrorImg.style.width = '320px';
mirrorImg.style.height = '240px';
mirrorImg.style.maxWidth = '100%';
mirrorImg.style.maxHeight = '100%';
mirrorImg.style.objectFit = 'contain';
mirrorImg.style.border = '1px solid #ddd';
mirrorImg.style.borderRadius = '4px';
preview.appendChild(mirrorImg);
} else {
// 기존 미러링 이미지도 기준 크기로 업데이트
mirrorImg.style.width = '320px';
mirrorImg.style.height = '240px';
}
mirrorImg.src = dataURL;
mirrorImg.style.display = 'block';
// 원본 이미지 숨기기
const originalImg = preview.querySelector('img:not(.mirror-image)');
if (originalImg) {
originalImg.style.display = 'none';
}
console.log('미러링 업데이트 완료');
} else {
// 그리기 데이터가 없으면 미러링 이미지 숨기기
const mirrorImg = preview.querySelector('.mirror-image');
if (mirrorImg) {
mirrorImg.style.display = 'none';
}
// 원본 이미지 표시
const originalImg = preview.querySelector('img:not(.mirror-image)');
if (originalImg) {
originalImg.style.display = '';
}
console.log('미러링 숨김 (그리기 데이터 없음)');
}
}
}
// 그리기 데이터 저장 함수
function saveDrawingData(type, data) {
drawingData.push({
type: type,
data: data,
color: drawColor,
lineWidth: ctx.lineWidth || 2
});
// 그리기 데이터가 추가되면 미러링 업데이트
setTimeout(() => {
updatePreviewMirror();
}, 10);
}
// 이벤트 핸들러들 정의
handlers = {
mousedown(e) {
if (!isEraser && drawMode!=='free' && drawMode!=='line' && drawMode!=='polyline') return;
if (drawMode === 'polyline') return;
drawing = true;
const rect = canvas.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
if (drawMode === 'free') {
ctx.beginPath();
ctx.moveTo(startX, startY);
currentPath = [{x: startX, y: startY}];
}
},
mousemove(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left, y = e.clientY - rect.top;
if (drawMode === 'polyline' && polyPrev) {
drawPreviewLine(x, y);
}
if (!drawing) return;
if (isEraser) {
ctx.save();
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = eraserRadius * 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const gradient = ctx.createRadialGradient(x, y, 0, x, y, eraserRadius);
gradient.addColorStop(0, 'rgba(0,0,0,1)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, eraserRadius, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
// 지우개 사용 시 실시간 미러링 업데이트
setTimeout(() => {
updatePreviewMirror();
}, 10);
} else {
if (drawMode !== 'free' || !drawing) return;
ctx.strokeStyle = drawColor;
if (e.shiftKey) {
const deltaX = Math.abs(x - startX);
const deltaY = Math.abs(y - startY);
if (deltaX > deltaY) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
redrawCanvas();
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(x, startY);
ctx.stroke();
currentPath = [{x: startX, y: startY}, {x: x, y: startY}];
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
redrawCanvas();
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startX, y);
ctx.stroke();
currentPath = [{x: startX, y: startY}, {x: startX, y: y}];
}
} else {
ctx.lineTo(x, y);
ctx.stroke();
currentPath.push({x: x, y: y});
}
}
},
mouseup(e) {
if (isEraser && drawing) {
// 1. 현재 캔버스 이미지를 저장
const erasedImage = canvas.toDataURL('image/png');
// 2. drawingData를 이미지로만 덮어쓰기
drawingData = [{
type: 'image',
data: erasedImage
}];
// 3. 미러링 업데이트
setTimeout(() => {
updatePreviewMirror();
}, 10);
}
if (drawMode === 'line' && drawing) {
const rect = canvas.getBoundingClientRect();
const endX = e.clientX - rect.left, endY = e.clientY - rect.top;
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.strokeStyle = drawColor;
ctx.stroke();
saveDrawingData('line', { startX, startY, endX, endY });
}
else if (drawMode === 'free' && drawing && currentPath.length > 1) {
saveDrawingData('free', { path: [...currentPath] });
}
drawing = false;
currentPath = [];
// 그리기 완료 후 미러링 업데이트
setTimeout(() => {
updatePreviewMirror();
}, 10);
},
mouseout(e) {
if (drawing) drawing = false;
if (drawMode === 'polyline' && previewLine) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
redrawCanvas();
previewLine = null;
}
}
};
// 점연결 폴리라인 클릭 핸들러
onPolyClick = function(e) {
if (drawMode !== 'polyline') return;
const rect = canvas.getBoundingClientRect();
const rawX = e.clientX - rect.left;
const rawY = e.clientY - rect.top;
let endX = rawX, endY = rawY;
const straight = document.getElementById('straightToggle') ? document.getElementById('straightToggle').checked : false;
if (straight && polyPrev) {
const dx = rawX - polyPrev.x, dy = rawY - polyPrev.y;
if (Math.abs(dx) > Math.abs(dy)) {
endY = polyPrev.y;
} else {
endX = polyPrev.x;
}
}
if (previewLine) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
redrawCanvas();
previewLine = null;
}
if (!polyPrev) {
polyPrev = { x: endX, y: endY };
return;
}
ctx.beginPath();
ctx.moveTo(polyPrev.x, polyPrev.y);
ctx.lineTo(endX, endY);
ctx.strokeStyle = drawColor;
ctx.lineWidth = 2;
ctx.setLineDash([]);
ctx.stroke();
saveDrawingData('polyline', {
startX: polyPrev.x,
startY: polyPrev.y,
endX: endX,
endY: endY
});
polyPrev = { x: endX, y: endY };
// 폴리라인 그리기 완료 후 미러링 업데이트
setTimeout(() => {
updatePreviewMirror();
}, 10);
};
// ESC 누르면 폴리라인 종료
onPolyKeydown = function(e) {
if (drawMode === 'polyline' && e.key === 'Escape') {
polyPrev = null;
if (previewLine) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
redrawCanvas();
previewLine = null;
}
}
};
// 캔버스 내용 다시 그리기
function redrawCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const imageItem = drawingData.find(item => item.type === 'image');
if (imageItem) {
// 이미지 객체가 없거나 src가 다르면 새로 생성
if (!erasedImageObj || erasedImageObj.src !== imageItem.data) {
erasedImageObj = new window.Image();
erasedImageObj.onload = function() {
ctx.drawImage(erasedImageObj, 0, 0, canvas.width, canvas.height);
// 이후 나머지 drawingData 그리기
drawingData.forEach(item => {
if (item.type !== 'image') {
ctx.save();
ctx.strokeStyle = item.color;
ctx.lineWidth = item.lineWidth || 2;
ctx.setLineDash([]);
if (item.type === 'line') {
ctx.beginPath();
ctx.moveTo(item.data.startX, item.data.startY);
ctx.lineTo(item.data.endX, item.data.endY);
ctx.stroke();
} else if (item.type === 'polyline') {
ctx.beginPath();
ctx.moveTo(item.data.startX, item.data.startY);
ctx.lineTo(item.data.endX, item.data.endY);
ctx.stroke();
} else if (item.type === 'free') {
if (item.data.path) {
ctx.beginPath();
ctx.moveTo(item.data.path[0].x, item.data.path[0].y);
for (let i = 1; i < item.data.path.length; i++) {
ctx.lineTo(item.data.path[i].x, item.data.path[i].y);
}
ctx.stroke();
}
} else if (item.type === 'text') {
ctx.save();
ctx.fillStyle = item.color;
ctx.font = item.font;
ctx.textBaseline = 'top';
ctx.fillText(item.data.text, item.data.x, item.data.y);
ctx.restore();
}
ctx.restore();
}
});
};
erasedImageObj.src = imageItem.data;
return; // 이미지가 로드될 때까지 대기
} else {
// 이미지가 이미 로드되어 있으면 바로 그림
ctx.drawImage(erasedImageObj, 0, 0, canvas.width, canvas.height);
drawingData.forEach(item => {
if (item.type !== 'image') {
ctx.save();
ctx.strokeStyle = item.color;
ctx.lineWidth = item.lineWidth || 2;
ctx.setLineDash([]);
if (item.type === 'line') {
ctx.beginPath();
ctx.moveTo(item.data.startX, item.data.startY);
ctx.lineTo(item.data.endX, item.data.endY);
ctx.stroke();
} else if (item.type === 'polyline') {
ctx.beginPath();
ctx.moveTo(item.data.startX, item.data.startY);
ctx.lineTo(item.data.endX, item.data.endY);
ctx.stroke();
} else if (item.type === 'free') {
if (item.data.path) {
ctx.beginPath();
ctx.moveTo(item.data.path[0].x, item.data.path[0].y);
for (let i = 1; i < item.data.path.length; i++) {
ctx.lineTo(item.data.path[i].x, item.data.path[i].y);
}
ctx.stroke();
}
} else if (item.type === 'text') {
ctx.save();
ctx.fillStyle = item.color;
ctx.font = item.font;
ctx.textBaseline = 'top';
ctx.fillText(item.data.text, item.data.x, item.data.y);
ctx.restore();
}
ctx.restore();
}
});
return;
}
}
// image 타입이 없으면 기존 방식대로
if (originalImage) {
ctx.drawImage(originalImage, 0, 0, canvas.width, canvas.height);
} else {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
drawingData.forEach(item => {
ctx.save();
ctx.strokeStyle = item.color;
ctx.lineWidth = item.lineWidth || 2;
ctx.setLineDash([]);
if (item.type === 'line') {
ctx.beginPath();
ctx.moveTo(item.data.startX, item.data.startY);
ctx.lineTo(item.data.endX, item.data.endY);
ctx.stroke();
} else if (item.type === 'polyline') {
ctx.beginPath();
ctx.moveTo(item.data.startX, item.data.startY);
ctx.lineTo(item.data.endX, item.data.endY);
ctx.stroke();
} else if (item.type === 'free') {
if (item.data.path) {
ctx.beginPath();
ctx.moveTo(item.data.path[0].x, item.data.path[0].y);
for (let i = 1; i < item.data.path.length; i++) {
ctx.lineTo(item.data.path[i].x, item.data.path[i].y);
}
ctx.stroke();
}
} else if (item.type === 'text') {
ctx.save();
ctx.fillStyle = item.color;
ctx.font = item.font;
ctx.textBaseline = 'top';
ctx.fillText(item.data.text, item.data.x, item.data.y);
ctx.restore();
}
ctx.restore();
});
}
// 미리보기 선 그리기 함수
function drawPreviewLine(x, y) {
if (!polyPrev || drawMode !== 'polyline') return;
const straight = document.getElementById('straightToggle') ? document.getElementById('straightToggle').checked : false;
let endX = x, endY = y;
if (straight) {
const dx = endX - polyPrev.x, dy = endY - polyPrev.y;
if (Math.abs(dx) > Math.abs(dy)) {
endY = polyPrev.y;
} else {
endX = polyPrev.x;
}
}
if (previewLine) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
redrawCanvas();
}
ctx.beginPath();
ctx.moveTo(polyPrev.x, polyPrev.y);
ctx.lineTo(endX, endY);
ctx.strokeStyle = drawColor;
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.stroke();
previewLine = { startX: polyPrev.x, startY: polyPrev.y, endX, endY };
}
// 그리기 모드 활성화 시 canvas 생성 및 이벤트 등록
function createAndShowCanvas() {
console.log('createAndShowCanvas 함수 시작 - 그리기 모드 활성화');
// 현재 버튼 상태 확인
const drawBtn = document.getElementById('<?= $drawBtnId ?>');
if (!drawBtn || !drawBtn.classList.contains('active')) {
console.log('그리기 버튼이 활성화되지 않음');
return;
}
// 기본 모드 설정
drawMode = 'polyline';
const modeSelect = document.getElementById('drawMode');
const lineBtn = document.getElementById('lineBtn');
const textBtn = document.getElementById('textBtn');
const eraserBtn = document.getElementById('eraserBtn');
if (modeSelect) modeSelect.value = 'polyline';
if (lineBtn) lineBtn.classList.add('active');
if (textBtn) textBtn.classList.remove('active');
if (eraserBtn) eraserBtn.classList.remove('active');
console.log('canvas 존재 여부:', !!canvas);
// 기존 canvas 완전 제거
if (canvas) {
console.log('기존 canvas 완전 제거');
try {
['mousedown','mousemove','mouseup','mouseout'].forEach(evt =>
canvas.removeEventListener(evt, handlers[evt])
);
canvas.removeEventListener('click', onPolyClick);
document.removeEventListener('keydown', onPolyKeydown);
canvas.remove();
} catch (e) {
console.log('canvas 제거 중 오류:', e);
}
canvas = null;
ctx = null;
}
// 기존 drawingCanvas도 제거
const existingCanvas = document.getElementById('drawingCanvas');
if (existingCanvas) {
console.log('기존 drawingCanvas 제거');
existingCanvas.remove();
}
console.log('createCanvasOverlay 함수 호출');
canvas = createCanvasOverlay();
console.log('생성된 canvas:', canvas);
// 그리기 영역에 캔버스 추가
const drawingCanvasArea = document.getElementById('drawingCanvasArea');
if (drawingCanvasArea) {
drawingCanvasArea.appendChild(canvas);
}
ctx = canvas.getContext('2d');
ctx.lineWidth = 2;
drawingData = [];
currentPath = [];
if (originalImage) {
console.log('originalImage가 존재합니다. 캔버스에 그립니다.');
ctx.drawImage(originalImage, 0, 0, canvas.width, canvas.height);
} else {
const preview = document.getElementById('<?= $previewContainerId ?>');
const img = preview ? preview.querySelector('img') : null;
console.log('preview 내부의 img 요소:', img);
if (img) ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
console.log('캔버스 이벤트 리스너 등록');
canvas.addEventListener('mousedown', handlers.mousedown);
canvas.addEventListener('mousemove', handlers.mousemove);
canvas.addEventListener('mouseup', handlers.mouseup);
canvas.addEventListener('mouseout', handlers.mouseout);
canvas.addEventListener('click', onPolyClick);
document.addEventListener('keydown', onPolyKeydown);
// 지우개 가상 커서를 위한 마우스 움직임 이벤트
canvas.addEventListener('mousemove', function(e) {
if (isEraser) {
updateEraserCursorPosition(e);
}
});
canvas.addEventListener('mouseout', function() {
const cursor = document.getElementById('eraserCursor');
if (cursor) {
cursor.style.display = 'none';
}
});
console.log('attachTextHandler 함수 호출');
attachTextHandler();
console.log('createAndShowCanvas 함수 완료 - 그리기 모드 준비 완료');
}
// 문자 클릭 찍어서 그리기
function attachTextHandler() {
if (!canvas) return;
canvas.addEventListener('click', e => {
if (!textMode) return;
// 캔버스 기준 좌표 계산
const rect = canvas.getBoundingClientRect();
const xCanvas = e.clientX - rect.left;
const yCanvas = e.clientY - rect.top;
// 캔버스 내부 좌표인지 확인
if (xCanvas < 0 || xCanvas > canvas.width || yCanvas < 0 || yCanvas > canvas.height) {
return;
}
// 미리보기 컨테이너 기준으로 위치 계산
const preview = document.getElementById('<?= $previewContainerId ?>');
const previewRect = preview.getBoundingClientRect();
// 캔버스와 미리보기 컨테이너의 비율 계산
const canvasRatioX = canvas.width / previewRect.width;
const canvasRatioY = canvas.height / previewRect.height;
// 미리보기 컨테이너 기준 좌표로 변환
const xInput = xCanvas / canvasRatioX;
const yInput = yCanvas / canvasRatioY;
const inp = document.createElement('input');
inp.type = 'text';
inp.className = 'noborder-input';
inp.style.position = 'absolute';
inp.style.left = `${xInput}px`;
inp.style.top = `${yInput}px`;
inp.style.width = '80px';
inp.style.height = '20px';
inp.style.zIndex = 1000;
inp.style.border = 'none';
inp.style.outline = 'none';
inp.style.background = 'transparent';
inp.style.fontSize = '16px';
inp.style.fontFamily = 'Arial';
inp.style.color = drawColor;
inp.style.padding = '0';
preview.appendChild(inp);
inp.focus();
inp.addEventListener('blur', () => {
try {
if (inp && inp.parentNode === preview) {
preview.removeChild(inp);
}
} catch (err) {
console.log('Input element already removed');
}
});
});
}
// drawing-row 동적 생성 함수 추가
function createDrawingRow() {
// 기존 그리기 row가 있다면 제거
const existingDrawingRow = document.querySelector('.drawing-row');
if (existingDrawingRow) {
existingDrawingRow.remove();
}
// 새로운 그리기 row 생성
const drawingRow = document.createElement('div');
drawingRow.className = 'row drawing-row';
drawingRow.innerHTML = `
<div class="col-sm-12">
<div class="drawing-container">
<div class="drawing-canvas-area" id="drawingCanvasArea">
<!-- 캔버스가 여기에 생성됩니다 -->
</div>
<div class="drawing-controls">
<h6 class="mb-3">그리기 도구</h6>
<div class="mb-3">
<label class="form-label">그리기 모드:</label>
<select id="drawMode" class="form-select form-select-sm">
<option value="polyline">점연결</option>
<option value="line">직선</option>
<option value="free">자유선</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">색상:</label>
<input type="color" id="drawColor" class="form-control form-control-sm" value="#000000">
</div>
<div class="mb-3">
<button type="button" id="lineBtn" class="btn btn-outline-primary btn-sm me-2">선</button>
<button type="button" id="textBtn" class="btn btn-outline-primary btn-sm me-2">텍스트</button>
<button type="button" id="eraserBtn" class="btn btn-outline-warning btn-sm">지우개</button>
</div>
<div class="mb-3" id="eraserSizeContainer" style="display: none;">
<label class="form-label">지우개 크기:</label>
<input type="range" id="eraserSize" class="form-range" min="5" max="50" value="15">
<small class="text-muted" id="eraserSizeLabel">15px</small>
</div>
<div class="mb-3" id="straightModeContainer" style="display: block;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="straightToggle">
<label class="form-check-label" for="straightToggle" id="straightToggleLabel">
직각모드
</label>
</div>
</div>
<div class="mb-3">
<button type="button" id="saveDrawingBtn" class="btn btn-success btn-sm me-2">저장</button>
<button type="button" id="clearDrawingBtn" class="btn btn-danger btn-sm">초기화</button>
</div>
</div>
</div>
</div>
`;
// previewContainerId 밑에 추가
const preview = document.getElementById('<?= $previewContainerId ?>');
if (preview) {
preview.appendChild(drawingRow);
setTimeout(() => {
attachDrawingControlEvents();
}, 50);
}
}
// 전역 함수로 노출
window.initializeDrawingFeatures = initializeDrawingFeatures;
window.drawBtnClickHandler = drawBtnClickHandler;
window.updatePreviewMirror = updatePreviewMirror;
window.showNoImageMessage = showNoImageMessage;
window.hideNoImageMessage = hideNoImageMessage;
// DOM이 로드되면 초기화
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeDrawingFeatures);
} else {
initializeDrawingFeatures();
}
})();
</script>