Skip to content
Open
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
173 changes: 110 additions & 63 deletions src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,52 @@ const NewsSectionDynamic = dynamic(() => import("./_ui/NewsSection"), {
loading: () => <NewsSectionSkeleton />,
});

const baseUrl = process.env.NEXT_PUBLIC_WEB_URL || "https://solid-connection.com";
const ogImageUrl = `${baseUrl}/opengraph-image.png`;

Comment on lines +20 to +22
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the relevant file to see current state
head -n 75 src/app/(home)/page.tsx | tail -n +15

Repository: solid-connection/solid-connect-web

Length of output: 213


🏁 Script executed:

# Search for NEXT_PUBLIC_WEB_URL usage across the codebase
rg "NEXT_PUBLIC_WEB_URL" --type ts --type tsx

Repository: solid-connection/solid-connect-web

Length of output: 105


🏁 Script executed:

# Search for environment files or configuration
fd -e env -e "env.*" | head -20

Repository: solid-connection/solid-connect-web

Length of output: 60


🏁 Script executed:

# Check for URL construction patterns (canonical, OG) in the file
rg "canonical|og|openGraph" src/app/\(home\)/page.tsx -A 2 -B 2

Repository: solid-connection/solid-connect-web

Length of output: 724


🏁 Script executed:

# Search for baseUrl usage throughout the file
rg "baseUrl" 'src/app/(home)/page.tsx' -n

Repository: solid-connection/solid-connect-web

Length of output: 376


🏁 Script executed:

# Check environment configuration files
find . -type f \( -name ".env*" -o -name "*.config.*" \) | grep -v node_modules | head -20

Repository: solid-connection/solid-connect-web

Length of output: 256


🏁 Script executed:

# Search for where NEXT_PUBLIC_WEB_URL might be set
rg "NEXT_PUBLIC_WEB_URL" . --type ts --type js -B 2 -A 2

Repository: solid-connection/solid-connect-web

Length of output: 984


🏁 Script executed:

# Check if there's any URL normalization utility already in use
rg "replace.*/$|removeTrailing|normaliz.*url" . --type ts --type js

Repository: solid-connection/solid-connect-web

Length of output: 60


🏁 Script executed:

# Read environment files to check for trailing slashes in NEXT_PUBLIC_WEB_URL
cat .env .env.development .env.production 2>/dev/null | grep NEXT_PUBLIC_WEB_URL

Repository: solid-connection/solid-connect-web

Length of output: 233


🏁 Script executed:

# Check for any other direct process.env.NEXT_PUBLIC_WEB_URL usage that might have same issue
rg "process\.env\.NEXT_PUBLIC_WEB_URL" . --type ts --type js -B 1 -A 1

Repository: solid-connection/solid-connect-web

Length of output: 712


🏁 Script executed:

# Verify the exact canonical construction to see if double slash would actually occur
head -n 35 'src/app/(home)/page.tsx' | tail -n +20

Repository: solid-connection/solid-connect-web

Length of output: 672


baseUrl 설정 시 후행 슬래시로 인한 canonical/OG URL 중복 방지

환경 변수로 전달되는 NEXT_PUBLIC_WEB_URL이 후행 슬래시로 끝나는 경우를 대비해야 합니다:

  1. 후행 슬래시 제거
    ${baseUrl}/ 패턴에서 NEXT_PUBLIC_WEB_URL="https://example.com/"이면 https://example.com//이 되어 URL 정규화를 깨뜨립니다.

  2. 영향 범위

    • Line 28: canonical URL
    • Line 34: openGraph.url
    • Line 60: JSON-LD url
    • Line 21: ogImageUrl
      모두 동일한 위험을 가집니다.
-const baseUrl = process.env.NEXT_PUBLIC_WEB_URL || "https://solid-connection.com";
+const baseUrl = (process.env.NEXT_PUBLIC_WEB_URL ?? "https://solid-connection.com").replace(/\/+$/, "");
 const ogImageUrl = `${baseUrl}/opengraph-image.png`;

