Skip to content

feat: 검색 최적화 기반 정비#538

Open
manNomi wants to merge 2 commits into
mainfrom
feat/seo-geo-foundation
Open

feat: 검색 최적화 기반 정비#538
manNomi wants to merge 2 commits into
mainfrom
feat/seo-geo-foundation

Conversation

@manNomi
Copy link
Copy Markdown
Contributor

@manNomi manNomi commented Jun 1, 2026

관련 이슈

  • resolves: 없음

작업 내용

  • 사이트 기준 URL을 공통 SEO 유틸로 통일하고 canonical, robots, sitemap의 기본 도메인을 https://www.solid-connection.com으로 맞췄습니다.
  • sitemap을 공개 검색 표면 중심으로 재구성해 홈, 대학 선택, 약관, 홈대학별 대학 목록, 대학 상세 URL을 노출합니다.
  • 로그인, 회원가입, 마이페이지, 멘토, 커뮤니티, 지원/성적 입력 등 인증 또는 클라이언트 의존 페이지는 noindex로 정리했습니다.
  • 홈 JSON-LD의 실제 라우트와 맞지 않는 SearchAction을 제거하고, 대학 상세 404는 notFound()로 처리하도록 정리했습니다.
  • Next 타입체크를 막던 revalidateTag 호출 시그니처를 현재 타입에 맞게 수정했습니다.

특이 사항

  • 독립 리뷰어 피드백을 반영해 멘토/커뮤니티 상세는 현재 서버 렌더 공개 콘텐츠와 실제 클라이언트 콘텐츠가 일치하지 않아 sitemap에서 제외하고 noindex로 두었습니다.
  • pnpm --filter @solid-connect/web run build는 컴파일 성공 후 static prerender 단계에서 Unsupported Server Component type: {...} 오류로 실패합니다. 오류는 루트, 로그인, 마이페이지, 대학 상세 등 다수 기존 라우트에 걸쳐 발생하며, 빌드 중 Next가 tsconfig.jsonjsx를 자동 변경하려 해 해당 변경은 원복했습니다.
  • 로컬 Node는 v23.10.0이고 패키지 요구 버전은 Node 22.x라 검증 시 경고가 출력됩니다.

리뷰 요구사항 (선택)

  • sitemap에 포함한 공개 URL 범위가 현재 서비스 정책과 맞는지 확인 부탁드립니다.
  • 멘토/커뮤니티를 향후 검색 노출하려면 서버 렌더 공개 상세, 개인정보 동의, UGC moderation, 실제 404/noindex 정책을 먼저 정리해야 합니다.

검증

  • pnpm --filter @solid-connect/web run lint:check 통과
  • pnpm --filter @solid-connect/web run typecheck:ci 통과
  • 커밋 훅의 ci:check 통과
  • push 훅의 ci:check 통과 후 next build prerender 오류로 실패하여, 위 특이 사항을 남기고 훅을 우회해 브랜치를 푸시했습니다.

@manNomi manNomi requested review from enunsnv and wibaek as code owners June 1, 2026 07:15
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
solid-connection-web Ready Ready Preview, Comment Jun 1, 2026 7:21am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
solid-connect-web-admin Skipped Skipped Jun 1, 2026 7:21am

@github-actions github-actions Bot added the web label Jun 1, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Warning

Review limit reached

@manNomi, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 58 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a05abd8d-714c-4305-9757-8015c8ccd547

📥 Commits

Reviewing files that changed from the base of the PR and between 9a527d7 and 0b07417.

📒 Files selected for processing (1)
  • apps/web/src/app/api/revalidate/route.ts

Walkthrough

