Skip to content

[Feat] #84 - 공통 Footer 컴포넌트 구현 및 Modal 상태 동기화 오류 수정#85

Merged
sebeeeen merged 6 commits into
devfrom
feat/footer
Apr 6, 2026
Merged

[Feat] #84 - 공통 Footer 컴포넌트 구현 및 Modal 상태 동기화 오류 수정#85
sebeeeen merged 6 commits into
devfrom
feat/footer

Conversation

@kimsman06
Copy link
Copy Markdown
Collaborator

@kimsman06 kimsman06 commented Apr 1, 2026

[Feat] #84 - 공통 Footer 컴포넌트 구현 및 Modal 상태 동기화 오류 수정

🔎 What is this PR?

서비스 전반의 레이아웃 일관성을 위한 공통 Footer 컴포넌트를 추가하고, 기존 Modal 컴포넌트의 스토리북 인터랙션 오류를
수정하여 안정성을 확보했습니다.


📝 Changes

  1. 공통 Footer 컴포넌트 구현 (src/shared/ui/footer)
  • 디자인 가이드 준수: 아주대학교 공식 로고(ajou-logo-text.svg) 및 운영 정보 스펙 반영
  • 레이아웃: 1440px 가로 너비 대응 및 mx-auto 중앙 정렬 (내부 여백 제거 가이드 준수)
  • 반응형 대응: 모바일 환경에서 정보 영역의 수직 정렬(flex-col) 및 간격 최적화
  • 정책 링크: 이용약관, 개인정보처리방침 등 필수 정책 링크 영역 구성
  1. Modal 컴포넌트 버그 수정 및 최적화
  • 스토리북 상태 동기화 해결: isOpen 속성 판단 시 발생하던 Boolean 단축 평가 이슈(??)를 수정하여 '열기' 버튼과 스토리북
    컨트롤이 모두 정상 작동하도록 개선
  • 웹 접근성(A11y) 강화:
    • useId 기반 고유 ID 생성 및 aria-labelledby 연동
    • ModalHeader의 제목(h2)과 컨테이너 간의 관계 명시
  • 중첩 모달 및 스크롤 잠금:
    • 전역 카운터(modalOpenCount)를 도입하여 여러 모달이 열려도 마지막 모달이 닫힐 때만 스크롤 잠금이 해제되도록 수정
    • 기존 body 스타일을 보존하는 안전한 스크롤 잠금 로직 적용
  1. 컴포넌트 통합 및 문서화
  • shared/ui/index.ts에 Footer 내보내기 추가
  • Storybook 내 Footer 기본 시나리오 및 Modal의 고도화된 인터랙션 예제(필터, 프로필 편집) 등록

📸 Screenshots

  • Storybook 내 다음 항목 확인 가능:
    • Shared/UI/Footer/Default: 아주대 테마가 적용된 공통 푸터
    • Shared/UI/Modal/FilterModal: 버튼 인터랙션이 복구된 필터 모달 예시

📚 Background / Context

  • 서비스 하단 영역의 표준화가 필요하여 Footer를 공통 UI로 구축함.
  • 모달 컴포넌트 배포 전, 실제 사용 시나리오(중첩 모달, 접근성, 스토리북 제어)에서 발견된 기술적 부채를 해결하여 품질을
    높임.

✔ Checklist

  • 코드는 로컬에서 정상적으로 빌드됩니다 (pnpm build)
  • ESLint / Prettier 통과 (pnpm lint)
  • 네이밍/레이어 컨벤션 준수 (camelCase/PascalCase, is·has 불린 접두사, alias 계층 규칙)
  • 관련 문서/주석 반영 (필요 시)
  • 주요 로직에 테스트 또는 검증 완료 (Storybook 확인)

Summary by CodeRabbit

  • New Features

    • Added a Footer component with customizable links, address, phone, and copyright text.
  • Documentation

    • Added Storybook examples showcasing the Footer (default, custom links/info, and on-white background).
  • Chores

    • Expanded public UI exports to include the new Footer and related items.
  • Tests

    • Updated modal stories to require explicit open/close props for clearer behavior.