참고: authUtils.ts에서도 직접 process.env.NEXT_PUBLIC_WEB_URL을 사용하므로 동일한 처리가 필요합니다.

📝 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 baseUrl = process.env.NEXT_PUBLIC_WEB_URL || "https://solid-connection.com";
const ogImageUrl = `${baseUrl}/opengraph-image.png`;
const baseUrl = (process.env.NEXT_PUBLIC_WEB_URL ?? "https://solid-connection.com").replace(/\/+$/, "");
const ogImageUrl = `${baseUrl}/opengraph-image.png`;
🤖 Prompt for AI Agents
In src/app/(home)/page.tsx around lines 20 to 22, baseUrl is taken directly from
NEXT_PUBLIC_WEB_URL which can end with a trailing slash and cause double-slashes
in constructed URLs (canonical, openGraph.url, JSON-LD url, ogImageUrl); trim
any trailing slash from the environment value before using it (e.g., normalize
process.env.NEXT_PUBLIC_WEB_URL by removing a trailing '/' unless the value is
just '/' or empty) and use that normalized baseUrl to build
canonical/OG/JSON-LD/ogImageUrl; apply the same normalization in authUtils.ts
where NEXT_PUBLIC_WEB_URL is used.

export const metadata: Metadata = {
title: "솔리드 커넥션 – 교환학생의 첫 걸음",
description: "교환학생 준비를 위한 모든 정보가 여기에!",
title: "교환학생 사이트 | 솔리드 커넥션 – 교환학생 커뮤니티",
description:
"교환학생 사이트 솔리드커넥션. 교환학생 커뮤니티에서 학교 검색, 성적 입력, 지원 현황 확인까지 한 번에. 교환학생 준비를 위한 모든 정보를 제공합니다.",
alternates: {
canonical: "https://solid-connection.com/",
canonical: `${baseUrl}/`,
},
openGraph: {
title: "교환학생 사이트 | 솔리드 커넥션 – 교환학생 커뮤니티",
description:
"교환학생 사이트 솔리드커넥션. 교환학생 커뮤니티에서 학교 검색, 성적 입력, 지원 현황 확인까지 한 번에. 교환학생 준비를 위한 모든 정보를 제공합니다.",
url: `${baseUrl}/`,
siteName: "솔리드커넥션",
locale: "ko_KR",
type: "website",
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: "솔리드 커넥션 - 교환학생 커뮤니티",
},
],
},
twitter: {
card: "summary_large_image",
title: "교환학생 사이트 | 솔리드 커넥션 – 교환학생 커뮤니티",
description: "교환학생 사이트 솔리드커넥션. 교환학생 커뮤니티에서 학교 검색, 성적 입력, 지원 현황 확인까지.",
images: [ogImageUrl],
},
};

// Structured Data (JSON-LD) for SEO - 교환학생 사이트, 커뮤니티 키워드 강화
const structuredData = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "솔리드커넥션",
url: `${baseUrl}/`,
description: "교환학생 학교 검색, 성적 입력, 지원 현황 확인까지 가능한 교환학생 플랫폼.",
potentialAction: {
"@type": "SearchAction",
target: `${baseUrl}/university?searchText={search_term_string}`,
"query-input": "required name=search_term_string",
},
};

