From e30e126e0db772e0c879772f4aed88a6d1ab0da5 Mon Sep 17 00:00:00 2001 From: Mahen Date: Sun, 5 Apr 2026 15:32:23 +0700 Subject: [PATCH] refactor: clean up spaghetti code --- prisma/seed.ts | 6 +- src/hooks/useAnalyzeText.ts | 7 +-- src/hooks/useDashboard.ts | 3 +- src/hooks/useHeader.ts | 3 +- src/hooks/useProfileClient.ts | 4 +- src/hooks/useReviewTable.ts | 3 +- src/hooks/useSentiment.ts | 88 ++++-------------------------- src/hooks/useSentimentForm.ts | 4 +- src/hooks/useSocket.ts | 4 +- src/hooks/useUrlInput.ts | 0 src/hooks/useWordCloud.ts | 6 +- src/services/analyze.service.ts | 11 +--- src/services/metric.service.ts | 6 +- src/services/profile.service.ts | 3 +- src/services/scrape.service.ts | 5 +- src/utils/const.ts | 97 ++++++++++++++++++++++++++++++--- src/utils/datas.ts | 30 +++++++++- src/utils/styleType.ts | 1 - 18 files changed, 160 insertions(+), 121 deletions(-) delete mode 100644 src/hooks/useUrlInput.ts diff --git a/prisma/seed.ts b/prisma/seed.ts index 5b34308..c150481 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -28,10 +28,10 @@ async function main() { modelName: "Model XGBoost (Optimized)", description: "Model final menggunakan teknik SMOTE untuk menyeimbangkan kelas, seleksi fitur Chi-Square, dan optimasi Grid Search.", - accuracy: 0.72, - macroF1: 0.66, + accuracy: 0.73, + macroF1: 0.67, f1Negative: 0.68, - f1Neutral: 0.43, + f1Neutral: 0.45, isActive: true, }, ]; diff --git a/src/hooks/useAnalyzeText.ts b/src/hooks/useAnalyzeText.ts index 0f69c71..46228de 100644 --- a/src/hooks/useAnalyzeText.ts +++ b/src/hooks/useAnalyzeText.ts @@ -2,15 +2,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useSession } from "next-auth/react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; import { AnalysisResults, AnalyzeFormData } from "../types"; import { scrapeProduct, getAIRecommendation, } from "../services/analyze.service"; -import { analyzeSchema } from "../app/validation/analyze.schema"; // Sesuaikan path-nya -import { getAnotherUserData } from "../app/profile/lib/action"; -import prisma from "@/lib/prisma"; +import { analyzeSchema } from "../app/validation/analyze.schema"; import { getMetricId } from "../services/metric.service"; export const useAnalyseText = () => { @@ -182,7 +179,7 @@ export const useAnalyseText = () => { } catch (error: any) { if (error.name === "AbortError" || signal.aborted) { console.log("🛠️ Request dibatalkan secara aman."); - return; // Keluar dari fungsi tanpa memunculkan alert error + return; } console.error("Analysis Error:", error); diff --git a/src/hooks/useDashboard.ts b/src/hooks/useDashboard.ts index a368018..09c2165 100644 --- a/src/hooks/useDashboard.ts +++ b/src/hooks/useDashboard.ts @@ -3,6 +3,7 @@ import { useState, useEffect, useMemo } from "react"; import { ModelDB, Review, StatCounts } from "@/src/types"; import { getClassificationReport } from "../app/dashboard/lib/actions"; +import { sentimentStatsPath } from "../utils/const"; export const useDashboards = () => { const [selectedBrand, setSelectedBrand] = useState(null); @@ -20,7 +21,7 @@ export const useDashboards = () => { async function fetchStats() { setLoading(true); - const res = await fetch("/api/review/sentiment-stats"); + const res = await fetch(sentimentStatsPath); const json = await res.json(); const statsData = json.data; diff --git a/src/hooks/useHeader.ts b/src/hooks/useHeader.ts index cb41595..7a0aeda 100644 --- a/src/hooks/useHeader.ts +++ b/src/hooks/useHeader.ts @@ -1,5 +1,6 @@ import { useSession } from "next-auth/react"; import { useEffect, useState } from "react"; +import { productPath } from "../utils/const"; export const useHeader = () => { const [isRefreshing, setIsRefreshing] = useState(false); @@ -17,7 +18,7 @@ export const useHeader = () => { setMounted(true); const getProductCount = async () => { try { - const res = await fetch("/api/product"); + const res = await fetch(productPath); if (!res.ok) throw new Error("Failed to fetch product count"); const data = await res.json(); diff --git a/src/hooks/useProfileClient.ts b/src/hooks/useProfileClient.ts index 077b07f..e3fd95b 100644 --- a/src/hooks/useProfileClient.ts +++ b/src/hooks/useProfileClient.ts @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { ProfileClientProps, ProfileFormData, ProfileState } from "@/src/types"; -import { Brand, OS, Profession } from "@prisma/client"; +import { OS, Profession } from "@prisma/client"; import { brandFormat } from "../utils/datas"; export const useProfileClient = (props: ProfileClientProps) => { @@ -47,7 +47,7 @@ export const useProfileClient = (props: ProfileClientProps) => { (newData.profession as Profession) || prev.preference.profession, preferredBrand: - (newData.preferredBrand ) || prev.preference.preferredBrand, + newData.preferredBrand || prev.preference.preferredBrand, preferredOS: (newData.preferredOS as OS) || prev.preference.preferredOS, diff --git a/src/hooks/useReviewTable.ts b/src/hooks/useReviewTable.ts index f6e2514..5cee93e 100644 --- a/src/hooks/useReviewTable.ts +++ b/src/hooks/useReviewTable.ts @@ -1,6 +1,7 @@ import { useEffect, useState, useMemo } from "react"; import { ApiResponse, ReviewItem } from "../types"; import { PaginationService } from "../services/review.service"; +import { reviewPath } from "../utils/const"; export const useReviewTable = ( itemsPerPage: number = 10, @@ -14,7 +15,7 @@ export const useReviewTable = ( const getReviewData = async () => { try { setIsLoading(true); - const req = await fetch("/api/review"); + const req = await fetch(reviewPath); const res: ApiResponse = await req.json(); if (res.data && Array.isArray(res.data)) { diff --git a/src/hooks/useSentiment.ts b/src/hooks/useSentiment.ts index 22baeb6..0e7d59b 100644 --- a/src/hooks/useSentiment.ts +++ b/src/hooks/useSentiment.ts @@ -1,6 +1,7 @@ import { useMemo, useState } from "react"; import { AnalysisResult } from "../types"; -import { Minus, ThumbsDown, ThumbsUp } from "lucide-react"; +import { configDisplay } from "../utils/datas"; +import { models, negativeWords, positiveWords } from "../utils/const"; export const useSentiment = () => { const [text, setText] = useState(""); @@ -23,32 +24,6 @@ export const useSentiment = () => { await new Promise((resolve) => setTimeout(resolve, 1500)); - const positiveWords = [ - "bagus", - "cepat", - "aman", - "baik", - "mulus", - "moga", - "awet", - "mantap", - "sangat", - "fungsi", - ]; - - const negativeWords = [ - "lebih", - "jual", - "baru", - "lalu", - "tahun", - "masalah", - "rusak", - "garansi", - "layar", - "kecewa", - ]; - const lowerText = text.toLowerCase(); let positiveScore = 0; let negativeScore = 0; @@ -91,53 +66,10 @@ export const useSentiment = () => { }; const getSentimentDisplay = (sentiment: AnalysisResult["sentiment"]) => { - const config = { - POSITIVE: { - icon: ThumbsUp, - label: "Positif", - bgClass: "bg-sentiment-positive-light", - textClass: "text-sentiment-positive", - borderClass: "border-sentiment-positive/30", - }, - NEGATIVE: { - icon: ThumbsDown, - label: "Negatif", - bgClass: "bg-sentiment-negative-light", - textClass: "text-sentiment-negative", - borderClass: "border-sentiment-negative/30", - }, - NEUTRAL: { - icon: Minus, - label: "Netral", - bgClass: "bg-sentiment-neutral-light", - textClass: "text-sentiment-neutral", - borderClass: "border-sentiment-neutral/30", - }, - }; - return config[sentiment]; + const config = configDisplay(sentiment); + return config; }; - const models = [ - { - code: "none", - value: "xgboost", - label: "XGBoost (Baseline)", - desc: "Model 1", - }, - { - code: "Grid Search", - value: "xgboost", - label: "XGBoost (Tuned)", - desc: "Model 2", - }, - { - code: "recommended", - value: "xgboost", - label: "XGBoost (Fully Optimized)", - desc: "Model 3", - }, - ]; - const filteredItems = useMemo(() => { if (!searchQuery) return models; return models.filter( @@ -149,18 +81,18 @@ export const useSentiment = () => { return { selectedModel, - setSelectedModel, text, - setText, laptopName, - setLaptopName, isAnalyzing, - analyzeText, result, - getSentimentDisplay, searchQuery, - setSearchQuery, filteredItems, isFormValid, + setSelectedModel, + setText, + setLaptopName, + analyzeText, + getSentimentDisplay, + setSearchQuery, }; }; diff --git a/src/hooks/useSentimentForm.ts b/src/hooks/useSentimentForm.ts index 1db9e62..7771b32 100644 --- a/src/hooks/useSentimentForm.ts +++ b/src/hooks/useSentimentForm.ts @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { MODEL_OPTIONS } from "../utils/const"; +import { MODEL_OPTIONS, predictPath } from "../utils/const"; export const useSentimentForm = () => { const [selectedModel, setSelectedModel] = useState(MODEL_OPTIONS[2]); @@ -31,7 +31,7 @@ export const useSentimentForm = () => { setResult(null); try { - const response = await fetch("http://127.0.0.1:8000/predict", { + const response = await fetch(predictPath, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/hooks/useSocket.ts b/src/hooks/useSocket.ts index 114279e..935eeee 100644 --- a/src/hooks/useSocket.ts +++ b/src/hooks/useSocket.ts @@ -1,6 +1,6 @@ -// src/hooks/useSocket.ts import { useEffect, useState } from "react"; import { io, Socket } from "socket.io-client"; +import { socketPath } from "../utils/const"; export const useSocket = () => { const [socket, setSocket] = useState(null); @@ -8,7 +8,7 @@ export const useSocket = () => { useEffect(() => { const socketInitializer = async () => { - await fetch("/api/socket"); // Panggil API untuk menyalakan server socket + await fetch(socketPath); const newSocket = io(); newSocket.on("progress", (data) => { diff --git a/src/hooks/useUrlInput.ts b/src/hooks/useUrlInput.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/hooks/useWordCloud.ts b/src/hooks/useWordCloud.ts index b92044b..d901266 100644 --- a/src/hooks/useWordCloud.ts +++ b/src/hooks/useWordCloud.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { KeywordStats, WordCloudReview, WordItem } from "@/src/types"; -import { WORD_LIMIT } from "../utils/const"; +import { WORD_LIMIT, wordCloudPath } from "../utils/const"; export const useWordCloud = () => { const [words, setWords] = useState([]); @@ -11,11 +11,11 @@ export const useWordCloud = () => { useEffect(() => { const fetchWords = async () => { try { - const res = await fetch("/api/word-cloud"); + const res = await fetch(wordCloudPath); const json = await res.json(); if (!json?.success || !Array.isArray(json.data)) { - console.error("Invalid response from /api/word-cloud"); + console.error(`Invalid response from ${wordCloudPath}`); return; } diff --git a/src/services/analyze.service.ts b/src/services/analyze.service.ts index a3ef6f4..f4a916b 100644 --- a/src/services/analyze.service.ts +++ b/src/services/analyze.service.ts @@ -1,11 +1,12 @@ import prisma from "@/lib/prisma"; import { AIRecommendationResponse } from "../types"; +import { aiRecommendPath, scrapePath } from "../utils/const"; export const scrapeProduct = async ( url: string, options?: { signal?: AbortSignal }, ) => { - const res = await fetch("/api/scrape", { + const res = await fetch(scrapePath, { method: "POST", headers: { "Content-Type": "application/json", @@ -70,7 +71,7 @@ export const getAIRecommendation = async ( options?: { signal?: AbortSignal }, ): Promise => { console.log("Fetching to FastAPI..."); - const aiRes = await fetch("https://citot123-tokped-scraper.hf.space/recommend", { + const aiRes = await fetch(aiRecommendPath, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), @@ -79,13 +80,7 @@ export const getAIRecommendation = async ( if (!aiRes.ok) { const errorData = await aiRes.json(); - // DEBUG: Munculkan di console agar bisa dibaca strukturnya - console.error( - "DETAILED VALIDATION ERROR:", - JSON.stringify(errorData, null, 2), - ); - // Ambil pesan error pertama dari list validation FastAPI const errorMessage = errorData.detail?.[0]?.msg || "Gagal melakukan analisis AI"; throw new Error(errorMessage); diff --git a/src/services/metric.service.ts b/src/services/metric.service.ts index 3375915..27cb414 100644 --- a/src/services/metric.service.ts +++ b/src/services/metric.service.ts @@ -1,7 +1,9 @@ +import { userMetricPath } from "../utils/const"; + export const getMetricId = async () => { - const response = await fetch("/api/user-metric"); + const response = await fetch(userMetricPath); if (!response.ok) return null; const data = await response.json(); - return data.metricId; + return data.metricId; }; diff --git a/src/services/profile.service.ts b/src/services/profile.service.ts index fdf357d..3fb6afb 100644 --- a/src/services/profile.service.ts +++ b/src/services/profile.service.ts @@ -1,8 +1,9 @@ import prisma from "@/lib/prisma"; import { ProfileFormData } from "../types"; +import { profilePath } from "../utils/const"; export const updateProfileService = async (formData: ProfileFormData) => { - const response = await fetch("/api/profile", { + const response = await fetch(profilePath, { method: "PATCH", body: JSON.stringify(formData), }); diff --git a/src/services/scrape.service.ts b/src/services/scrape.service.ts index 1728b39..e3c9d31 100644 --- a/src/services/scrape.service.ts +++ b/src/services/scrape.service.ts @@ -2,6 +2,7 @@ import puppeteer from "puppeteer-core"; import chromium from "@sparticuz/chromium-min"; import { ScrapeResult } from "../types"; import { getFallbackData } from "../utils/datas"; +import { chromiumUrl } from "../utils/const"; function normalizeToReviewUrl(rawUrl: string): string { try { @@ -28,8 +29,6 @@ export async function scrapeTokopediaProduct( ): Promise { const targetUrl = normalizeToReviewUrl(url); let browser; - const CHROMIUM_URL = - "https://github.com/Sparticuz/chromium/releases/download/v131.0.1/chromium-v131.0.1-pack.tar"; try { browser = await puppeteer.launch({ @@ -39,7 +38,7 @@ export async function scrapeTokopediaProduct( "--disable-setuid-sandbox", "--disable-blink-features=AutomationControlled", ], - executablePath: await chromium.executablePath(CHROMIUM_URL), + executablePath: await chromium.executablePath(chromiumUrl), headless: true, defaultViewport: { width: 1280, height: 800 }, }); diff --git a/src/utils/const.ts b/src/utils/const.ts index 369a5ef..eb9dfd6 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -8,9 +8,8 @@ import { import { SiAcer, SiAsus, SiLenovo, SiLinux, SiMacos } from "react-icons/si"; import { FaWindows } from "react-icons/fa"; import { Sentiment } from "@prisma/client"; -import { useAnalyseText } from "../hooks/useAnalyzeText"; -export const MODEL_OPTIONS = [ +const MODEL_OPTIONS = [ { label: "Model XGBoost (Baseline)", code: "baseline", @@ -28,9 +27,9 @@ export const MODEL_OPTIONS = [ }, ]; -export const WORD_LIMIT = 30; +const WORD_LIMIT = 30; -export const professionItems = [ +const professionItems = [ { value: "PROGRAMMER", label: "Programmer", icon: Code }, { value: "DESIGNER", label: "Designer", icon: Palette }, { value: "STUDENT", label: "Student", icon: Book }, @@ -38,21 +37,21 @@ export const professionItems = [ { value: "OTHER", label: "Other", icon: LucideCircleEllipsis }, ]; -export const brandItems = [ +const brandItems = [ { value: "ASUS", label: "Asus", icon: SiAsus }, { value: "ACER", label: "Acer", icon: SiAcer }, { value: "LENOVO", label: "Lenovo", icon: SiLenovo }, { value: "OTHER", label: "Other", icon: LucideCircleEllipsis }, ]; -export const OSItems = [ +const OSItems = [ { value: "WINDOWS", label: "Windows", icon: FaWindows }, { value: "MACOS", label: "Macos", icon: SiMacos }, { value: "LINUX", label: "Linux", icon: SiLinux }, { value: "OTHER", label: "Other", icon: LucideCircleEllipsis }, ]; -export const reviewDatas = [ +const reviewDatas = [ { productId: 2, modelId: 1, @@ -79,3 +78,87 @@ export const reviewDatas = [ confidenceScore: 0.88, }, ]; + +const scrapePath = "/api/scrape"; +const backendUrl = process.env.BACKEND_URL || "http://localhost:8000"; +const aiRecommendPath = `${backendUrl}/recommend`; +const userMetricPath = "/api/user-metric"; +const profilePath = "/api/profile"; +const chromiumUrl = + "https://github.com/Sparticuz/chromium/releases/download/v131.0.1/chromium-v131.0.1-pack.tar"; +const sentimentStatsPath = "/api/review/sentiment-stats"; +const productPath = "/api/product"; +const reviewPath = "/api/review"; +const positiveWords = [ + "bagus", + "cepat", + "aman", + "baik", + "mulus", + "moga", + "awet", + "mantap", + "sangat", + "fungsi", +]; + +const negativeWords = [ + "lebih", + "jual", + "baru", + "lalu", + "tahun", + "masalah", + "rusak", + "garansi", + "layar", + "kecewa", +]; + +const models = [ + { + code: "none", + value: "xgboost", + label: "XGBoost (Baseline)", + desc: "Model 1", + }, + { + code: "Grid Search", + value: "xgboost", + label: "XGBoost (Tuned)", + desc: "Model 2", + }, + { + code: "recommended", + value: "xgboost", + label: "XGBoost (Fully Optimized)", + desc: "Model 3", + }, +]; + +const predictPath = `${backendUrl}/predict`; +const socketPath = "/api/socket"; +const wordCloudPath = "/api/word-cloud"; + +export { + scrapePath, + aiRecommendPath, + userMetricPath, + profilePath, + chromiumUrl, + sentimentStatsPath, + productPath, + reviewPath, + positiveWords, + negativeWords, + models, + predictPath, + MODEL_OPTIONS, + WORD_LIMIT, + professionItems, + brandItems, + OSItems, + reviewDatas, + socketPath, + wordCloudPath, +}; diff --git a/src/utils/datas.ts b/src/utils/datas.ts index 97cf905..c6b615c 100644 --- a/src/utils/datas.ts +++ b/src/utils/datas.ts @@ -1,5 +1,6 @@ -import { useReviewTable } from "../hooks/useReviewTable"; +import { Minus, ThumbsDown, ThumbsUp } from "lucide-react"; import { + AnalysisResult, RadarProps, ScrapeResult, VisiblePageProps, @@ -142,3 +143,30 @@ export const radarFormat = ({ data }: RadarProps) => { return { chartData, colors }; }; + +export const configDisplay = (sentiment: AnalysisResult["sentiment"]) => { + const config = { + POSITIVE: { + icon: ThumbsUp, + label: "Positif", + bgClass: "bg-sentiment-positive-light", + textClass: "text-sentiment-positive", + borderClass: "border-sentiment-positive/30", + }, + NEGATIVE: { + icon: ThumbsDown, + label: "Negatif", + bgClass: "bg-sentiment-negative-light", + textClass: "text-sentiment-negative", + borderClass: "border-sentiment-negative/30", + }, + NEUTRAL: { + icon: Minus, + label: "Netral", + bgClass: "bg-sentiment-neutral-light", + textClass: "text-sentiment-neutral", + borderClass: "border-sentiment-neutral/30", + }, + }; + return config[sentiment]; +}; diff --git a/src/utils/styleType.ts b/src/utils/styleType.ts index ed5c44f..ff17692 100644 --- a/src/utils/styleType.ts +++ b/src/utils/styleType.ts @@ -12,5 +12,4 @@ const iconStyles = { neutral: "bg-sentiment-neutral/10 text-sentiment-neutral", }; - export { variantStyles, iconStyles };