Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 49 additions & 35 deletions apps/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,45 @@
import bundleAnalyzer from "@next/bundle-analyzer";
import { withSentryConfig } from "@sentry/nextjs";

const shouldRunBundleAnalyzer = process.env.ANALYZE === "true";
const svgComponentLoaders = ["@svgr/webpack"];

const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === "true",
enabled: shouldRunBundleAnalyzer,
});

const imageRemotePatterns = [
"k.kakaocdn.net",
"cdn.default.solid-connection.com",
"cdn.upload.solid-connection.com",
].map((hostname) => ({
protocol: "https",
hostname,
}));

/** @type {import('next').NextConfig} */
const nextConfig = {
swcMinify: true,
transpilePackages: ["@solid-connect/ai-inspector"],
turbopack: {
rules: {
"*.svg": {
loaders: svgComponentLoaders,
as: "*.js",
},
},
},
images: {
unoptimized: true,
domains: ["k.kakaocdn.net", "cdn.default.solid-connection.com", "cdn.upload.solid-connection.com"],
remotePatterns: imageRemotePatterns,
formats: ["image/avif", "image/webp"],
deviceSizes: [360, 640, 768, 1024, 1280],
},
// 폰트 최적화 설정
optimizeFonts: true,
// 압축 활성화
compress: true,
// 정적 리소스 최적화
experimental: {
optimizeCss: true,
gzipSize: true,
// Sentry instrumentation 활성화 (Web Vitals 수집에 필요)
instrumentationHook: true,
optimizePackageImports: [
"lucide-react",
"@radix-ui/react-select",
Expand All @@ -41,39 +56,38 @@ const nextConfig = {
"@hookform/resolvers",
],
},
eslint: {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
webpack: (config) => {
// CSS 최적화 - ensure nested objects exist
if (!config.optimization) {
config.optimization = {};
}
if (!config.optimization.splitChunks) {
config.optimization.splitChunks = {};
}
if (!config.optimization.splitChunks.cacheGroups) {
config.optimization.splitChunks.cacheGroups = {};
}
...(shouldRunBundleAnalyzer
? {
webpack: (config) => {
// Bundle analyzer still runs through webpack because it is webpack-plugin based.
if (!config.optimization) {
config.optimization = {};
}
if (!config.optimization.splitChunks) {
config.optimization.splitChunks = {};
}
if (!config.optimization.splitChunks.cacheGroups) {
config.optimization.splitChunks.cacheGroups = {};
}

config.optimization.splitChunks.cacheGroups.styles = {
name: "styles",
test: /\.(css|scss)$/,
chunks: "all",
enforce: true,
};
config.optimization.splitChunks.cacheGroups.styles = {
name: "styles",
test: /\.(css|scss)$/,
chunks: "all",
enforce: true,
};

config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
});
return config;
},
config.module.rules.push({
test: /\.svg$/,
use: svgComponentLoaders,
});
return config;
},
}
: {}),
};

