Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions apps/client/src/components/homeWorship/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,38 @@ const HomeWorshipCreate = () => {
const onSubmit: SubmitHandler<IHomeWorshipForm> = async (data) => {
const formData = new FormData();

if (data.image.length === 0 || !data.date || !data.title || !data.password || !data.userName || !content)
// FIXME: 추후 validate 및 디자인 변경해야 함
if (!data.date || !data.title || !data.password || !data.userName || !content) {
return alert("모든 정보를 입력해주세요.");
if (data.image.length !== 1) return alert("사진은 한 장 업로드 가능합니다.");
}

if (data.image.length === 0 && data.video.length === 0) {
return alert("사진 또는 영상은 반드시 업로드해야 합니다.");
}

if (data.image.length > 1) {
return alert("사진은 1장만 가능 합니다.");
}

if (data.video.length > 1) {
return alert("영상은 1개만 가능 합니다.");
}

formData.append("title", data.title);
formData.append("date", data.date);
formData.append("content", content);
formData.append("password", data.password);
formData.append("userName", data.userName);

Array.from(data.image).forEach((image) => {
formData.append(`image-file`, image);
formData.append(`image-name`, image.name);
const imageFile = data.image || [];
const videoFile = data.video || [];

Array.from(imageFile).forEach((image) => {
formData.append("image-file", image);
});

Array.from(videoFile).forEach((video) => {
formData.append("video-file", video);
});

mutate(formData, {
Expand Down Expand Up @@ -70,6 +89,10 @@ const HomeWorshipCreate = () => {
<p className="text-xl font-bold">사진 업로드</p>
<input type="file" accept="image/*" {...register("image")} />
</label>
<label className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-5">
<p className="text-xl font-bold">영상 업로드</p>
<input type="file" accept="video/*" {...register("video")} />
</label>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-5">
<p className="text-xl font-bold">글</p>
<Editor setValue={setContent} />
Expand Down
19 changes: 12 additions & 7 deletions apps/client/src/components/homeWorship/detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@ import dayjs from "dayjs";
import { useParams, usePathname, useRouter } from "next/navigation";
import { SafeHTML, Spinner } from "ui";
import HomeWorshipDetailComments from "./comments";
import HomeWorshipVideoPlayer from "./videoPlayer";

const HomeWorshipDetail = () => {
const { push } = useRouter();
const pathname = usePathname();
const params = useParams();
const homeWorshipId = params?.id as string;

const { data } = useGetHomeWorship({ homeWorshipId });
const { data, isLoading, isError } = useGetHomeWorship({ homeWorshipId });
const { mutate } = useDeleteHomeWorship();

if (!data)
if (isLoading || isError || !data || !data.homeWorship) {
return (
<div className="flex justify-center">
<Spinner />
</div>
);
}

const homeWorship = data.homeWorship;

Expand All @@ -44,7 +46,7 @@ const HomeWorshipDetail = () => {
};

return (
<div className="px-5 sm:px-10 md:px-20 lg:px-28 xl:px-36 2xl:mx-auto 2xl:max-w-screen-lg 2xl:px-40">
<div className="overflow-x-hidden px-5 sm:px-10 md:px-20 lg:px-28 xl:px-36 2xl:mx-auto 2xl:max-w-screen-lg 2xl:px-40">
<h1 className="mb-2 font-SCoreDream text-3xl">{homeWorship.title}</h1>
<div className="flex justify-between">
<div className="flex gap-2">
Expand All @@ -61,11 +63,14 @@ const HomeWorshipDetail = () => {
</button>
</div>
</div>
<div className="mb-10">
<div className="mb-10 flex w-full flex-col items-center justify-center gap-5">
<SafeHTML html={homeWorship.content} />
<div className="relative h-full w-full">
<img src={`${homeWorship.image}/full`} alt="이미지" />
</div>
{homeWorship.image && (
<div className="relative flex h-full w-full justify-center">
<img src={`${homeWorship.image}/full`} alt="이미지" />
</div>
)}
{homeWorship.video && <HomeWorshipVideoPlayer video={homeWorship.video} />}
</div>
<HomeWorshipDetailComments />
</div>
Expand Down
32 changes: 32 additions & 0 deletions apps/client/src/components/homeWorship/detail/videoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from "react";
import { Spinner, cn } from "ui";

interface IHomeWorshipVideoPlayerProps {
video: string;
}

const HomeWorshipVideoPlayer = ({ video }: IHomeWorshipVideoPlayerProps) => {
const [loading, setLoading] = useState(true);

return (
<div className="relative aspect-[16/9] w-full">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<Spinner />
</div>
)}
<iframe
className={cn(
"absolute top-0 h-full w-full rounded-md border-none transition-opacity duration-500",
loading ? "opacity-0" : "opacity-100",
)}
src={video}
onLoad={() => setLoading(false)}
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
allowFullScreen
/>
</div>
);
};

export default HomeWorshipVideoPlayer;
87 changes: 67 additions & 20 deletions apps/client/src/pages/api/cloudflare/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,80 @@
import { api } from "@/api";
import { NextApiRequest, NextApiResponse } from "next";

interface IRequestOptions {
expiry: string;
maxDurationSeconds?: number;
requireSignedURLs?: boolean;
allowedOrigins?: string[];
creator?: string;
}

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const {
method,
body: { expireDate },
body: { userName, title, date, expireDate, type },
} = req;

switch (method) {
// Direct Creator Upload URL 가져오기
case "POST":
// CloudFlare DirectCreatorUpload 3시간 유효한 uploadURL 가져오기
const {
data: {
result: { uploadURL },
},
} = await api.post(`${process.env.CLOUDFLARE_REQ_URL}`, null, {
headers: {
ContentType: "application/json",
Authorization: `Bearer ${process.env.CLOUDFLARE_API_KEY}`,
},
data: {
expiry: expireDate,
},
});

return res.status(200).json({
success: uploadURL ? true : false,
uploadURL,
});
const reqTypes = {
video: process.env.CLOUDFLARE_REQ_VIDEO_URL,
image: process.env.CLOUDFLARE_REQ_IMG_URL,
};

const fileTypes = type as keyof typeof reqTypes;

// Request 옵션 설정
const options: IRequestOptions = {
expiry: expireDate,
};
if (fileTypes === "video") {
options.maxDurationSeconds = 300; // 최대길이 5분(60초*5)
options.creator = userName;
}
// Request URL 설정
const reqURL = reqTypes[fileTypes];

if (reqURL) {
switch (fileTypes) {
// 1. IMG Direct Upload
case "image":
const {
data: {
result: { uploadURL: imgUploadURL },
},
} = await api.post(reqURL, null, {
headers: {
Authorization: `Bearer ${process.env.CLOUDFLARE_API_KEY}`,
ContentType: "application/json",
},
data: options,
});
return res.status(200).json({
success: imgUploadURL ? true : false,
uploadURL: imgUploadURL,
});

// 2. VIDEO Direct Upload
case "video":
const {
data: {
result: { uploadURL: videoUploadURL },
},
} = await api.post(reqURL, options, {
headers: {
Authorization: `Bearer ${process.env.CLOUDFLARE_API_KEY}`,
"Content-Type": "application/json",
},
});
return res.status(200).json({
success: videoUploadURL ? true : false,
uploadURL: videoUploadURL,
});
}
}
return res.status(405).end("Request URL이 존재하지 않습니다.");

default:
res.setHeader("Allow", ["POST"]);
Expand Down
Loading