이 게시물에서는 Upstash, SvelteKit 및 Firebase Storage를 사용하여 Jira Kanban Board에 대한 오픈 소스 대안을 구축한 방법에 대해 이야기합니다.

우리가 사용할 것
- SvelteKit(UI 및 API 경로)
- Upstash(CRUD 작업)
- Tailwind CSS(스타일링)
- Firebase 저장소(애셋[이미지, PDF 등] 저장소)
- Auth.js를 통한 SvelteKit 인증
필요한 것
- 데이터베이스 생성을 위한 Upstash 계정
- 저장소 컨테이너를 생성하기 위한 Firebase 계정
- OAuth 자격 증명을 얻기 위한 Google OAuth 2.0 설정
Upstash Redis 설정
Upstash 계정을 생성하고 로그인하면 Redis 탭으로 이동하여 데이터베이스를 생성하게 됩니다.


데이터베이스를 생성한 후 세부정보 탭으로 이동합니다. 데이터베이스 연결 섹션을 찾을 때까지 아래로 스크롤합니다. 콘텐츠를 복사하여 안전한 곳에 저장하세요.

또한 REST API 섹션을 찾을 때까지 아래로 스크롤하고 .env 버튼을 선택합니다. 콘텐츠를 복사하여 안전한 곳에 저장하세요.

프로젝트 설정
설정하려면 앱 저장소를 복제하고 이 튜토리얼에 따라 그 안에 있는 모든 내용을 알아보세요. 프로젝트를 포크하려면 다음을 실행하세요:
git clone https://github.com/rishi-raj-jain/jira-sveltekit-firebase-storage-upstash-starter
cd jira-sveltekit-firebase-storage-upstash-starter
npm install 저장소를 복제한 후에는 .env 파일을 생성하게 됩니다. 위 섹션에서 저장한 항목을 추가하게 됩니다.
다음과 같아야 합니다:
# .env
# Obtained from Google OAuth 2.0 setup
# https://support.google.com/cloud/answer/6158849?hl=en
GOOGLE_ID="..."
GOOGLE_SECRET="..."
# SvelteKit Auth
AUTH_SECRET="..." # A random 32 char string
AUTH_TRUST_HOST=true
# Obtained from Upstash as from the steps done above
UPSTASH_REDIS_REST_URL="your_upstash_redis_rest__url_from_above"
UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_rest__token_from_above" // firebase-adminsdk.json
// with the firebase config obtained from your firebase project
// Read more about firebase config
// https://firebase.google.com/docs/web/learn-more#config-object
{
"type": "...",
"project_id": "...",
"private_key_id": "...",
"private_key": "...",
"client_email": "...",
"client_id": "...",
"auth_uri": "...",
"token_uri": "...",
"auth_provider_x509_cert_url": "...",
"client_x509_cert_url": "...",
"universe_domain": "...",
"storageBucket": "..."
} 이 단계 후에는 다음 명령을 사용하여 로컬 환경을 시작할 수 있습니다:
npm run dev 저장소 구조
이는 프로젝트의 기본 폴더 구조입니다. CRUD 작업, SvelteKit 인증 및 파일 업로드 처리기를 다루는 이 게시물에서 추가로 논의될 파일과 해당 파일이 참조되는 파일을 빨간색으로 사각형으로 표시했습니다.

