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

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

그누보드 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의 내용이 렌더링되어서 나온다든지... 

[code]

test

[/code]

이런식으로 하면 되는지

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

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

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

 

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

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

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

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

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

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

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

게시판 목록

AI

글쓰기

222

3주 전
🐛 버그신고