Skip to content

chore: release v1.1.7 — UX 정합성 hotfix bundle (5건 통합)#35

Merged
chan9yu merged 40 commits into
mainfrom
develop
May 7, 2026
Merged

chore: release v1.1.7 — UX 정합성 hotfix bundle (5건 통합)#35
chan9yu merged 40 commits into
mainfrom
develop

Conversation

@chan9yu
Copy link
Copy Markdown
Owner

@chan9yu chan9yu commented May 7, 2026

변경 사항

🚑 post-v1.1.6 UX 정합성 hotfix bundle — 5건 통합 정정. v1.1.6 release 직후 사용자 검수에서 발견된 UX 어색함을 즉시 해소.

🛡️ Fixed

  • Build version badge mobile hide (Footer.tsx) — <a> className에 hidden md:inline-block 추가. 모바일·태블릿(<768px) 숨김, PC(md ≥ 768px)에서만 노출
  • PostList list↔grid 전환 첫 카드 layout 일관성 (PostList.tsx) — 첫 카드만 framer-motion 우회하던 부자연스러움 제거. <motion.div layout> (variants 없이 layout prop만) wrap → layout 보간 일관 + LCP 보호 유지
  • ImageLightbox 작은 이미지 깨짐 차단 (ImageLightbox.tsx) — <Image fill> + 90vh×90vw 컨테이너로 작은 이미지가 강제 확대돼 픽셀 깨지던 문제. width={0} height={0} sizes="100vw" + w-auto h-auto max-w-lightbox max-h-lightbox로 변경 → 자연 크기 보호 + 큰 이미지만 한도 내 축소

