Skip to content

Commit

Permalink
feat: AI 서버 연동
Browse files Browse the repository at this point in the history
  • Loading branch information
ArpaAP committed Aug 5, 2024
1 parent 1cec2ac commit a647f98
Show file tree
Hide file tree
Showing 12 changed files with 304 additions and 16 deletions.
1 change: 1 addition & 0 deletions apps/web/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
output: 'standalone',
images: {
remotePatterns: [
Expand Down
69 changes: 69 additions & 0 deletions apps/web/src/app/analyze/page.layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use client'

import MainLayout from '@/components/MainLayout'
import { Meal, MealItem } from '@repo/database'
import { IconFileAnalytics, IconUpload } from '@tabler/icons-react'
import axios from 'axios'
import { useEffect, useState } from 'react'

interface AnalyzePageLayoutProps {
mealId: string
meal: Meal
mealItems: MealItem[]
}

export default function AnalyzePageLayout({
mealId,
meal,
mealItems,
}: AnalyzePageLayoutProps) {
const [successCount, setSuccessCount] = useState(0)

const totalCount = mealItems.length

useEffect(() => {
;(async () => {
for (let mealItem of mealItems) {
try {
await axios.post(`/api/analyze?mealItemId=${mealItem.mealItemId}`)
setSuccessCount((count) => count + 1)
} catch (error) {
console.error(error)
}
}
})()
}, [mealItems])

console.log(mealItems)
const progress = (successCount / totalCount) * 100
console.log(progress)

return (
<MainLayout>
<div className="container mx-auto px-36">
<div className="h-screen flex flex-col justify-center items-center pb-12">
<div className="text-primary-800 text-4xl font-extrabold flex items-center gap-2 pb-4">
<IconFileAnalytics size={36} className="animate-bounce" />
<span>AI 분석 중입니다...</span>
</div>

<div className="text-gray-500 font-regular pb-12">
업로드하신 사진을 AI가 살펴보고 영양 정보를 분석하고 있어요, 조금만
기다려주세요!
</div>

<div className="w-2/3 bg-gray-200 my-4">
<div
className="bg-primary-700 h-1 transition-all duration-200"
style={{ width: `${progress}%` }}
/>
</div>

<div className="text-primary-700 font-regular pb-12">
{successCount} / {totalCount} 개의 사진 분석 완료
</div>
</div>
</div>
</MainLayout>
)
}
53 changes: 53 additions & 0 deletions apps/web/src/app/analyze/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import UserGuard from '@/components/common/UserGuard'
import AnalyzePageLayout from './page.layout'

import { PrismaClient } from '@repo/database'
import { getServerSession } from 'next-auth'

const prisma = new PrismaClient()

export default async function AnalyzePage({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined }
}) {
const session = await getServerSession()

if (!session?.user) {
return <div>로그인이 필요합니다.</div>
}

const user = await prisma.user.findUnique({
where: { email: session.user.email ?? undefined },
})

if (!user) {
return <div>사용자를 찾을 수 없습니다.</div>
}

const { mealId } = searchParams

if (!mealId) {
return <div>mealId가 필요합니다.</div>
}

const meal = await prisma.meal.findUnique({
where: { mealId: mealId as string, userId: user.userId },
})

if (!meal) {
return <div>meal을 찾을 수 없습니다.</div>
}

const mealItems = await prisma.mealItem.findMany({
where: { mealId: mealId as string },
})

return (
<AnalyzePageLayout
mealId={mealId as string}
meal={meal}
mealItems={mealItems}
/>
)
}
106 changes: 104 additions & 2 deletions apps/web/src/app/api/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,108 @@
import { NextRequest } from 'next/server'
import { NextRequest, NextResponse } from 'next/server'
import { PrismaClient } from '@repo/database'
import { getServerSession } from 'next-auth'
import { storage } from '@/lib/firebase/firebaseClient'
import { getBytes, ref } from 'firebase/storage'
import axios, { AxiosError } from 'axios'

const prisma = new PrismaClient()

export async function POST(request: NextRequest) {
const { searchParams } = request.nextUrl
const mealId = searchParams.get('mealId')
const mealItemId = searchParams.get('mealItemId')

if (!mealItemId) {
return NextResponse.json(
{ error: 'mealItemId is required' },
{ status: 400 }
)
}

const session = await getServerSession()

if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const mealItem = await prisma.mealItem.findUnique({
where: {
mealItemId,
meal: {
user: {
email: session.user.email,
},
},
},
include: {
mealItemAnalysis: true,
},
})

if (!mealItem) {
return NextResponse.json({ error: 'mealItem not found' }, { status: 404 })
}

if (mealItem.mealItemAnalysis) {
return NextResponse.json(
{ error: 'mealItemAnalysis already exists' },
{ status: 400 }
)
}

const reference = ref(storage, `images/${mealItem.imageName}`)
const bytes = await getBytes(reference)

const file = new File([bytes], mealItem.imageName)

const formData = new FormData()
formData.append('file', file)

try {
var r = await axios.post(`${process.env.AI_API_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
} catch (_e) {
const e = _e as AxiosError

return NextResponse.json(
{ message: 'Failed to analyze', error: e.response?.data },
{ status: 500 }
)
}

const rawData = JSON.parse(r.data.replace(/NaN/g, '0.0'))

const analysisResult = rawData.result.result[0]

if (!analysisResult) {
return NextResponse.json({ message: 'No analysis result' }, { status: 400 })
}

const mealItemAnalysis = await prisma.mealItemAnalysis.create({
data: {
mealItemId: mealItem.mealItemId,
classId: analysisResult.cls,
className: analysisResult.class,
confidence: analysisResult.confidence,
amount: analysisResult.nut.food_amount,
kcal: analysisResult.nut.food_kcal,
carbohydrate: analysisResult.nut.food_carbohydrate,
sugar: analysisResult.nut.food_sugar,
fat: analysisResult.nut.food_fat,
protein: analysisResult.nut.food_protein,
calcium: analysisResult.nut.food_calcium,
phosphorus: analysisResult.nut.food_phosphorus,
natrium: analysisResult.nut.food_natrium,
kalium: analysisResult.nut.food_kalium,
magnesium: analysisResult.nut.food_magnesium,
iron: analysisResult.nut.food_iron,
zinc: analysisResult.nut.food_zinc,
cholesterol: analysisResult.nut.food_cholesterol,
transfat: analysisResult.nut.food_transfat,
},
})

return NextResponse.json({ message: 'OK', data: mealItemAnalysis })
}
19 changes: 18 additions & 1 deletion apps/web/src/app/api/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,26 @@ import { ref, uploadBytes } from 'firebase/storage'
import { v4 } from 'uuid'
import { storage } from '@/lib/firebase/firebaseClient'
import { PrismaClient } from '@repo/database'
import { getServerSession } from 'next-auth'
import axios from 'axios'

const prisma = new PrismaClient()

export async function POST(request: NextRequest) {
const session = await getServerSession()

if (!session?.user) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 })
}

const user = await prisma.user.findUnique({
where: { email: session.user.email ?? undefined },
})

if (!user) {
return NextResponse.json({ message: 'User not found' }, { status: 404 })
}

const formData = await request.formData()

const { data, error } = uploadSchema.safeParse(formData)
Expand Down Expand Up @@ -40,6 +56,7 @@ export async function POST(request: NextRequest) {

const meal = await prisma.meal.create({
data: {
userId: user.userId,
date: new Date(date),
type,
},
Expand All @@ -48,7 +65,7 @@ export async function POST(request: NextRequest) {
const items = await prisma.mealItem.createMany({
data: fileNames.map((fileName) => ({
mealId: meal.mealId,
imageUrl: `images/${fileName}`,
imageName: fileName,
})),
})

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
import './globals.css'
import NextTopLoader from 'nextjs-toploader'
import AuthContext from '@/context/AuthContext'
import AuthContext from '@/contexts/AuthContext'
import { getServerSession } from 'next-auth'
import { Toaster } from 'react-hot-toast'

Expand Down
11 changes: 9 additions & 2 deletions apps/web/src/app/upload/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Button from '@/components/Button'
import MainLayout from '@/components/MainLayout'
import { Radio, RadioGroup } from '@headlessui/react'
import { Meal } from '@repo/database'
import {
IconArrowRight,
IconCircle,
Expand All @@ -12,6 +13,7 @@ import {
} from '@tabler/icons-react'
import axios from 'axios'
import clsx from 'clsx'
import { useRouter } from 'next/navigation'
import { useRef, useState } from 'react'
import { Controller, SubmitHandler, useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
Expand All @@ -23,7 +25,9 @@ interface UploadFormState {
}

export default function UploadPage() {
const router = useRouter()
const inputRef = useRef<HTMLInputElement>(null)

const { register, watch, control, setValue, handleSubmit } =
useForm<UploadFormState>({
defaultValues: {
Expand Down Expand Up @@ -103,9 +107,12 @@ export default function UploadPage() {
})
.then((res) => {
if (res.status === 200) {
toast.success('분석이 완료되었습니다.')
toast.success('사진 업르드를 완료했습니다.')

let meal = res.data.data as Meal
router.push(`/analyze?mealId=${meal.mealId}`)
} else {
toast.error('분석에 실패했습니다.')
toast.error('사진 업로드에 실패했습니다.')
}
})
}
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ CREATE TABLE `Meal` (
CREATE TABLE `MealItem` (
`mealItemId` VARCHAR(191) NOT NULL,
`mealId` VARCHAR(191) NOT NULL,
`imageUrl` VARCHAR(191) NOT NULL,
`imageName` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,

Expand All @@ -41,7 +41,7 @@ CREATE TABLE `MealItemAnalysis` (
`mealItemId` VARCHAR(191) NOT NULL,
`kcal` DOUBLE NOT NULL,
`carbohydrate` DOUBLE NOT NULL,
`sugars` DOUBLE NOT NULL,
`sugar` DOUBLE NOT NULL,
`fat` DOUBLE NOT NULL,
`protein` DOUBLE NOT NULL,
`calcium` DOUBLE NOT NULL,
Expand Down
11 changes: 11 additions & 0 deletions packages/database/prisma/migrations/20240805054930_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:
- Added the required column `userId` to the `Meal` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `Meal` ADD COLUMN `userId` VARCHAR(191) NOT NULL;

-- CreateIndex
CREATE INDEX `Meal_userId_idx` ON `Meal`(`userId`);
18 changes: 18 additions & 0 deletions packages/database/prisma/migrations/20240805062955_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Warnings:
- A unique constraint covering the columns `[mealItemId]` on the table `MealItemAnalysis` will be added. If there are existing duplicate values, this will fail.
- Added the required column `amount` to the `MealItemAnalysis` table without a default value. This is not possible if the table is not empty.
- Added the required column `classId` to the `MealItemAnalysis` table without a default value. This is not possible if the table is not empty.
- Added the required column `className` to the `MealItemAnalysis` table without a default value. This is not possible if the table is not empty.
- Added the required column `confidence` to the `MealItemAnalysis` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `MealItemAnalysis` ADD COLUMN `amount` DOUBLE NOT NULL,
ADD COLUMN `classId` INTEGER NOT NULL,
ADD COLUMN `className` VARCHAR(191) NOT NULL,
ADD COLUMN `confidence` DOUBLE NOT NULL;

-- CreateIndex
CREATE UNIQUE INDEX `MealItemAnalysis_mealItemId_key` ON `MealItemAnalysis`(`mealItemId`);
Loading

0 comments on commit a647f98

Please sign in to comment.