ai도움을 받아서 정말 초초초 간단한 게시판을 만들었습니다. 정보
ai도움을 받아서 정말 초초초 간단한 게시판을 만들었습니다.
본문
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
소스도 첨부합니다.(왜 하이라이팅이 안될까요?)
<?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, "<").replace(/>/g, ">");
}
function encB64(msg) {
return btoa(unescape(encodeURIComponent(msg)));
}
function decB64(msg) {
return decodeURIComponent(escape(atob(msg)));
}
function urlLink(source) {
source = source.replace(/&/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>
추천
3
3
관련링크
댓글 4개

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

감사합니다 ^^

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