[Feat] #84 - 공통 Footer 컴포넌트 구현 및 Modal 상태 동기화 오류 수정#85
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (2)
📝 WalkthroughWalkthroughAdds 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 anindex.tsbarrel 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.hrefas 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.svgassumes a specific public asset structure. Consider either:
- Adding a
logoSrcprop with a default value for flexibility- 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
⛔ Files ignored due to path filters (1)
public/assets/ajou-logo-text.svgis excluded by!**/*.svg
📒 Files selected for processing (5)
src/shared/ui/footer/footer.stories.tsxsrc/shared/ui/footer/footer.tsxsrc/shared/ui/footer/index.tssrc/shared/ui/index.tssrc/shared/ui/modal/modal.stories.tsx
| <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> |
There was a problem hiding this comment.
🧩 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 tsRepository: ajou-industry-matching/aim-frontend
Length of output: 154
🏁 Script executed:
# Find Tailwind configuration files
fd -e 'js' -e 'ts' | grep -i 'tailwind' | head -20Repository: 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 tsRepository: 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 -20Repository: ajou-industry-matching/aim-frontend
Length of output: 2954
🏁 Script executed:
cat ./tailwind.config.jsRepository: 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.
| const [isOpen, setIsOpen] = useState(propsIsOpen); | ||
|
|
||
| // 스토리북 args(propsIsOpen)가 변경될 때 내부 상태 동기화 | ||
| useEffect(() => { | ||
| setIsOpen(propsIsOpen); | ||
| }, [propsIsOpen]); | ||
|
|
||
| const handleOpen = () => setIsOpen(true); | ||
| const handleClose = () => { | ||
| setIsOpen(false); | ||
| propsOnClose(); // 스토리북 상태에도 알림 | ||
| }; |
There was a problem hiding this comment.
🧩 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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.
- 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로 변경 - 하드코딩된 기본값 상수로 분리
[Feat] #84 - 공통 Footer 컴포넌트 구현 및 Modal 상태 동기화 오류 수정
🔎 What is this PR?
서비스 전반의 레이아웃 일관성을 위한 공통 Footer 컴포넌트를 추가하고, 기존 Modal 컴포넌트의 스토리북 인터랙션 오류를
수정하여 안정성을 확보했습니다.
📝 Changes
컨트롤이 모두 정상 작동하도록 개선
📸 Screenshots
📚 Background / Context
높임.
✔ Checklist
Summary by CodeRabbit
New Features
Documentation
Chores
Tests