🎨 Changed

  • PostList 기본 뷰 모드 grid 전환 (useViewMode.ts·PostList.tsx·ViewToggle.tsx) — getSnapshot·getServerSnapshot default "grid". PostList·ViewToggle의 hydrated 전 fallback도 "grid"로 통일 (React #418 mismatch 차단)

✨ Added

  • ImageLightbox 좌우 슬라이드 애니메이션 (ImageLightbox.tsx) — framer-motion AnimatePresence custom={direction} + slide+fade variants. onNext/onPrev/키보드 ←/→ wrapper handler로 direction(+1/-1) 추적
  • max-h-lightbox·max-w-lightbox Tailwind utility (globals.css) — h-lightbox·w-lightbox(90vh/90vw 고정)의 max 변형. styling.md arbitrary value 회피 룰 정합

Ralph QA Cycle 1 검증 (PASS)

_workspace/verification/ralph_qa_2026-05-07_cycle_1.md (157 lines):

영역 결과
Footer mobile hide PC display:block / mobile 375px display:none
PostList 기본 grid 홈·/posts 모두 gridPressed=true, lg:grid-cols-3 ✅
list↔grid 전환 flex flex-col 적용, console 0/0 ✅
Lightbox 슬라이드 next/prev/ArrowRight 모두 동작 ✅
Lightbox 적응형 640² 자연 크기 / 640×942 → 489×720 비율 보존 ✅
검색 ⌘K (Fuse.js) 5건 매칭 ✅
테마 토글 dark + localStorage + bg #09090b ✅
한글 tag slug /tags/실시간통신 정상 ✅
sitemap 90 / RSS 23
/og?thumbnail=evil.com redirect 0 (Open Redirect 차단) ✅

회귀 후보 1건 (별도 트랙): Lightbox ESC 후 focus가 BODY로 빠짐. v1.1.7과 무관, v1.1.8 후보.

관련 작업

  • CHANGELOG.md [1.1.7] 섹션 추가
  • package.json 1.1.6 → 1.1.7
  • 머지 후 v1.1.7 tag push → release.yaml 자동 트리거

테스트

  • pnpm build 성공 (97/97 static, exit 0)
  • lefthook (prettier·type-check·eslint·commit-msg) 4회 PASS
  • Ralph QA cycle 1 — 14 PASS / 0 블로커
  • pnpm type:check (CI)
  • pnpm lint (CI)
  • 머지 후 v1.1.7 tag push → release workflow 자동 발행

🤖 Generated with Claude Code

chan9yu and others added 30 commits May 4, 2026 17:57
v1.1.0 production 배포 후 ~30분부터 시작된 모든 dynamic 라우트 무한 loading.tsx stuck의
진짜 root cause는 `cacheComponents: true`(Next.js 16 PPR)가 RSC를 default dynamic으로
만들어 매 요청 fs.readdirSync(contents/posts) 호출 → Vercel lambda contents/ 부재 시
ENOENT throw → streaming chunk close → loading.tsx 영구 stuck.

본질 fix:
- next.config.ts: cacheComponents=false. SSG-first(PRD G-1)에 ISR/dynamic 불필요.
  Next.js가 모든 페이지를 빌드 타임 정적 prerender → runtime contents/ 의존 0%.
- scripts/build-search-index.mjs (신규): contents/posts/* MDX → src/shared/data/search-index.json
  (gitignore) 정적 emit. prebuild에 등록.
- app/layout.tsx: getPublicPosts() runtime 호출 제거 → 정적 JSON import.
- app/posts/page.tsx: searchParams 제거 + TagList variant=navigation으로 일원화 →
  /posts가 ƒ Dynamic → ○ Static. 태그 필터링은 /tags/[tag]가 담당.
- app/rss/route.ts: export const dynamic = "force-static" → ƒ Dynamic → ○ Static.
- shared/components/mdx/CustomMDX.tsx: "use cache" directive 제거 (cacheComponents 의존).
- app/og/route.tsx: 스테일 cacheComponents 주석 정정.

검증 (production-like 시뮬레이션):
- 빌드 결과: /, /about, /posts, /series, /tags, /sitemap.xml, /rss 모두 ○ Static.
  /posts/[slug], /series/[slug], /tags/[tag]는 ● SSG. /og /api/views만 의도된 ƒ Dynamic.
- pnpm start + contents/ 디렉토리 일시 제거 + 모든 라우트 fetch:
  - 모든 페이지 200, ENOENT 로그 0건, Suspense pending 0건.
  - Playwright 시각 검증: 홈 hero·최근 포스트 카드 3개·Popular sidebar 정상 렌더.

Note: /api/views 500은 별개 fault (KV sync throw 미흡수). 후속 v1.1.3 격리 처리.
- .claude/rules/no-fallback.md (신규): try/catch + 빈 값 반환·임시 플래그·setTimeout
  우회·기본값으로 누락 데이터 가림 같은 fallback/workaround 안티패턴 거부 + 본질 fix
  가이드 룰. v1.1.2 incident 회고 사례 포함.
- CLAUDE.md / README.md / docs/AI_WORKFLOW_GUIDE.md: 15개 → 16개 규칙 카운트 갱신.

Why: v1.1.0 핫픽스 v1.1.1 시도에서 잘못된 fail-soft fallback(getAllPosts try/catch +
빈 배열, getAboutContent 빈 문자열, layout try/catch)을 짠 회귀가 사용자 지적으로
드러남. 본질 root cause는 cacheComponents+RSC dynamic 평가였고, fail-soft는 검색
인덱스를 무음 비활성화시키는 회피였음. 본 룰이 그 회귀를 차단.
src/shared/data/search-index.json은 prebuild script가 빌드 타임에 emit하는 자동 생성
파일이라 .gitignore에 포함. CI에서 type:check 단계가 layout.tsx의 정적 import 검증을
시도할 때 파일 부재로 TS2307 fail.

해결: typecheck 전에 `node scripts/build-search-index.mjs` 실행 단계 추가. next-env.d.ts
emit 단계 직후, type:check 직전에 위치해 의존성 순서 명확.
v1.1.2의 fail-soft 보강책으로 도입한 search-index pre-bake 접근을 제거. cacheComponents=false로 SSG-first가 정착되어 getPublicPosts()는 빌드 타임에만 호출되므로 runtime fs 의존이 원천적으로 없음. 빌드 단계 추가·.gitignore·CI step·prebuild script·outputFileTracingIncludes 모두 over-engineering으로 판단되어 제거 (사용자 지시: "심플하게 가자고").

- 삭제: scripts/build-search-index.mjs, .gitignore의 search-index.json 항목, CI workflow의 Generate search index step
- next.config.ts: outputFileTracingIncludes 제거 (contents/posts/**/*.mdx 명시 포함 불필요), outputFileTracingExcludes는 contents/**/images/**만 유지 (lambda 155MB 절감)
- app/layout.tsx: searchIndexJson import → getPublicPosts() 직접 호출, alternates.types에 application/rss+xml 추가 (RSS autodiscovery)
- app/posts/page.tsx, app/providers.tsx: comments.md 룰 정합 차원에서 주석 단순화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.1.2 Notes에서 격리 약속한 두 production fault를 본질적으로 해결.

- /api/views 500: @vercel/kv lazy 초기화가 환경변수 누락 시 sync throw하는 패턴을 isKvConfigured 가드 + try/catch로 흡수. KV 미설정 환경에서 GET 200 + views=0, POST 204 fail-soft 응답 (이전 .catch()는 promise rejection만 잡아 sync throw 우회 불가).
- React #418 hydration mismatch (/posts localStorage="grid" 시나리오): server snapshot(false)과 client snapshot(localStorage 값)이 다른 hook을 직접 className에 사용해 mismatch 발생. set-state-in-effect 룰을 위반하지 않는 useHydrated 훅(useSyncExternalStore 기반: server=false, client=true)으로 일관 게이팅.
- DRY: useTheme·PageTransition·PostList의 인라인 useState + useEffect(() => setMounted(true), []) 중복(set-state-in-effect 위반)을 useHydrated 한 곳으로 통합.
- PostList 첫 카드 priority preload hint 정리: view-mode 전환 시 list/grid sizes 불일치로 발생하는 _next/image preload 워닝 차단. RecentPostsList의 동일 회피 패턴과 정렬해 priority 제거 (framer-motion 우회로 paint timing 안정화만 유지).
- PostList.test.tsx: ViewToggle role="toolbar" 변경 정합.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WAI-ARIA APG·Next.js 16 metadata best practice·React 19 cache 패턴을 묶어 정합성 보강.

SEO/Sitemap:
- PostFrontmatter.updated?: string optional 필드 추가 (Zod schema에 ISO 8601 regex). sitemap lastmod가 frontmatter.updated ?? frontmatter.date를 사용하도록 정렬 (seo.md §Sitemap 룰 정합).

a11y Tier 2 (WAI-ARIA APG):
- MdxImage: useId()로 figcaption id 발급 + button aria-describedby 연결 (스크린 리더 캡션 정밀 안내).
- ViewToggle: role="group" → role="toolbar" + aria-label="뷰 모드" (toolbar pattern). aria-pressed 정상 전이.
- SearchModal: sr-only role="status" aria-live="polite" aria-atomic="true" 항상 mount, 검색 상태 동적 안내.
- CommentsSection: lazy 안내에 role="status" aria-live="polite".

Next.js 16 best practice:
- /og route: export const dynamic = "force-dynamic" 명시.
- 홈 ISR: app/page.tsx에 export const revalidate = 3600 명시.
- /manifest.webmanifest: MetadataRoute.Manifest 타입 명시.
- /tags/[tag] page: generateMetadata + Page가 동일 getPostsByTag 결과를 React cache()로 공유 (lookup dedupe, series/[slug] 패턴과 일관).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- comments.md 신규 룰: "No comments by default" + Why-over-What + 4가지 허용 케이스(비명시적 제약·subtle invariant·workaround·회귀 차단). AI 협업 코드의 과도 주석 회귀 차단 (사용자 명시 요청).
- react.md: Hydration mismatch / mounted gate 패턴 절 추가. set-state-in-effect 위반 anti-pattern + useHydrated 권장 패턴 명문화.
- CLAUDE.md, README.md, AI_WORKFLOW_GUIDE.md: 16 → 17개 규칙 카운트 동기화.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.1.2 후속 정리 + Next.js 16 best practice audit + a11y Tier 2 개선 묶음 release.
v1.1.2 Notes에서 격리 약속한 /api/views 500 본질 fix와 React #418 hydration mismatch 본질 fix 포함.

검증 baseline:
- pnpm type:check / lint / build / test 250/250 PASS
- Playwright MCP E2E (port 3110): 17개 라우트 + 6개 인터랙티브 시나리오 모두 console 0 errors / 0 warnings
- Lambda 48MB, contents/images 0건 trace
- v1.1.0~v1.1.2 회귀 패턴 5종 0건 재현

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ct develop 측 채택

origin/main의 decdbe1(v1.1.2 squash merge)을 develop에 merge.
develop은 이미 v1.1.3 진화로 main의 모든 의도(cacheComponents=false 등)를 superset으로 포함하므로 9개 conflict 파일 모두 develop 측 채택. main에서 자동 staged된 search-index 관련 3개(scripts/build-search-index.mjs, ci.yaml step, layout.tsx import)는 v1.1.3에서 의도적으로 제거된 상태이므로 develop HEAD로 복원.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CommentsSection.tsx:80의 data-term이 ${slug} 형식이지만 GitHub Discussion title은 posts/${slug} 형식으로 자동 생성되어 mapping="specific" 매칭 실패. 모든 23개 포스트 페이지에서 "Discussion not found" 404 + 빈 댓글 영역. data-term을 posts/${slug}로 정정해 기존 5개 discussion(#13·#14·#15·#17 등)과 즉시 매칭.

회귀 테스트 assertion 동시 갱신 (이전엔 buggy spec 박제로 회귀 차단 못함):
- expect dataset.term toBe "react-19-use" → "posts/react-19-use"
- 서로 다른 slug 케이스 동일하게 prefix 추가

Refs: #30

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.1.3에서 PostList는 useHydrated로 게이팅했으나 동일 hook(useViewMode)을 쓰는 ViewToggle이 누락되어 server snapshot "list" vs client localStorage "grid" 불일치 시 aria-pressed·className 텍스트 mismatch 재발생 (production /posts에서 React #418).

ViewToggle에도 useHydrated 적용해 effectiveView로 일관 게이팅. PostList와 동일 패턴 정합.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Header.tsx:21의 className이 relative top-0 (의미 없는 조합)이어서 스크롤 시 헤더가 같이 흘러감. sticky top-0로 정정. layout.tsx의 Suspense fallback(sticky top-0 z-40 h-16)과 마크업 정합 복원. md:mt-12은 sticky와 호환 (처음엔 떠있다가 스크롤 시 viewport top에 fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
배포된 빌드의 버전을 즉시 확인할 수 있도록 Footer 카피라이트 옆에 v{APP_VERSION} 작은 링크 표시. 클릭 시 GitHub Release tag(/releases/tag/v{version})로 직접 이동해 release notes 확인 가능.

- src/shared/config/site.ts: package.json version을 APP_VERSION으로 export. JSON import는 ES2025 표준 with { type: "json" } 구문 사용 (Node 22 + TS 6 + Next.js 16 Turbopack 호환).
- src/shared/components/layouts/Footer.tsx: 카피라이트 영역에 v{APP_VERSION} 링크. aria-label로 SR 안내, focus-visible 스타일.
- .github/workflows/release.yaml 신규: v*.*.* tag push 시 CHANGELOG.md의 해당 버전 섹션을 awk로 추출해 gh release create 자동 publish. 다음 release부터 수동 단계 제거.

사용자 명시 요청 (배포 상태 가시화 + release 자동화).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
production hotfix — 댓글 표시 회귀·React #418 잔재·Header sticky·배포 버전 가시화 + 자동 release workflow.

검증: type:check / lint / test 250/250 / build 97 정적 페이지 모두 PASS. v1.1.3 production Playwright MCP 검증에서 발견한 3개 회귀를 즉시 정정.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
신규 포스트 발행 시 SEO 회귀를 자동 차단하기 위한 prebuild 검증
스크립트. 위반 발견 시 pnpm build가 실패하므로 lambda 배포 자체를
막는다.

검증 항목
- title ≤ 60자 (검색 결과 잘림 방지)
- description 120~160자 (스니펫 cut-off 최적)
- 포스트 slug 영문·소문자·숫자·하이픈 한정
- slug ↔ 디렉토리명 정합

prebuild 체인에 통합 + pnpm validate:seo 단독 실행 스크립트 추가.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frontmatter.updated를 BlogPosting JSON-LD dateModified와 article OG
modifiedAt에 동시 전달. sitemap lastmod(이미 정합)와 시그널을 통일해
크롤러에 일관된 갱신 신호 전달.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GC 결과 사용처 0건. 동적 OG 라우트(/og)가 도입된 이후 정적
default-og-image.png 참조가 모두 제거되었으나 설정만 남아 있던
잔재. 단일 진실 공급원 원칙대로 정리.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
코드 SEO baseline 사이클 회고를 룰에 명문화하고, 코드로 해결 못
하는 외부 SEO 작업을 별도 가이드로 분리.

- .claude/rules/seo.md: "회고 — 도입 보류한 항목" 표 추가.
  /llms.txt·robots AI 크롤러 명시·FAQPage·HowTo·Organization·
  CreativeWorkSeries JSON-LD를 효과 미검증 또는 Google rich result
  자격 축소 사유로 보류 결정 명문화. 미래 재도입 판단 근거 보존.
- .claude/agents/content/seo-auditor.md: 단일 진실 공급원 표기
  + 외부 인프라 가이드 참조 추가.
- docs/AI_WORKFLOW_GUIDE.md: SEO 빌드 게이트 + 회고 사이클 이력 추가.
- docs/SEO_EXTERNAL.md (신규): GSC/Bing/Naver Webmaster 등록·
  dev.to/Medium/Hashnode canonical 유지 크로스 포스팅·Otterly/Peec/
  ZipTie AI visibility 모니터링·news.hada/Disquiet 공유·위키피디아
  인용 전략. velog 같은 platform 도메인 권위 추격은 코드 외 작업이
  결정적이라는 회고 반영.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
블로그 SEO baseline은 .claude/rules/seo.md 단일 진실 공급원을
유지하되, 일반 SEO 컨설팅 지식(AI 검색·programmatic SEO 패턴·
SERP audit 절차)은 보조 스킬로 분리해 필요 시 참조 가능하게 함.

- ai-seo: AI 검색(GPT/Claude/Perplexity) 인용 최적화 패턴.
- programmatic-seo: 대량 페이지 자동 생성 플레이북.
- seo-audit: 사이트 audit 절차 + AI writing detection·
  international SEO 레퍼런스.

각 스킬은 SKILL.md + references/ + evals/evals.json 구조.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이번 SEO 정합성 강화 사이클의 모든 변경(빌드 게이트·JSON-LD
정합·룰·문서·보조 스킬·서브모듈 콘텐츠 정리)을 [Unreleased]에
Added/Changed/Removed/Notes 4 섹션으로 종합 기록.

서브모듈 contents pointer를 description 정합화 + WebRTC 본문
보강이 반영된 60f2d33로 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
직전 SEO 사이클의 메타 정합성 drift를 정정.

- CHANGELOG.md [Unreleased]: SEO 보조 스킬 항목을 Removed에서 Added로
  이동. 디스크에는 .claude/skills/{ai-seo,programmatic-seo,seo-audit}
  3개가 그대로 등록되어 있는데 Removed로 적혀있던 모순 해소.
- CLAUDE.md·README.md·docs/AI_WORKFLOW_GUIDE.md의 "12개 스킬·17개 규칙"
  표기를 실제 디스크 카운트(15개 스킬·18개 룰)로 동기화.
- README.md 스킬 분류도 갱신 (참조 지식 6 → 8).
- AI_WORKFLOW_GUIDE.md 스킬 트리에 보조 SEO 카테고리 신설.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
오발화 위험 차단 — 영문 일반형 description은 dev-blog 도메인 컨텍스트
키워드(velog·MDX·1인 블로그)가 없어 사용자가 'SEO 점검'·'AI 검색
노출' 같은 한글 표현을 쓸 때 트리거 미스 가능성.

- 한글 트리거 키워드(AI SEO·AEO·GEO·LLMO·AI 검색 노출·SEO 진단·
  랭킹 떨어짐·core web vitals 등) 추가.
- 각 스킬에 ".claude/rules/seo.md가 단일 진실 공급원이므로 충돌 시
  본 스킬보다 룰 우선" 명시 — 컨설팅 지식 vs 도메인 룰 우선순위
  자율 판단 가능.
- programmatic-seo는 "1인 블로그에 적극 사용 X, 외부 사이드 프로젝트
  컨설팅 위주"로 적용 범위 한정.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
직전 SEO 정리 사이클의 "실험적·과한 것 모두 제거" 결정과 같은
사이클 안에서 새 인프라(룰 + 스크립트 245줄) 신설은 자기 모순.
정리 정신 일관성 회복.

- .claude/rules/meta-consistency.md 삭제
- scripts/check-meta-consistency.mjs 삭제
- package.json: validate:meta 명령 제거 (net-zero, 추가했다 뺌)
- 카운트 18→17 되돌림 (CLAUDE.md·README.md·AI_WORKFLOW_GUIDE.md)
- docs/PRD_TECHNICAL.md: 룰 인용 부분만 제거. PRD frontmatter
  version vs package.json version 분리 운영 명시 한 줄은 자체 가치
  있어 보존.

판단 근거 (자기 비판):
- GC가 발견하는 영역이면 GC가 책임지는 영역. 별도 룰 추가는
  ceremony 중첩.
- 1인 블로그에 자동 게이트(validate:seo·STRICT_FRONTMATTER·
  prebuild·lefthook)가 이미 누적된 상태에서 추가 게이트는
  유지보수 부채만 증가.
- "GC 4회 연속 동일 카테고리 drift" 통계로 정당화했지만, 카운트
  drift는 본인이 5초에 발견하는 영역 — 자동화 ROI 낮음.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.1.4 hotfix release 이후 진행된 SEO 정합성 강화 + 메타 정합성
실험·revert 사이클을 release로 묶음. 코드 SEO baseline을 시스템화
하되, Google rich result 자격 축소 또는 효과 미검증 항목은 도입
보류해 코드 부채 누적 차단.

핵심 변경:
- frontmatter SEO 빌드 게이트(scripts/validate-frontmatter-seo.mjs):
  title ≤60·description 120~160·slug 영문 강제 prebuild 자동 차단.
- BlogPosting/article OG dateModified 정합 (sitemap lastmod와 통일).
- siteMetadata.ogImage dead config 제거.
- ai-seo·programmatic-seo·seo-audit 보조 스킬 등록 + 한글 도메인
  키워드 description 보강 (오발화 차단).
- 23개 포스트 description 일괄 정합화 (wil8/wil9 중복 해소 등).
- WebRTC 시리즈 5편 본문 SEO 보강 + 시리즈 hub 양방향 내부 링크.
- docs/SEO_EXTERNAL.md (외부 인프라 가이드) 신설.
- 실험 항목 정리: /llms.txt, robots AI 크롤러 명시, FAQPage·HowTo·
  Organization·CreativeWorkSeries JSON-LD 모두 도입 보류 (회고는
  .claude/rules/seo.md 하단 표).
- meta-consistency 자동 검증 게이트는 도입 즉시 보류 (정리 정신과
  모순으로 같은 사이클 안에서 자기 비판 후 revert).

상세는 CHANGELOG [1.1.5] 섹션 참조.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존 /og?thumbnail=https://evil.com/x.png을 검증 없이 302 redirect
하던 패턴을 allowlist 방식으로 차단. siteMetadata.url의 hostname
또는 same-origin만 외부 redirect 허용, 그 외는 default ImageResponse
fallback 렌더로 안전 회복.

- ALLOWED_THUMBNAIL_HOSTS: siteMetadata.url의 hostname 명시 allowlist
- isAllowedThumbnailUrl(): URL parse 실패·non-http 프로토콜·미허용
  hostname 모두 false. javascript:·data:·ftp: 등 우회 시도 차단.
- protocol-relative URL(//evil.com) 가드 추가 — startsWith("/") &&
  !startsWith("//")로 same-origin 절대 경로만 허용.
- 미허용 thumbnail은 redirect 대신 default OG 렌더로 fallback —
  기능 손실 0, phishing 벡터 차단.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vercel 외 배포(Cloudflare Pages·Netlify·self-host)에서도 명시적 도메인
주입만으로 canonical/OG/sitemap/rss가 정확히 동작하도록 일반화.
잘못된 환경변수 주입으로 canonical/OG가 깨지는 회귀를 production
빌드 단계에서 throw로 차단.

우선순위: NEXT_PUBLIC_SITE_URL > Vercel production > Vercel preview
> 로컬 dev fallback. NEXT_PUBLIC_* prefix는 Next.js 빌드 타임에
정적 치환되어 RSC·Client 모두에서 zero-runtime 접근 가능.

production 검증:
- https:// 스킴 강제 (다른 프로토콜이면 throw with diagnostic).
- new URL() parse 실패 시 throw — invalid value 빠른 fail.

dev/preview lenient: invalid URL 무시 + 다음 분기 fallback (개발
중 빌드 차단 방지).

동작표 (NEXT_PUBLIC_SITE_URL · VERCEL_ENV · VERCEL_URL → 결과):
- (-, -, -) → http://localhost:3100
- (https://staging, -, -) → https://staging
- (-, production, *) → siteMetadata.url
- (-, preview, xxx.vercel.app) → https://xxx.vercel.app
- (https://example.com, *, *) → https://example.com (override 우선)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
manifest.ts의 "#4f46e5" 하드코딩이 siteMetadata.themeColor와 중복
SSOT 위반. siteMetadata는 이미 import되어 있어 추가 import 0,
brand color 변경 시 site.ts 한 곳만 수정.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
grep -rn "ScrollToTopButton" 결과 정의 self만 매칭. 외부 import 0건.
별도로 ScrollToTop.tsx(다른 컴포넌트, 활성 사용 중)가 존재하므로
명백한 dead code. 추후 필요 시 5줄로 재작성 가능.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chan9yu and others added 9 commits May 5, 2026 17:25
shadcn/ui v3는 native button cursor 정책(=default, Apple HIG)을 따라
cursor-pointer를 의도적으로 제거. dev-blog는 사용자 정책으로 오버라이드
— 모든 버튼이 시각적으로 인터랙티브함을 명확히 한다.

- shared/components/ui/Button.tsx cva baseline에 cursor-pointer 1단어
  추가 → 모든 shadcn Button 사용처 자동 적용. disabled:pointer-events-none
  이 이미 baseline에 있어 disabled 상태에서 hover/cursor 자연 무효화.
- shadcn 미경유 native button 5건에 cursor-pointer 명시:
  - features/lightbox/components/ImageLightbox.tsx (이전·다음·Close 3개)
  - features/theme/components/ThemeSwitcher.tsx
  - features/search/components/SearchButton.tsx
  - shared/components/mdx/MdxPre.tsx (코드 복사 버튼)

보존 (의도적 cursor 의미론):
- shared/components/mdx/MdxImage.tsx의 cursor-zoom-in (확대 표시).
- <a>·<Link>는 브라우저 기본 cursor=pointer로 자동 처리.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존 카피라이트와 같은 줄(가운데 정렬)에서 분리해 카피라이트 줄
아래의 좌측 단독 줄로 이동. breakpoint 무관하게 항상 좌측 정렬
("왼쪽 대각선 아래" 위치).

- 카피라이트 줄: text-center 단순화 (sm:flex-row 분기 제거)
- v{APP_VERSION}: 새 div(mt-3 text-left)로 분리, text-muted-foreground
  를 a 태그에 직접 명시 (분리 후 색상 일관성)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.1.5에서 도입한 Footer v{APP_VERSION} 링크가 footer 컨테이너 내부 흐름 위치라
스크롤 시 함께 흘러갔다. 사용자 의도는 viewport 좌하단에 항상 떠있는 build badge
(페이지 기준 fixed). <a> 링크를 <footer> 외부 fragment로 분리하고 fixed
bottom-4 left-4 z-30 opacity-50 hover:opacity-100로 viewport 기준 좌하단 고정.
모든 페이지에서 항상 노출되며 GitHub Release tag로 직접 연결.

z-30이라 헤더(z-40)·모달(z-50)보다 낮음 → 자연스럽게 가려짐.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build version badge viewport fixed positioning hotfix.
v1.1.5 직후 사용자 검수 결과 즉시 hotfix.

상세는 CHANGELOG.md [1.1.6] 섹션 참조.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nflict develop 측 채택

origin/main의 18c81af(v1.1.5 squash merge #33)을 develop에 merge.
develop은 v1.1.6 hotfix(Footer fixed bottom-left + CHANGELOG [1.1.6] + package.json 1.1.6 bump)로
main의 모든 의도(v1.1.5)를 superset으로 포함하므로 3개 conflict 파일 모두 develop 측 채택.

PR #32, PR #31 흡수와 동일 패턴.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.1.6의 viewport fixed bottom-left badge가 모바일 화면에서 시각 noise.
모바일은 좁은 화면 + 한 손 조작 + content 우선이라 fixed badge 거슬림.
className에 hidden md:inline-block 추가 → 모바일·태블릿(<768px) 숨김,
PC(md ≥ 768px)에서만 노출.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- useViewMode.ts: getSnapshot·getServerSnapshot default를 "grid"로 전환
- PostList.tsx: effectiveView fallback "grid", 첫 카드 motion.div layout wrap
  (variants 없이 layout prop만 → initial 애니메이션 없이 layout 보간만 적용,
  view 전환 시 jump 차단 + LCP 보호 유지)
- ViewToggle.tsx: effectiveView fallback "grid" (PostList와 정합)

기본값 grid 선택 근거: 21개 색인 포스트 + Popular sidebar 카드 패턴 +
모바일·태블릿 thumbnail 강조 일관성. list 사용자는 localStorage 명시 시 유지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 슬라이드 전환 (Added):
  framer-motion AnimatePresence custom={direction} + slide+fade variants.
  onNext/onPrev/키보드 ←/→ wrapper handler로 direction(+1/-1) 추적,
  mode="wait" + duration 0.25s + EASE_OUT.

- 적응형 크기 (Fixed):
  <Image fill> + h-lightbox w-lightbox 컨테이너로 작은 이미지가 90vh×90vw로
  늘어나 픽셀 깨지던 문제. width={0} height={0} sizes="100vw" +
  w-auto h-auto max-w-lightbox max-h-lightbox로 변경 → 작은 이미지는 자연 크기,
  큰 이미지만 90vh/90vw 한도 내 축소.

- max-h-lightbox·max-w-lightbox utility 신규 (globals.css):
  h-lightbox·w-lightbox(고정 90vh/90vw)의 max 변형. styling.md
  arbitrary value 회피 룰 정합.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UX 정합성 hotfix bundle (5건 통합):
- Footer 버전 배지 모바일 숨김 (md+ 한정 노출)
- PostList 기본 뷰 모드 grid 전환
- list↔grid 전환 시 첫 카드 layout 보간 일관성 회복
- ImageLightbox 좌우 슬라이드 애니메이션
- ImageLightbox 작은 이미지 깨짐 차단 (적응형 크기)

상세는 CHANGELOG.md [1.1.7] 섹션 참조.

Ralph QA cycle 1 (2026-05-07) PASS — _workspace/verification/ralph_qa_2026-05-07_cycle_1.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

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

Project Deployment Actions Updated (UTC)
chan9yu-blog Ready Ready Preview, Comment May 7, 2026 1:54pm

…nflict develop 측 채택

origin/main의 51d8f6e(v1.1.6 squash merge #34)을 develop에 merge.
develop은 v1.1.7 hotfix bundle(Footer mobile hide + PostList grid + Lightbox 슬라이드/적응형 + max-h/w-lightbox utility)로
main의 모든 의도(v1.1.6)를 superset으로 포함하므로 3개 conflict 파일 모두 develop 측 채택.

PR #32, PR #33, PR #34 흡수와 동일 패턴. 본 commit은 PR #35 conflict 사전 해소용 (사용자 명시 요청 — 반복 실수 차단).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chan9yu chan9yu merged commit 0381a9e into main May 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant