테스트 사이트 - 개발 중인 베타 버전입니다

[AI]최근 그누보드 quill에디터 접목 연구/업글해보고 있는데... (남들이 잘안하는 quill editor)

· 1개월 전 · 116 · 7

그누보드 quill에디터 접목 연구/업글해보고 있는데... 

폰에서 찍은 사진 바로 올리고 할 때 너무 커서 줄여서 올리도록 아이디어를 말했더니 코파일럿 클로드가 답변을 주네요... 

 

-------------------

 

와! 정말 좋은 아이디어네요! 😄

## 관리자 설정 항목 아이디어

**그누보드 관리자 > 환경설정 > 에디터 설정** 같은 곳에:

```
📷 이미지 업로드 설정
├── 최대 가로크기: [1920] px
├── 최대 세로크기: [1080] px  
├── 이미지 품질: [90] % (70-100)
├── 변환 형식: [WebP] ▼ (WebP/JPEG/원본유지)
├── 자동 리사이징: [활성화] ☑️
└── 파일명 규칙: [timestamp-random] ▼
```

## 구현 방향

1. **DB 테이블**: `g5_config`에 컬럼 추가
   ```sql
   ALTER TABLE g5_config ADD COLUMN cf_editor_max_width int DEFAULT 1920;
   ALTER TABLE g5_config ADD COLUMN cf_editor_max_height int DEFAULT 1080;
   ALTER TABLE g5_config ADD COLUMN cf_editor_quality int DEFAULT 90;
   ```

2. **관리자 페이지**: 설정 폼 추가

3. **JavaScript**: PHP 변수로 설정값 전달
   ```javascript
   const MAX_WIDTH = <?php echo $config['cf_editor_max_width'] ?? 1920; ?>;
   ```

## 추가 기능들

- 📊 **용량 제한 설정** (예: 10MB 이상은 강제 리사이징)
- 🎨 **워터마크 옵션** (사이트 로고 자동 삽입)
- 📱 **모바일/PC 다른 설정**
- 📁 **저장 폴더 구조 설정** (`/년/월/` vs `/년월/` vs `/보드별/`)

정말 **차세대 그누보드**가 될 것 같아요! 🚀

언젠가 이런 기능들이 들어간 그누보드를 만나게 되면 좋겠네요~ 😊

 

-----------

 

아래는 많은 에너지를 들여 실험과 AI와 대화를 통해서

계속 테스팅 및 실험중인 코드입니다. (upload.php는 기존 것 활용, 약간 변환후...)

파일 경로는 아래같은 방식

theme/mytheme/mobile/skin/board/quillbasic/write.skin.php

 

[code]

<!-- 쓰기 영역 -->

 <div id="editor" class="border border-red-300 border-base-300 rounded-lg min-h-32"><?= isset($write['wr_content']) ? $write['wr_content'] : '' ?></div>

 

<!-- 스크립트 영역 -->

<!-- Include the Quill library -->

<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>


<!-- Initialize Quill editor -->

