diff --git a/dev-server.log b/dev-server.log new file mode 100644 index 00000000..d3dba065 --- /dev/null +++ b/dev-server.log @@ -0,0 +1,3 @@ + +> bsmhub@0.1.0 dev /app +> next dev diff --git a/generate-lighthouserc.ts b/generate-lighthouserc.ts index 17fc9740..b5c8d71b 100644 --- a/generate-lighthouserc.ts +++ b/generate-lighthouserc.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { config } from 'dotenv'; -import { getAllRoutes } from './src/app/sitemap.xml/sitemap'; +import sitemap from './src/app/sitemap.xml/sitemap'; config({ path: '.env.local' }); @@ -15,13 +15,14 @@ async function generateConfig() { console.log('๐Ÿš€ Generating Lighthouse CI config...'); // 1. sitemap์—์„œ ๋™์ ์œผ๋กœ URL ๊ฒฝ๋กœ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. - const staticPaths = await getAllRoutes(); + const sitemapData = await sitemap(); + const urls = sitemapData.map((item) => item.url.replace('https://bsmhub.vercel.app', 'http://localhost:3000')); // 2. Lighthouse CI ์„ค์ • ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. const config = { ci: { collect: { - url: staticPaths.map((path) => `http://localhost:3000${path}`), + url: urls, startServerCommand: 'npm run start', }, assert: { @@ -50,4 +51,4 @@ async function generateConfig() { } // ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ -generateConfig(); +generateConfig(); \ No newline at end of file diff --git a/jules-scratch/verification/check_portfolio_console.py b/jules-scratch/verification/check_portfolio_console.py new file mode 100644 index 00000000..f5485209 --- /dev/null +++ b/jules-scratch/verification/check_portfolio_console.py @@ -0,0 +1,24 @@ +from playwright.sync_api import sync_playwright + +def run(playwright): + browser = playwright.chromium.launch(headless=True) + context = browser.new_context() + page = context.new_page() + + # Collect console messages + messages = [] + page.on("console", lambda msg: messages.append(f"{msg.type}: {msg.text}")) + + # Go to a portfolio page + page.goto("http://localhost:3000/portfolio/Minjae-Kwon") + page.wait_for_load_state("domcontentloaded") + + # Print console messages + for msg in messages: + print(msg) + + context.close() + browser.close() + +with sync_playwright() as playwright: + run(playwright) \ No newline at end of file diff --git a/jules-scratch/verification/find_bad_requests.py b/jules-scratch/verification/find_bad_requests.py new file mode 100644 index 00000000..42fe9a3f --- /dev/null +++ b/jules-scratch/verification/find_bad_requests.py @@ -0,0 +1,23 @@ +from playwright.sync_api import sync_playwright + +def run(playwright): + browser = playwright.chromium.launch(headless=True) + context = browser.new_context() + page = context.new_page() + + def handle_response(response): + if response.status == 400: + print(f"Bad Request URL: {response.url}") + print(f"Request Method: {response.request.method}") + # print(f"Request Post Data: {response.request.post_data}") + + page.on("response", handle_response) + + # Go to a portfolio page + page.goto("http://localhost:3000/portfolio/Minjae-Kwon", wait_until="networkidle") + + context.close() + browser.close() + +with sync_playwright() as playwright: + run(playwright) \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index 60ef0e67..4dc948b8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -9,7 +9,8 @@ const nextConfig: NextConfig = { 'bsmhubsp.insert.team', ], }, - output: 'standalone', + // output: 'standalone', + productionBrowserSourceMaps: true, }; export default nextConfig; diff --git a/src/app/components/layout/Tabs.tsx b/src/app/components/layout/Tabs.tsx index bb4c2331..3fefd478 100644 --- a/src/app/components/layout/Tabs.tsx +++ b/src/app/components/layout/Tabs.tsx @@ -28,7 +28,7 @@ const Tabs = ({ tabs }: TabsProps) => { return ( { const { isOpen, modalContent, closeModal } = useModal(); diff --git a/src/app/components/modal/inputs/SingleInput.tsx b/src/app/components/modal/inputs/SingleInput.tsx index d967735b..d8c7d692 100644 --- a/src/app/components/modal/inputs/SingleInput.tsx +++ b/src/app/components/modal/inputs/SingleInput.tsx @@ -51,7 +51,9 @@ function Inputs({ `} /> {icon && iconMap[icon] && ( - + )} ); diff --git a/src/app/components/modal/inputs/SkillTag.tsx b/src/app/components/modal/inputs/SkillTag.tsx index fdc754bf..584aa3a9 100644 --- a/src/app/components/modal/inputs/SkillTag.tsx +++ b/src/app/components/modal/inputs/SkillTag.tsx @@ -3,7 +3,7 @@ // import { IconX } from "@tabler/icons-react"; // import { useRef, useEffect } from "react"; // // import AutosizeInput from 'react-input-autosize'; -// import './common/common.css'; +import './common/common.css'; // type WriteProps = { // mode: 'write'; diff --git a/src/app/editor/page.tsx b/src/app/editor/page.tsx index da3985ec..4d6b1ff9 100644 --- a/src/app/editor/page.tsx +++ b/src/app/editor/page.tsx @@ -12,5 +12,9 @@ export default function Editor() { .catch((err) => console.error('Failed to load rich text editor:', err)); }, []); - return
{RichTextEditorComponent && }
; + return ( +
+ {RichTextEditorComponent && } +
+ ); } diff --git a/src/app/globals.css b/src/app/globals.css index bc0189a6..ad727c77 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -26,6 +26,7 @@ body { font-family: 'Threat'; font-style: normal; font-weight: 400; + font-display: swap; src: local('Threat'), url('https://fonts.cdnfonts.com/s/95719/Threat-2OAeX.woff') format('woff'); @@ -35,6 +36,7 @@ body { font-family: 'Material Symbols Outlined'; font-style: normal; font-weight: 400; + font-display: swap; src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v215/kJF1BvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzByHX9rA6RzaxHMPdY43zj-jCxv3fzvRNU22ZXGJpEpjC_1v-p_4MrImHCIJIZrDCvHOejbd5zrDAt.woff2) format('woff2'); } @@ -59,6 +61,7 @@ body { font-family: 'Material Icons'; font-style: normal; font-weight: 400; + font-display: swap; src: url(https://fonts.gstatic.com/s/materialicons/v142/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2'); } @@ -101,5 +104,5 @@ input[type="search"]::-webkit-search-results-decoration { } html { - font-size: 12px; + font-size: 16px; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8fc53ddf..dd503b9a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,6 @@ import Footer from './components/layout/Footer'; import './globals.css'; import './responsive.css'; -import '@components/modal/inputs/common/common.css'; -import '@components/modal/modal.css'; import Header from '@components/layout/Header'; import { ModalProvider, Modal } from '@components/modal'; diff --git a/src/app/page.tsx b/src/app/page.tsx index 1936eb67..cfeae1ea 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,12 +2,11 @@ import { useEffect } from 'react'; // import Detail from '@components/detail/Detail'; -import Buttons from '@components/modal/inputs/Buttons'; -import Inputs from '@components/modal/inputs/SingleInput'; -import PictureUpload from '@components/modal/inputs/PictureUpload'; -import LabelInputs from '@components/modal/inputs/LabelOfInputs'; import InputListProvider from '@components/modal/inputs/InputListProvider'; -import InputOfModal from '@components/modal/inputs/InputOfModal'; +import dynamic from 'next/dynamic'; +const InputOfModal = dynamic( + () => import('@components/modal/inputs/InputOfModal'), +); import { FormConfig } from '@components/modal/inputs/types/inputTypes'; import { useModal } from '@components/modal'; import Checkbox from './components/modal/inputs/Checkbox'; @@ -104,33 +103,6 @@ export default function Home() { return (
- {/* ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ */} -
-

๊ฐœ๋ณ„ ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ

- - - - - - - - - - - -
{/* React Hook Form ํ†ตํ•ฉ ํผ */}
diff --git a/src/app/portfolio/Portfolio.tsx b/src/app/portfolio/Portfolio.tsx index 64840266..007cedc7 100644 --- a/src/app/portfolio/Portfolio.tsx +++ b/src/app/portfolio/Portfolio.tsx @@ -1,4 +1,3 @@ -import Image from 'next/image'; import { Body, TitleEN } from '../components/system/text'; import Tabs from '../components/layout/Tabs'; @@ -23,12 +22,17 @@ const Portfolio = async ({ profileName, path = 'home' }: PortfolioProps) => { const uuid = profile.profile_id; const studentInfo = profile.profile_permission[0].student; - const profileDetail = await getProfileDetail(profileName); - const cooperationProjects = await getCooperationProjects(uuid); - const personalProjects = (await getPersonalProjects(uuid)).map((project) => ({ + const [profileDetail, cooperationProjects, personalProjectsResult] = + await Promise.all([ + getProfileDetail(profileName), + getCooperationProjects(uuid), + getPersonalProjects(uuid), + ]); + + const personalProjects = personalProjectsResult.map((project) => ({ ...project, authors: [ - { profileImage: convertFromDatabaseImageURL(profile.profile_image) }, // getPersonalProjects ํ•จ์ˆ˜์—์„œ Join์œผ๋กœ ๊ฐ€์ ธ์˜ค์ง€ ์•Š์€ ๊ฐœ์ธ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ + { profileImage: convertFromDatabaseImageURL(profile.profile_image) }, ], })); @@ -58,12 +62,13 @@ const Portfolio = async ({ profileName, path = 'home' }: PortfolioProps) => { return (
- ํ”„๋กœํ•„ ์‚ฌ์ง„ {profile.profile_name}
diff --git a/src/app/portfolio/SearchTab.tsx b/src/app/portfolio/SearchTab.tsx index 508967cb..f5671839 100644 --- a/src/app/portfolio/SearchTab.tsx +++ b/src/app/portfolio/SearchTab.tsx @@ -2,7 +2,10 @@ import Link from 'next/link'; import { useState } from 'react'; -import PortfolioCard from '../components/card/portfolio/PortfolioCard'; +import dynamic from 'next/dynamic'; +const PortfolioCard = dynamic( + () => import('../components/card/portfolio/PortfolioCard'), +); import { PortfolioData } from './types'; import Inputs from '../components/modal/inputs/SingleInput'; import Checkbox from '../components/modal/inputs/Checkbox'; @@ -143,6 +146,7 @@ export default function SearchTab({ diff --git a/src/app/portfolio/[profileName]/getPortfolioParams.ts b/src/app/portfolio/[profileName]/getPortfolioParams.ts new file mode 100644 index 00000000..dfb242eb --- /dev/null +++ b/src/app/portfolio/[profileName]/getPortfolioParams.ts @@ -0,0 +1,8 @@ +import { getPersonalPortfolioData } from '@/services/server/portfolio/getPersonalPortfolioData'; + +export async function getPortfolioParams() { + const portfolioData = await getPersonalPortfolioData(); + return portfolioData.map((data) => ({ + profileName: encodeURIComponent(data.profile.name), + })); +} \ No newline at end of file diff --git a/src/app/portfolio/[profileName]/page.tsx b/src/app/portfolio/[profileName]/page.tsx index 7c9e0288..80fdbf6d 100644 --- a/src/app/portfolio/[profileName]/page.tsx +++ b/src/app/portfolio/[profileName]/page.tsx @@ -1,4 +1,9 @@ import Portfolio from '../Portfolio'; +import { getPortfolioParams } from './getPortfolioParams'; + +export async function generateStaticParams() { + return getPortfolioParams(); +} interface PortfolioProps { params: Promise<{ profileName: string }>; diff --git a/src/app/sitemap.xml/sitemap.ts b/src/app/sitemap.xml/sitemap.ts index 7a024f33..2d3c640a 100644 --- a/src/app/sitemap.xml/sitemap.ts +++ b/src/app/sitemap.xml/sitemap.ts @@ -1,115 +1,23 @@ -import fs from 'fs'; -import path from 'path'; - +import { getPortfolioParams } from '../portfolio/[profileName]/getPortfolioParams'; import type { MetadataRoute } from 'next'; const siteConfig = { - url: process.env.SITE_URL || 'https://example.com', + url: process.env.SITE_URL || 'https://bsmhub.vercel.app', }; -const exclusiveRoutes = ['/auth*']; - -// Check if a route should be excluded based on exclusiveRoutes patterns -function shouldExcludeRoute(route: string): boolean { - return exclusiveRoutes.some((pattern) => { - if (pattern.includes('*')) { - // Convert wildcard pattern to regex - const regexPattern = pattern.replace(/\\/g, '\\\\').replace(/\*/g, '.*').replace(/\//g, '\\/'); - const regex = new RegExp(`^${regexPattern}$`); - return regex.test(route); - } - return route === pattern; - }); -} - -// Recursively collect all pages with `page.tsx` or `page.jsx` -export async function getStaticRoutes( - dir = 'src/app', // Updated default directory to match the actual structure - parentPath = '', -): Promise { - const currentDir = path.join(process.cwd(), dir); - const entries = fs.readdirSync(currentDir, { withFileTypes: true }); - - let routes: string[] = []; - - for (const entry of entries) { - const fullPath = path.join(currentDir, entry.name); - - if (entry.isDirectory()) { - const routePath = path.join(parentPath, entry.name); - - // Collect possible page file paths - const possiblePageFiles = ['page.tsx', 'page.jsx'].map((file) => - path.join(fullPath, file), - ); - // Check if any of the possible page files exist - const hasPage = possiblePageFiles.some((filePath) => - fs.existsSync(filePath), - ); - - if (hasPage) { - routes.push(`/${routePath}`); - } - - // Continue scanning nested folders recursively - const nestedRoutes = await getStaticRoutes( - path.join(dir, entry.name), - routePath, - ); - - // Excludes routes with dynamic segments (e.g., [slug]) - const nestedStaticRoutes = nestedRoutes.filter( - (route) => !route.match(/\[.+\]/), - ); - - routes = routes.concat(nestedStaticRoutes); - } - } - - return parentPath === '' ? ['/', ...routes] : routes; -} - -export interface StaticParam { - [key: string]: string; -} - -// Get dynamic routes by calling `generateStaticParams` from dynamic pages -async function getDynamicRoutes( - subpath: string, - dynamicSegment: string, -): Promise { - const filePath = path.join(process.cwd(), 'src/app', subpath); - try { - const staticParamsGenerator = ( - await import( - path.join(filePath, `[${dynamicSegment}]`, 'staticParamsGenerator') - ) - ).default; - const params = (await staticParamsGenerator()) as string[]; - return params.map((route) => `/${subpath}/${route}`); - } catch (error) { - console.error('Error loading dynamic routes:', error); - return []; // Return empty array on error - } -} - -export async function getAllRoutes(): Promise { - return Promise.all([ - getStaticRoutes(), - getDynamicRoutes('portfolio', 'profileName'), - ]) - .then((results) => results.flat()) - .then((allRoutes) => - allRoutes.filter((route) => !shouldExcludeRoute(route)), - ); -} - export default async function sitemap(): Promise { - const allRoutes = await getAllRoutes(); + const staticRoutes = ['/', '/editor', '/portfolio']; - return allRoutes.map((route) => ({ - url: encodeURI(`${siteConfig.url}${route}`), + const portfolioParams = await getPortfolioParams(); + const portfolioRoutes = portfolioParams.map((param) => ({ + url: `${siteConfig.url}/portfolio/${param.profileName}`, lastModified: new Date().toISOString(), - priority: route === '/' ? 1 : 0.8, })); -} + + const routes = staticRoutes.map((route) => ({ + url: `${siteConfig.url}${route}`, + lastModified: new Date().toISOString(), + })); + + return [...routes, ...portfolioRoutes]; +} \ No newline at end of file diff --git a/src/services/server/profile/getProfileDetail.ts b/src/services/server/profile/getProfileDetail.ts index b1c22a6d..bb72dbe3 100644 --- a/src/services/server/profile/getProfileDetail.ts +++ b/src/services/server/profile/getProfileDetail.ts @@ -30,7 +30,7 @@ export const getProfileDetail = async (profileName: string): Promise ({ + datas: (data?.profile_permission?.[0]?.student.student_certificates ?? []).map((item) => ({ value: item.certificates.certificate_name })) }, diff --git a/tailwind.config.ts b/tailwind.config.ts index 488a8654..ae972cfd 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -57,8 +57,8 @@ const config: Config = { 'light-gray-footer-bg': '#F5F5F5', 'light-gray': '#EAEAEC', 'gray-footer': '#51515C', - 'placeholder-gray': '#858587', - 'gray-base': '#5E5E5E', + 'placeholder-gray': '#757575', + 'gray-base': '#4A4A4A', black: '#131313', 'blue-primary': '#1462FF', 'red-primary': '#FD462D',