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

ai도움을 받아서 정말 초초초 간단한 게시판을 만들었습니다.

· 8개월 전 · 714 · 4
이미지 114.png

ai도움을 받아서 정말 초초초 간단한 게시판을 만들었습니다.

 

일단 게시판 화면은 레디ai로부터 만들었습니다. 이거 만들다가 토큰이 부족해서 유료결제했네요. ㅋㅋ
https://readdy.site/share/3aa75bb61a5b2346d5759f87cc0dcd3b
https://readdy.site/share/2f8f9c5a40ac5c20ddb96a298cf97731
https://readdy.site/share/fe49ba6d575f383fdc4c3f01e13e1696
https://readdy.site/share/0a822335fb0612ab4f0c58ba136ccf5c
https://readdy.site/share/ff81801ee890937150008aa36edbba07

 

위 html코드를 그록에 첨부하고 그록에게 첨부한 디자인을 바탕으로 php파일 한개로된 게시판을 만들어달라고 했더니
그럭저럭 일단 만들어 주더군요.

 

결론적으로 간단한 기능은 정말 ai가 잘 만들어주더라구요.
커서나 윈드서버를 사용했다면 더 편했을것 같아요.

 

실행했더니 오류가 나서 좀 수정을 하고 세세한 기능은 직접 추가했습니다.


테스트 주소입니다.(링크는 테스트주소라서 없어질수도 있습니다.)

https://bluewind.iwinv.net/ai/memotblm.php

 

소스도 첨부합니다.(왜 하이라이팅이 안될까요?)

[code]

<?php
// MySQL 데이터베이스 연결 설정
$dbHost = 'localhost'; // 호스트명
$dbUser = '사용자 이름'; // MySQL 사용자 이름
$dbPass = '비밀번호'; // MySQL 비밀번호
$dbName = '데이터베이스 이름'; // 데이터베이스 이름


// 데이터베이스 연결 및 생성
function getDbConnection() {
    global $dbHost, $dbUser, $dbPass, $dbName;
    
    $dsn = "mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4";
    $db = new PDO($dsn, $dbUser, $dbPass);
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    // 테이블이 존재하지 않으면 생성,나중에 테이블생성이 되면 이 코드는 제거하는게 성능상 좋습니다.
    $db->exec('CREATE TABLE IF NOT EXISTS `memo` (
        `idx` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
        `title` VARCHAR(255) NULL,
        `content` VARCHAR(1000) NULL,
        `settime` VARCHAR(14) NULL
    )');
    
    return $db;
}

// 유틸 함수: 조회 (SELECT)
function query($db, $sql, ...$params) {
    $stmt = $db->prepare($sql);
    foreach ($params as $i => $param) {
        $stmt->bindValue($i + 1, $param);
    }
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// 유틸 함수: 변경 (INSERT, UPDATE, DELETE)
function execute($db, $sql, ...$params) {
    $stmt = $db->prepare($sql);
    foreach ($params as $i => $param) {
        $stmt->bindValue($i + 1, $param);
    }
    return $stmt->execute();
}

// 요청 처리
$method = $_SERVER['REQUEST_METHOD'];
$action = isset($_GET['action']) ? $_GET['action'] : '';

if (!empty($action)) {
    header('Content-Type: application/json');
    $db = getDbConnection();
    $result = [];

    if ($action === 'create' && $method === 'POST') {
        $title = $_POST['title'] ?? '';
        $content = $_POST['content'] ?? '';
        if (!empty($title) && !empty($content)) {
            $settime = date('YmdHis');
            execute($db, 'INSERT INTO memo (title, content, settime) VALUES (?, ?, ?)', $title, $content, $settime);
            $result = ['success' => true];
        } else {
            $result = ['success' => false, 'message' => 'Title and content are required'];
        }
    }

    elseif ($action === 'read' && $method === 'GET') {
        $idx = $_GET['idx'] ?? '';
        $page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
        $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 5;
        $offset = ($page - 1) * $limit;

        if (!empty($idx)) {
            $rows = query($db, 'SELECT * FROM memo WHERE idx = ?', $idx);
            $result = $rows ? $rows[0] : ['success' => false, 'message' => 'Memo not found'];
        } else {
            $totalRows = query($db, 'SELECT COUNT(*) as total FROM memo');
            $total = $totalRows[0]['total'];
            $memos = query($db, "SELECT * FROM memo ORDER BY idx DESC LIMIT $limit OFFSET $offset");
            $result = ['memos' => $memos, 'total' => $total, 'page' => $page, 'limit' => $limit];
        }
    }

    elseif ($action === 'update' && $method === 'POST') {
        $idx = $_POST['idx'] ?? '';
        $title = $_POST['title'] ?? '';
        $content = $_POST['content'] ?? '';
        if (!empty($idx) && !empty($title) && !empty($content)) {
            $settime = date('YmdHis');
            execute($db, 'UPDATE memo SET title = ?, content = ?, settime = ? WHERE idx = ?', $title, $content, $settime, $idx);
            $result = ['success' => true];
        } else {
            $result = ['success' => false, 'message' => 'Idx, title, and content are required'];
        }
    }

    elseif ($action === 'delete' && $method === 'POST') {
        $idx = $_POST['idx'] ?? '';
        if (!empty($idx)) {
            execute($db, 'DELETE FROM memo WHERE idx = ?', $idx);
            $result = ['success' => true];
        } else {
            $result = ['success' => false, 'message' => 'Idx is required'];
        }
    }

    else {
        $result = ['success' => false, 'message' => 'Invalid action'];
    }

    echo json_encode($result);
    exit;
}
?>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Memotbl CRUD</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css" rel="stylesheet">
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/jqPaginator@1.2.0/dist/1.2.0/jqPaginator.min.js"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: "#4F46E5",
                        secondary: "#6366F1",
                    },
                    borderRadius: {
                        none: "0px",
                        sm: "4px",
                        DEFAULT: "8px",
                        md: "12px",
                        lg: "16px",
                        xl: "20px",
                        "2xl": "24px",
                        "3xl": "32px",
                        full: "9999px",
                        button: "8px",
                    },
                },
            },
        };
    </script>
    <style>
        :where([class^="ri-"])::before { content: "\f3c2"; }
    </style>