<script>

    // Delta 클래스 가져오기

    const Delta = Quill.import('delta');

 

    // 첫 번째 에디터 설정

    const quill = new Quill('#editor', {

        theme: 'snow',

        modules: {

            clipboard: {

                matchVisual: false,

                matchers: [

                    // 이미지 붙여넣기 차단

                    ['IMG', function(node, delta) {

                        console.log('Blocking IMG paste via matcher');

                        return new Delta(); // 빈 Delta 반환으로 삽입 차단

                    }]

                ]

            },

            toolbar: {

                container: [

                    ['bold', 'italic', 'underline'], // 기본 서식

                    [{

                        'header': [1, 2, 3, 4, false] // 헤딩 (H1, H2, H3, H4, 기본크기)

                    }],

                    [{

                        'color': [

                            // 빨간색 (Red) 계열

                            '#fca5a5', '#f87171', '#ef4444', '#dc2626', '#b91c1c',

                            // 주황색 (Orange) 계열

                            '#fdba74', '#fb923c', '#f97316', '#ea580c', '#c2410c',

                            // 노란색 (Yellow) 계열

                            '#fde047', '#facc15', '#eab308', '#ca8a04', '#a16207',

                            // 초록색 (Green) 계열

                            '#86efac', '#4ade80', '#22c55e', '#16a34a', '#15803d',

                            // 파란색 (Blue) 계열

                            '#93c5fd', '#60a5fa', '#3b82f6', '#2563eb', '#1d4ed8',

                            // 남색 (Indigo) 계열

                            '#a5b4fc', '#818cf8', '#6366f1', '#4f46e5', '#4338ca',

                            // 보라색 (Purple) 계열

                            '#c4b5fd', '#a78bfa', '#8b5cf6', '#7c3aed', '#6d28d9',

                            // 기본 색상

                            '#000000', '#ffffff'

                        ]

                    }], // 글자 색깔 (빨주노초파남보 + 흑백)

                    [{

                        'align': []

                    }], // 정렬 (left, center, right, justify)

                    ['clean'], // 서식 제거,

                    ['image']

                ],

                handlers: {

                    image: function() {

                        const input = document.createElement('input');

                        input.setAttribute('type', 'file');

                        input.setAttribute('accept', 'image/*');

                        input.setAttribute('name', 'files[]');

                        input.click();

 

                        input.onchange = async () => {

                            if (input.files && input.files[0]) {

                                await uploadImage(input.files[0]);

                            }

                        };

                    }

                }

            },

            keyboard: {

                bindings: {

                    tab: {

                        key: 9, // Tab 키코드

                        handler: function(range, context) {

                            // 미리보기 업데이트

                            updatePreview();

                            // Preview 버튼으로 포커스 이동

                            document.getElementById('previewBtn') && document.getElementById('previewBtn').focus();

                            return false; // 기본 Tab 동작 방지

                        }

                    }

                }

            }

        },

        height: 300,

        placeholder: '내용을 입력하세요...'

    });

 

    // Quill 이벤트 감지 - 간단한 방식으로 변경

    const quillEditor = quill.root;

   

    // 드래그 앤 드롭 상태 추적 변수

    window.isDragDropActive = false;

   

    // 이미지 paste 처리를 위한 이벤트 리스너

    quillEditor.addEventListener('paste', function(e) {

        console.log('Quill paste event detected!');

       

        // 클립보드에 이미지가 있는지 확인

        const items = e.clipboardData.items;

        let hasImageFile = false;

       

        for (let i = 0; i < items.length; i++) {

            if (items[i].type.indexOf('image') !== -1) {

                hasImageFile = true;

                const file = items[i].getAsFile();

                console.log('Pasted image file:', file.name);

               

                // 기본 붙여넣기 완전 차단

                e.preventDefault();

                e.stopPropagation();

                e.stopImmediatePropagation();

               

                // 우리 방식으로만 업로드

                uploadImage(file);

                return false; // 추가 보험

            }

        }

       

        if (!hasImageFile) {

            console.log('No image in clipboard - allowing normal paste');

        }

    }, true); // capture 단계에서 처리

 

    // Quill 내용 변화 감지 - base64 이미지를 서버 업로드로 교체

    quill.on('text-change', function(delta, oldDelta, source) {

        console.log('Quill text changed!', source);

       

        if (source === 'user') {

            // 약간의 딜레이를 두어 다른 처리가 완료된 후 실행

            setTimeout(() => {

                const contents = quill.getContents();

                console.log('Checking for base64 images in contents:', contents);

               

                // base64 이미지 찾아서 서버 업로드로 교체

                let foundBase64 = false;

                contents.ops.forEach(function(op, index) {

                    if (op.insert && op.insert.image && typeof op.insert.image === 'string' && op.insert.image.startsWith('data:image/')) {

                        console.log('Found base64 image at index', index, '- converting to server upload');

                        foundBase64 = true;

                       

                        // base64를 파일로 변환

                        const base64Data = op.insert.image;

                        const byteCharacters = atob(base64Data.split(',')[1]);

                        const byteNumbers = new Array(byteCharacters.length);

                        for (let i = 0; i < byteCharacters.length; i++) {

                            byteNumbers[i] = byteCharacters.charCodeAt(i);

                        }

                        const byteArray = new Uint8Array(byteNumbers);

                       

                        // 원본 MIME 타입 감지

                        const mimeType = base64Data.split(',')[0].split(':')[1].split(';')[0];

                        const extension = mimeType === 'image/png' ? 'png' :

                                        mimeType === 'image/gif' ? 'gif' :

                                        mimeType === 'image/webp' ? 'webp' : 'jpg';

                       

                        // 고유한 파일명 생성 (타임스탬프 + 랜덤)

                        const timestamp = Date.now();

                        const random = Math.floor(Math.random() * 1000);

                        const uniqueFileName = `pasted-${timestamp}-${random}.${extension}`;

                       

                        const blob = new Blob([byteArray], {type: mimeType});

                       

                        // 파일 객체 생성

                        const file = new File([blob], uniqueFileName, {type: mimeType});

                       

                        // 현재 이미지 위치 계산

                        let imagePosition = 0;

                        for (let i = 0; i < index; i++) {

                            if (contents.ops[i].insert) {

                                if (typeof contents.ops[i].insert === 'string') {

                                    imagePosition += contents.ops[i].insert.length;

                                } else {

                                    imagePosition += 1; // 임베드 요소는 1칸

                                }

                            }

                        }                       

                        console.log('Replacing base64 image at position:', imagePosition);

                       

                        // base64 이미지 삭제

                        quill.deleteText(imagePosition, 1);

                       

                        // 서버 업로드로 교체

                        uploadImageAtPosition(file, imagePosition);

                    }

                });

               

                if (foundBase64) {

                    console.log('Found and processed base64 images');

                }

            }, 100); // 100ms 딜레이

        }

    });

 

    // 이미지 업로드 공통 함수

    async function uploadImage(file) {

        const range = quill.getSelection() || { index: quill.getLength() };

        await uploadImageAtPosition(file, range.index);

    }

   

    // 이미지 리사이징 및 WebP 변환 함수

    async function processImageFile(file) {

        return new Promise((resolve) => {

            const canvas = document.createElement('canvas');

            const ctx = canvas.getContext('2d');

            const img = new Image();

           

            img.onload = function() {

                console.log('Original image size:', img.width, 'x', img.height, 'File size:', (file.size / 1024 / 1024).toFixed(2) + 'MB');

               

                // 리사이징 설정 (FHD: 1920x1080 기준)

                const MAX_WIDTH = 1280;  // 1080 → 720으로 변경 가능  

                const MAX_HEIGHT = 720;  // 1080 → 720으로 변경 가능  

                const QUALITY = 0.9;     // 0.85 → 0.9로 품질 향상 가능

               

                let { width, height } = img;

               

                // 비율 유지하면서 리사이징

                if (width > MAX_WIDTH || height > MAX_HEIGHT) {

                    const ratio = Math.min(MAX_WIDTH / width, MAX_HEIGHT / height);

                    width = Math.floor(width * ratio);

                    height = Math.floor(height * ratio);

                    console.log('Resized to:', width, 'x', height);

                } else {

                    console.log('No resizing needed');

                }

               

                canvas.width = width;

                canvas.height = height;

               

                // 이미지 그리기

                ctx.drawImage(img, 0, 0, width, height);

               

                // WebP로 변환 (브라우저 지원 확인)

                if (canvas.toBlob && 'toBlob' in canvas) {

                    canvas.toBlob((blob) => {

                        const timestamp = Date.now();

                        const random = Math.floor(Math.random() * 1000);

                        const fileName = `processed-${timestamp}-${random}.webp`;

                        const processedFile = new File([blob], fileName, { type: 'image/webp' });

                        console.log('Processed file size:', (processedFile.size / 1024 / 1024).toFixed(2) + 'MB');

                        resolve(processedFile);

                    }, 'image/webp', QUALITY);

                } else {

                    // WebP 지원 안 되면 JPEG로 폴백

                    canvas.toBlob((blob) => {

                        const timestamp = Date.now();

                        const random = Math.floor(Math.random() * 1000);

                        const fileName = `processed-${timestamp}-${random}.jpg`;

                        const processedFile = new File([blob], fileName, { type: 'image/jpeg' });

                        console.log('Fallback to JPEG. File size:', (processedFile.size / 1024 / 1024).toFixed(2) + 'MB');

                        resolve(processedFile);

                    }, 'image/jpeg', QUALITY);

                }

            };

           

            img.onerror = function() {

                console.error('Image processing failed, using original file');

                resolve(file);

            };

           

            img.src = URL.createObjectURL(file);

        });

    }

 

    // 위치 지정 이미지 업로드 함수

    async function uploadImageAtPosition(file, position) {

        console.log('Starting image upload process...');

       

        // 이미지 처리 (리사이징 + WebP 변환)

        const processedFile = await processImageFile(file);

       

        const formData = new FormData();

        formData.append('files[]', processedFile);

 

        try {

            // 그누보드 플러그인의 upload.php 사용

            const res = await fetch('<?php echo G5_PLUGIN_URL ?>/editor/quill/upload.php', {

                method: 'POST',

                body: formData,

            });

 

            const result = await res.json();

 

            if (result.success && result.data.images && result.data.images.length > 0) {

                // 지정된 위치에 이미지 삽입

                const imageUrl = result.data.baseurl + result.data.images[0];

                console.log('Inserting image at position:', position);

                quill.insertEmbed(position, 'image', imageUrl);

                // 커서를 이미지 다음으로 이동

                quill.setSelection(position + 1);

            } else {

                alert('이미지 업로드에 실패했습니다: ' + (result.data.messages ? result.data.messages.join(', ') : '알 수 없는 오류'));

            }

        } catch (error) {

            console.error('Upload error:', error);

            alert('이미지 업로드 중 오류가 발생했습니다.');

        }

    }

 

    // 스타일을 daisyUI 클래스로 변환하는 함수

    function convertStylesToClasses(htmlContent) {

        // RGB 값을 가장 가까운 Tailwind 색상으로 변환하는 함수

        function rgbToTailwind(r, g, b) {

            // 확장된 Tailwind 색상 팔레트 (빨주노초파남보 300-700)

            const tailwindColors = {

                // Red 계열

                'red-300': [252, 165, 165],

                'red-400': [248, 113, 113],

                'red-500': [239, 68, 68],

                'red-600': [220, 38, 38],

                'red-700': [185, 28, 28],

 

                // Orange 계열

                'orange-300': [253, 186, 116],

                'orange-400': [251, 146, 60],

                'orange-500': [249, 115, 22],

                'orange-600': [234, 88, 12],

                'orange-700': [194, 65, 12],

 

                // Yellow 계열

                'yellow-300': [253, 224, 71],

                'yellow-400': [250, 204, 21],

                'yellow-500': [234, 179, 8],

                'yellow-600': [202, 138, 4],

                'yellow-700': [161, 98, 7],

 

                // Green 계열

                'green-300': [134, 239, 172],

                'green-400': [74, 222, 128],

                'green-500': [34, 197, 94],

                'green-600': [22, 163, 74],

                'green-700': [21, 128, 61],

 

                // Blue 계열

                'blue-300': [147, 197, 253],

                'blue-400': [96, 165, 250],

                'blue-500': [59, 130, 246],

                'blue-600': [37, 99, 235],

                'blue-700': [29, 78, 216],

 

                // Indigo 계열 (남색)

                'indigo-300': [165, 180, 252],

                'indigo-400': [129, 140, 248],

                'indigo-500': [99, 102, 241],

                'indigo-600': [79, 70, 229],

                'indigo-700': [67, 56, 202],

 

                // Purple 계열

                'purple-300': [196, 181, 253],

                'purple-400': [167, 139, 250],

                'purple-500': [139, 92, 246],

                'purple-600': [124, 58, 237],

                'purple-700': [109, 40, 217],

 

                // 기본 색상

                'black': [0, 0, 0],

                'white': [255, 255, 255]

            };

 

            let closestColor = 'gray-500';

            let minDistance = Infinity;

 

            // 각 색상과의 거리 계산 (유클리드 거리)

            for (const [colorName, [tr, tg, tb]] of Object.entries(tailwindColors)) {

                const distance = Math.sqrt(

                    Math.pow(r - tr, 2) +

                    Math.pow(g - tg, 2) +

                    Math.pow(b - tb, 2)

                );

 

                if (distance < minDistance) {

                    minDistance = distance;

                    closestColor = colorName;

                }

            }

 

            // 특별한 경우들 처리

            if (closestColor === 'black') return 'text-black';

            if (closestColor === 'white') return 'text-white';

 

            return `text-${closestColor}`;

        }

 

        // 헥스 색상을 RGB로 변환

        function hexToRgb(hex) {

            const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);

            return result ? {

                r: parseInt(result[1], 16),

                g: parseInt(result[2], 16),

                b: parseInt(result[3], 16)

            } : null;

        }

 

        // RGB 문자열을 파싱

        function parseRgb(rgbString) {

            const match = rgbString.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);

            return match ? {

                r: parseInt(match[1]),

                g: parseInt(match[2]),

                b: parseInt(match[3])

            } : null;

        }

 

        // HTML 요소들을 파싱

        const parser = new DOMParser();

        const doc = parser.parseFromString(`<div>${htmlContent}</div>`, 'text/html');

 

        // 헤딩 태그를 Tailwind 클래스로 변환

        const headings = doc.querySelectorAll('h1, h2, h3, h4');

        headings.forEach(heading => {

            let classes = heading.getAttribute('class') || '';

            const tagName = heading.tagName.toLowerCase();

 

            // 기존 헤딩 클래스 제거 (중복 방지)

            classes = classes.replace(/text-\d*xl|text-lg|text-base/g, '').trim();

 

            // 헤딩별 Tailwind 클래스 추가

            switch (tagName) {

                case 'h1':

                    classes += (classes ? ' ' : '') + 'text-4xl font-bold';

                    break;

                case 'h2':

                    classes += (classes ? ' ' : '') + 'text-3xl font-bold';

                    break;

                case 'h3':

                    classes += (classes ? ' ' : '') + 'text-2xl font-bold';

                    break;

                case 'h4':

                    classes += (classes ? ' ' : '') + 'text-xl font-bold';

                    break;

            }

 

            if (classes) {

                heading.setAttribute('class', classes);

            }

        });

 

        // Quill 클래스명을 Tailwind 클래스로 변환

        const quillElements = doc.querySelectorAll('.ql-align-center, .ql-align-right, .ql-align-justify');

        quillElements.forEach(element => {

            let classes = element.getAttribute('class') || '';

 

            if (classes.includes('ql-align-center')) {

                classes = classes.replace('ql-align-center', 'text-center');

            }

            if (classes.includes('ql-align-right')) {

                classes = classes.replace('ql-align-right', 'text-right');

            }

            if (classes.includes('ql-align-justify')) {

                classes = classes.replace('ql-align-justify', 'text-justify');

            }

 

            element.setAttribute('class', classes);

        });

 

        // 스타일 속성이 있는 요소들 처리

        const styledElements = doc.querySelectorAll('[style]');

        styledElements.forEach(element => {

            const style = element.getAttribute('style');

            let classes = element.getAttribute('class') || '';

 

            // 색상 스타일 변환 (동적)

            const colorMatch = style.match(/color:\s*([^;]+)/);

            if (colorMatch) {

                const colorValue = colorMatch[1].trim();

                let tailwindClass = '';

 

                if (colorValue.startsWith('#')) {

                    // 헥스 색상 처리

                    const rgb = hexToRgb(colorValue);

                    if (rgb) {

                        tailwindClass = rgbToTailwind(rgb.r, rgb.g, rgb.b);

                    }

                } else if (colorValue.startsWith('rgb(')) {

                    // RGB 색상 처리

                    const rgb = parseRgb(colorValue);

                    if (rgb) {

                        tailwindClass = rgbToTailwind(rgb.r, rgb.g, rgb.b);

                    }

                }

 

                if (tailwindClass && !classes.includes(tailwindClass)) {

                    classes += (classes ? ' ' : '') + tailwindClass;

                }

            }

 

            // 정렬 스타일 변환

            if (style.includes('text-align: center')) {

                classes += (classes ? ' ' : '') + 'text-center';

            } else if (style.includes('text-align: right')) {

                classes += (classes ? ' ' : '') + 'text-right';

            } else if (style.includes('text-align: justify')) {

                classes += (classes ? ' ' : '') + 'text-justify';

            }

 

            // 변환된 클래스 적용

            if (classes) {

                element.setAttribute('class', classes);

            }

 

            // 처리된 스타일 속성 제거

            let newStyle = style

                .replace(/color:\s*[^;]+;?/g, '')

                .replace(/text-align:\s*[^;]+;?/g, '')

                .trim();

 

            if (newStyle) {

                element.setAttribute('style', newStyle);

            } else {

                element.removeAttribute('style');

            }

        });

 

        return doc.querySelector('div').innerHTML;

    }

 

    // Tailwind 클래스를 Quill이 이해할 수 있는 인라인 스타일로 변환

    function convertTailwindToInlineStyles(htmlContent) {

        // Tailwind 색상 클래스와 대응되는 색상값 (전체 팔레트)

        const tailwindColorMap = {

            // Red 계열

            'text-red-300': '#fca5a5',

            'text-red-400': '#f87171',

            'text-red-500': '#ef4444',

            'text-red-600': '#dc2626',

            'text-red-700': '#b91c1c',

            // Orange 계열

            'text-orange-300': '#fdba74',

            'text-orange-400': '#fb923c',

            'text-orange-500': '#f97316',

            'text-orange-600': '#ea580c',

            'text-orange-700': '#c2410c',

            // Yellow 계열

            'text-yellow-300': '#fde047',

            'text-yellow-400': '#facc15',

            'text-yellow-500': '#eab308',

            'text-yellow-600': '#ca8a04',

            'text-yellow-700': '#a16207',

            // Green 계열

            'text-green-300': '#86efac',

            'text-green-400': '#4ade80',

            'text-green-500': '#22c55e',

            'text-green-600': '#16a34a',

            'text-green-700': '#15803d',

            // Blue 계열

            'text-blue-300': '#93c5fd',

            'text-blue-400': '#60a5fa',

            'text-blue-500': '#3b82f6',

            'text-blue-600': '#2563eb',

            'text-blue-700': '#1d4ed8',

            // Indigo 계열

            'text-indigo-300': '#a5b4fc',

            'text-indigo-400': '#818cf8',

            'text-indigo-500': '#6366f1',

            'text-indigo-600': '#4f46e5',

            'text-indigo-700': '#4338ca',

            // Purple 계열

            'text-purple-300': '#c4b5fd',

            'text-purple-400': '#a78bfa',

            'text-purple-500': '#8b5cf6',

            'text-purple-600': '#7c3aed',

            'text-purple-700': '#6d28d9',

            // 기본 색상

            'text-black': '#000000',

            'text-white': '#ffffff'

        };

 

        // 정렬 클래스

        const alignMap = {

            'text-center': 'center',

            'text-right': 'right',

            'text-justify': 'justify'

        };

 

        // DOM 파싱

        const parser = new DOMParser();

        const doc = parser.parseFromString(`<div>${htmlContent}</div>`, 'text/html');

 

        // 색상 클래스 변환

        Object.keys(tailwindColorMap).forEach(twClass => {

            doc.querySelectorAll('.' + twClass).forEach(el => {

                let style = el.getAttribute('style') || '';

                style += `color: ${tailwindColorMap[twClass]};`;

                el.setAttribute('style', style);

                el.classList.remove(twClass);

            });

        });

 

        // 정렬 클래스 변환

        Object.keys(alignMap).forEach(twClass => {

            doc.querySelectorAll('.' + twClass).forEach(el => {

                let style = el.getAttribute('style') || '';

                style += `text-align: ${alignMap[twClass]};`;

                el.setAttribute('style', style);

                el.classList.remove(twClass);

            });

        });

 

        // 헤딩 크기 등도 필요하다면 추가 변환

 

        return doc.querySelector('div').innerHTML;

    }

 

    // 변환된 내용을 저장할 변수

    let lastConvertedContent = "";

 

    // 미리보기 업데이트 함수 (첫 번째 에디터)

    function updatePreview() {

        // Quill 에디터의 HTML 내용 가져오기

        const htmlContent = quill.root.innerHTML;

 

        // 스타일을 클래스로 변환

        const convertedContent = convertStylesToClasses(htmlContent);

        lastConvertedContent = convertedContent; // 변환된 내용 저장

 

        console.log('Converted Content:', lastConvertedContent);

    }