이 PR은 SEO 메타데이터 관리를 체계화하고 검색 엔진 인덱싱 정책을 통합하는 작업입니다. 주요 변경사항은 다음과 같습니다:

  1. SEO 유틸리티 기반 구축

    • apps/web/src/utils/seo.ts 신규 작성으로 URL 생성(createUrl, createAbsoluteUrl), 환경 기반 인덱싱 판정(isNonIndexSiteUrl), 텍스트 정규화(stripHtml, truncateDescription) 등의 함수 제공
    • 모든 메타데이터 변경의 기초가 됨
  2. 메타데이터 인프라 업데이트

    • 루트 레이아웃의 metadataBase 계산을 getSiteUrl() 함수 기반으로 변경
    • robots.ts를 isNonIndexSiteUrl로 단순화, sitemap.ts를 비동기로 전환해 동적 대학 페이지 포함
  3. 정적 NO_INDEX_ROBOTS 적용

    • 로그인, 사인업, 마이페이지, 커뮤니티, 멘토, 대학 신청 등 40개 이상의 페이지에 robots: NO_INDEX_ROBOTS 추가
    • 검색 엔진 크롤링/인덱싱 제외 필요 페이지들 대상
  4. Canonical/OpenGraph URL 동적 생성

    • 홈페이지, 약관, 대학 홈 페이지에서 createUrl 기반으로 canonical 및 OpenGraph URL 설정
    • Structured Data(JSON-LD)도 pageUrl 기준으로 업데이트
  5. 동적 메타데이터 생성으로 전환

    • 커뮤니티 보드, 대학 상세 페이지, 검색 결과 페이지 등에서 generateMetadata 함수 도입
    • 라우트 파라미터에 따라 메타데이터 동적 생성, 데이터 미존재 시 noindex 적용
  6. API 엔드포인트 정리

    • revalidateTag 호출에서 { expire: 0 } 옵션 제거

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Suggested reviewers

  • wibaek
  • enunsnv
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경사항의 핵심(검색 최적화 기반 정비)을 명확하게 요약하고 있으며, SEO 유틸 통일, robots/canonical 정비, sitemap 재구성 등 주요 변경을 포괄적으로 설명합니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고, 작업 내용(사이트 URL 통일, sitemap 재구성, noindex 정리 등)을 구체적으로 기술하며, 특이사항과 리뷰 요구사항을 충분히 포함하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/seo-geo-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a527d7693

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (boardCode) {
revalidatePath(`/community/${boardCode}`);
revalidateTag(`posts-${boardCode}`, { expire: 0 });
revalidateTag(`posts-${boardCode}`);
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 Restore the required revalidateTag profile

With this repo on Next 16.2.6, the official API docs define revalidateTag(tag: string, profile: string | { expire?: number }) and note that the single-argument form is deprecated and a TypeScript error (docs); because the root workflow asks for pnpm typecheck, this route will fail type checking for both this board invalidation and the generic tag branch unless the second argument is kept, e.g. { expire: 0 } or an appropriate cache profile.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/web/src/app/sitemap.ts (1)

29-49: ⚡ Quick win

1. 대학 데이터 패치 실패 시에도 정적 경로는 살려두면 어떨까요?

getAllUniversities()가 거절(reject)되면 sitemap 생성 전체가 실패해서, 안전한 정적 경로(/, /university, /terms, 홈 대학 목록)까지 함께 사라지게 됩니다. PR 설명에도 prerender 단계 불안정이 언급되어 있으니, 동적 경로만 실패하도록 감싸두면 검색 노출의 핵심 경로를 지킬 수 있어요.

♻️ 동적 경로 실패를 흡수하는 제안
 const getUniversityDetailRoutes = async (): Promise<MetadataRoute.Sitemap> => {
-  const universities = await getAllUniversities();
+  let universities;
+  try {
+    universities = await getAllUniversities();
+  } catch {
+    return [];
+  }
   const seenUrls = new Set<string>();
🤖 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/sitemap.ts` around lines 29 - 49, The
getUniversityDetailRoutes function currently lets getAllUniversities rejection
bubble up and break sitemap generation; wrap the await getAllUniversities() call
in a try/catch inside getUniversityDetailRoutes (or handle its Promise
rejection) so that on error you log the failure and return an empty array (i.e.,
no dynamic university entries) instead of throwing, preserving the static
routes; reference getUniversityDetailRoutes, getAllUniversities, seenUrls, and
toSitemapEntry when making the change.
apps/web/src/app/university/[homeUniversity]/[id]/page.tsx (1)

126-144: 💤 Low value

1. 404 분기 이후 남은 삼항 조건은 이제 죽은 코드예요.

isNotFoundErrortrue면 위에서 notFound()가 즉시 throw하기 때문에, 아래 fallback 렌더는 항상 404가 아닌 경우(준비 중)에만 도달합니다. 그래서 title/descriptionisNotFoundError ? ... : undefined 삼항은 늘 undefined로만 평가되는 죽은 분기가 되었어요. 의도는 정확하니 가독성 차원에서 정리해두면 다음 사람이 헷갈리지 않습니다.

♻️ 불필요해진 조건 정리 제안
-    const isNotFoundError = universityDetailResult.status === 404;
-
-    if (isNotFoundError) {
+    if (universityDetailResult.status === 404) {
       notFound();
     }

     return (
       <>
         <TopDetailNavigation title="파견 학교 상세" backHref={`/university/${homeUniversity}`} />
-        <UniversityDetailPreparingFallback
-          backHref={`/university/${homeUniversity}`}
-          title={isNotFoundError ? "해당 대학 정보를 찾을 수 없어요." : undefined}
-          description={
-            isNotFoundError ? "요청하신 파견학교를 찾지 못했습니다. 목록에서 다른 학교를 선택해 주세요." : undefined
-          }
-        />
+        <UniversityDetailPreparingFallback backHref={`/university/${homeUniversity}`} />
       </>
     );
🤖 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/university/`[homeUniversity]/[id]/page.tsx around lines 126
- 144, The ternary checks using isNotFoundError are dead code because notFound()
short-circuits when isNotFoundError is true; remove the redundant conditionals
in the UniversityDetailPreparingFallback props (title and description) and
simplify them to their preparing-state values or omit them (i.e., pass undefined
directly or remove the props) so the component only reflects the “preparing”
state; update references to isNotFoundError, notFound(), and
UniversityDetailPreparingFallback to ensure clarity.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@apps/web/src/app/api/revalidate/route.ts`:
- Line 60: The call to revalidateTag(`posts-${boardCode}`) is missing the
required second argument causing TS2554; restore the profile argument (either
"max" or the original object like { expire: 0 }) to match the intended cache
behavior. Update both calls of revalidateTag (the one at
revalidateTag(`posts-${boardCode}`) and the other similar call at line 81) to
pass the appropriate profile as the second parameter so the function signature
for revalidateTag(tag, profile) is satisfied and the desired revalidation policy
(e.g., "max" or { expire: 0 }) is preserved.

In `@apps/web/src/app/my/match/page.tsx`:
- Line 9: The page metadata title ("metadata.title") currently reads "프로필 수정"
but the rendered header uses TopDetailNavigation with the label "매칭 멘토"; update
the metadata to match the visible header by changing metadata.title to "매칭 멘토"
(or alternatively make TopDetailNavigation read from metadata.title) in the
page.tsx so the browser tab and page header show the same title; locate the
export/const named metadata and the TopDetailNavigation usage in this file and
ensure they use the identical string.

---

Nitpick comments:
In `@apps/web/src/app/sitemap.ts`:
- Around line 29-49: The getUniversityDetailRoutes function currently lets
getAllUniversities rejection bubble up and break sitemap generation; wrap the
await getAllUniversities() call in a try/catch inside getUniversityDetailRoutes
(or handle its Promise rejection) so that on error you log the failure and
return an empty array (i.e., no dynamic university entries) instead of throwing,
preserving the static routes; reference getUniversityDetailRoutes,
getAllUniversities, seenUrls, and toSitemapEntry when making the change.

In `@apps/web/src/app/university/`[homeUniversity]/[id]/page.tsx:
- Around line 126-144: The ternary checks using isNotFoundError are dead code
because notFound() short-circuits when isNotFoundError is true; remove the
redundant conditionals in the UniversityDetailPreparingFallback props (title and
description) and simplify them to their preparing-state values or omit them
(i.e., pass undefined directly or remove the props) so the component only
reflects the “preparing” state; update references to isNotFoundError,
notFound(), and UniversityDetailPreparingFallback to ensure clarity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 20373075-2365-4913-8ada-a5ebcdf992e6

📥 Commits

Reviewing files that changed from the base of the PR and between 857e527 and 9a527d7.

📒 Files selected for processing (41)
  • apps/web/src/app/(home)/page.tsx
  • apps/web/src/app/api/revalidate/route.ts
  • apps/web/src/app/community/[boardCode]/[postId]/modify/page.tsx
  • apps/web/src/app/community/[boardCode]/[postId]/page.tsx
  • apps/web/src/app/community/[boardCode]/create/page.tsx
  • apps/web/src/app/community/[boardCode]/page.tsx
  • apps/web/src/app/community/page.tsx
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/login/apple/callback/page.tsx
  • apps/web/src/app/login/kakao/callback/page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/mentor/[id]/page.tsx
  • apps/web/src/app/mentor/chat/[chatId]/page.tsx
  • apps/web/src/app/mentor/chat/page.tsx
  • apps/web/src/app/mentor/modify/page.tsx
  • apps/web/src/app/mentor/page.tsx
  • apps/web/src/app/mentor/waiting/page.tsx
  • apps/web/src/app/my/apply-mentor/layout.tsx
  • apps/web/src/app/my/favorite/page.tsx
  • apps/web/src/app/my/match/page.tsx
  • apps/web/src/app/my/modify/page.tsx
  • apps/web/src/app/my/page.tsx
  • apps/web/src/app/my/password/page.tsx
  • apps/web/src/app/robots.ts
  • apps/web/src/app/sign-up/email/page.tsx
  • apps/web/src/app/sign-up/page.tsx
  • apps/web/src/app/sitemap.ts
  • apps/web/src/app/terms/page.tsx
  • apps/web/src/app/university/(home)/page.tsx
  • apps/web/src/app/university/[homeUniversity]/[id]/page.tsx
  • apps/web/src/app/university/[homeUniversity]/page.tsx
  • apps/web/src/app/university/[homeUniversity]/search/page.tsx
  • apps/web/src/app/university/application/apply/page.tsx
  • apps/web/src/app/university/application/page.tsx
  • apps/web/src/app/university/list/[homeUniversityName]/page.tsx
  • apps/web/src/app/university/score/example/layout.tsx
  • apps/web/src/app/university/score/page.tsx
  • apps/web/src/app/university/score/submit/gpa/page.tsx
  • apps/web/src/app/university/score/submit/language-test/page.tsx
  • apps/web/src/app/university/search/page.tsx
  • apps/web/src/utils/seo.ts

Comment thread apps/web/src/app/api/revalidate/route.ts Outdated
import MatchContent from "./_ui/MatchContent";

export const metadata: Metadata = {
title: "프로필 수정",
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 | 🟡 Minor | ⚡ Quick win

메타데이터 제목이 페이지 네비게이션 제목과 일치하지 않습니다.

메타데이터의 title이 "프로필 수정"으로 설정되어 있지만, 16번 줄의 TopDetailNavigation은 "매칭 멘토"를 표시합니다. 브라우저 탭과 페이지 헤더의 제목이 일치하도록 수정해주세요.

🔧 제안하는 수정
 export const metadata: Metadata = {
-  title: "프로필 수정",
+  title: "매칭 멘토",
   robots: NO_INDEX_ROBOTS,
 };
📝 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
title: "프로필 수정",
export const metadata: Metadata = {
title: "매칭 멘토",
robots: NO_INDEX_ROBOTS,
};
🤖 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/my/match/page.tsx` at line 9, The page metadata title
("metadata.title") currently reads "프로필 수정" but the rendered header uses
TopDetailNavigation with the label "매칭 멘토"; update the metadata to match the
visible header by changing metadata.title to "매칭 멘토" (or alternatively make
TopDetailNavigation read from metadata.title) in the page.tsx so the browser tab
and page header show the same title; locate the export/const named metadata and
the TopDetailNavigation usage in this file and ensure they use the identical
string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant