diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index de2a027..e348fc9 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -1,14 +1,14 @@ "use client"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -26,421 +26,457 @@ import { Toaster } from "@/components/ui/sonner"; import { todayAssignment } from "@/types"; import { Answered } from "@/components/Answered"; import { AssignmentBadge } from "@/components/AssignmentBadge"; +import imageCompression from "browser-image-compression"; interface ImagePreviewProps { - image: string | null; - onClick: () => void; + image: string | null; + onClick: () => void; } interface UploadResponse { - url: string; - success: boolean; + url: string; + success: boolean; } interface ScoreData { - similarity: number; - answerTime: Date; - imageUrl: string; - assignmentId: number; - userId: number; + similarity: number; + answerTime: Date; + imageUrl: string; + assignmentId: number; + userId: number; } const BUCKET_NAME = "kz2404"; const ImagePreview = ({ image, onClick }: ImagePreviewProps) => ( -
{ - if (e.key === "Enter" || e.key === " ") { - onClick(); - } - }} - /> +
{ + if (e.key === "Enter" || e.key === " ") { + onClick(); + } + }} + /> ); const DialogImagePreview = ({ image }: { image: string | null }) => { - if (!image) return null; - - return ( -
-
-
- ); + if (!image) return null; + + return ( +
+
+
+ ); }; const LoadingSpinner = () => ( -
-
-

アップロード中...

-
+
+
+

アップロード中...

+
); const imageDataToBase64 = (imageData: ImageData): string => { - const canvas = document.createElement("canvas"); - canvas.width = imageData.width; - canvas.height = imageData.height; + const canvas = document.createElement("canvas"); + canvas.width = imageData.width; + canvas.height = imageData.height; - const ctx = canvas.getContext("2d"); - if (!ctx) throw new Error("Failed to get 2D context"); + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("Failed to get 2D context"); - ctx.putImageData(imageData, 0, 0); - return canvas.toDataURL("image/jpeg"); + ctx.putImageData(imageData, 0, 0); + return canvas.toDataURL("image/jpeg"); }; const CameraApp = () => { - const [image, setImage] = useState(null); - const [showImage, setShowImage] = useState(false); - const [isUploading, setIsUploading] = useState(false); - const [showConfirmDialog, setShowConfirmDialog] = useState(false); - const [tempImage, setTempImage] = useState(null); - const camera = useRef(null); - const [devices, setDevices] = useState([]); - const [activeDeviceId, setActiveDeviceId] = useState(undefined); - const [currentDeviceIndex, setCurrentDeviceIndex] = useState(0); - const [todayAssignment, setTodayAssignment] = useState(); - const [assignments, setAssignments] = useState([]); - const [isActive, setIsActive] = useState(true); - - useEffect(() => { - const getDevices = async () => { - const user = localStorage.getItem("userID"); - if (user === null) { - console.error("ユーザー情報が取得できませんでした。"); - return; - } - const userInfo = JSON.parse(user); - const resAssignment = await fetch(`/api/assignment/today?uid=${userInfo?.uid}`); - const assignmentData = await resAssignment.json(); - - if (assignmentData.length === 0) { - setIsActive(false); - return; - } - - const isAnsweredAll = assignmentData.every( - (assignment: todayAssignment) => assignment.isAnswered, - ); - if (isAnsweredAll) { - setIsActive(false); - return; - } - - const notAnsweredAssignment = assignmentData.find( - (assignment: todayAssignment) => !assignment.isAnswered, - ); - - setTodayAssignment(notAnsweredAssignment); - setAssignments(assignmentData); - - if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - console.error("メディアデバイスAPIがサポートされていません。"); - } - - try { - const devices = await navigator.mediaDevices.enumerateDevices(); - const videoDevices = devices.filter((device) => device.kind === "videoinput"); - setDevices(videoDevices); - if (videoDevices.length > 0) { - setActiveDeviceId(videoDevices[0].deviceId); - } - } catch (error) { - console.error("デバイスの取得中にエラーが発生しました:", error); - } - }; - - getDevices(); - }, []); - - const switchCamera = () => { - if (devices.length > 1) { - const nextIndex = (currentDeviceIndex + 1) % devices.length; - setActiveDeviceId(devices[nextIndex].deviceId); - setCurrentDeviceIndex(nextIndex); - } - }; - - const uploadImage = async ( - imageData: string, - ): Promise<{ imageName: string; data: UploadResponse }> => { - setIsUploading(true); - try { - const base64Response = await fetch(imageData); - const blob = await base64Response.blob(); - - // 拡張子取得 - const Extension = blob.type.split("/")[1]; - - // 日付取得 - const date = new Date(); - const thisMonth = date.getMonth() + 1; - const month = thisMonth < 10 ? "0" + thisMonth : thisMonth; - const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate(); - const formattedDate = `${date.getFullYear()}${month}${day}`; - - // ランダム文字列を生成する関数 - const generateRandomString = (charCount = 7): string => { - const str = Math.random().toString(36).substring(2).slice(-charCount); - return str.length < charCount ? str + "a".repeat(charCount - str.length) : str; - }; - - const randomStr = generateRandomString(); - // ファイル名作成 - const imageName = `${formattedDate}_${randomStr}.${Extension}`; - - const formData = new FormData(); - formData.append("image", blob, imageName); - - const response = await fetch("/api/minio", { - method: "POST", - body: formData, - }); - - const data = await response.json(); - - return { imageName, data }; - } catch (error) { - console.error("画像のアップロードに失敗しました:", error); - throw error; - } - }; - - const getCaption = async (imageName: string): Promise<{ caption: string }> => { - try { - const response = await fetch(`/api/image?imageName=${imageName}`); - if (!response.ok) { - throw new Error("キャプションの取得に失敗しました"); - } - - return await response.json(); - } catch (error) { - console.error("キャプションの取得に失敗しました:", error); - throw error; - } - }; - - // スコア計算を行います。 - const similarityRequest = async (caption: string) => { - const words: string[] = shapeCaption(caption); - const assignmentWord: string = todayAssignment?.english || ""; - const resSimilarity = await postSimilarity(assignmentWord, words); - return { - similarity: resSimilarity.similarity as number, - assignmentId: todayAssignment?.assignmentId as number, - }; - }; - - // userIdの取得 - const getUserId = async () => { - const userString = localStorage.getItem("userID"); - if (userString === null) { - return null; - } - const userData = JSON.parse(userString); - - const resUserId = await fetch("/api/user?uid=" + userData.uid, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - return await resUserId.json(); - }; - - // scoreの送信 - const submitScore = async (scoreData: ScoreData) => { - const response = await fetch("/api/score", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ scoreData }), - }); - - if (!response.ok) { - console.error("スコアの送信に失敗しました", response.statusText); - return; - } - - return await response.json(); - }; - - const handleConfirm = async () => { - if (tempImage) { - try { - const { imageName } = await uploadImage(tempImage); - const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; - - const res = await getCaption(imageName); - const caption = res.caption; - - setShowConfirmDialog(false); - setImage(tempImage); - setShowImage(true); - setTempImage(null); - - const { similarity, assignmentId } = await similarityRequest(caption); - - const user = await getUserId(); - const userId: number = user.id; - - const scoreData: ScoreData = { - similarity: similarity, - answerTime: new Date(), - imageUrl: imageURL, - assignmentId: assignmentId, - userId: userId, - }; - const response = await submitScore(scoreData); - const score = response.score; - const percentSimilarity = Math.floor(similarity * 100); - const message = `${caption} 類似度 ${percentSimilarity}% スコア: ${score.point} ランキングから順位を確認しましょう!`; - const newAssignments = assignments.map((assignment) => { - if (assignment.assignmentId === assignmentId) { - assignment.isAnswered = true; - } - return assignment; - }); - const notAnsweredAssignment = newAssignments.find( - (assignment: todayAssignment) => !assignment.isAnswered, - ); - - setTodayAssignment(notAnsweredAssignment); - - setIsUploading(false); - toast(message); - setAssignments(newAssignments); - - if (newAssignments.every((assignment) => assignment.isAnswered)) { - setIsActive(false); - } - - - } catch (error) { - setIsUploading(false); - console.error("アップロード中にエラーが発生しました:", error); - } - } - }; - - const handleCancel = () => { - setShowConfirmDialog(false); - setTempImage(null); - }; - - const handleImageCapture = (capturedImage: string | ImageData) => { - const imageStr = - capturedImage instanceof ImageData ? imageDataToBase64(capturedImage) : capturedImage; - - setTempImage(imageStr); - setShowConfirmDialog(true); - }; - - return ( - <> - {isActive ? ( - <> -
- -
-
-
- - { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onload = () => { - handleImageCapture(reader.result as string); - }; - reader.readAsDataURL(file); - } - }} - /> -
- - -
- - - - - 画像のアップロード確認 - - この画像をアップロードしてもよろしいですか? - - - - - - - いいえ - はい - - - - {isUploading && } - - {todayAssignment?.english && ( - - )} - - ) : ( - - )} - - ); + const [image, setImage] = useState(null); + const [showImage, setShowImage] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [tempImage, setTempImage] = useState(null); + const camera = useRef(null); + const [devices, setDevices] = useState([]); + const [activeDeviceId, setActiveDeviceId] = useState( + undefined + ); + const [currentDeviceIndex, setCurrentDeviceIndex] = useState(0); + const [todayAssignment, setTodayAssignment] = useState< + todayAssignment | undefined + >(); + const [assignments, setAssignments] = useState([]); + const [isActive, setIsActive] = useState(true); + + useEffect(() => { + const getDevices = async () => { + const user = localStorage.getItem("userID"); + if (user === null) { + console.error("ユーザー情報が取得できませんでした。"); + return; + } + const userInfo = JSON.parse(user); + const resAssignment = await fetch( + `/api/assignment/today?uid=${userInfo?.uid}` + ); + const assignmentData = await resAssignment.json(); + + if (assignmentData.length === 0) { + setIsActive(false); + return; + } + + const isAnsweredAll = assignmentData.every( + (assignment: todayAssignment) => assignment.isAnswered + ); + if (isAnsweredAll) { + setIsActive(false); + return; + } + + const notAnsweredAssignment = assignmentData.find( + (assignment: todayAssignment) => !assignment.isAnswered + ); + + setTodayAssignment(notAnsweredAssignment); + setAssignments(assignmentData); + + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + console.error("メディアデバイスAPIがサポートされていません。"); + } + + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter( + (device) => device.kind === "videoinput" + ); + setDevices(videoDevices); + if (videoDevices.length > 0) { + setActiveDeviceId(videoDevices[0].deviceId); + } + } catch (error) { + console.error("デバイスの取得中にエラーが発生しました:", error); + } + }; + + getDevices(); + }, []); + + const switchCamera = () => { + if (devices.length > 1) { + const nextIndex = (currentDeviceIndex + 1) % devices.length; + setActiveDeviceId(devices[nextIndex].deviceId); + setCurrentDeviceIndex(nextIndex); + } + }; + + const uploadImage = async ( + imageData: string + ): Promise<{ imageName: string; data: UploadResponse }> => { + setIsUploading(true); + try { + const base64Response = await fetch(imageData); + const originalBlob = await base64Response.blob(); + + const compressOptions = { + maxSizeMB: 0.01, + maxWidthOrHeight: 1920, + useWebWorker: true, + }; + + const originalFile = new File([originalBlob], "tempImage", { + type: originalBlob.type, + }); + const compressedBlob = await imageCompression( + originalFile, + compressOptions + ); + + // 拡張子取得 + const Extension = compressedBlob.type.split("/")[1]; + + // 日付取得 + const date = new Date(); + const thisMonth = date.getMonth() + 1; + const month = thisMonth < 10 ? "0" + thisMonth : thisMonth; + const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate(); + const formattedDate = `${date.getFullYear()}${month}${day}`; + + // ランダム文字列を生成する関数 + const generateRandomString = (charCount = 7): string => { + const str = Math.random().toString(36).substring(2).slice(-charCount); + return str.length < charCount + ? str + "a".repeat(charCount - str.length) + : str; + }; + + const randomStr = generateRandomString(); + // ファイル名作成 + const imageName = `${formattedDate}_${randomStr}.${Extension}`; + + const formData = new FormData(); + formData.append("image", compressedBlob, imageName); + + const response = await fetch("/api/minio", { + method: "POST", + body: formData, + }); + + const data = await response.json(); + + return { imageName, data }; + } catch (error) { + console.error("画像のアップロードに失敗しました:", error); + throw error; + } + }; + + const getCaption = async ( + imageName: string + ): Promise<{ caption: string }> => { + try { + const response = await fetch(`/api/image?imageName=${imageName}`); + if (!response.ok) { + throw new Error("キャプションの取得に失敗しました"); + } + + return await response.json(); + } catch (error) { + console.error("キャプションの取得に失敗しました:", error); + throw error; + } + }; + + // スコア計算を行います。 + const similarityRequest = async (caption: string) => { + const words: string[] = shapeCaption(caption); + const assignmentWord: string = todayAssignment?.english || ""; + const resSimilarity = await postSimilarity(assignmentWord, words); + return { + similarity: resSimilarity.similarity as number, + assignmentId: todayAssignment?.assignmentId as number, + }; + }; + + // userIdの取得 + const getUserId = async () => { + const userString = localStorage.getItem("userID"); + if (userString === null) { + return null; + } + const userData = JSON.parse(userString); + + const resUserId = await fetch("/api/user?uid=" + userData.uid, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + return await resUserId.json(); + }; + + // scoreの送信 + const submitScore = async (scoreData: ScoreData) => { + const response = await fetch("/api/score", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ scoreData }), + }); + + if (!response.ok) { + console.error("スコアの送信に失敗しました", response.statusText); + return; + } + + return await response.json(); + }; + + const handleConfirm = async () => { + if (tempImage) { + try { + const { imageName } = await uploadImage(tempImage); + const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; + + const res = await getCaption(imageName); + const caption = res.caption; + + setShowConfirmDialog(false); + setImage(tempImage); + setShowImage(true); + setTempImage(null); + + const { similarity, assignmentId } = await similarityRequest(caption); + + const user = await getUserId(); + const userId: number = user.id; + + const scoreData: ScoreData = { + similarity: similarity, + answerTime: new Date(), + imageUrl: imageURL, + assignmentId: assignmentId, + userId: userId, + }; + const response = await submitScore(scoreData); + const score = response.score; + const percentSimilarity = Math.floor(similarity * 100); + const message = `${caption} 類似度 ${percentSimilarity}% スコア: ${score.point} ランキングから順位を確認しましょう!`; + const newAssignments = assignments.map((assignment) => { + if (assignment.assignmentId === assignmentId) { + assignment.isAnswered = true; + } + return assignment; + }); + const notAnsweredAssignment = newAssignments.find( + (assignment: todayAssignment) => !assignment.isAnswered + ); + + setTodayAssignment(notAnsweredAssignment); + + setIsUploading(false); + toast(message); + setAssignments(newAssignments); + + if (newAssignments.every((assignment) => assignment.isAnswered)) { + setIsActive(false); + } + } catch (error) { + setIsUploading(false); + console.error("アップロード中にエラーが発生しました:", error); + } + } + }; + + const handleCancel = () => { + setShowConfirmDialog(false); + setTempImage(null); + }; + + const handleImageCapture = (capturedImage: string | ImageData) => { + const imageStr = + capturedImage instanceof ImageData + ? imageDataToBase64(capturedImage) + : capturedImage; + + setTempImage(imageStr); + setShowConfirmDialog(true); + }; + + return ( + <> + {isActive ? ( + <> +
+ +
+
+
+ + { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + handleImageCapture(reader.result as string); + }; + reader.readAsDataURL(file); + } + }} + /> +
+ + +
+ + + + + + 画像のアップロード確認 + + + この画像をアップロードしてもよろしいですか? + + + + + + + + いいえ + + + はい + + + + + {isUploading && } + + {todayAssignment?.english && ( + + )} + + ) : ( + + )} + + ); }; export default CameraApp;