</script>

 

<script>

... 기존 스크립트 부분 중략... 

    function fwrite_submit(f) {

        // Quill 에디터 내용 hidden input에 넣기

        var quillHtml = quill.root.innerHTML.trim();

 

        // 변환된 내용이 없으면 updatePreview를 먼저 호출

        if (!lastConvertedContent) updatePreview();

 

        document.getElementById('wr_content').value = lastConvertedContent;

 

        <?php echo $editor_js; // 에디터 사용시 자바스크립트에서 내용을 폼필드로 넣어주며 내용이 입력되었는지 검사함  

        ?>

 

        var subject = "";

        var content = "";

<!-- ... 기존 스크립트들 중략... -->

 

        document.getElementById("btn_submit").disabled = "disabled";

        return true;

    }

 

    var uploadFile = $('.filebox .uploadBtn');

    uploadFile.on('change', function() {

        if (window.FileReader) {

            var filename = $(this)[0].files[0].name;

        } else {

            var filename = $(this).val().split('/').pop().split('\\').pop();

        }

        $(this).siblings('.fileName').val(filename);

    });

</script>

 

<?php if ($w == 'u' && $write['wr_content']) { ?>

    <script>

        // 수정 모드에서는 기존 콘텐츠 로드를 위해 clipboard matcher를 임시 제거

        console.log('Edit mode: temporarily removing image matchers for content loading');

       

        // 기존 matcher들을 임시 저장

        const originalMatchers = quill.clipboard.matchers.slice();

       

        // 이미지 관련 matcher들만 제거

        quill.clipboard.matchers = quill.clipboard.matchers.filter(matcher => {

            return !(matcher[0] === 'IMG' ||

                    (matcher[0] === Node.ELEMENT_NODE && matcher[1].toString().includes('IMG')));

        });

       

        // Tailwind 클래스를 인라인 스타일로 변환하여 Quill에 삽입

        (function() {

            var tailwindHtml = <?php echo json_encode($write['wr_content']); ?>;

            console.log('Loading content:', tailwindHtml);

            var quillHtml = convertTailwindToInlineStyles(tailwindHtml);

            quill.clipboard.dangerouslyPasteHTML(quillHtml);

           

            // 콘텐츠 로드 완료 후 matcher들을 다시 복원

            setTimeout(function() {

                console.log('Content loaded, restoring image matchers');

                quill.clipboard.matchers = originalMatchers;

            }, 100);

        })();

    </script>

<?php } ?>