@kimsman06 kimsman06 self-assigned this Apr 1, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b885d082-e16c-4f65-90eb-82645d842675

📥 Commits

Reviewing files that changed from the base of the PR and between 2a3d882 and 1fbabf4.

📒 Files selected for processing (2)
  • src/shared/ui/footer/footer.stories.tsx
  • src/shared/ui/footer/footer.tsx
✅ Files skipped from review due to trivial changes (2)
  • src/shared/ui/footer/footer.stories.tsx
  • src/shared/ui/footer/footer.tsx

📝 Walkthrough

Walkthrough

Adds a new Footer component with types and Storybook stories, re-exports the Footer and expands shared UI exports, and tightens prop types in two modal story components.

Changes

Cohort / File(s) Summary
Footer component
src/shared/ui/footer/footer.tsx
Added Footer component plus exported FooterLink and FooterProps types, default data, Tailwind classes, Next/Image usage, and Footer.displayName.
Footer stories
src/shared/ui/footer/footer.stories.tsx
Added Storybook meta and four stories: Default, CustomLinks, CustomInfo, OnWhiteBackground (background override).
Module re-exports
src/shared/ui/footer/index.ts, src/shared/ui/index.ts
Re-exported Footer, FooterProps, FooterLink; expanded src/shared/ui/index.ts export surface (added export * for form, checkbox, file-uploader, modal).
Modal stories (types)
src/shared/ui/modal/modal.stories.tsx
Updated story-local prop types for FilterModalExample and ProfileEditModalExample to make isOpen and onClose required (removed optionality).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jaeu5325
  • kimsman06

Poem

🐇 I planted links where footers grow,
A logo hops, the stories show,
Types tightened up, the modals sing,
Small changes make the UI spring,
Hop, celebrate — the code will glow!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: adding a shared Footer component and fixing Modal state synchronization issues, matching the primary objectives.
Description check ✅ Passed The description follows the template with all major sections completed: objectives, detailed changes, screenshots, background context, and a fully checked verification checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/footer

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
Contributor

@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 (3)
src/shared/ui/index.ts (1)

8-9: Consider exporting from the barrel file for consistency.

The export uses "./footer/footer" directly, but there's an index.ts barrel file at ./footer. For consistency with the module structure, consider importing from the barrel:

♻️ Proposed change
-export { Footer } from "./footer/footer";
-export type { FooterProps, FooterLink } from "./footer/footer";
+export { Footer } from "./footer";
+export type { FooterProps, FooterLink } from "./footer";

That said, the current approach also works and matches patterns seen elsewhere in this file (e.g., ./pagination/pagination).

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

In `@src/shared/ui/index.ts` around lines 8 - 9, The export lines for Footer
(export { Footer } and export type { FooterProps, FooterLink }) should import
from the footer barrel instead of the nested file; update the exports to
reference the barrel module ("./footer") by changing the module specifier used
for the Footer, FooterProps, and FooterLink exports so they come from "./footer"
rather than "./footer/footer" to keep module resolution consistent with other
exports.
src/shared/ui/footer/footer.tsx (2)

68-72: Consider using a more robust key for link mapping.

Using link.href as a key assumes all hrefs are unique. While unlikely to be an issue for footer links, using an index or combining label+href would be more defensive.

