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

345 lines
13 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>그리기 에디터 대화상자 (HTML 텍스트입력)</title>
<!-- Bootstrap 5.3 CSS + Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { padding: 1rem; }
dialog {
width: 90vw; height: 90vh;
border: none; border-radius: .5rem;
padding: 0; overflow: hidden;
}
dialog::backdrop { background: rgba(0,0,0,.5); }
#editorHeader {
background: #f8f9fa; padding: .5rem 1rem;
border-bottom: 1px solid #dee2e6;
}
#editorHeader h5 { margin: 0; display: inline; }
#editorHeader .btn-close { float: right; }
#editorToolbar, #editorToolbar2 {
background: #fff;
padding: .5rem; gap: .25rem;
}
#editorToolbar { display: flex; align-items: center; flex-wrap: wrap; }
#editorToolbar2 { display: flex; align-items: center; }
.toolbar-btn { width: 40px; height: 40px; padding: 0; }
.toolbar-btn i { font-size: 1.2rem; }
.toolbar-btn.active { background-color: #0d6efd; color: #fff; }
.color-btn { width: 24px; height: 24px; padding: 0; border: 2px solid #fff; cursor: pointer; }
.color-btn.active { border-color: #000; }
#editorBody { position: relative; width: 100%; height: calc(100% - 128px); }
#editorBody canvas { width: 100%; height: 100%; cursor: crosshair; }
.form-range { width: 150px; }
/* HTML textarea 스타일 */
.canvas-text-input {
position: absolute;
border: 1px dashed #666;
background: transparent;
resize: none;
outline: none;
padding: 2px;
font-family: sans-serif;
}
</style>
</head>
<body>
<!-- 그리기 시작 버튼 -->
<button id="openEditorBtn" class="btn btn-primary mb-3">
<i class="bi bi-pencil-square"></i> 그리기 시작
</button>
<!-- 대화상자 -->
<dialog id="editorDialog">
<div id="editorHeader">
<h5>이미지 편집기</h5>
<button type="button" class="btn-close" aria-label="닫기" id="closeEditorBtn"></button>
</div>
<!-- 툴바 -->
<div id="editorToolbar">
<button id="polyBtn" class="btn btn-outline-primary toolbar-btn" title="폴리라인"><i class="bi bi-vector-pen"></i></button>
<button id="freeBtn" class="btn btn-outline-primary toolbar-btn" title="자유선"><i class="bi bi-brush"></i></button>
<button id="lineBtn" class="btn btn-outline-primary toolbar-btn" title="직선 (L키)"><i class="bi bi-slash-lg"></i></button>
<button id="textBtn" class="btn btn-outline-primary toolbar-btn" title="문자입력"><i class="bi bi-type"></i></button>
<button id="eraserBtn" class="btn btn-outline-warning toolbar-btn" title="지우개"><i class="bi bi-eraser-fill"></i></button>
<button id="selectBtn" class="btn btn-outline-secondary toolbar-btn" title="객체선택"><i class="bi bi-cursor-text"></i></button>
<div class="form-check form-switch ms-3">
<input class="form-check-input" type="checkbox" id="rightAngle" checked>
<label class="form-check-label" for="rightAngle">직각 고정</label>
</div>
<button id="clearBtn" class="btn btn-outline-danger ms-auto">전체 지우기</button>
</div>
<!-- 지우개 크기 & 색상 -->
<div id="editorToolbar2" class="d-flex align-items-center px-3 pb-2">
<div class="btn-group" role="group" id="colorPicker">
<button type="button" class="btn color-btn" data-color="#000000" style="background:#000;"></button>
<button type="button" class="btn color-btn" data-color="#ff0000" style="background:#f00;"></button>
<button type="button" class="btn color-btn" data-color="#0000ff" style="background:#00f;"></button>
<button type="button" class="btn color-btn" data-color="#00aa00" style="background:#0a0;"></button>
<button type="button" class="btn color-btn" data-color="#ff8800" style="background:#f80;"></button>
<button type="button" class="btn color-btn" data-color="#800080" style="background:#808;"></button>
<button type="button" class="btn color-btn" data-color="#888888" style="background:#888;"></button>
</div>
<label class="mb-0 me-2">지우개 크기</label>
<input type="range" class="form-range me-2" id="eraserRange" min="5" max="100" step="1" value="20">
<span id="eraserSizeLabel" class="fw-bold">20</span>px
</div>
<!-- 캔버스 영역 -->
<div id="editorBody">
<canvas id="c" width="800" height="600"></canvas>
</div>
</dialog>
<!-- 의존 스크립트 -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script>
<script>
$(function(){
const dialog = document.getElementById('editorDialog');
const editorBody = document.getElementById('editorBody');
// “그리기 시작” 버튼
$('#openEditorBtn').click(() => {
if (!dialog.open) dialog.showModal();
});
// 우측 X 버튼
$('#closeEditorBtn').click(() => dialog.close());
// ESC로 닫히는 기본 동작 막기
dialog.addEventListener('cancel', e => e.preventDefault());
// Fabric.js 초기화
const canvas = new fabric.Canvas('c',{ selection:false });
let mode='polyline', selectMode=false, isLine=false, lineObj=null, isRight=true;
let polyPoints=[], previewLine=null, isPreview=true, currentColor='#0000ff';
// 브러시 설정
const freeBrush = canvas.freeDrawingBrush; freeBrush.width=2; freeBrush.color=currentColor;
const eraserBrush = new fabric.PencilBrush(canvas);
eraserBrush.width = +$('#eraserRange').val(); eraserBrush.color='#ffffff';
function updateEraserCursor(r){
const svg=`<svg xmlns="http://www.w3.org/2000/svg" width="${r}" height="${r}"><circle cx="${r/2}" cy="${r/2}" r="${r/2}" stroke="black" fill="none"/></svg>`;
canvas.freeDrawingCursor = `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${r/2} ${r/2}, auto`;
}
updateEraserCursor(eraserBrush.width);
// 배경 이미지
fabric.Image.fromURL('https://via.placeholder.com/800x600.png', img=>{
img.selectable = false;
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
});
// path 생성 시 스타일
canvas.on('path:created', e=>{
e.path.selectable = selectMode;
e.path.stroke = (mode==='eraser')? '#ffffff' : currentColor;
});
// 버튼 강조
function highlight(){
$('.toolbar-btn').removeClass('active');
switch(mode){
case 'polyline': $('#polyBtn').addClass('active'); break;
case 'free': $('#freeBtn').addClass('active'); break;
case 'line': $('#lineBtn').addClass('active'); break;
case 'text': $('#textBtn').addClass('active'); break;
case 'eraser': $('#eraserBtn').addClass('active'); break;
case 'select': $('#selectBtn').addClass('active'); break;
}
}
// 모드 전환
function setMode(m){
if(mode==='polyline'&&m!=='polyline'){
if(previewLine){ canvas.remove(previewLine); previewLine=null; }
polyPoints=[];
}
mode=m; canvas.isDrawingMode=false;
if(m==='free'){ canvas.isDrawingMode=true; canvas.freeDrawingBrush=freeBrush; }
if(m==='eraser'){ canvas.isDrawingMode=true; canvas.freeDrawingBrush=eraserBrush; }
const el=canvas.upperCanvasEl;
el.style.cursor = (m==='eraser')? canvas.freeDrawingCursor : 'crosshair';
selectMode = (m==='select');
canvas.selection = selectMode;
canvas.forEachObject(o=>{
if(o===canvas.backgroundImage) return;
o.selectable=selectMode;
o.evented =selectMode;
});
highlight();
}
setMode('polyline');
// 툴바 클릭
$('#polyBtn').click(()=>setMode('polyline'));
$('#freeBtn').click(()=>setMode('free'));
$('#lineBtn').click(()=>setMode('line'));
$('#textBtn').click(()=>setMode('text'));
$('#eraserBtn').click(()=>setMode('eraser'));
$('#selectBtn').click(()=> setMode($('#selectBtn').hasClass('active')?'free':'select'));
$('#clearBtn').click(()=>{
canvas.clear();
fabric.Image.fromURL('https://via.placeholder.com/800x600.png',img=>{
img.selectable=false; canvas.setBackgroundImage(img,canvas.renderAll.bind(canvas));
});
polyPoints=[]; if(previewLine)canvas.remove(previewLine); previewLine=null;
canvas.upperCanvasEl.style.cursor='crosshair';
setMode('polyline');
});
// 색상 선택
$('#colorPicker .color-btn').click(function(){
$('#colorPicker .color-btn').removeClass('active');
$(this).addClass('active');
currentColor = $(this).data('color');
freeBrush.color = currentColor;
});
$('#rightAngle').change(function(){ isRight=this.checked; });
$('#eraserRange').on('input',function(){
const s=+this.value;
$('#eraserSizeLabel').text(s);
eraserBrush.width = s; updateEraserCursor(s);
if(mode==='eraser') canvas.upperCanvasEl.style.cursor=canvas.freeDrawingCursor;
});
// 전체 keydown: polyline ESC, text ESC, delete, shortcut L
$(document).keydown(e=>{
if(e.key.toLowerCase()==='l') setMode('line');
if(e.key==='Escape'&&mode==='polyline'){
isPreview=false;
if(previewLine){ canvas.remove(previewLine); previewLine=null; canvas.requestRenderAll(); }
}
if(e.key==='Escape'&&mode==='text'){
const existing = editorBody.querySelector('.canvas-text-input');
existing && existing.remove();
}
if(e.key==='Delete'&&selectMode){
const objs=canvas.getActiveObjects();
objs.forEach(o=>canvas.remove(o));
canvas.discardActiveObject();
canvas.requestRenderAll();
}
});
// 마우스 다운: line/poly/text
canvas.on('mouse:down',o=>{
const p=canvas.getPointer(o.e);
if(mode==='line'){
isLine=true;
lineObj=new fabric.Line([p.x,p.y,p.x,p.y],{
stroke:currentColor, strokeWidth:2,
originX:'center', originY:'center',
selectable:selectMode
});
canvas.add(lineObj);
}
else if(mode==='polyline'){
isPreview=true;
let x=p.x,y=p.y;
if(isRight&&polyPoints.length){
const L=polyPoints[polyPoints.length-1];
const dx=Math.abs(x-L.x),dy=Math.abs(y-L.y);
if(dx>dy) y=L.y; else x=L.x;
}
if(polyPoints.length){
const prev=polyPoints[polyPoints.length-1];
const seg=new fabric.Line([prev.x,prev.y,x,y],{
stroke:currentColor, strokeWidth:2,
originX:'center', originY:'center',
selectable:selectMode
});
canvas.add(seg);
}
polyPoints.push({x,y});
if(previewLine){ canvas.remove(previewLine); previewLine=null; }
}
else if (mode === 'text') {
// 기존에 떠 있는 textarea 제거
editorBody.querySelectorAll('.canvas-text-input').forEach(el => el.remove());
// HTML textarea 오버레이 생성
const ta = document.createElement('textarea');
ta.className = 'canvas-text-input';
ta.style.left = p.x + 'px';
ta.style.top = p.y + 'px';
ta.style.fontSize = '14px';
ta.rows = 1;
ta.cols = 20;
editorBody.appendChild(ta);
// 바로 포커스
setTimeout(() => {
ta.focus();
// 전체 텍스트를 선택하려면 아래를 추가하세요.
// ta.select();
}, 0);
// Enter / Esc 처리
ta.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
const txt = ta.value.trim();
if (txt) {
const txtObj = new fabric.Text(txt, {
left: p.x,
top: p.y,
fontSize: 14,
fill: currentColor
});
canvas.add(txtObj);
}
ta.remove();
}
else if (e.key === 'Escape') {
ta.remove();
}
});
}
});
// 마우스 무브: line/poly preview
canvas.on('mouse:move',o=>{
const p=canvas.getPointer(o.e);
if(mode==='line'&&isLine){
let x2=p.x,y2=p.y;
if(isRight){
const dx=Math.abs(x2-lineObj.x1), dy=Math.abs(y2-lineObj.y1);
if(dx>dy) y2=lineObj.y1; else x2=lineObj.x1;
}
lineObj.set({x2,y2});
canvas.requestRenderAll();
}
else if(mode==='polyline'&&polyPoints.length&&isPreview){
const L=polyPoints[polyPoints.length-1];
let x2=p.x,y2=p.y;
if(isRight){
const dx=Math.abs(x2-L.x),dy=Math.abs(y2-L.y);
if(dx>dy) y2=L.y; else x2=L.x;
}
if(previewLine){
previewLine.set({x1:L.x,y1:L.y,x2,y2});
} else {
previewLine=new fabric.Line([L.x,L.y,x2,y2],{
stroke:'gray',strokeWidth:1,strokeDashArray:[5,5],selectable:false
});
canvas.add(previewLine);
}
canvas.requestRenderAll();
}
});
canvas.on('mouse:up',()=>{
if(mode==='line'){ isLine=false; lineObj=null; }
});
});
</script>
</body>
</html>