PocketBase: 단일 파일로 구축하는 실시간 백엔드 완전 가이드
⏱️ 예상 읽기 시간: 14분
서론
현대 웹 애플리케이션 개발에서 백엔드 구축은 복잡하고 시간이 많이 소요되는 작업입니다. 데이터베이스 설정, 인증 시스템 구현, API 개발, 실시간 기능 추가 등 수많은 컴포넌트를 연결해야 합니다.
PocketBase는 이러한 복잡성을 혁신적으로 해결하는 오픈소스 솔루션입니다. 48.2k GitHub 스타를 받으며 검증된 이 Go 기반 백엔드는 단일 바이너리 파일로 완전한 백엔드 시스템을 제공합니다.
본 튜토리얼에서는 PocketBase의 핵심 기능부터 고급 활용법까지 macOS 환경에서 실습과 함께 완전히 마스터하는 방법을 알아보겠습니다.
PocketBase란?
핵심 특징
🗃️ 통합 백엔드 솔루션
- SQLite 임베디드 데이터베이스
- 실시간 구독 (WebSocket)
- REST-ish API 자동 생성
- Admin 대시보드 UI 내장
👥 사용자 관리 시스템
- 내장 사용자 인증 및 권한 관리
- 이메일 인증, 소셜 로그인 지원
- 역할 기반 접근 제어 (RBAC)
- 세션 관리 및 토큰 인증
📁 파일 관리
- 파일 업로드/다운로드 API
- 이미지 리사이징 및 썸네일 생성
- S3 호환 스토리지 지원
- 파일 접근 권한 제어
🔧 개발자 친화적
- Go 프레임워크로 확장 가능
- JavaScript 플러그인 지원
- 마이그레이션 시스템
- 풍부한 SDK (JavaScript, Dart)
기술 스택
언어: Go 71.4%, Svelte 16.9% 라이센스: MIT (상업적 사용 가능) 데이터베이스: SQLite (임베디드) 지원 플랫폼: 크로스 플랫폼 최소 요구사항: Go 1.23+
MIT 라이센스 장점
✅ 완전 자유 사용
- 상업적 프로젝트에 무제한 사용
- 소스코드 수정 및 재배포 자유
- 사적/공적 사용 모두 허용
- 라이센스 비용 없음
📋 간단한 의무사항
- 저작권 고지만 포함하면 됨
- 복잡한 컴플라이언스 요구사항 없음
시스템 요구사항 및 설치
macOS 환경 준비
# Go 설치 확인 (1.23+ 필요)
go version
# Go가 없다면 Homebrew로 설치
brew install go
# 작업 디렉토리 생성
mkdir ~/pocketbase-tutorial
cd ~/pocketbase-tutorial
설치 방법
방법 1: 사전 빌드된 바이너리 사용 (권장)
#!/bin/bash
# 파일명: install_pocketbase.sh
echo "🚀 PocketBase 설치 스크립트"
# 최신 릴리즈 정보 가져오기
LATEST_RELEASE=$(curl -s https://api.github.com/repos/pocketbase/pocketbase/releases/latest)
VERSION=$(echo $LATEST_RELEASE | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$')
echo "📦 최신 버전: $VERSION"
# macOS용 바이너리 다운로드
if [[ $(uname -m) == "arm64" ]]; then
ARCH="darwin_arm64"
else
ARCH="darwin_amd64"
fi
DOWNLOAD_URL="https://github.com/pocketbase/pocketbase/releases/download/$VERSION/pocketbase_${VERSION:1}_${ARCH}.zip"
echo "📥 다운로드 중: $DOWNLOAD_URL"
curl -LO "$DOWNLOAD_URL"
# 압축 해제
unzip "pocketbase_${VERSION:1}_${ARCH}.zip"
rm "pocketbase_${VERSION:1}_${ARCH}.zip"
# 실행 권한 부여
chmod +x pocketbase
# 버전 확인
./pocketbase --version
echo "✅ PocketBase 설치 완료!"
echo "💡 실행 방법: ./pocketbase serve"
방법 2: Go 소스코드 빌드
# PocketBase 예제 클론
git clone https://github.com/pocketbase/pocketbase.git
cd pocketbase/examples/base
# 의존성 설치
go mod tidy
# 빌드
CGO_ENABLED=0 go build -o pocketbase
# 실행 권한 부여
chmod +x pocketbase
# 버전 확인
./pocketbase --version
방법 3: Docker 사용
# Docker로 실행
docker run --name pocketbase \
-p 8080:8080 \
-v $(pwd)/pb_data:/pb_data \
spectrocloud/pocketbase:latest
# 또는 docker-compose.yml 생성
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
pocketbase:
image: spectrocloud/pocketbase:latest
container_name: pocketbase
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./pb_data:/pb_data
environment:
- POCKETBASE_ADMIN_EMAIL=admin@example.com
- POCKETBASE_ADMIN_PASSWORD=adminpassword
EOF
# Docker Compose로 실행
docker-compose up -d
첫 실행 및 설정
# PocketBase 서버 시작
./pocketbase serve
# 또는 특정 포트로 실행
./pocketbase serve --http=0.0.0.0:8090
# 백그라운드 실행
./pocketbase serve &
# 로그 출력 확인
tail -f pb_data/logs/pocketbase.log
기본 사용법
Admin 대시보드 설정
# 서버 시작 후 브라우저에서 접속
# http://localhost:8080/_/
# 첫 접속 시 관리자 계정 생성이 필요합니다.
관리자 계정 생성:
- 브라우저에서
http://localhost:8080/_/
접속 - 이메일과 패스워드 입력하여 계정 생성
- 로그인하여 Admin 대시보드 접근
컬렉션(테이블) 생성
# CLI로 컬렉션 생성 (옵션)
./pocketbase collections create \
--name="posts" \
--type="base" \
--schema='[
{"name": "title", "type": "text", "required": true},
{"name": "content", "type": "text"},
{"name": "author", "type": "relation", "options": {"collectionId": "_pb_users_auth_"}},
{"name": "published", "type": "bool", "default": false},
{"name": "tags", "type": "json"}
]'
Admin UI에서 컬렉션 생성:
Collections
탭 클릭New collection
버튼 클릭- 컬렉션 이름 입력:
posts
- 필드 추가:
title
: Text (required)content
: Editorauthor
: Relation → userspublished
: Bool (default: false)created_at
: Date (auto)
REST API 활용
기본 CRUD 작업
#!/usr/bin/env node
// 파일명: api_examples.js
const BASE_URL = 'http://localhost:8080/api';
// 1. 사용자 생성 (회원가입)
async function createUser() {
const response = await fetch(`${BASE_URL}/collections/users/records`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: 'johndoe',
email: 'john@example.com',
password: 'securepassword123',
passwordConfirm: 'securepassword123'
})
});
const user = await response.json();
console.log('✅ 사용자 생성:', user);
return user;
}
// 2. 로그인 (인증)
async function loginUser() {
const response = await fetch(`${BASE_URL}/collections/users/auth-with-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
identity: 'john@example.com',
password: 'securepassword123'
})
});
const auth = await response.json();
console.log('🔐 로그인 성공:', auth);
return auth.token;
}
// 3. 게시글 생성
async function createPost(token) {
const response = await fetch(`${BASE_URL}/collections/posts/records`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
title: 'PocketBase로 백엔드 구축하기',
content: '단일 파일로 완전한 백엔드를 구축할 수 있습니다.',
published: true,
tags: ['tutorial', 'backend', 'go']
})
});
const post = await response.json();
console.log('📝 게시글 생성:', post);
return post;
}
// 4. 게시글 목록 조회
async function getPosts() {
const response = await fetch(
`${BASE_URL}/collections/posts/records?sort=-created&expand=author`
);
const posts = await response.json();
console.log('📚 게시글 목록:', posts);
return posts;
}
// 5. 게시글 업데이트
async function updatePost(postId, token) {
const response = await fetch(`${BASE_URL}/collections/posts/records/${postId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
content: '업데이트된 내용입니다.',
published: true
})
});
const updatedPost = await response.json();
console.log('✏️ 게시글 업데이트:', updatedPost);
return updatedPost;
}
// 6. 파일 업로드
async function uploadFile(token) {
const formData = new FormData();
formData.append('title', '이미지 포스트');
formData.append('content', '파일이 첨부된 게시글입니다.');
// 실제 환경에서는 File 객체 사용
// formData.append('image', fileInput.files[0]);
const response = await fetch(`${BASE_URL}/collections/posts/records`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const postWithFile = await response.json();
console.log('📎 파일 업로드:', postWithFile);
return postWithFile;
}
// 전체 플로우 실행
async function runExample() {
try {
console.log('🚀 PocketBase API 예제 시작\n');
// 사용자 생성 및 로그인
await createUser();
const token = await loginUser();
// 게시글 CRUD
const post = await createPost(token);
await getPosts();
await updatePost(post.id, token);
console.log('\n✅ 모든 API 테스트 완료!');
} catch (error) {
console.error('❌ 오류 발생:', error);
}
}
// Node.js 환경에서 실행
if (typeof require !== 'undefined' && require.main === module) {
// fetch polyfill for Node.js
global.fetch = require('node-fetch');
global.FormData = require('form-data');
runExample();
}
고급 쿼리 기능
#!/bin/bash
# 파일명: advanced_queries.sh
echo "🔍 PocketBase 고급 쿼리 예제"
BASE_URL="http://localhost:8080/api"
echo "📊 1. 필터링 쿼리"
# 제목에 'PocketBase'가 포함된 게시글
curl -G "$BASE_URL/collections/posts/records" \
--data-urlencode "filter=title~'PocketBase'"
echo -e "\n📊 2. 정렬 및 페이징"
# 최신 순으로 10개, 2페이지
curl -G "$BASE_URL/collections/posts/records" \
--data-urlencode "sort=-created" \
--data-urlencode "page=2" \
--data-urlencode "perPage=10"
echo -e "\n📊 3. 관계 확장"
# 작성자 정보와 함께 조회
curl -G "$BASE_URL/collections/posts/records" \
--data-urlencode "expand=author"
echo -e "\n📊 4. 복합 필터"
# 게시됨 상태이고 특정 태그 포함
curl -G "$BASE_URL/collections/posts/records" \
--data-urlencode "filter=published=true && tags?~'tutorial'"
echo -e "\n📊 5. 날짜 범위 검색"
# 최근 7일 내 생성된 게시글
curl -G "$BASE_URL/collections/posts/records" \
--data-urlencode "filter=created>=@now(-7d)"
echo -e "\n📊 6. 사용자별 게시글 개수"
# 집계 쿼리 (Admin API 필요)
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"$BASE_URL/collections/posts/records?groupBy=author&aggregate=count"
실시간 구독 구현
JavaScript 실시간 클라이언트
<!DOCTYPE html>
<!-- 파일명: realtime_example.html -->
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PocketBase 실시간 예제</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.container { max-width: 800px; margin: 0 auto; }
.post { border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px; }
.form-group { margin: 10px 0; }
input, textarea { width: 100%; padding: 8px; margin: 5px 0; }
button { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 3px; cursor: pointer; }
.status { padding: 10px; border-radius: 3px; margin: 10px 0; }
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<div class="container">
<h1>📡 PocketBase 실시간 게시판</h1>
<!-- 로그인 폼 -->
<div id="loginForm">
<h2>🔐 로그인</h2>
<div class="form-group">
<input type="email" id="email" placeholder="이메일" value="john@example.com">
</div>
<div class="form-group">
<input type="password" id="password" placeholder="패스워드" value="securepassword123">
</div>
<button onclick="login()">로그인</button>
</div>
<!-- 게시글 작성 폼 -->
<div id="postForm" style="display: none;">
<h2>✏️ 새 게시글</h2>
<div class="form-group">
<input type="text" id="title" placeholder="제목">
</div>
<div class="form-group">
<textarea id="content" placeholder="내용" rows="4"></textarea>
</div>
<button onclick="createPost()">게시글 작성</button>
<button onclick="logout()" style="background: #dc3545; margin-left: 10px;">로그아웃</button>
</div>
<!-- 상태 메시지 -->
<div id="status"></div>
<!-- 실시간 게시글 목록 -->
<div id="posts">
<h2>📚 실시간 게시글</h2>
<div id="postsList"></div>
</div>
</div>
<!-- PocketBase JavaScript SDK -->
<script src="https://unpkg.com/pocketbase@latest/dist/pocketbase.umd.js"></script>
<script>
// PocketBase 클라이언트 초기화
const pb = new PocketBase('http://localhost:8080');
let currentUser = null;
// 상태 메시지 표시
function showStatus(message, type = 'success') {
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = `<div class="${type}">${message}</div>`;
setTimeout(() => statusDiv.innerHTML = '', 5000);
}
// 로그인
async function login() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
try {
const authData = await pb.collection('users').authWithPassword(email, password);
currentUser = authData.record;
document.getElementById('loginForm').style.display = 'none';
document.getElementById('postForm').style.display = 'block';
showStatus(`환영합니다, ${currentUser.username || currentUser.email}!`);
// 실시간 구독 시작
subscribeToRealtime();
// 기존 게시글 로드
loadPosts();
} catch (error) {
showStatus(`로그인 실패: ${error.message}`, 'error');
}
}
// 로그아웃
function logout() {
pb.authStore.clear();
currentUser = null;
document.getElementById('loginForm').style.display = 'block';
document.getElementById('postForm').style.display = 'none';
document.getElementById('postsList').innerHTML = '';
// 실시간 구독 해제
pb.realtime.unsubscribe();
showStatus('로그아웃되었습니다.');
}
// 게시글 작성
async function createPost() {
const title = document.getElementById('title').value;
const content = document.getElementById('content').value;
if (!title || !content) {
showStatus('제목과 내용을 모두 입력해주세요.', 'error');
return;
}
try {
const data = {
title: title,
content: content,
author: currentUser.id,
published: true
};
await pb.collection('posts').create(data);
// 폼 초기화
document.getElementById('title').value = '';
document.getElementById('content').value = '';
showStatus('게시글이 작성되었습니다!');
} catch (error) {
showStatus(`게시글 작성 실패: ${error.message}`, 'error');
}
}
// 기존 게시글 로드
async function loadPosts() {
try {
const posts = await pb.collection('posts').getList(1, 50, {
sort: '-created',
expand: 'author'
});
displayPosts(posts.items);
} catch (error) {
showStatus(`게시글 로드 실패: ${error.message}`, 'error');
}
}
// 게시글 표시
function displayPosts(posts) {
const postsContainer = document.getElementById('postsList');
postsContainer.innerHTML = posts.map(post => `
<div class="post" id="post-${post.id}">
<h3>${post.title}</h3>
<p>${post.content}</p>
<small>
작성자: ${post.expand?.author?.username || post.expand?.author?.email || 'Unknown'} |
작성일: ${new Date(post.created).toLocaleString('ko-KR')}
</small>
</div>
`).join('');
}
// 실시간 구독
function subscribeToRealtime() {
// 게시글 컬렉션 실시간 구독
pb.collection('posts').subscribe('*', function (e) {
console.log('실시간 이벤트:', e);
if (e.action === 'create') {
showStatus(`새 게시글: "${e.record.title}"이 작성되었습니다!`);
addPostToTop(e.record);
} else if (e.action === 'update') {
showStatus(`게시글 "${e.record.title}"이 수정되었습니다.`);
updatePostInList(e.record);
} else if (e.action === 'delete') {
showStatus(`게시글이 삭제되었습니다.`);
removePostFromList(e.record.id);
}
});
showStatus('실시간 구독이 활성화되었습니다.', 'success');
}
// 새 게시글을 목록 맨 위에 추가
async function addPostToTop(post) {
// 작성자 정보 로드
try {
const fullPost = await pb.collection('posts').getOne(post.id, {
expand: 'author'
});
const postsContainer = document.getElementById('postsList');
const newPostHtml = `
<div class="post" id="post-${fullPost.id}" style="border-color: #28a745;">
<h3>${fullPost.title}</h3>
<p>${fullPost.content}</p>
<small>
작성자: ${fullPost.expand?.author?.username || fullPost.expand?.author?.email || 'Unknown'} |
작성일: ${new Date(fullPost.created).toLocaleString('ko-KR')}
</small>
</div>
`;
postsContainer.insertAdjacentHTML('afterbegin', newPostHtml);
// 강조 효과 제거
setTimeout(() => {
const postElement = document.getElementById(`post-${fullPost.id}`);
if (postElement) {
postElement.style.borderColor = '#ddd';
}
}, 3000);
} catch (error) {
console.error('게시글 정보 로드 실패:', error);
}
}
// 게시글 업데이트
function updatePostInList(post) {
const postElement = document.getElementById(`post-${post.id}`);
if (postElement) {
postElement.style.borderColor = '#ffc107';
postElement.querySelector('h3').textContent = post.title;
postElement.querySelector('p').textContent = post.content;
setTimeout(() => {
postElement.style.borderColor = '#ddd';
}, 3000);
}
}
// 게시글 삭제
function removePostFromList(postId) {
const postElement = document.getElementById(`post-${postId}`);
if (postElement) {
postElement.style.opacity = '0.5';
setTimeout(() => postElement.remove(), 1000);
}
}
// 페이지 로드 시 인증 상태 확인
window.addEventListener('load', function() {
if (pb.authStore.isValid) {
currentUser = pb.authStore.model;
document.getElementById('loginForm').style.display = 'none';
document.getElementById('postForm').style.display = 'block';
subscribeToRealtime();
loadPosts();
showStatus(`다시 오신 것을 환영합니다, ${currentUser.username || currentUser.email}!`);
}
});
// 페이지 종료 시 구독 해제
window.addEventListener('beforeunload', function() {
pb.realtime.unsubscribe();
});
</script>
</body>
</html>
Go 프레임워크로 확장
커스텀 PocketBase 애플리케이션
// 파일명: main.go
package main
import (
"log"
"net/http"
"encoding/json"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/tools/types"
)
// 커스텀 API 응답 구조체
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func main() {
app := pocketbase.New()
// 커스텀 라우트 추가
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
// API 통계 엔드포인트
se.Router.GET("/api/stats", func(re *core.RequestEvent) error {
return getStats(app, re)
})
// 게시글 검색 엔드포인트
se.Router.GET("/api/search/posts", func(re *core.RequestEvent) error {
return searchPosts(app, re)
})
// 사용자 프로필 업데이트 훅
se.Router.POST("/api/profile/update", func(re *core.RequestEvent) error {
return updateProfile(app, re)
})
return se.Next()
})
// 레코드 생성 후 훅 (게시글 알림)
app.OnRecordAfterCreateSuccess("posts").BindFunc(func(rce *core.RecordEvent) error {
return sendPostNotification(app, rce.Record)
})
// 사용자 등록 후 훅 (환영 이메일)
app.OnRecordAfterCreateSuccess("users").BindFunc(func(rce *core.RecordEvent) error {
return sendWelcomeEmail(app, rce.Record)
})
// 레코드 삭제 전 검증
app.OnRecordBeforeDeleteRequest("posts").BindFunc(func(rde *core.RecordEvent) error {
return validatePostDeletion(app, rde)
})
if err := app.Start(); err != nil {
log.Fatal(err)
}
}
// API 통계 정보 제공
func getStats(app *pocketbase.PocketBase, re *core.RequestEvent) error {
stats := map[string]interface{}{}
// 총 사용자 수
userCount, err := app.Dao().DB().
Select("count(*)").
From("users").
Build().
Execute()
if err == nil {
stats["total_users"] = userCount
}
// 총 게시글 수
postCount, err := app.Dao().DB().
Select("count(*)").
From("posts").
Build().
Execute()
if err == nil {
stats["total_posts"] = postCount
}
// 오늘 생성된 게시글 수
todayPosts, err := app.Dao().DB().
Select("count(*)").
From("posts").
Where("DATE(created) = DATE('now')").
Build().
Execute()
if err == nil {
stats["today_posts"] = todayPosts
}
response := APIResponse{
Success: true,
Message: "통계 정보를 성공적으로 조회했습니다.",
Data: stats,
}
return re.JSON(http.StatusOK, response)
}
// 게시글 검색 기능
func searchPosts(app *pocketbase.PocketBase, re *core.RequestEvent) error {
query := re.Request.URL.Query().Get("q")
if query == "" {
return re.JSON(http.StatusBadRequest, APIResponse{
Success: false,
Message: "검색어를 입력해주세요.",
})
}
// 제목 또는 내용에서 검색
records, err := app.Dao().FindRecordsByFilter(
"posts",
"title ~ {:query} || content ~ {:query}",
"-created",
100,
0,
map[string]any{"query": query},
)
if err != nil {
return re.JSON(http.StatusInternalServerError, APIResponse{
Success: false,
Message: "검색 중 오류가 발생했습니다.",
})
}
response := APIResponse{
Success: true,
Message: "검색이 완료되었습니다.",
Data: records,
}
return re.JSON(http.StatusOK, response)
}
// 사용자 프로필 업데이트
func updateProfile(app *pocketbase.PocketBase, re *core.RequestEvent) error {
// 인증 확인
authRecord, _ := re.Auth.(*models.Record)
if authRecord == nil {
return apis.NewForbiddenError("인증이 필요합니다.", nil)
}
data := struct {
Username string `json:"username"`
Name string `json:"name"`
Avatar string `json:"avatar"`
}{}
if err := json.NewDecoder(re.Request.Body).Decode(&data); err != nil {
return re.JSON(http.StatusBadRequest, APIResponse{
Success: false,
Message: "잘못된 요청 데이터입니다.",
})
}
// 프로필 업데이트
authRecord.Set("username", data.Username)
authRecord.Set("name", data.Name)
if data.Avatar != "" {
authRecord.Set("avatar", data.Avatar)
}
if err := app.Dao().SaveRecord(authRecord); err != nil {
return re.JSON(http.StatusInternalServerError, APIResponse{
Success: false,
Message: "프로필 업데이트에 실패했습니다.",
})
}
response := APIResponse{
Success: true,
Message: "프로필이 성공적으로 업데이트되었습니다.",
Data: authRecord,
}
return re.JSON(http.StatusOK, response)
}
// 게시글 생성 알림 발송
func sendPostNotification(app *pocketbase.PocketBase, record *models.Record) error {
// 실제 환경에서는 이메일, 푸시 알림 등을 발송
log.Printf("📬 새 게시글 알림: %s", record.GetString("title"))
// 여기에 실제 알림 로직 구현
// - 이메일 발송
// - 푸시 알림
// - Slack, Discord 메시지 등
return nil
}
// 환영 이메일 발송
func sendWelcomeEmail(app *pocketbase.PocketBase, record *models.Record) error {
email := record.GetString("email")
username := record.GetString("username")
log.Printf("📧 환영 이메일 발송: %s (%s)", username, email)
// 실제 이메일 발송 로직
message := &types.Message{
From: "noreply@example.com",
To: []string{email},
Subject: "PocketBase 가입을 환영합니다!",
HTML: `
<h1>환영합니다!</h1>
<p>안녕하세요 ` + username + `님,</p>
<p>PocketBase 커뮤니티에 가입해주셔서 감사합니다.</p>
<p>이제 다양한 기능을 사용하실 수 있습니다.</p>
`,
}
return app.NewMailClient().Send(message)
}
// 게시글 삭제 권한 검증
func validatePostDeletion(app *pocketbase.PocketBase, rde *core.RecordEvent) error {
// 작성자만 삭제 가능하도록 검증
authRecord, _ := rde.RequestEvent.Auth.(*models.Record)
if authRecord == nil {
return apis.NewForbiddenError("인증이 필요합니다.", nil)
}
postAuthor := rde.Record.GetString("author")
if postAuthor != authRecord.Id {
return apis.NewForbiddenError("자신의 게시글만 삭제할 수 있습니다.", nil)
}
return rde.Next()
}
빌드 및 실행
```bash #!/bin/bash
파일명: build_custom_pocketbase.sh
echo “🔨 커스텀 PocketBase 빌드”
Go 모듈 초기화
go mod init custom-pocketbase
PocketBase 의존성 추가
go mod tidy
빌드
echo “📦 바이너리 빌드 중…” CGO_ENABLED=0 go build -o custom-pocketbase main.go
실행 권한 부여
chmod +x custom-pocketbase
echo “✅ 빌드 완료!” echo “💡 실행 방법: ./custom-pocketbase serve”
실행
./custom-pocketbase serve –http=0.0.0.0:8080