//... 잘리지 않기를... 칼럼크기로 인하여... 

[/code]

1명이 반응했습니다

댓글 작성

댓글을 작성하시려면 로그인이 필요합니다.

로그인하기

댓글 7개

아... 코드 하이라이트를 제가 잘하지 못해서일까요? 코드가 엉망으로 보이네요

어떻게 해야 코드를 그대로 넣어서 보여줄 수 있을지... ㅜㅜ;

(아 그리고 글자 lenghth 제한이 있는지 잘려서 올라갔네요... )

md 코드들도 바로 렌더링해서 view페이지가 적용되었으면 좋겠네요. 

그것도 AI를 통하면 구현가능하겠지요? 

[code:md] [/code:md] 이렇게 하면 md의 내용이 렌더링되어서 나온다든지... 

1개월 전

[code]

test

[/code]

이런식으로 하면 되는지

@마젠토 처음에는 `<code>~~~</code>` 이렇게 하니까 마구 깨져서 기억을 더듬어 중괄호인 [code]~~~[/code] 로 한 것이 위의 코드입니다. : )

하이라이트는 안되어도 중간에 코드가 끊기는 것은 아마도 mysql column 크기가 mediumtext가 되어야할까요? 일단 잘린듯해요. ㅜㅜ;

1개월 전

@xpem [ code ] [/ code ] 입니다.

 

마크다운은 ``` 으로..

@마젠토 지금 위의 올라간 텍스트는 말씀하신대로 올린 것입니다. 

마크다운에 대해서는 배웠네요. 

원래 제가이용하는 마크다운 에디터에서 코드 인서트할 때 ``` 이걸 이용했는데, 여긴 분리를 했군요.

( [ code ] 로 시작했으나 폰트가 약간 이상하더니, 소스코드가 길어서 잘리는 바람에 수정하면서 html이 적용되었었나봅니다. 이젠 어느정도까지는 제대로 코드 하이라이팅이 되어 보이네요)

줄이고 줄여 16000자 정도로 줄였더니 안잘리고 올라가네요. 

여튼 안정화되면 스킨게시판에 올려볼 수 있기를 소망해봅니다. 

게시글 목록

번호 제목
965
958
942
934
929
918
909
904
899
892
890
883
880
879
865
863
861
859
855
854
846
842
841
840
837
829
822
816
813
799