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, "<").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>
[/code]
댓글 4개
게시글 목록
| 번호 | 제목 |
|---|---|
| 1717629 | |
| 1717626 | |
| 1717625 | |
| 1717621 | |
| 1717619 | |
| 1717611 | |
| 1717610 | |
| 1717609 | |
| 1717607 | |
| 1717601 | |
| 1717598 | |
| 1717591 | |
| 1717590 | |
| 1717583 | |
| 1717575 | |
| 1717572 | |
| 1717568 | |
| 1717566 | |
| 1717549 | |
| 1717545 | |
| 1717533 | |
| 1717512 | |
| 1717511 | |
| 1717508 | |
| 1717495 | |
| 1717479 | |
| 1717473 | |
| 1717470 | |
| 1717463 | |
| 1717452 |
댓글 작성
댓글을 작성하시려면 로그인이 필요합니다.
로그인하기