<style>
.pagination {display: inline-block; padding-left: 0; margin: 10px 0; border-radius: 4px; }
.pagination>li {display: inline; }
.pagination>li>a {position: relative; float: left; padding: 6px 12px; margin-left: -1px; line-height: 1.42857143; color: #337ab7; text-decoration: none; background-color: #fff; border: 1px solid #ddd; font-size:14px; }
.pagination>.active>a, .pagination>.active>a:focus, .pagination>.active>a:hover {color: #fff; cursor: default; background-color: #337ab7; border-color: #337ab7; }
.pagination>.disabled>a, .pagination>.disabled>a:focus, .pagination>.disabled>a:hover {color: #777; cursor: not-allowed; /* 클릭금지 */ background-color: #fff; border-color: #ddd; }
.pagination>li:first-child>a {margin-left: 0; border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
.pagination>li:last-child>a {border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
.pagination>li.first a,.pagination>li.prev a,.pagination>li.next a,.pagination>li.last a {font-size:0; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; }
.pagination>li.first a:before {content:'≪'; font-size:14px; }
.pagination>li.prev a:before {content:'‹'; font-size:14px; }
.pagination>li.next a:before {content:'›'; font-size:14px; }
.pagination>li.last a:before {content:'≫'; font-size:14px; }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
    <nav class="bg-white shadow fixed w-full z-10">
        <div class="max-w-7xl mx-auto px-4">
            <div class="flex justify-between h-16">
                <div class="flex items-center">
                    <span class="text-2xl font-['Pacifico'] text-primary">Memo</span>
                </div>
                <div class="flex items-center space-x-4">
                    <button onclick="openCreateModal()" class="bg-primary text-white px-4 py-2 rounded-button flex items-center cursor-pointer">
                        <i class="ri-pencil-line mr-2"></i> 새 메모
                    </button>
                </div>
            </div>
        </div>
    </nav>

    <main class="pt-20 pb-8 px-4">
        <div class="max-w-7xl mx-auto">
            <div class="bg-white rounded-lg shadow-sm p-6">
                <h1 class="text-2xl font-bold text-gray-900 mb-6">메모 목록(mysql사용)</h1>
                <div class="overflow-x-auto">
                    <table class="w-full">
                        <thead>
                            <tr class="text-left bg-gray-50">
                                <th class="px-6 py-3 w-20">번호</th>
                                <th class="px-6 py-3 w-68">제목</th>
                                <th class="px-6 py-3 w-36">작성일</th>
                                <th class="px-6 py-3 w-32">관리</th>
                            </tr>
                        </thead>
                        <tbody id="memoList"></tbody>
                    </table>
                </div>
                <div class="mt-6 flex justify-center">
                    <ul id="pagination" class="pagination"></ul>
                </div>
            </div>
        </div>
    </main>

    <!-- Create Modal -->
    <div id="createModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center">
        <div class="bg-white rounded-lg p-6 w-full max-w-md">
            <div class="flex justify-between items-center mb-6">
                <h3 class="text-xl font-bold">새 메모 작성</h3>
                <button onclick="closeCreateModal()" class="text-gray-500 hover:text-gray-700">
                    <i class="ri-close-line text-2xl"></i>
                </button>
            </div>
            <form id="createForm" class="space-y-4">
                <input type="text" name="title" required class="w-full px-4 py-2 border border-gray-300 rounded-lg" placeholder="제목을 입력하세요">
                <textarea name="content" required class="w-full px-4 py-2 border border-gray-300 rounded-lg" rows="5" placeholder="메모 내용을 입력하세요"></textarea>
                <button type="submit" class="w-full bg-primary text-white py-2 rounded-button">추가</button>
            </form>
        </div>
    </div>

    <!-- Update Modal -->
    <div id="updateModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center">
        <div class="bg-white rounded-lg p-6 w-full max-w-md">
            <div class="flex justify-between items-center mb-6">
                <h3 class="text-xl font-bold">메모 수정</h3>
                <button onclick="closeUpdateModal()" class="text-gray-500 hover:text-gray-700">
                    <i class="ri-close-line text-2xl"></i>
                </button>
            </div>
            <form id="updateForm" class="space-y-4">
                <input type="hidden" name="idx">
                <input type="text" name="title" required class="w-full px-4 py-2 border border-gray-300 rounded-lg" placeholder="제목을 입력하세요">
                <textarea name="content" required class="w-full px-4 py-2 border border-gray-300 rounded-lg" rows="5" placeholder="메모 내용을 입력하세요"></textarea>
                <button type="submit" class="w-full bg-primary text-white py-2 rounded-button">수정</button>
            </form>
        </div>
    </div>

    <!-- View Modal -->
    <div id="viewModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center">
        <div class="bg-white rounded-lg p-6 w-full max-w-md">
            <div class="flex justify-between items-center mb-6">
                <h3 class="text-xl font-bold">메모 내용</h3>
                <button onclick="closeViewModal()" class="text-gray-500 hover:text-gray-700">
                    <i class="ri-close-line text-2xl"></i>
                </button>
            </div>
            <div class="space-y-4">
                <h4 id="viewTitle" class="text-lg font-semibold"></h4>
                <p id="viewContent" class="text-gray-700" style="word-break:break-word; white-space:pre-wrap;"></p>
                <p id="viewSettime" class="text-sm text-gray-500"></p>
            </div>
        </div>
    </div>

    <script>
        const itemsPerPage = 5;
        let currentPage = 1;

        function loadMemos(page = 1) {
            fetch(`memotblm.php?action=read&page=${page}&limit=${itemsPerPage}`)
                .then(response => response.json())
                .then(data => {
                    const list = document.getElementById('memoList');
                    list.innerHTML = '';
                    data.memos.forEach(memo => {
                        const tr = document.createElement('tr');
                        tr.className = 'border-t';
                        tr.innerHTML = `
                            <td class="px-6 py-4">${memo.idx}</td>
                            <td class="px-6 py-4">
                                <a href="#" onclick="openViewModal(${memo.idx}, '${encB64(memo.title || '제목 없음')}', '${encB64(memo.content)}', '${memo.settime}'); return false;" 
                                   class="text-primary hover:underline">
                                    ${memo.title ? noTag(memo.title) : '제목 없음'}
                                </a>
                            </td>
                            <td class="px-6 py-4">${formatDate(memo.settime)}</td>
                            <td class="px-6 py-4">
                                <div class="flex space-x-2">
                                    <button onclick="openUpdateModal(${memo.idx}, '${memo.title ? encB64(memo.title) : ''}', '${encB64(memo.content)}')" class="text-gray-600 hover:text-primary">
                                        <i class="ri-edit-line"></i>
                                    </button>
                                    <button onclick="deleteMemo(${memo.idx})" class="text-gray-600 hover:text-red-600">
                                        <i class="ri-delete-bin-line"></i>
                                    </button>
                                </div>
                            </td>
                        `;
                        list.appendChild(tr);
                    });

                    // jqPaginator로 페이징 구현
                    renderPagination(data.total, page);
                });
        }

        function renderPagination(total, currentPage) {
            if(!total)return;
            const totalPages = Math.ceil(total / itemsPerPage);
            $('#pagination').jqPaginator({
                totalPages: totalPages,
                visiblePages: 5, // 한 번에 보여줄 페이지 수
                currentPage: currentPage,
                prev: '<li class="prev"><a href="javascript:;">이전</a></li>',
                next: '<li class="next"><a href="javascript:;">다음</a></li>',
                page: '<li class="page"><a href="javascript:;">{{page}}</a></li>',
                onPageChange: function (num, type) {
                    if (type === 'change') {
                        loadMemos(num);
                    }
                }
            });
        }

        function openCreateModal() {
            document.getElementById('createModal').classList.remove('hidden');
            document.getElementById('createModal').classList.add('flex');
        }

        function closeCreateModal() {
            document.getElementById('createModal').classList.add('hidden');
            document.getElementById('createModal').classList.remove('flex');
            document.getElementById('createForm').reset();
        }

        function openUpdateModal(idx, title, content) {
            const modal = document.getElementById('updateModal');
            modal.classList.remove('hidden');
            modal.classList.add('flex');
            modal.querySelector('input[name="idx"]').value = idx;
            modal.querySelector('input[name="title"]').value = decB64(title);
            modal.querySelector('textarea[name="content"]').value = decB64(content);
        }

        function closeUpdateModal() {
            document.getElementById('updateModal').classList.add('hidden');
            document.getElementById('updateModal').classList.remove('flex');
        }

        function openViewModal(idx, title, content, settime) {
            const modal = document.getElementById('viewModal');
            modal.classList.remove('hidden');
            modal.classList.add('flex');
            document.getElementById('viewTitle').textContent = decB64(title);
            document.getElementById('viewContent').innerHTML = urlLink(noTag(decB64(content)));
            document.getElementById('viewSettime').textContent = `작성일: ${formatDate(settime)}`;
        }

        function closeViewModal() {
            const modal = document.getElementById('viewModal');
            modal.classList.add('hidden');
            modal.classList.remove('flex');
        }

        function noTag(msg) {
            return msg.replace(/</g, "&lt;").replace(/>/g, "&gt;");
        }

        function encB64(msg) {
            return btoa(unescape(encodeURIComponent(msg)));
        }

        function decB64(msg) {
            return decodeURIComponent(escape(atob(msg)));
        }

        function urlLink(source) {
            source = source.replace(/&amp;/gim, "&").replace(/(https?)(:\/\/[^\s<>]+)/g, '<a href="$1$2" target="_blank" class="text-primary hover:underline">$1$2</a>');
            return source;
        }

        function formatDate(settime) {
            if (settime.length === 14) {
                const year = settime.substring(0, 4);
                const month = settime.substring(4, 6);
                const day = settime.substring(6, 8);
                return `${year}-${month}-${day}`;
            }
            return settime;
        }
        
        // 모달 바깥 클릭 시 닫기
        window.addEventListener('click', function(event) {
            const createModal = document.getElementById('createModal');
            const updateModal = document.getElementById('updateModal');
            const viewModal = document.getElementById('viewModal');
    
            if (event.target === createModal) {
                closeCreateModal();
            }
            if (event.target === updateModal) {
                closeUpdateModal();
            }
            if (event.target === viewModal) {
                closeViewModal();
            }
        });

        document.getElementById('createForm').addEventListener('submit', e => {
            e.preventDefault();
            const title = e.target.title.value;
            const content = e.target.content.value;
            fetch('memotblm.php?action=create', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: `title=${encodeURIComponent(title)}&content=${encodeURIComponent(content)}`
            })
            .then(() => {
                closeCreateModal();
                loadMemos(1);
            });
        });

        document.getElementById('updateForm').addEventListener('submit', e => {
            e.preventDefault();
            const idx = e.target.idx.value;
            const title = e.target.title.value;
            const content = e.target.content.value;
            fetch('memotblm.php?action=update', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: `idx=${idx}&title=${encodeURIComponent(title)}&content=${encodeURIComponent(content)}`
            })
            .then(() => {
                closeUpdateModal();
                loadMemos(currentPage);
            });
        });

        function deleteMemo(idx) {
            if (confirm('정말 삭제하시겠습니까?')) {
                fetch('memotblm.php?action=delete', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: `idx=${idx}`
                })
                .then(() => loadMemos(currentPage));
            }
        }

        // 초기 로드
        loadMemos(1);
    </script>
</body>
</html>

[/code]

댓글 작성

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

로그인하기

댓글 4개

^^

와 그냥 깔끔 하네요. 좋다~

감사합니다 ^^

짧은 코드가 엄청나게 다양한 기능을 하는군요. 

게시판 목록

자유게시판

글쓰기
🐛 버그신고