Skip to content

Comments

refactor: 레이아웃 리팩토링#88

Merged
mgYang53 merged 2 commits intodevelopfrom
fix/refactor-layout-83
Feb 19, 2026
Merged

refactor: 레이아웃 리팩토링#88
mgYang53 merged 2 commits intodevelopfrom
fix/refactor-layout-83

Conversation

@mgYang53
Copy link
Contributor

@mgYang53 mgYang53 commented Feb 19, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

CSS viewport 핵(100dvh 강제 지정 등)을 제거하고, FullWidthLayout 기반으로 서브 페이지들의 레이아웃 구조를 리팩토링했습니다. 공통 헤더 컴포넌트를 추출하고 스크롤 shadow 로직을 커스텀 훅으로 통합했습니다.

🔧 변경 사항

  • 9개 라우트를 MainLayout → FullWidthLayout으로 이동 (BookDetail, BookReviewHistory, MeetingDetail, MeetingCreate, MeetingSetting, GatheringSetting, TopicCreate, PreOpinionList, Retrospective 등)
  • SubPageHeader / FormPageHeader 공통 컴포넌트 추출 및 각 페이지에 적용
  • useScrollShadow 커스텀 훅으로 SubPageHeader, FormPageHeader, LandingLayout의 중복 스크롤 shadow 로직 통합
  • sticky 헤더 그림자 값을 shadow-drop-bottom으로 통일 (BookLogList, LandingLayout 등)
  • Retrospective 페이지 CSS 변수 버그 수정 (--gnb-height--spacing-gnb-height)

📄 기타

관련 이슈: #83

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 스크롤 시 헤더에 그림자 효과 추가로 시각적 계층감 개선
    • 새로운 폼 페이지 헤더 컴포넌트로 일관성 있는 헤더 UI 제공
  • Style

    • 전체 페이지 레이아웃 및 간격 조정으로 더 균형 잡힌 디자인
    • 헤더 스타일 및 배치 개선
  • Chores

    • 라우팅 구조 재구성으로 페이지 로드 최적화