export default withSentryConfig(
Expand Down
14 changes: 7 additions & 7 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
"typecheck": "tsc --noEmit",
"typecheck:ci": "tsc --noEmit -p tsconfig.ci.json",
"ci:check": "pnpm run lint:check && pnpm run typecheck:ci",
"analyze": "ANALYZE=true next build"
"analyze": "ANALYZE=true next build --webpack"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@next/third-parties": "^14.2.4",
"@next/third-parties": "^16.2.6",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-progress": "^1.1.2",
Expand All @@ -37,10 +37,10 @@
"linkify-react": "^4.3.2",
"linkifyjs": "^4.3.2",
"lucide-react": "^0.479.0",
"next": "^14.2.35",
"next": "^16.2.6",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 웹팩 기반 SVG 로더를 dev 실행에도 강제하세요

Next를 ^16.2.6로 올리면 next dev의 기본 번들러가 Turbopack으로 바뀌는데, 현재 설정은 build--webpack으로 고정되어 있어 개발 서버에서는 next.config.mjswebpack()(여기서 @svgr/webpack을 등록) 구성이 적용되지 않습니다. 이 저장소는 public/svgs/*.svg를 React 컴포넌트로 광범위하게 import하므로, pnpm dev에서 SVG import가 깨지거나 런타임 렌더링 오류가 발생할 수 있습니다.

Useful? React with 👍 / 👎.

"next-render-analyzer": "^0.1.2",
"react": "^18",
"react-dom": "^18",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-hook-form": "^7.60.0",
"react-hot-toast": "^2.6.0",
"sockjs-client": "^1.6.1",
Expand All @@ -50,10 +50,10 @@
"zustand": "^5.0.7"
},
"devDependencies": {
"@next/bundle-analyzer": "^16.1.6",
"@next/bundle-analyzer": "^16.2.6",
"@svgr/webpack": "^8.1.0",
"@types/node": "^20.11.19",
"@types/react": "19.2.10",
"@types/react": "19.2.15",
"@types/react-dom": "19.2.3",
"autoprefixer": "^10.4.20",
"critters": "^0.0.23",
Expand Down
45 changes: 0 additions & 45 deletions apps/web/sentry.client.config.ts

This file was deleted.

13 changes: 1 addition & 12 deletions apps/web/src/app/(home)/_ui/PopularUniversitySection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import dynamic from "next/dynamic";
import { Suspense } from "react";
import type { ListUniversity } from "@/types/university";
import PopularUniversityCard from "./_ui/PopularUniversityCard";

// PopularUniversityCard를 동적 임포트
const PopularUniversityCardDynamic = dynamic(() => import("./_ui/PopularUniversityCard"), {
ssr: false,
loading: () => (
<div className="relative w-[153px]">
<div className="h-[120px] w-[153px] animate-pulse rounded-lg bg-gray-200" />
</div>
),
});

type PopularUniversitySectionProps = {
universities: ListUniversity[];
};
Expand Down Expand Up @@ -46,7 +35,7 @@ const PopularUniversitySection = ({ universities }: PopularUniversitySectionProp
</div>
}
>
<PopularUniversityCardDynamic
<PopularUniversityCard
university={university}
priority={false}
loading="lazy"
Expand Down
10 changes: 2 additions & 8 deletions apps/web/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import type { Metadata } from "next";
import nextDynamic from "next/dynamic";
import { getHomeNewsList } from "@/apis/news/server/getNewsList";
import { getCategorizedUniversities, getRecommendedUniversity } from "@/apis/universities/server";
import { type ListUniversity, RegionEnumExtend } from "@/types/university";
import FindLastYearScoreBar from "./_ui/FindLastYearScoreBar";
import HomeEntrySection from "./_ui/HomeEntrySection";
import NewsSectionSkeleton from "./_ui/NewsSection/skeleton";
import NewsSection from "./_ui/NewsSection";
import PopularUniversitySection from "./_ui/PopularUniversitySection";
import UniversityList from "./_ui/UniversityList";

const NewsSectionDynamic = nextDynamic(() => import("./_ui/NewsSection"), {
ssr: false,
loading: () => <NewsSectionSkeleton />,
});

const baseUrl = process.env.NEXT_PUBLIC_WEB_URL || "https://solid-connection.com";
const ogImageUrl = `${baseUrl}/opengraph-image.png`;
const homeMetaTitle = "교환학생 사이트 | 솔리드 커넥션 – 교환학생 커뮤니티, 플랫폼";
Expand Down Expand Up @@ -106,7 +100,7 @@ const HomePage = async () => {
<UniversityList allRegionsUniversityList={allRegionsUniversityList} />
</div>

<NewsSectionDynamic newsList={newsList} />
<NewsSection newsList={newsList} />
</div>
</>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async function POST(request: NextRequest) {
// boardCode가 있으면 해당 커뮤니티 페이지 revalidate
if (boardCode) {
revalidatePath(`/community/${boardCode}`);
revalidateTag(`posts-${boardCode}`);
revalidateTag(`posts-${boardCode}`, { expire: 0 });

return NextResponse.json({
revalidated: true,
Expand All @@ -78,7 +78,7 @@ async function POST(request: NextRequest) {

// 특정 태그 revalidate
if (tag) {
revalidateTag(tag);
revalidateTag(tag, { expire: 0 });
return NextResponse.json({
revalidated: true,
message: `Tag ${tag} revalidated`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import TopDetailNavigation from "@/components/layout/TopDetailNavigation";
import PostModifyContent from "./PostModifyContent";

interface PostModifyPageProps {
params: {
params: Promise<{
boardCode: string;
postId: string;
};
}>;
}

export const metadata: Metadata = {
title: "글 수정",
};

const PostModifyPage = ({ params }: PostModifyPageProps) => {
const { boardCode, postId } = params;
const PostModifyPage = async ({ params }: PostModifyPageProps) => {
const { boardCode, postId } = await params;

return (
<>
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/app/community/[boardCode]/[postId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import type { Metadata } from "next";
import PostPageContent from "./PostPageContent";

interface PostPageProps {
params: {
params: Promise<{
boardCode: string;
postId: string;
};
}>;
}

export const metadata: Metadata = {
title: "게시글",
};

const PostPage = ({ params }: PostPageProps) => {
const { boardCode } = params;
const postId = Number(params.postId);
const PostPage = async ({ params }: PostPageProps) => {
const { boardCode, postId: postIdParam } = await params;
const postId = Number(postIdParam);
Comment on lines +17 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

1) 숫자 파라미터 검증이 빠져서 NaN이 그대로 내려갈 수 있어요.

- Line 18에서 `Number(postIdParam)`만 수행하고 검증이 없습니다.
- 잘못된 URL 입력 시 `PostPageContent`로 `NaN` 전달되어 500/잘못된 요청으로 이어질 수 있습니다.
수정 예시
 import type { Metadata } from "next";
+import { notFound } from "next/navigation";
@@
 const PostPage = async ({ params }: PostPageProps) => {
   const { boardCode, postId: postIdParam } = await params;
   const postId = Number(postIdParam);
+  if (Number.isNaN(postId)) notFound();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { boardCode, postId: postIdParam } = await params;
const postId = Number(postIdParam);
import type { Metadata } from "next";
import { notFound } from "next/navigation";
const PostPage = async ({ params }: PostPageProps) => {
const { boardCode, postId: postIdParam } = await params;
const postId = Number(postIdParam);
if (Number.isNaN(postId)) notFound();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/community/`[boardCode]/[postId]/page.tsx around lines 17 -
18, params.postIdParam을 Number로 변환만 하고 검증하지 않아 postId가 NaN으로 PostPageContent에
전달될 수 있으니, params와 postIdParam을 사용해 변환 후 Number.isInteger/!Number.isNaN 검사로 유효성
확인하고 유효하지 않을 경우 PostPageContent로 NaN을 넘기지 말고 적절히 처리(예: next/navigation의
notFound() 호출 또는 400 응답/리다이렉트)하도록 변경하세요; 수정 대상 식별자: params, postIdParam, postId,
PostPageContent.


return (
<div className="w-full">
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/community/[boardCode]/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export const metadata = {
title: "글쓰기",
};

const PostCreatePage = ({ params }: { params: { boardCode: string } }) => {
const { boardCode } = params;
const PostCreatePage = async ({ params }: { params: Promise<{ boardCode: string }> }) => {
const { boardCode } = await params;
return (
<div className="w-full">
<PostForm boardCode={boardCode} />
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/app/community/[boardCode]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ export const metadata: Metadata = {
};

interface CommunityPageProps {
params: {
params: Promise<{
boardCode: string;
};
}>;
}

const CommunityPage = ({ params }: CommunityPageProps) => {
const { boardCode } = params;
const CommunityPage = async ({ params }: CommunityPageProps) => {
const { boardCode } = await params;

return (
<div className="w-full">
Expand Down
Binary file modified apps/web/src/app/favicon.ico
Binary file not shown.
11 changes: 4 additions & 7 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type { Metadata, Viewport } from "next";
import dynamic from "next/dynamic";
import localFont from "next/font/local";
import type { ReactNode } from "react";
import { Toaster } from "react-hot-toast";

import GlobalLayout from "@/components/layout/GlobalLayout";
import ReissueProvider from "@/components/layout/ReissueProvider";

import QueryProvider from "@/lib/react-query/QueryProvider";
import AppleScriptLoader from "@/lib/ScriptLoader/AppleScriptLoader";
import "@/styles/globals.css";
import { GoogleAnalytics } from "@next/third-parties/google";
import { SpeedInsights } from "@vercel/speed-insights/next";

const siteUrl = process.env.NEXT_PUBLIC_WEB_URL || "https://solid-connection.com";

export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: "솔리드 커넥션",
description: "솔리드 커넥션. 교환학생의 첫 걸음",
verification: {
Expand All @@ -39,11 +41,6 @@ const pretendard = localFont({
],
});

const AppleScriptLoader = dynamic(() => import("@/lib/ScriptLoader/AppleScriptLoader"), {
ssr: false,
loading: () => null,
});

declare global {
interface Window {
Kakao: {
Expand Down
Loading
Loading