사용자 인증을 통한 SvelteKit의 Edge 기능 보호
Auth.js 팀의 훌륭한 작업 SvelteKit으로 인증을 원활하게 수행할 수 있게 되었습니다. 프로젝트는 다음을 구현합니다:
Google OAuth 2.0을 사용하는 모든 페이지에 대한 승인
SvelteKit의 서버 후크를 사용하여 (모든 페이지에) 들어오는 모든 요청에 대해 인증을 시행합니다:
// File: @/hooks.server.ts
import Google from "@auth/core/providers/google";
import { SvelteKitAuth } from "@auth/sveltekit";
import type { Handle } from "@sveltejs/kit";
import { GOOGLE_ID, GOOGLE_SECRET } from "$env/static/private";
// Read more on
// https://kit.svelte.dev/docs/hooks#server-hooks-handle
export const handle = SvelteKitAuth({
// @ts-ignore
providers: [Google({ clientId: GOOGLE_ID, clientSecret: GOOGLE_SECRET })],
}) satisfies Handle; SvelteKit의 서버 로컬을 사용하여 Edge 기능에 대한 인증
SvelteKit의 서버 로컬을 사용하면 서버 측 전용 작업에서 사용자가 인증되었는지 확인할 수 있습니다. 다음은 새 이슈를 생성하는 동안 사용자가 인증되었는지 확인하는 데 이를 사용하는 예입니다:
import { json } from '@sveltejs/kit'
import { isAuth } from '@/lib/auth'
import type { RequestEvent } from './$types'
import { getTask, getTasks } from '@/lib/issues'
import type { LayoutServerLoadEvent } from '../routes/$types'
import type { RequestEvent, ServerLoadEvent } from '@sveltejs/kit'
// Get user session if available in event locals
const isAuth = async (event: LayoutServerLoadEvent | ServerLoadEvent | RequestEvent) => {
const session = await event.locals.getSession()
if (session?.user?.image) {
return { session }
}
return false
}
export async function GET(event: RequestEvent) {
// If user is not authenticated throw a 403
if (!(await isAuth(event))) {
return new Response(undefined, {
status: 403
})
}
const url = event.url
const idSearchParam = url.searchParams.get('id')
if (idSearchParam) {
const res = await getTask(idSearchParam)
return json(res)
} else if (url.searchParams.get('all')) {
const res = await getTasks()
return json(res)
}
return new Response(JSON.stringify({ code: 0, error: 'Invalid Request.' }), {
status: 400,
headers: {
'content-type': 'application/json'
}
})
} Upstash Redis를 통한 CRUD 작업 문제
이 섹션에서는 Kanban 보드의 각 문제에 대한 데이터 가져오기, 업데이트 및 삭제가 수행되는 방법에 대해 자세히 살펴보겠습니다. 우리는 Upstash DB(@upstash/redis를 통해)를 지속적으로 사용하고 있습니다. ) 데이터를 가져오고 표시하고 새로 고칩니다.
getTask:이슈 데이터 함수 가져오기
getTask 함수는 Upstash의 hget을 사용합니다. id를 통해 고유한 id로 식별되는 관련 문제 ata에 대해 Upstash에 API 요청을 하기 위한 키로 사용됩니다. . 해당 문제가 없거나 오류가 있는 경우 함수는 { code: 0 }가 포함된 객체를 반환하도록 설정됩니다. 그러면 사용자는 SvelteKit의 동적 경로에서 자동으로 404(문제를 찾을 수 없음)로 리디렉션될 수 있습니다.
type Task = { [key: string]: any } | null;
// Get Issue Data
// File: @/lib/issues/get.ts
export async function getTask(id: string) {
try {
const redis = (await import("../upstash/setup")).default;
const task: Task = await redis.hget("issues", id);
if (!task) {
return {
code: 0,
error: "No such issue found.",
};
}
return { ...task, code: 1 };
} catch (e: any) {
const error = e.message || e.toString();
console.log(error);
return {
code: 0,
error,
};
}
} 마찬가지로 나머지 CRUD 작업은 다음과 같습니다:
// Create Issue
// File: @/lib/issues/create.ts
export async function createTask(info: any) {
try {
const redis = (await import("../upstash/setup")).default;
const id =
Math.random().toString().slice(2) + new Date().getUTCMilliseconds();
await redis.hset("issues", { [id]: info });
return { code: 1, id, message: "Issue Created Succesfully ✅" };
} catch (e: any) {
const error = e.message || e.toString();
console.log(error);
return {
code: 0,
error,
};
}
} // Delete Issue
// File: @/lib/issues/delete.ts
export async function deleteTask(id: string) {
try {
const redis = (await import("../upstash/setup")).default;
await redis.hdel("issues", id);
return { code: 1, message: "Deleted Succesfully!" };
} catch (e: any) {
const error = e.message || e.toString();
console.log(error);
return {
code: 0,
error,
};
}
} // Update Issue Data
// File: @/lib/issues/update.ts
export async function updateTask(info: any, id: string) {
try {
const redis = (await import("../upstash/setup")).default;
if (id) {
const task = await redis.hget("issues", id);
if (task) {
await redis.hset("issues", { [id]: info });
return { code: 1, message: "Updated Successfully" };
}
}
return {
code: 0,
error: "No such issue was found.",
};
} catch (e: any) {
const error = e.message || e.toString();
console.log(error);
return {
code: 0,
error,
};
}
} 속도 제한
에지에서 속도 제한을 구현하려면 Upstash Redis을 사용합니다. 데이터베이스 클라이언트 및 @upstash/ratelimit이라는 속도 제한기 라이브러리 .
// Reference Function to ratelimiting
// File: @/lib/upstash/ratelimit.ts
import { Ratelimit } from "@upstash/ratelimit";
import redis from "./setup";
export const ratelimit = {
upload: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(2, "60s"),
}),
issues: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(5, "60s"),
}),
}; Rate Limiting을 사용하여 다음을 달성할 수 있었습니다:
아. 분당 사용자당 이슈 생성 횟수 제한
속도 제한을 사용하면 인증된 사용자당 분당 5개의 이슈 생성을 제한할 수 있습니다. 인증된 사용자의 사용자 이메일을 기반으로 이 비율 제한을 시행할 수 있습니다.
// File: @/routes/api/issue/+server.ts
// Issue Creation POST API SvelteKit Handler
import { ratelimit } from "@/lib/upstash/ratelimit";
export async function POST(event: RequestEvent) {
const user = await isAuth(event);
if (!user) {
return new Response(undefined, {
status: 403,
});
}
if (user.session.user?.email) {
// Look at the user email of authenticated user at edge
// Rate limit 5 issues creation per minute
const result = await ratelimit.issues.limit(user.session.user.email);
if (!result.success) {
return new Response(
JSON.stringify({
code: 0,
error: `You can't create more than 5 issues per minute.`,
}),
{
status: 403,
headers: {
"content-type": "application/json",
},
},
);
}
const { info } = await event.request.json();
const res = await createTask(info);
return json(res);
}
return new Response(undefined, {
status: 403,
});
} 베. 사용자당 분당 문제당 파일 업로드 수 제한
속도 제한을 사용하면 인증된 사용자당 분당 작업별로 파일 업로드를 최대 2개로 제한할 수 있습니다. 인증된 사용자의 사용자 이메일과 작업 ID를 기반으로 이 속도 제한을 적용할 수 있습니다. 업로드가 성공적으로 완료될 때마다 Upstash DB의 작업에 fileURL이 추가되어 업데이트됩니다.
// File: @/routes/api/content/+server.ts
// File Upload POST API SvelteKit Handler
import { ratelimit } from "@/lib/upstash/ratelimit";
export async function POST(event: RequestEvent) {
// User Authentication Code
if (user.session.user?.email) {
// Validate User, Task ID and if a file is uploaded
// Look at the user email of authenticated user and task's ID at edge
// Rate limit 2 uploads per minute
const result = await ratelimit.upload.limit(
`${user.session.user.email}_${taskID}`,
);
if (!result.success) {
return new Response(
JSON.stringify({
code: 0,
error: `You can't upload more than 2 files per issue per minute.`,
}),
{
status: 403,
headers: {
"content-type": "application/json",
},
},
);
}
// File upload code
// Continue reading the blog to see how
// file uploads are being taken care of
}
return new Response(undefined, {
status: 403,
});
} Firebase 저장소로 파일 업로드 및 다운로드 처리
이 섹션에서는 SvelteKit의 엣지에서 이슈의 파일 업로드 및 다운로드가 안전하고 인증된 방식으로 처리되는 방법에 대해 자세히 알아볼 것입니다. 우리는 Firebase(v9) 저장소를 활용하여 파일을 가져오고 업로드합니다.
아, 그런데 스토리지용 Cloudflare R2는 왜 안 되나요?
Cloudflare R2의 무료 저장소 계획과 그 장점에 대한 많은 커뮤니티 옹호를 보았지만, 저를 실망시켰던 한 가지는 시스템을 사용해 보기도 전에 내 신용 카드를 Cloudflare에 맡겨야 한다는 것이었습니다. 이로 인해 다른 저장소 솔루션에 대해 고민하게 되었고 5GB의 무료 저장소를 제공하는 Firebase 저장소를 선택하게 되었습니다. 이를 초과할 경우 내 승인 없이 신용카드에 요금을 청구하는 대신 서비스가 중지되며 무슨 일이 일어나고 있는지 알 수 있습니다.
Firebase 저장소에 파일을 업로드하는 SvelteKit Edge 기능
다음 Edge 함수에서는 POST 요청 이벤트를 살펴보고 사용자가 인증되면 taskID를 얻습니다. 및 file 이벤트의 formData에서. 완료되면 파일 크기가 5MB 미만인 경우 계속할지 여부를 추가로 평가합니다. 모든 필수 구성 요소가 처리되면 고유 ID를 생성한 다음 파일이 업로드되는 고유 폴더에 대한 Firebase 참조를 생성합니다. 파일이 Firebase에 업로드되자마자 업로드된 파일에 액세스하는 데 사용할 수 있는 URL이 반환됩니다. 이 고유 URL을 files에 추가합니다. 이슈 데이터의 핵심입니다.
// File: @/routes/api/content/+server.ts
// File Upload POST API SvelteKit Handler
import { initializeApp } from "firebase/app";
import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";
import fireBaseConfig from "../../../../firebase-adminsdk.json";
export async function POST(event: RequestEvent) {
// User Authentication Code
if (user.session.user?.email) {
const app = initializeApp(fireBaseConfig);
const storage = getStorage(app);
const data = await event.request.formData();
const taskID = data.get("taskID");
const file = data.get("file");
// ...Validate User, Task ID and if a file is uploaded
// ...Rate Limiting Code
// File Size Restriction(s)
if (file.size > 5 * 1024 * 1024) {
return new Response(
JSON.stringify({
code: 0,
error: "File size exceeds the limit of 5 MB.",
}),
{
status: 400,
headers: {
"content-type": "application/json",
},
},
);
}
// Start File Upload Code
try {
// Create a unique ID
const fileId = uuidv4();
// If uploaded is not a File type
if (!(file instanceof File)) return;
// Create a ref to firebase storage
const storageRef = ref(storage, `uploads/${fileId}/${file.name}`);
// Obtain the arrayBuffer of the file uploaded
const fileBuffer = await file.arrayBuffer();
// Upload file to Firebase Storage in bytes using Uint8Array
const { metadata } = await uploadBytes(
storageRef,
new Uint8Array(fileBuffer),
);
const { fullPath } = metadata;
// No fullPath is received, the API errored out
if (!fullPath) {
return new Response(
JSON.stringify({
code: 0,
error: `<span>There was some error while uploading the file.</span> <span class="mt-1 text-xs text-gray-500">Report an issue with the current URL that you are on and with the code XXX.</span>`,
}),
{
status: 403,
headers: {
"content-type": "application/json",
},
},
);
}
// If a file is uploaded successfully, append the file to list of attachments to the issue's data
const { code, ...taskValues } = await getTask(taskID);
if (code === 1) {
if (taskValues) {
if (taskValues.hasOwnProperty("files")) {
taskValues["files"].push(
`https://storage.googleapis.com/${storageRef.bucket}/${storageRef.fullPath}`,
);
} else {
taskValues["files"] = [
`https://storage.googleapis.com/${storageRef.bucket}/${storageRef.fullPath}`,
];
}
}
// Update the task's data in Upstash
await updateTask(taskValues, taskID);
}
return json({
code: 1,
message: "Uploaded Successfully",
});
} catch (error) {
return new Response(
JSON.stringify({ code: 0, error: error.message || error.toString() }),
{
status: 403,
headers: {
"content-type": "application/json",
},
},
);
}
}
return new Response(undefined, {
status: 403,
});
} Firebase 저장소에서 파일의 공개 URL을 다운로드하는 SvelteKit Edge 기능
기억하시겠지만 문제의 files에 Firebase에서 반환한 고유 URL을 추가했습니다. 열쇠. 원본 파일을 검색하기 위해 SvelteKit의 Edge Function에 대한 GET 요청의 이미지 매개변수로 해당 고유 URL을 받습니다. Firebase 라이브러리의 getDownloadURL 함수를 사용하여 원본 미디어의 공개 URL을 가져옵니다.
// File: @/routes/api/content/+server.ts
// File Upload GET API SvelteKit Handler
import { initializeApp } from "firebase/app";
import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";
import fireBaseConfig from "../../../../firebase-adminsdk.json";
export async function GET(event: RequestEvent) {
if (!(await isAuth(event))) {
return new Response(undefined, {
status: 403,
});
}
const url = event.url;
const image = url.searchParams.get("image");
if (image) {
try {
const app = initializeApp(fireBaseConfig);
const storage = getStorage(app);
const fileRef = ref(storage, image);
const imagePublicURL = await getDownloadURL(fileRef);
return json({ code: 1, image: imagePublicURL });
} catch (error) {
return new Response(
JSON.stringify({ code: 0, error: error.message || error.toString() }),
{
status: 500,
headers: {
"content-type": "application/json",
},
},
);
}
}
return new Response(JSON.stringify({ code: 0, error: "Invalid Request." }), {
status: 400,
headers: {
"content-type": "application/json",
},
});
} 이미 생각하셨겠지만 업로드할 수 있는 미디어는 여러 개가 있을 수 있으므로 이미지와 비디오의 사소한 경우를 처리하기 위해 프런트엔드에 다음 if else를 추가했습니다.
<!-- File: @/routes/issue/[slug]/+page.svelte -->
{#each fieldFiles as file}
<div class="mt-8 w-full border border-white/25 p-3">
{#if /\.(mp4|mov|mkv)/i.test(file)}
<video class="h-auto w-full" src="{file}" controls>
<track kind="captions" />
</video>
{:else}
<img alt="{file}" src="{file}" class="h-auto w-full" />
{/if}
</div>
{/each} 그런데 Jira Kanban Board를 대체하는 오픈소스가 왜 필요한가요?
고가의 솔루션을 구매하는 대신 Jira Kanban Board라는 오픈 소스 대안을 선택할 수 있는 다양한 이점이 있습니다.
- 많은 비용 절감:오픈 소스 대안을 사용할 때 얻을 수 있는 가장 중요한 이점 중 하나는 비용 절감입니다. Jira와 같은 유료 Kanban 보드 솔루션과 달리 SvelteKit, TailwindCSS, Firebase Storage, Upstash의 서버리스 DB 및 Rate Limiting으로 구축된 오픈 소스 대안은 라이선스 비용 없이 사용할 수 있습니다.
- 무제한 사용자 정의 가능성:오픈 소스 대안을 사용하면 코드베이스를 완전히 제어할 수 있으며 특정 요구 사항에 따라 Kanban 보드를 사용자 정의할 수 있습니다. 맞춤설정 옵션이 제한된 유료 솔루션에서는 이러한 유연성이 불가능한 경우가 많습니다.
- 간편한 통합:API의 강력한 기능을 활용하여 Kanban 보드를 프로젝트 관리 시스템, 버전 제어 도구, 알림 서비스 등과 연결할 수 있습니다. 또한 프로젝트의 오픈 소스 특성을 통해 개발자는 기능을 확장하고 특정 요구 사항에 맞는 플러그인 또는 통합을 만들 수 있습니다.
결론
결론적으로, 이 프로젝트는 세분화된 속도 제한 구현, CRUD 데이터 작업, 파일 가져오기 및 업로드를 위한 Firebase Storage API 구현 등 모든 작업이 Upstash의 @upstash/redis를 사용하여 엣지에서 수행되는 귀중한 경험을 제공했습니다. 도서관!