Expand All @@ -33,71 +74,77 @@ const HomePage = async () => {
const allRegionsUniversityList = await getCategorizedUniversities();

return (
<div className="w-full">
<FindLastYearScoreBar />
<div className="flex flex-col gap-2.5 px-5 py-3.5">
<div className="flex gap-2">
<Link
className="h-26 bg-bg-accent-blue flex flex-1 flex-col gap-2 rounded-lg p-2.5"
href="/university/search"
>
<div className="flex flex-col">
<span className="typo-bold-5 text-secondary">학교 검색하기</span>
<span className="typo-medium-4 text-k-700">모든 학교 목록을 확인해보세요</span>
</div>
<div className="flex justify-end">
<IconMagnifyingGlass />
</div>
</Link>
<Link className="h-26 bg-bg-accent-sky flex flex-1 flex-col gap-2 rounded-lg p-2.5" href="/university/score">
<div className="flex flex-col">
<span className="typo-bold-5 text-sub-a">성적 입력하기</span>
<span className="typo-medium-4 text-k-700">성적을 입력해보세요</span>
</div>
<div className="flex justify-end">
<IconPaper />
</div>
</Link>
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
<div className="w-full">
Comment on lines +77 to +79
Copy link

@coderabbitai coderabbitai bot Dec 12, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

4) dangerouslySetInnerHTML는 린트 에러 + 잠재 XSS 포인트라 교체가 필요해요.

  • 현재는 상수 기반이라 “즉시 취약”까지는 아닐 수 있지만, 린트가 에러로 잡고 있어 머지 블로커에 가까워요.
  • next/script로 바꾸면 의도도 명확해지고 규칙도 피할 수 있어요(그리고 < escaping도 같이 해두면 더 단단해요).
 import { Metadata } from "next";
 import dynamic from "next/dynamic";
 import Link from "next/link";
+import Script from "next/script";
@@
 const structuredData = {
   "@context": "https://schema.org",
   "@type": "WebSite",
@@
 };
+
+const structuredDataJson = JSON.stringify(structuredData).replace(/</g, "\\u003c");
@@
   return (
     <>
-      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
+      <Script id="home-structured-data" type="application/ld+json" strategy="beforeInteractive">
+        {structuredDataJson}
+      </Script>
       <div className="w-full">

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ast-grep (0.40.0)

[warning] 77-77: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
In src/app/(home)/page.tsx around lines 77 to 79, replace the raw <script
dangerouslySetInnerHTML> with Next's Script component and ensure the JSON-LD
string has '<' escaped to avoid XSS lint errors; import Script from
'next/script', then render <Script id="structured-data"
type="application/ld+json" dangerouslySetInnerHTML={{ __html:
JSON.stringify(structuredData).replace(/</g, '\\u003c') }} /> so the linter no
longer flags the pattern and the payload is safer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

rsc라서 서버 작업이고 상수인데 xss 공격 위험이 있습니까?

서버데이터는 안전하게 보관되고 있다는 가정하입니다

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

Next.js 15 JSON-LD structured data best practices Script component

💡 Result:

Here are concise Next.js 15 best practices for adding JSON‑LD (structured data) using script tags:

  1. Render JSON‑LD as a <script type="application/ld+json"> inside your page or layout (server component) — Next.js recommends this. [1][2]

  2. Sanitize/escape before inlining to avoid XSS — e.g. JSON.stringify(jsonLd).replace(/</g, '\u003c') or use a safe serializer (serialize-javascript) per your security policy. Example:

    <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/. [1]
  3. Placement:

    • Put JSON‑LD in the layout/page that matches the content (head or top of page) so server-rendered HTML includes it for crawlers. Avoid duplicating the same schema across layouts/pages. [1][2]
  4. When to use next/script:

    • Prefer plain <script type="application/ld+json"> for static/server-known JSON‑LD.
    • If you must inject dynamic JSON‑LD client-side, use next/script with strategy="beforeInteractive" (or push to dataLayer) to ensure it appears correctly and avoid hydration issues — but use this only when JSON‑LD cannot be produced server-side. [3]
  5. Content correctness & scope:

    • Use the most specific Schema.org type, keep values accurate and visible on the page, include required/recommended properties, and don’t mark up content that isn’t present (avoids search penalties). [4]
  6. Types & tooling:

    • Use TypeScript typing libs (e.g., schema-dts) to model JSON‑LD objects and validate during dev. Validate final output with Google Rich Results Test / Schema Markup Validator and monitor Search Console. [1][4]
  7. Avoid common pitfalls:

    • Duplicate scripts (check layouts and components), invalid JSON syntax, mismatched server/client JSON (causes hydration diffs), and leaking unsanitized user content into JSON‑LD. [1][4]

Sources:

  • Next.js JSON‑LD guide and metadata docs. [1][2]
  • Community guidance on using next/script for JSON‑LD (strategy beforeInteractive). [3]
  • JSON‑LD best practices / validation guidance. [4]

@manNomi 맞습니다. 귀하의 평가가 정확해요.

1. RSC + 상수 데이터 → XSS 위험 거의 없음

  • 서버 컴포넌트에서 하드코딩된 상수만 사용하고 있고, 사용자 입력이 전혀 들어가지 않기 때문에 실질적인 XSS 공격 벡터가 없습니다.
  • baseUrl도 환경 변수(빌드 타임)라서 안전합니다.

2. 린트 경고의 성격

  • 린트 도구는 패턴 기반으로 dangerouslySetInnerHTML을 발견하면 맥락을 파악하지 못하고 무조건 경고를 띄웁니다.
  • 실제 위험이 아니라 "잠재적으로 위험할 수 있는 패턴"을 감지한 것입니다.

3. Next.js 권장사항

  • 웹 검색 결과에 따르면, Next.js는 정적 JSON-LD의 경우 RSC에서 일반 <script type="application/ld+json">을 사용하는 것을 권장합니다.
  • 다만 방어적 차원에서 JSON.stringify(structuredData).replace(/</g, '\\u003c') 같은 이스케이핑을 "best practice"로 제안하고 있지만, 현재 코드처럼 상수만 사용하는 경우엔 필수는 아닙니다.

현재 코드가 안전하다고 판단하신다면 그대로 유지하셔도 괜찮습니다. 린트 경고를 해소하고 싶으시면 간단한 < 이스케이핑만 추가하시면 됩니다. 선택은 귀하께 맡기겠습니다! 😊


🧠 Learnings used
Learnt from: manNomi
Repo: solid-connection/solid-connect-web PR: 242
File: src/components/ui/TabSelector.tsx:10-11
Timestamp: 2025-08-12T09:41:44.182Z
Learning: manNomi prefers to keep reusable UI components simple and focused on core functionality rather than adding many features. They don't want to over-engineer flexible/reusable UI components at the initial stage.

Learnt from: manNomi
Repo: solid-connection/solid-connect-web PR: 245
File: src/components/mentor/MentorChatCard/index.tsx:17-21
Timestamp: 2025-08-24T11:14:34.297Z
Learning: manNomi prefers not to receive accessibility suggestions or recommendations during code reviews.

Learnt from: manNomi
Repo: solid-connection/solid-connect-web PR: 245
File: src/api/auth/client/usePostLogout.ts:17-33
Timestamp: 2025-08-24T11:13:08.477Z
Learning: manNomi prefers to prioritize user experience over perfect state consistency in auth flows. Specifically, in logout scenarios, they prefer to keep tokens intact on API failure to avoid forcing users to re-login, even if it means temporary UI state inconsistency.

Learnt from: manNomi
Repo: solid-connection/solid-connect-web PR: 245
File: src/api/auth/client/useDeleteUserAccount.ts:17-27
Timestamp: 2025-08-24T11:11:40.758Z
Learning: manNomi prefers optimistic navigation in auth-related flows (like account deletion) to prevent race conditions where token clearing before navigation could cause intermediate redirects to login page due to pending async requests failing.

Learnt from: manNomi
Repo: solid-connection/solid-connect-web PR: 242
File: src/types/mentor.ts:70-73
Timestamp: 2025-08-12T04:07:04.134Z
Learning: manNomi prefers using Korean labels directly in enum values (e.g., MentorTab.MY_MENTEE = "나의 멘티") over separating enum keys from UI labels for efficiency and intuitiveness, considering scalability concerns as excessive for their current project needs.

<FindLastYearScoreBar />
<div className="flex flex-col gap-2.5 px-5 py-3.5">
<div className="flex gap-2">
<Link
className="h-26 bg-bg-accent-blue flex flex-1 flex-col gap-2 rounded-lg p-2.5"
href="/university/search"
>
<div className="flex flex-col">
<span className="typo-bold-5 text-secondary">학교 검색하기</span>
<span className="typo-medium-4 text-k-700">모든 학교 목록을 확인해보세요</span>
</div>
<div className="flex justify-end">
<IconMagnifyingGlass />
</div>
</Link>
<Link
className="h-26 bg-bg-accent-sky flex flex-1 flex-col gap-2 rounded-lg p-2.5"
href="/university/score"
>
<div className="flex flex-col">
<span className="typo-bold-5 text-sub-a">성적 입력하기</span>
<span className="typo-medium-4 text-k-700">성적을 입력해보세요</span>
</div>
<div className="flex justify-end">
<IconPaper />
</div>
</Link>
</div>
<div className="flex gap-2">
<Link
className="h-26 bg-bg-accent-orange flex flex-1 flex-col gap-2 rounded-lg p-2.5"
href="/university/application/apply"
>
<div className="flex flex-col">
<span className="typo-bold-5 text-accent-custom-orange">학교 지원하기</span>
<span className="typo-medium-4 text-k-700">학교를 지원해주세요</span>
</div>
<div className="flex justify-end">
<IconMuseum />
</div>
</Link>
<Link
className="h-26 bg-bg-accent-green flex flex-1 flex-col gap-2 rounded-lg p-2.5"
href="/university/application"
>
<div className="flex flex-col">
<span className="typo-bold-5 text-accent-custom-green">지원자 현황 확인</span>
<span className="typo-medium-4 text-k-700">경쟁률을 바로 분석해드려요</span>
</div>
<div className="flex justify-end">
<IconIdCard />
</div>
</Link>
</div>
</div>
<div className="flex gap-2">
<Link
className="h-26 bg-bg-accent-orange flex flex-1 flex-col gap-2 rounded-lg p-2.5"
href="/university/application/apply"
>
<div className="flex flex-col">
<span className="typo-bold-5 text-accent-custom-orange">학교 지원하기</span>
<span className="typo-medium-4 text-k-700">학교를 지원해주세요</span>
</div>
<div className="flex justify-end">
<IconMuseum />
</div>
</Link>
<Link
className="h-26 bg-bg-accent-green flex flex-1 flex-col gap-2 rounded-lg p-2.5"
href="/university/application"
>
<div className="flex flex-col">
<span className="typo-bold-5 text-accent-custom-green">지원자 현황 확인</span>
<span className="typo-medium-4 text-k-700">경쟁률을 바로 분석해드려요</span>
</div>
<div className="flex justify-end">
<IconIdCard />
</div>
</Link>

<div className="border-t-[5px] border-k-50 py-5 pl-5">
<div className="typo-sb-7 mb-2 flex items-center gap-1.5 font-serif text-k-700">실시간 인기있는 파견학교</div>
<PopularUniversitySection universities={recommendedUniversities} />
</div>
</div>

<div className="border-t-[5px] border-k-50 py-5 pl-5">
<div className="typo-sb-7 mb-2 flex items-center gap-1.5 font-serif text-k-700">실시간 인기있는 파견학교</div>
<PopularUniversitySection universities={recommendedUniversities} />
</div>
<div className="p-5">
<UniversityList allRegionsUniversityList={allRegionsUniversityList} />
</div>

<div className="p-5">
<UniversityList allRegionsUniversityList={allRegionsUniversityList} />
<NewsSectionDynamic newsList={newsList} />
</div>

<NewsSectionDynamic newsList={newsList} />
</div>
</>
);
};

Expand Down