FullWidthLayout으로 9개 페이지 전환하여 뷰포트 핵 제거.
SubPageHeader/FormPageHeader 공유 컴포넌트로 뒤로가기 헤더 통일.
useScrollShadow 훅 추출로 스크롤 shadow 로직 DRY 개선.
shadow 값을 shadow-drop-bottom으로 전체 통일.
회고 페이지 CSS 변수 버그(--gnb-height) 수정 및 콘텐츠 래퍼 추가.
라우트 중복 경로 제거.
@mgYang53 mgYang53 linked an issue Feb 19, 2026 that may be closed by this pull request
23 tasks
@mgYang53 mgYang53 changed the title refactor: 레이아웃 리팩토링 및 CSS 핵 제거 (#83) refactor: 레이아웃 리팩토링 Feb 19, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Walkthrough

여러 페이지의 레이아웃을 재구성하고, FormPageHeader와 SubPageHeader를 도입하여 일관된 헤더 처리를 구현했습니다. 레이아웃 컨테이너(mx-auto, max-w-layout-max, px-layout-padding)를 추가하고, 새로운 useScrollShadow 훅으로 스크롤 기반 그림자 처리를 통합하며, 라우트 트리를 재조정하여 페이지를 적절한 레이아웃 아래로 이동시켰습니다.

Changes

Cohort / File(s) Summary
페이지 헤더 교체
src/pages/Meetings/MeetingCreatePage.tsx, src/pages/Topics/TopicCreatePage.tsx
FormPageHeader로 기존 인라인 헤더 교체; 스티키 컨테이너 제거 및 폼 제출 흐름을 헤더 컴포넌트로 통합.
페이지 레이아웃 재조정
src/pages/Books/BookDetailPage.tsx, src/pages/Books/BookReviewHistoryPage.tsx, src/pages/Meetings/MeetingDetailPage.tsx, src/pages/Meetings/MeetingSettingPage.tsx, src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx, src/pages/Retrospectives/MeetingRetrospectivePage.tsx, src/pages/PreOpinions/PreOpinionListPage.tsx
레이아웃 인식 컨테이너(mx-auto, max-w-layout-max, px-layout-padding) 추가로 페이지 콘텐츠 래핑.
컴포넌트 스타일링
src/features/book/components/BookLogList.tsx
섀도우 클래스 업데이트(shadow-drop-bottom) 및 전체 너비 핸들링 제거.
라우트 트리 재구성
src/routes/index.tsx
페이지들을 MainLayout과 FullWidthLayout 간 재배치; 중첩 구조 평탄화.
공유 훅 추가
src/shared/hooks/useScrollShadow.ts, src/shared/hooks/index.ts
스크롤 상태 추적 커스텀 훅 신규 추가 및 내보내기.
공유 컴포넌트
src/shared/components/FormPageHeader.tsx, src/shared/components/SubPageHeader.tsx, src/shared/layout/LandingLayout.tsx
FormPageHeader 신규 추가; SubPageHeader에 useScrollShadow 적용; LandingLayout 훅 기반 리팩토링.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

사유: 라우트 트리 재구성(레이아웃 할당 변경), 다수 페이지 간 일관성 있는 헤더/컨테이너 적용, 신규 훅 및 컴포넌트 추가, 변경 사항이 12개 이상 파일에 분산되어 있어 상호의존성 검증 필요.

Possibly related PRs

Suggested labels

refactor

Suggested reviewers

  • choiyoungae
  • haruyam15
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 주요 변경사항을 명확하게 반영합니다. 레이아웃 리팩토링이 이 PR의 핵심이며, raw_summary의 모든 파일 변경사항이 레이아웃 구조 재정렬 및 컴포넌트 추출과 관련됩니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/refactor-layout-83

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.

@mgYang53 mgYang53 self-assigned this Feb 19, 2026
@mgYang53 mgYang53 added refactor 코드 리팩토링 labels Feb 19, 2026
Copy link

@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: 4

🧹 Nitpick comments (6)
src/features/book/components/BookLogList.tsx (1)

177-178: CSS 핵 제거 방향 LGTM.

w-screen / left-1/2 / -translate-x-1/2 조합을 제거하고 bg-grey-100 + 내부 layout-constrained <section> 패턴으로 바꾼 것은 명확하고 일관성 있는 개선입니다.

한 가지 사소한 점: 컴포넌트 루트가 이미 <section> (line 112)인데 line 178에도 <section>이 중첩됩니다. 의미 구분이 명확하다면 무방하지만, 단순 레이아웃 컨테이너라면 <div>로 바꾸는 것도 고려해볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/book/components/BookLogList.tsx` around lines 177 - 178, In
BookLogList, the root is already a semantic <section> but you’ve added a nested
<section> with className="bg-grey-100" and an inner layout-constrained
<section>; if the inner element is only a layout container (no new semantic
grouping), replace the inner nested <section> with a <div> (the element that
currently has className="bg-grey-100" or the immediate child layout container)
to avoid semantic nesting; update the element around the "mx-auto
max-w-layout-max px-layout-padding py-xlarge" container accordingly while
keeping the same classNames and structure.
src/shared/components/FormPageHeader.tsx (1)

72-73: 뒤로가기 레이블 하드코딩 — SubPageHeader와의 불일치

SubPageHeaderlabel prop으로 뒤로가기 텍스트를 커스터마이징할 수 있지만, FormPageHeader"뒤로가기"가 고정되어 있습니다. 현재 사용처에선 문제없으나, 향후 다른 텍스트가 필요한 페이지에 쓸 경우 변경이 필요합니다.

♻️ backLabel prop 추가 제안
 export interface FormPageHeaderProps {
+  /** 뒤로가기 버튼 텍스트 (기본값: '뒤로가기') */
+  backLabel?: string
   title: string
   ...
 }

 export default function FormPageHeader({
+  backLabel = '뒤로가기',
   title,
   ...
 }: FormPageHeaderProps) {
   ...
-        <TextButton size="medium" icon={ChevronLeft} onClick={handleBack} className="py-small">
-          뒤로가기
+        <TextButton size="medium" icon={ChevronLeft} onClick={handleBack} className="py-small">
+          {backLabel}
         </TextButton>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/components/FormPageHeader.tsx` around lines 72 - 73,
FormPageHeader currently hardcodes the back button text ("뒤로가기") which creates
inconsistency with SubPageHeader's customizable label; update FormPageHeader to
accept an optional prop (e.g., backLabel) and pass it to the TextButton instead
of the literal string, defaulting to the existing "뒤로가기" when backLabel is
undefined, and ensure the prop is documented/typed in the FormPageHeader props
interface (reference FormPageHeader, TextButton, handleBack, and SubPageHeader
for consistent behavior).
src/shared/hooks/useScrollShadow.ts (1)

3-13: 훅 인스턴스마다 window 스크롤 리스너 중복 등록

현재 구조에서는 이 훅을 사용하는 컴포넌트가 동시에 마운트될 때마다 개별 scroll 리스너가 window에 추가됩니다. 예를 들어 SubPageHeader, FormPageHeader, LandingLayout이 모두 같은 컨텍스트에서 마운트될 경우 3개의 동일한 리스너가 등록됩니다.

passive 옵션 덕분에 현재 심각한 성능 문제는 아니지만, 훅 사용처가 늘어날수록 누적됩니다. 모듈 수준 싱글톤 구독 또는 React Context로 공유하는 방식을 고려해보세요.

♻️ 모듈 레벨 싱글톤 패턴 예시
-import { useEffect, useState } from 'react'
+import { useEffect, useState, useSyncExternalStore } from 'react'
+
+let listeners: (() => void)[] = []
+let isScrolled = false
+
+function getSnapshot() { return isScrolled }
+function subscribe(cb: () => void) {
+  const handler = () => {
+    const next = window.scrollY > 0
+    if (next !== isScrolled) { isScrolled = next; listeners.forEach((l) => l()) }
+  }
+  if (listeners.length === 0) window.addEventListener('scroll', handler, { passive: true })
+  listeners.push(cb)
+  return () => {
+    listeners = listeners.filter((l) => l !== cb)
+    if (listeners.length === 0) window.removeEventListener('scroll', handler)
+  }
+}
 
 export function useScrollShadow() {
-  const [isScrolled, setIsScrolled] = useState(false)
-
-  useEffect(() => {
-    const handleScroll = () => setIsScrolled(window.scrollY > 0)
-    handleScroll()
-    window.addEventListener('scroll', handleScroll, { passive: true })
-    return () => window.removeEventListener('scroll', handleScroll)
-  }, [])
-
-  return isScrolled
+  return useSyncExternalStore(subscribe, getSnapshot)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/hooks/useScrollShadow.ts` around lines 3 - 13, The hook
useScrollShadow currently adds a separate window 'scroll' listener per instance
(handleScroll) causing duplicate listeners; refactor to a module-level singleton
subscription that maintains the current scroll state and a set of subscriber
callbacks so only one window listener is ever attached. Implement a
module-scoped store (e.g., currentIsScrolled boolean and a Set of subscriber
functions) and a single handleScroll function that updates currentIsScrolled and
notifies subscribers; then change useScrollShadow to subscribe its setIsScrolled
on mount and unsubscribe on unmount, initializing the listener only when the
first subscriber registers and removing it when the last unsubscribes.
src/routes/index.tsx (1)

97-100: 라우트 패턴 정의 방식이 일관되지 않습니다.

ROUTES.PRE_OPINIONS(':gatheringId', ':meetingId')는 네비게이션용 헬퍼 함수를 경로 패턴 정의에 사용하고 있습니다. 동일 블록 내 다른 라우트들은 모두 인라인 템플릿 리터럴을 사용 중입니다.

→ 동작에는 문제없으나, 패턴이 섞이면 유지보수 시 혼란을 줄 수 있습니다.

제안
-              {
-                path: ROUTES.PRE_OPINIONS(':gatheringId', ':meetingId'),
-                element: <PreOpinionListPage />,
-              },
+              {
+                path: `${ROUTES.GATHERINGS}/:gatheringId/meetings/:meetingId/pre-opinions`,
+                element: <PreOpinionListPage />,
+              },

Based on learnings: "In React Router route definitions, define path patterns inline instead of using ROUTES.() helper functions. ROUTES.() are intended for generating actual URLs during navigation."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/index.tsx` around lines 97 - 100, The route path uses the
navigation helper ROUTES.PRE_OPINIONS(':gatheringId', ':meetingId') instead of
an inline route pattern, which mixes conventions; update the route definition
for PreOpinionListPage to use an inline path pattern (i.e. the literal route
string with :gatheringId and :meetingId parameters) so it matches the other
routes' style and keep ROUTES.*() reserved for URL generation during navigation.
src/pages/Meetings/MeetingDetailPage.tsx (1)

89-114: 좌측 컬럼 w-[300px]이 모바일에서 레이아웃을 깨뜨릴 수 있습니다.

flex justify-between gap-[36px]w-[300px] flex-none 조합은 데스크탑에선 문제없지만, 뷰포트가 좁아지면 우측 영역이 극도로 압축됩니다. 반응형 breakpoint(md:flex-row flex-col 등)를 검토해보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Meetings/MeetingDetailPage.tsx` around lines 89 - 114, The left
column's fixed width ("w-[300px] flex-none") inside the container div ("flex
justify-between gap-[36px]") breaks narrow viewports; update the parent layout
to be responsive (e.g., switch to column on small screens and row on md+) and
make the left column responsive (e.g., md:w-[300px] w-full and md:flex-none
flex-auto or similar) so the right content can shrink naturally; adjust gap
classes to responsive variants as well. Edit the container div around the
columns and the left column div that renders MeetingDetailHeader,
MeetingDetailInfo, and MeetingDetailButton to apply these responsive class
changes.
src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx (1)

44-62: 두 패널 레이아웃이 모바일 반응형을 고려하지 않고 있습니다.

flex gap-medium으로 두 패널을 나란히 배치하는데, 작은 화면에서 패널이 너무 좁아질 수 있습니다. md:flex-row flex-col 등의 반응형 처리를 추가하면 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx` around lines 44
- 62, The two-panel layout currently uses a single non-responsive flex container
(the outer div with class "mx-auto max-w-layout-max px-layout-padding flex
gap-medium mt-base") and fixed side panels (the two child divs with "flex flex-1
..."); update the outer container to switch to column layout on small screens
and row on medium+ (e.g., use responsive utility ordering like "flex flex-col
md:flex-row" or "md:flex-row flex-col") so the panels stack on mobile, and keep
the child panels as flexible ("flex-1") so they fill available width; optionally
add responsive spacing (e.g., "md:gap-medium" vs "gap-medium") to control gaps
per breakpoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/book/components/BookLogList.tsx`:
- Line 114: The sticky container in BookLogList uses a hardcoded top value
(`top-[108px]`) which should be changed to reference the project CSS variable
pattern used elsewhere; update the class on the div in BookLogList to use
`top-[calc(var(--spacing-gnb-height)+59px)]` (matching MeetingRetrospectivePage
pattern) so GNB height changes are respected, and consider extracting the 44px
SubPageHeader constant into a design token/variable if that value is a shared
component height.

In `@src/pages/Meetings/MeetingDetailPage.tsx`:
- Around line 84-87: The useParams generic is incorrect and the non-null
assertion on gatheringId is unsafe; change useParams to use the default (no
generic) so params are typed as string | undefined, then add an explicit guard
like in MeetingSettingPage that returns/redirects (or shows a loader) when
gatheringId or meetingId is undefined before rendering SubPageHeader; remove the
trailing ! from ROUTES.GATHERING_DETAIL(gatheringId!) and only call
ROUTES.GATHERING_DETAIL with the validated gatheringId, and ensure any code that
uses meeting (e.g., meeting?.gathering.gatheringName) still handles meeting
being undefined.

In `@src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx`:
- Line 21: The hardcoded 59px magic number in the sticky container top calc
inside MeetingRetrospectiveCreatePage.tsx should be replaced with a CSS variable
(e.g. --spacing-subpage-header-height) or moved into the SubPageHeader
component; update the div with className "sticky
top-[calc(var(--spacing-gnb-height)+59px)] ..." to use
"top-[calc(var(--spacing-gnb-height)+var(--spacing-subpage-header-height))]" (or
relocate the second sticky into the SubPageHeader component), and then
add/declare --spacing-subpage-header-height in theme.css (or your theme token
file) so the offset is driven by the SubPageHeader height instead of a hardcoded
value.

In `@src/pages/Retrospectives/MeetingRetrospectivePage.tsx`:
- Around line 65-66: The sticky header uses a hardcoded "59px" magic number in
the top calc (top-[calc(var(--spacing-gnb-height)+59px)]) that implicitly
depends on SubPageHeader height; change this to use a shared CSS variable or an
explicit height class on SubPageHeader so the value is maintained in one place.
Update SubPageHeader (component name: SubPageHeader) to either define/emit a CSS
variable like --spacing-sub-header-height or add a fixed h-* Tailwind class, and
replace the 59px in MeetingRetrospectivePage's sticky container with
var(--spacing-sub-header-height) (i.e. top calc should reference
var(--spacing-sub-header-height) instead of 59px). Ensure both places are
updated so layout stays consistent when padding/font sizes change.

---

Nitpick comments:
In `@src/features/book/components/BookLogList.tsx`:
- Around line 177-178: In BookLogList, the root is already a semantic <section>
but you’ve added a nested <section> with className="bg-grey-100" and an inner
layout-constrained <section>; if the inner element is only a layout container
(no new semantic grouping), replace the inner nested <section> with a <div> (the
element that currently has className="bg-grey-100" or the immediate child layout
container) to avoid semantic nesting; update the element around the "mx-auto
max-w-layout-max px-layout-padding py-xlarge" container accordingly while
keeping the same classNames and structure.

In `@src/pages/Meetings/MeetingDetailPage.tsx`:
- Around line 89-114: The left column's fixed width ("w-[300px] flex-none")
inside the container div ("flex justify-between gap-[36px]") breaks narrow
viewports; update the parent layout to be responsive (e.g., switch to column on
small screens and row on md+) and make the left column responsive (e.g.,
md:w-[300px] w-full and md:flex-none flex-auto or similar) so the right content
can shrink naturally; adjust gap classes to responsive variants as well. Edit
the container div around the columns and the left column div that renders
MeetingDetailHeader, MeetingDetailInfo, and MeetingDetailButton to apply these
responsive class changes.

In `@src/pages/Retrospectives/MeetingRetrospectiveCreatePage.tsx`:
- Around line 44-62: The two-panel layout currently uses a single non-responsive
flex container (the outer div with class "mx-auto max-w-layout-max
px-layout-padding flex gap-medium mt-base") and fixed side panels (the two child
divs with "flex flex-1 ..."); update the outer container to switch to column
layout on small screens and row on medium+ (e.g., use responsive utility
ordering like "flex flex-col md:flex-row" or "md:flex-row flex-col") so the
panels stack on mobile, and keep the child panels as flexible ("flex-1") so they
fill available width; optionally add responsive spacing (e.g., "md:gap-medium"
vs "gap-medium") to control gaps per breakpoint.

In `@src/routes/index.tsx`:
- Around line 97-100: The route path uses the navigation helper
ROUTES.PRE_OPINIONS(':gatheringId', ':meetingId') instead of an inline route
pattern, which mixes conventions; update the route definition for
PreOpinionListPage to use an inline path pattern (i.e. the literal route string
with :gatheringId and :meetingId parameters) so it matches the other routes'
style and keep ROUTES.*() reserved for URL generation during navigation.

In `@src/shared/components/FormPageHeader.tsx`:
- Around line 72-73: FormPageHeader currently hardcodes the back button text
("뒤로가기") which creates inconsistency with SubPageHeader's customizable label;
update FormPageHeader to accept an optional prop (e.g., backLabel) and pass it
to the TextButton instead of the literal string, defaulting to the existing
"뒤로가기" when backLabel is undefined, and ensure the prop is documented/typed in
the FormPageHeader props interface (reference FormPageHeader, TextButton,
handleBack, and SubPageHeader for consistent behavior).

In `@src/shared/hooks/useScrollShadow.ts`:
- Around line 3-13: The hook useScrollShadow currently adds a separate window
'scroll' listener per instance (handleScroll) causing duplicate listeners;
refactor to a module-level singleton subscription that maintains the current
scroll state and a set of subscriber callbacks so only one window listener is
ever attached. Implement a module-scoped store (e.g., currentIsScrolled boolean
and a Set of subscriber functions) and a single handleScroll function that
updates currentIsScrolled and notifies subscribers; then change useScrollShadow
to subscribe its setIsScrolled on mount and unsubscribe on unmount, initializing
the listener only when the first subscriber registers and removing it when the
last unsubscribes.

Copy link
Contributor

@haruyam15 haruyam15 left a comment

Choose a reason for hiding this comment

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

UI라서 믿고 승인합니다~~~~~ 깨지면 1px당 100원 (코드 보긴 봤음)

@mgYang53 mgYang53 merged commit 364c84d into develop Feb 19, 2026
1 check passed
@mgYang53 mgYang53 deleted the fix/refactor-layout-83 branch February 19, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[refactor] 레이아웃 리팩토링

3 participants