♻️ Proposed change
-                {links.map((link) => (
-                  <a key={link.href} href={link.href} className={linkClasses}>
+                {links.map((link, index) => (
+                  <a key={`${link.href}-${index}`} href={link.href} className={linkClasses}>
                     {link.label}
                   </a>
                 ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/footer/footer.tsx` around lines 68 - 72, The current links.map
uses link.href as the React key which risks collisions; update the key in the
mapping inside the footer component (the links.map callback where link.href,
link.label and linkClasses are used) to a more robust unique value—preferably a
stable identifier such as link.id if available, otherwise combine label and href
(e.g. `${link.href}-${link.label}`) or fall back to the map index as last
resort—to ensure stable keys for the rendered <a> elements.

57-61: Consider making the logo path configurable or using an import.

The hard-coded path /assets/ajou-logo-text.svg assumes a specific public asset structure. Consider either:

  1. Adding a logoSrc prop with a default value for flexibility
  2. Importing the SVG directly to ensure build-time resolution
♻️ Option: Add logoSrc prop
 export type FooterProps = {
   links?: FooterLink[];
   address?: string;
   phone?: string;
   copyright?: string;
   className?: string;
+  logoSrc?: string;
+  logoAlt?: string;
 };

 export const Footer = ({
   links = defaultLinks,
   address = "16499 경기도 수원시 영통구 월드컵로 206 아주대학교",
   phone = "T. 031-219-2114",
   copyright = "© 2025 AJOU University. All rights reserved.",
   className,
+  logoSrc = "/assets/ajou-logo-text.svg",
+  logoAlt = "Ajou University Logo",
 }: FooterProps) => {
   // ...
   <img
-    src="/assets/ajou-logo-text.svg"
-    alt="Ajou University Logo"
+    src={logoSrc}
+    alt={logoAlt}
     className="w-full h-full object-contain"
   />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/footer/footer.tsx` around lines 57 - 61, The Footer component
currently uses a hard-coded image path "/assets/ajou-logo-text.svg" which
couples it to a specific public asset layout; modify the Footer component in
footer.tsx to accept a new prop logoSrc (with default value
"/assets/ajou-logo-text.svg") and use that prop for the img src, or
alternatively import the SVG at the top (e.g., import AjouLogo from '...') and
replace the src with the imported symbol; update the Footer props/interface and
any Footer usage sites to pass logoSrc where needed to enable
configurable/build-time-resolved logos.
🤖 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/shared/ui/footer/footer.tsx`:
- Around line 56-62: The footer component uses non-standard Tailwind classes
"h-15" and "w-58" which won't be generated; update the element in
src/shared/ui/footer/footer.tsx (the div wrapping the logo image) to use valid
Tailwind arbitrary values like h-[60px] and w-[232px], or alternatively add
these sizes to your tailwind.config.js theme if you prefer named utilities;
ensure the img container's className is updated so the logo renders with the
intended dimensions.

In `@src/shared/ui/modal/modal.stories.tsx`:
- Around line 29-40: Replace the component-managed open-state in
FilterModalExample and ProfileEditModalExample with Storybook's single source of
truth via useArgs: remove the useState/useEffect pair that mirror propsIsOpen
and stop calling the no-op propsOnClose; instead call useArgs (from
`@storybook/preview-api`) in the story render, derive isOpen from args, and update
args (setArgs) in the handleOpen/handleClose handlers so Controls reflect
changes immediately; update the story definitions to pass the args handlers into
the examples and remove duplicated local state logic from the example
components.

---

Nitpick comments:
In `@src/shared/ui/footer/footer.tsx`:
- Around line 68-72: The current links.map uses link.href as the React key which
risks collisions; update the key in the mapping inside the footer component (the
links.map callback where link.href, link.label and linkClasses are used) to a
more robust unique value—preferably a stable identifier such as link.id if
available, otherwise combine label and href (e.g. `${link.href}-${link.label}`)
or fall back to the map index as last resort—to ensure stable keys for the
rendered <a> elements.
- Around line 57-61: The Footer component currently uses a hard-coded image path
"/assets/ajou-logo-text.svg" which couples it to a specific public asset layout;
modify the Footer component in footer.tsx to accept a new prop logoSrc (with
default value "/assets/ajou-logo-text.svg") and use that prop for the img src,
or alternatively import the SVG at the top (e.g., import AjouLogo from '...')
and replace the src with the imported symbol; update the Footer props/interface
and any Footer usage sites to pass logoSrc where needed to enable
configurable/build-time-resolved logos.

In `@src/shared/ui/index.ts`:
- Around line 8-9: The export lines for Footer (export { Footer } and export
type { FooterProps, FooterLink }) should import from the footer barrel instead
of the nested file; update the exports to reference the barrel module
("./footer") by changing the module specifier used for the Footer, FooterProps,
and FooterLink exports so they come from "./footer" rather than
"./footer/footer" to keep module resolution consistent with other exports.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49367914-1847-4a48-930b-b408a372253b

📥 Commits

Reviewing files that changed from the base of the PR and between 37630fd and da69fd6.

⛔ Files ignored due to path filters (1)
  • public/assets/ajou-logo-text.svg is excluded by !**/*.svg
📒 Files selected for processing (5)
  • src/shared/ui/footer/footer.stories.tsx
  • src/shared/ui/footer/footer.tsx
  • src/shared/ui/footer/index.ts
  • src/shared/ui/index.ts
  • src/shared/ui/modal/modal.stories.tsx

Comment thread src/shared/ui/footer/footer.tsx Outdated
Comment on lines +56 to +62
<div className="shrink-0 h-15 w-58">
<img
src="/assets/ajou-logo-text.svg"
alt="Ajou University Logo"
className="w-full h-full object-contain"
/>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if custom spacing is defined in Tailwind config or CSS theme
fd -e css -e config.js -e config.ts | xargs rg -l 'h-15|w-58|spacing' 2>/dev/null || echo "No custom spacing config found"

# Check if these classes are used elsewhere in the codebase
rg -n 'h-15|w-58' --type tsx --type ts

Repository: ajou-industry-matching/aim-frontend

Length of output: 154


🏁 Script executed:

# Find Tailwind configuration files
fd -e 'js' -e 'ts' | grep -i 'tailwind' | head -20

Repository: ajou-industry-matching/aim-frontend

Length of output: 97


🏁 Script executed:

# Search for h-15 and w-58 using correct rg syntax (tsx files are matched with --type ts)
rg -n 'h-15|w-58' --type ts

Repository: ajou-industry-matching/aim-frontend

Length of output: 162


🏁 Script executed:

# Find and check Tailwind config file location
find . -maxdepth 2 -name "tailwind.config.*" -o -name "tailwind.css"

Repository: ajou-industry-matching/aim-frontend

Length of output: 99


🏁 Script executed:

# Check if arbitrary value syntax is used elsewhere for sizing
rg 'h-\[|w-\[' --type ts | head -20

Repository: ajou-industry-matching/aim-frontend

Length of output: 2954


🏁 Script executed:

cat ./tailwind.config.js

Repository: ajou-industry-matching/aim-frontend

Length of output: 201


Use arbitrary values for non-standard spacing classes.

h-15 and w-58 are not part of Tailwind's default spacing scale and are not defined in your theme. Based on the config, these classes won't be generated by Tailwind. Use h-[60px] and w-[232px] instead, or configure custom spacing in your tailwind.config.js theme if these values are used frequently elsewhere.

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

In `@src/shared/ui/footer/footer.tsx` around lines 56 - 62, The footer component
uses non-standard Tailwind classes "h-15" and "w-58" which won't be generated;
update the element in src/shared/ui/footer/footer.tsx (the div wrapping the logo
image) to use valid Tailwind arbitrary values like h-[60px] and w-[232px], or
alternatively add these sizes to your tailwind.config.js theme if you prefer
named utilities; ensure the img container's className is updated so the logo
renders with the intended dimensions.

Comment thread src/shared/ui/modal/modal.stories.tsx Outdated
Comment on lines +29 to +40
const [isOpen, setIsOpen] = useState(propsIsOpen);

// 스토리북 args(propsIsOpen)가 변경될 때 내부 상태 동기화
useEffect(() => {
setIsOpen(propsIsOpen);
}, [propsIsOpen]);

const handleOpen = () => setIsOpen(true);
const handleClose = () => {
setIsOpen(false);
propsOnClose(); // 스토리북 상태에도 알림
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify dual-source-of-truth pattern still exists in this story file.
rg -n "useArgs|const \\[isOpen, setIsOpen\\] = useState\\(propsIsOpen\\)|setIsOpen\\(false\\);\\s*propsOnClose\\(" src/shared/ui/modal/modal.stories.tsx

Repository: ajou-industry-matching/aim-frontend

Length of output: 191


🏁 Script executed:

#!/bin/bash
# View the actual code structure around the first occurrence (lines 20-50)
echo "=== Lines 20-50 (FilterModalExample) ==="
sed -n '20,50p' src/shared/ui/modal/modal.stories.tsx

echo ""
echo "=== Lines 100-130 (ProfileEditModalExample) ==="
sed -n '100,130p' src/shared/ui/modal/modal.stories.tsx

echo ""
echo "=== Check for useArgs import ==="
rg -n "useArgs" src/shared/ui/modal/modal.stories.tsx

Repository: ajou-industry-matching/aim-frontend

Length of output: 1877


🏁 Script executed:

#!/bin/bash
# Check the story definitions and their args
echo "=== FilterModal story definition ==="
rg -n -A 10 "export const FilterModal.*=" src/shared/ui/modal/modal.stories.tsx

echo ""
echo "=== ProfileEditModal story definition ==="
rg -n -A 10 "export const ProfileEditModal.*=" src/shared/ui/modal/modal.stories.tsx

Repository: ajou-industry-matching/aim-frontend

Length of output: 652


Storybook control state can drift from modal state due to dual sources of truth.

Both FilterModalExample and ProfileEditModalExample manage isOpen in local state while also receiving it via args. When closing via the modal button, the local state updates to false but the story args remain unchanged (since onClose is a no-op () => {}). This creates a desynchronization where Controls show a stale value while the UI reflects a different state.

Consider using useArgs from @storybook/preview-api in the story's render function to update Controls directly, eliminating the dual source of truth:

Suggested pattern (single source of truth via Storybook args)
+import { useArgs } from "@storybook/preview-api";

 export const FilterModal: Story = {
-  render: (args) => <FilterModalExample {...args} />,
+  render: (args) => {
+    const [{ isOpen }, updateArgs] = useArgs();
+    const handleOpen = () => updateArgs({ isOpen: true });
+    const handleClose = () => {
+      updateArgs({ isOpen: false });
+      args.onClose?.();
+    };
+
+    return (
+      <>
+        <Button onClick={handleOpen}>필터 모달 열기</Button>
+        <FilterModalExample {...args} isOpen={isOpen} onClose={handleClose} />
+      </>
+    );
+  },
   args: {
     isOpen: false,
     onClose: () => {},
     children: null,
   },
 };

Apply the same pattern to ProfileEditModal and remove the duplicated useState/useEffect logic from the example components.

Affects: FilterModalExample (lines 22–40), ProfileEditModalExample (lines 102–126), and their corresponding story definitions (lines 91–98, 173–180).

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

In `@src/shared/ui/modal/modal.stories.tsx` around lines 29 - 40, Replace the
component-managed open-state in FilterModalExample and ProfileEditModalExample
with Storybook's single source of truth via useArgs: remove the
useState/useEffect pair that mirror propsIsOpen and stop calling the no-op
propsOnClose; instead call useArgs (from `@storybook/preview-api`) in the story
render, derive isOpen from args, and update args (setArgs) in the
handleOpen/handleClose handlers so Controls reflect changes immediately; update
the story definitions to pass the args handlers into the examples and remove
duplicated local state logic from the example components.

@kimsman06 kimsman06 linked an issue Apr 4, 2026 that may be closed by this pull request
5 tasks
sebeeeen added 2 commits April 6, 2026 22:09
- shared/ui/index.ts: Footer 내보내기(feat/footer) + Tabs 내보내기(dev) 모두 유지
- modal.stories.tsx: Next.js 스토리북 import 및 controlled/uncontrolled 패턴(dev) 적용,
  handleClose 참조를 onClose로 통일
- next/image, next/link로 교체 (img, a 태그 제거)
- footer.stories.tsx storybook import @storybook/nextjs로 변경
- 하드코딩된 기본값 상수로 분리
@sebeeeen sebeeeen merged commit b19479f into dev Apr 6, 2026
1 check passed
@sebeeeen sebeeeen deleted the feat/footer branch April 6, 2026 13:13
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.

[Feat] 공통 푸터(Footer) 컴포넌트 구현

2 participants