CHORE(merge): merge from develop
Some checks failed
Build Docker Image / build-and-push (push) Has been cancelled
CI / lint-and-build (push) Has been cancelled

- Initial setup and all features from develop branch
- Includes: auth, deploy, docker, style fixes
- K3S deployment configuration
This commit is contained in:
2026-01-06 17:29:16 +09:00
parent b4ce36ba3b
commit f78454c2a1
159 changed files with 18365 additions and 774 deletions

View File

@@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
// PUT: 영상 순서 변경 (두 비디오의 order만 교환)
export async function PUT(request: NextRequest) {
try {
const body = await request.json();
const { videoId1, videoId2 } = body;
// 입력 검증
if (!videoId1 || !videoId2) {
return NextResponse.json(
{ success: false, message: "두 개의 비디오 ID가 필요합니다." },
{ status: 400 }
);
}
if (videoId1 === videoId2) {
return NextResponse.json(
{ success: false, message: "동일한 비디오는 교환할 수 없습니다." },
{ status: 400 }
);
}
// 두 비디오 조회
const [video1, video2] = await Promise.all([
prisma.worshipVideo.findUnique({ where: { id: Number(videoId1) } }),
prisma.worshipVideo.findUnique({ where: { id: Number(videoId2) } }),
]);
if (!video1 || !video2) {
return NextResponse.json(
{ success: false, message: "비디오를 찾을 수 없습니다." },
{ status: 404 }
);
}
// 같은 카테고리인지 확인
if (video1.category !== video2.category) {
return NextResponse.json(
{ success: false, message: "같은 카테고리의 비디오만 교환할 수 있습니다." },
{ status: 400 }
);
}
await prisma.$transaction([
prisma.worshipVideo.update({
where: { id: video1.id },
data: { order: -1 }, // 임시 음수 값 (unique constraint 회피)
}),
prisma.worshipVideo.update({
where: { id: video2.id },
data: { order: video1.order },
}),
prisma.worshipVideo.update({
where: { id: video1.id },
data: { order: video2.order },
}),
]);
// 업데이트된 카테고리의 모든 비디오 반환
const updatedVideos = await prisma.worshipVideo.findMany({
where: { category: video1.category },
orderBy: { order: 'desc' },
});
return NextResponse.json({
success: true,
data: updatedVideos,
});
} catch (error) {
console.error("Error reordering worship videos:", error);
const errorMessage = error instanceof Error ? error.message : "예배 영상 순서 변경에 실패했습니다.";
return NextResponse.json(
{ success: false, message: errorMessage },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,151 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { isValidYouTubeUrl, getYouTubeThumbnailUrl, getYouTubeEmbedUrl } from "@/lib/utils/youtube";
// GET: 모든 예배 영상 또는 특정 카테고리 영상 가져오기
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const category = searchParams.get('category');
if (category) {
const videos = await prisma.worshipVideo.findMany({
where: { category },
orderBy: { order: 'desc' },
});
// 썸네일 및 embed URL 추가
const videosWithUrls = videos.map(video => ({
...video,
thumbnailUrl: getYouTubeThumbnailUrl(video.videoUrl),
embedUrl: getYouTubeEmbedUrl(video.videoUrl),
}));
return NextResponse.json({
success: true,
data: videosWithUrls,
});
}
const videos = await prisma.worshipVideo.findMany({
orderBy: [
{ category: 'asc' },
{ order: 'desc' },
],
});
// 썸네일 및 embed URL 추가
const videosWithUrls = videos.map(video => ({
...video,
thumbnailUrl: getYouTubeThumbnailUrl(video.videoUrl),
embedUrl: getYouTubeEmbedUrl(video.videoUrl),
}));
return NextResponse.json({
success: true,
data: videosWithUrls,
});
} catch (error) {
console.error("Error fetching worship videos:", error);
const errorMessage = error instanceof Error ? error.message : "예배 영상 조회에 실패했습니다.";
return NextResponse.json(
{ success: false, error: errorMessage },
{ status: 500 }
);
}
}
// POST: 새 영상 추가
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { category, videoUrl } = body;
if (!category || typeof category !== 'string') {
return NextResponse.json(
{ success: false, message: "카테고리가 유효하지 않습니다." },
{ status: 400 }
);
}
if (!videoUrl || typeof videoUrl !== 'string') {
return NextResponse.json(
{ success: false, message: "영상 URL이 유효하지 않습니다." },
{ status: 400 }
);
}
// YouTube URL 유효성 검사
if (!isValidYouTubeUrl(videoUrl)) {
return NextResponse.json(
{ success: false, message: "유효한 YouTube URL이 아닙니다." },
{ status: 400 }
);
}
// 카테고리 내 기존 영상 확인 (order 내림차순)
const existingVideos = await prisma.worshipVideo.findMany({
where: { category },
orderBy: { order: 'desc' },
});
// 기존 영상이 9개 이상이면 order가 가장 낮은(마지막) 영상 삭제
if (existingVideos.length >= 9) {
const videoToDelete = existingVideos[existingVideos.length - 1];
await prisma.worshipVideo.delete({
where: { id: videoToDelete.id },
});
}
// 새 영상은 현재 최고 order + 1로 설정 (맨 앞에 추가)
const maxOrder = existingVideos.length > 0 ? existingVideos[0].order : 0;
const newOrder = maxOrder + 1;
const newVideo = await prisma.worshipVideo.create({
data: {
category,
videoUrl,
order: newOrder,
},
});
return NextResponse.json({
success: true,
data: newVideo,
}, { status: 201 });
} catch (error) {
console.error("Error creating worship video:", error);
const errorMessage = error instanceof Error ? error.message : "예배 영상 생성에 실패했습니다.";
return NextResponse.json(
{ success: false, message: errorMessage },
{ status: 500 }
);
}
}
// DELETE: 영상 삭제
export async function DELETE(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const id = searchParams.get('id');
if (!id || isNaN(Number(id))) {
return NextResponse.json(
{ success: false, message: "영상 ID가 유효하지 않습니다." },
{ status: 400 }
);
}
await prisma.worshipVideo.delete({
where: { id: Number(id) },
});
return NextResponse.json({
success: true,
message: "영상이 삭제되었습니다."
});
} catch (error) {
console.error("Error deleting worship video:", error);
const errorMessage = error instanceof Error ? error.message : "예배 영상 삭제에 실패했습니다.";
return NextResponse.json(
{ success: false, message: errorMessage },
{ status: 500 }
);
}
}