diff --git a/app/package-lock.json b/app/package-lock.json index 9de9045..b4317cf 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -4990,6 +4990,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "license": "MIT", "dependencies": { "uzip": "0.20201231.0" } diff --git a/app/src/app/api/image/route.ts b/app/src/app/api/image/route.ts index dae8025..e8be7ee 100644 --- a/app/src/app/api/image/route.ts +++ b/app/src/app/api/image/route.ts @@ -4,74 +4,74 @@ import type { NextRequest } from "next/server"; import { OpenAI } from "openai"; type ResponseData = { - message: string; + message: string; }; const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, + apiKey: process.env.OPENAI_API_KEY, }); const BUCKET_NAME = "kz2404"; export async function GET( - req: NextRequest, - res: NextApiResponse + req: NextRequest, + res: NextApiResponse, ) { - // クエリパラメータを取得 + // クエリパラメータを取得 const { searchParams } = new URL(req.url || ""); const imageName = searchParams.get("imageName") || "default-image-name"; - const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; + const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${imageName}`; - return await generateCaption(imageURL) - .then((caption) => { - console.log(caption); - return new Response(JSON.stringify({ caption }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }) - .catch((error) => { - console.error("Error generating caption:", error); - return new Response(JSON.stringify({ message: "エラーが発生しました" }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }); - }); + return await generateCaption(imageURL) + .then((caption) => { + console.log(caption); + return new Response(JSON.stringify({ caption }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }) + .catch((error) => { + console.error("Error generating caption:", error); + return new Response(JSON.stringify({ message: "エラーが発生しました" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + }); } // 画像URLからキャプションを生成する関数 const generateCaption = async (imageUrl: string) => { - try { - const completion = await openai.chat.completions.create({ - model: "gpt-4o", - messages: [ - { - role: "system", - content: [ - { - type: "text", - text: "A robot that can generate very short captions for images.", - }, - ], - }, - { - role: "user", - content: [ - { - type: "image_url", - image_url: { - url: imageUrl, - detail: "low", - }, - }, - ], - }, - ], - }); + try { + const completion = await openai.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "system", + content: [ + { + type: "text", + text: "A robot that can generate very short captions for images.", + }, + ], + }, + { + role: "user", + content: [ + { + type: "image_url", + image_url: { + url: imageUrl, + detail: "low", + }, + }, + ], + }, + ], + }); - const caption = await completion.choices[0].message.content; - return caption; - } catch (error) { - console.error("Error generating caption:", error); - } + const caption = await completion.choices[0].message.content; + return caption; + } catch (error) { + console.error("Error generating caption:", error); + } }; diff --git a/app/src/app/api/minio/route.ts b/app/src/app/api/minio/route.ts index f2f04b2..573c84e 100644 --- a/app/src/app/api/minio/route.ts +++ b/app/src/app/api/minio/route.ts @@ -1,76 +1,127 @@ -import fs from 'fs'; -import { formidable, IncomingForm } from 'formidable'; +import * as fs from "node:fs"; +import type { IncomingMessage } from "node:http"; +import { Readable } from "node:stream"; +import { generateCaption } from "@/functions/gpt"; +import { scoreRegister } from "@/functions/scoreRegister"; +import { shapeCaption } from "@/functions/shapeCaption"; +import { postSimilarity } from "@/functions/simirality"; +import { prisma } from "@/lib/prisma"; +import type { ScoreData, ScoreResponse } from "@/types"; +import { formidable } from "formidable"; +import { Client } from "minio"; import type { NextRequest } from "next/server"; -import type { IncomingMessage } from 'http'; -import { Readable } from 'stream'; -import { Client } from 'minio'; const minioClient = new Client({ - endPoint: process.env.NEXT_PUBLIC_ENDPOINT || '', - port: Number(process.env.NEXT_PUBLIC_PORT), - accessKey: process.env.NEXT_PUBLIC_ACCESS_KEY || '', - secretKey: process.env.NEXT_PUBLIC_SECRET_KEY || '', - useSSL: false, + endPoint: process.env.NEXT_PUBLIC_ENDPOINT || "", + port: Number(process.env.NEXT_PUBLIC_PORT), + accessKey: process.env.NEXT_PUBLIC_ACCESS_KEY || "", + secretKey: process.env.NEXT_PUBLIC_SECRET_KEY || "", + useSSL: false, }); -const BUCKET_NAME = 'kz2404'; +const BUCKET_NAME = "kz2404"; // NextRequestをIncomingMessageのようにラップする関数 function toIncomingMessage(request: NextRequest): IncomingMessage { - const readable = new Readable({ - read() { - request.body?.getReader().read().then(({ done, value }) => { - if (done) { - this.push(null); - } else { - this.push(Buffer.from(value)); - } - }); - }, - }); - - const msg = Object.assign(readable, { - headers: Object.fromEntries(request.headers), - method: request.method, - url: request.nextUrl.pathname, - }); - - return msg as IncomingMessage; -} + const readable = new Readable({ + read() { + request.body + ?.getReader() + .read() + .then(({ done, value }) => { + if (done) { + this.push(null); + } else { + this.push(Buffer.from(value)); + } + }); + }, + }); + + const msg = Object.assign(readable, { + headers: Object.fromEntries(request.headers), + method: request.method, + url: request.nextUrl.pathname, + }); + return msg as IncomingMessage; +} export async function POST(req: NextRequest) { - const form = formidable(); - const incomingMessage = toIncomingMessage(req); - - form.parse(incomingMessage, async (err: any, fields: any, files: any) => { - if (err) { - throw new Error('Error parsing form'); - } - const image = files.image[0]; - const mimetype = image.type; - const metaData = { - 'Content-Type': mimetype - }; - - try { - await minioClient.putObject( - BUCKET_NAME, - image.originalFilename, - fs.createReadStream(image.filepath), - image.size, - metaData, - ); - } catch (err) { - return new Response(JSON.stringify({ message: '失敗' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - }); - - return new Response(JSON.stringify({ message: "成功" }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); + const form = formidable(); + const incomingMessage = toIncomingMessage(req); + + const { searchParams } = new URL(req.url || ""); + const fileName = searchParams.get("file") || ""; + const assignment = searchParams.get("assignment") || ""; + const uid = searchParams.get("uid") || ""; + const assignmentId = Number(searchParams.get("assignmentId") || 0); + + form.parse(incomingMessage, async (err: Error | null, _, files: any) => { + if (err || !files.image) { + throw new Error("Error parsing form"); + } + const image = files.image[0]; + const mimetype = image.type; + const metaData = { + "Content-Type": mimetype, + }; + + try { + await minioClient.putObject( + BUCKET_NAME, + image.originalFilename, + fs.createReadStream(image.filepath), + image.size, + metaData, + ); + } catch (err) { + return new Response(JSON.stringify({ message: "失敗" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + }); + + const imageURL = `${process.env.NEXT_PUBLIC_MINIO_ENDPOINT}${BUCKET_NAME}/${fileName}`; + + // キャプション生成 + const caption = await generateCaption(imageURL); + + // 類似度分割 + const words: string[] = shapeCaption(caption || ""); + + // 類似度計算 + const resSimilarity: { similarity: number } = await postSimilarity( + assignment, + words, + ); + + const user = await prisma.user.findUnique({ + where: { + uid: uid, + }, + }); + + const scoreData: ScoreData = { + similarity: resSimilarity.similarity, + answerTime: new Date(), + imageUrl: imageURL, + assignmentId: assignmentId, + userId: user?.id || 0, + }; + + const score = await scoreRegister(scoreData); + + const response: ScoreResponse = { + text: caption || "", + score: score?.point || 0, + similarity: resSimilarity.similarity, + assignmentId: assignmentId, + }; + + return new Response(JSON.stringify(response), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); } diff --git a/app/src/app/camera/page.tsx b/app/src/app/camera/page.tsx index 4a225db..c43cf8f 100644 --- a/app/src/app/camera/page.tsx +++ b/app/src/app/camera/page.tsx @@ -3,22 +3,20 @@ import { Answered } from "@/components/Answered"; import { AssignmentBadge } from "@/components/AssignmentBadge"; 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"; import { Label } from "@/components/ui/label"; import { Toaster } from "@/components/ui/sonner"; -import { shapeCaption } from "@/functions/shapeCaption"; -import { postSimilarity } from "@/functions/simirality"; -import type { todayAssignment } from "@/types"; +import type { ScoreResponse, User, todayAssignment } from "@/types"; import imageCompression from "browser-image-compression"; import type React from "react"; import { useEffect, useRef, useState } from "react"; @@ -29,454 +27,367 @@ import RotateCameraIcon from "../../../public/icons/icon-rotate-camera.svg"; import ShutterIcon from "../../../public/icons/icon-shutter.svg"; interface ImagePreviewProps { - image: string | null; - onClick: () => void; + image: string | null; + onClick: () => void; } -interface UploadResponse { - url: string; - success: boolean; -} - -interface ScoreData { - 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< - 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 && ( - - )} - - ) : ( - - )} - - ); + 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); + const [loginUser, setLoginUser] = useState(); + + useEffect(() => { + const getDevices = async () => { + const user = localStorage.getItem("userID"); + if (user === null) { + console.error("ユーザー情報が取得できませんでした。"); + return; + } + const userInfo = JSON.parse(user); + setLoginUser(userInfo); + 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 uploadImageAndRegisterScore = async ( + imageData: string, + ): Promise<{ data: ScoreResponse }> => { + 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?file=${imageName}&&assignment=${todayAssignment?.english}&&uid=${loginUser?.uid}&&assignmentId=${todayAssignment?.assignmentId}`, + { + method: "POST", + body: formData, + }, + ); + + const data = await response.json(); + + return { data }; + } catch (error) { + console.error("画像のアップロードに失敗しました:", error); + throw error; + } + }; + + const handleConfirm = async () => { + if (tempImage) { + try { + const { data } = await uploadImageAndRegisterScore(tempImage); + + setShowConfirmDialog(false); + setImage(tempImage); + setShowImage(true); + setTempImage(null); + + const percentSimilarity = Math.floor(data.similarity * 100); + + const message = `${data.text} 類似度 ${percentSimilarity}% スコア: ${data.score} ランキングから順位を確認しましょう!`; + const newAssignments = assignments.map((assignment) => { + if (assignment.assignmentId === data.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; diff --git a/app/src/functions/scoreRegister.ts b/app/src/functions/scoreRegister.ts new file mode 100644 index 0000000..ab604a4 --- /dev/null +++ b/app/src/functions/scoreRegister.ts @@ -0,0 +1,55 @@ +import { prisma } from "@/lib/prisma"; +import type { Score, ScoreData } from "@/types"; + +const assignmentDate = async function GET() { + const assignment = await prisma.assignment.findFirst({ + select: { date: true }, + }); + const date = assignment?.date ?? null; + + return new Response(JSON.stringify({ date }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +}; + +export const scoreRegister = async (scoreData: ScoreData) => { + // 必須データの存在確認 + if ( + scoreData.similarity == null || + scoreData.answerTime == null || + scoreData.imageUrl == null || + scoreData.assignmentId == null || + scoreData.userId == null + ) { + return + } + + // ポイントの計算 + const minTime = 60; + const maxTime = 3600; + + const response = await assignmentDate(); + const assignmentDateValue = await response.json(); + + const answerIntervalTime = + new Date(scoreData.answerTime).getTime() - + new Date(assignmentDateValue.date).getTime(); + const normalizedTime = Math.max( + 0, + Math.min(1, (answerIntervalTime / 1000 - minTime) / (maxTime - minTime)), + ); + const point = scoreData.similarity * 70 + (1 - normalizedTime) * 30; + + const score: Score = await prisma.score.create({ + data: { + point: point, + similarity: scoreData.similarity, + imageUrl: scoreData.imageUrl, + assignmentId: scoreData.assignmentId, + userId: scoreData.userId, + }, + }); + + return score; +}; diff --git a/app/src/functions/simirality.ts b/app/src/functions/simirality.ts index 9d6a3b0..798577b 100644 --- a/app/src/functions/simirality.ts +++ b/app/src/functions/simirality.ts @@ -1,7 +1,6 @@ export const postSimilarity = async (assignment: string, words: string[]) => { // TODO クライアントサイドかサーバーサイドかでURLを変更する - // サーバーサイドの場合は、http://FastAPI:9004/similarity - const response = await fetch("http://localhost:9004/similarity", { + const response = await fetch("http://FastAPI:9004/similarity", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/app/src/types/index.tsx b/app/src/types/index.tsx index 5849032..738bd5d 100644 --- a/app/src/types/index.tsx +++ b/app/src/types/index.tsx @@ -100,13 +100,13 @@ export type todayAssignment = { assignmentId: number; english: string; assignTime?: Date; - isAnswered: boolean; + isAnswered: boolean; }; export type latestAssignment = { - assignmentId: number; - english: string; - isAnswered: boolean; + assignmentId: number; + english: string; + isAnswered: boolean; }; export type experiencePoint = { @@ -121,3 +121,18 @@ export type experiencePoint = { user?: User; }; + +export interface ScoreData { + similarity: number; + answerTime: Date; + imageUrl: string; + assignmentId: number; + userId: number; +} + +export interface ScoreResponse { + text: string; + score: number; + similarity: number; + assignmentId: number; +}