CHORE(merge): merge from develop
- Initial setup and all features from develop branch - Includes: auth, deploy, docker, style fixes - K3S deployment configuration
This commit is contained in:
79
nextjs/app/api/disciple-videos/reorder/route.ts
Normal file
79
nextjs/app/api/disciple-videos/reorder/route.ts
Normal 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.discipleVideo.findUnique({ where: { id: Number(videoId1) } }),
|
||||
prisma.discipleVideo.findUnique({ where: { id: Number(videoId2) } }),
|
||||
]);
|
||||
|
||||
if (!video1 || !video2) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "비디오를 찾을 수 없습니다." },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// 같은 stage/step인지 확인
|
||||
if (video1.stage !== video2.stage || video1.step !== video2.step) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "같은 stage/step의 비디오만 교환할 수 있습니다." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.discipleVideo.update({
|
||||
where: { id: video1.id },
|
||||
data: { order: -1 }, // 임시 음수 값 (unique constraint 회피)
|
||||
}),
|
||||
prisma.discipleVideo.update({
|
||||
where: { id: video2.id },
|
||||
data: { order: video1.order },
|
||||
}),
|
||||
prisma.discipleVideo.update({
|
||||
where: { id: video1.id },
|
||||
data: { order: video2.order },
|
||||
}),
|
||||
]);
|
||||
|
||||
// 업데이트된 stage/step의 모든 비디오 반환
|
||||
const updatedVideos = await prisma.discipleVideo.findMany({
|
||||
where: { stage: video1.stage, step: video1.step },
|
||||
orderBy: { order: 'desc' },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: updatedVideos,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error reordering disciple videos:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "제자훈련 영상 순서 변경에 실패했습니다.";
|
||||
return NextResponse.json(
|
||||
{ success: false, message: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
148
nextjs/app/api/disciple-videos/route.ts
Normal file
148
nextjs/app/api/disciple-videos/route.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { isValidYouTubeUrl, getYouTubeThumbnailUrl, getYouTubeEmbedUrl } from "@/lib/utils/youtube";
|
||||
|
||||
// GET: 모든 제자훈련 영상 또는 특정 stage 영상 가져오기
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const stage = searchParams.get('stage');
|
||||
|
||||
if (stage) {
|
||||
const videos = await prisma.discipleVideo.findMany({
|
||||
where: { stage },
|
||||
orderBy: [
|
||||
{ step: '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,
|
||||
});
|
||||
}
|
||||
|
||||
const videos = await prisma.discipleVideo.findMany({
|
||||
orderBy: [
|
||||
{ stage: 'asc' },
|
||||
{ step: '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 disciple 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 { stage, step, videoUrl } = body;
|
||||
|
||||
if (!stage || typeof stage !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: "stage가 유효하지 않습니다." },
|
||||
{ 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 }
|
||||
);
|
||||
}
|
||||
|
||||
// stage/step 내 기존 영상 확인하여 최고 order 값 가져오기
|
||||
const existingVideo = await prisma.discipleVideo.findFirst({
|
||||
where: { stage, step: step || null },
|
||||
orderBy: { order: 'desc' },
|
||||
});
|
||||
|
||||
// 새 영상은 현재 최고 order + 1로 설정 (맨 앞에 추가)
|
||||
const maxOrder = existingVideo?.order ?? 0;
|
||||
const newOrder = maxOrder + 1;
|
||||
|
||||
const newVideo = await prisma.discipleVideo.create({
|
||||
data: {
|
||||
stage,
|
||||
step: step || null,
|
||||
videoUrl,
|
||||
order: newOrder,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: newVideo,
|
||||
}, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating disciple 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.discipleVideo.delete({
|
||||
where: { id: Number(id) },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "영상이 삭제되었습니다."
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting disciple video:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "제자훈련 영상 삭제에 실패했습니다.";
|
||||
return NextResponse.json(
|
||||
{ success: false, message: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user