-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(design-system): 팝업 공통 컴포넌트 #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fe6dc65
ba77031
caaaa45
21380a7
545e6ce
c982724
942b4b1
525ed2b
aec1ac0
879c1cc
526699d
d0131c9
d6927fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ import { InputHTMLAttributes, Ref } from 'react'; | |
| import { cn } from '../../lib'; | ||
|
|
||
| interface InputProps extends InputHTMLAttributes<HTMLInputElement> { | ||
| ref: Ref<HTMLInputElement>; | ||
| ref?: Ref<HTMLInputElement>; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainref를 선택적(prop)으로 전환 — React 19 채택 전제에서는 합리적 이 변경은 React 19의 “함수 컴포넌트가 ref를 일반 prop으로 받는다”는 팀 컨벤션/러닝과 일치합니다. 하위 호환성(React 18 이하)에서는 깨질 수 있으므로, 워크스페이스 전반이 React 19.x 타입을 사용 중인지 한 번 확인을 부탁드립니다. 다음 스크립트로 워크스페이스의 React/타입 버전을 점검해 주세요. 🏁 Script executed: #!/bin/bash
# 리포지토리 내 package.json에서 react / @types/react 버전 확인
fd -a 'package.json' | while read -r f; do
echo "==> $f"
jq -r '.dependencies.react,.devDependencies.react,.peerDependencies.react' "$f" 2>/dev/null | awk 'NF{print "react:",$0}'
jq -r '.dependencies["@types/react"],.devDependencies["@types/react"],.peerDependencies["@types/react"]' "$f" 2>/dev/null | awk 'NF{print "@types/react:",$0}'
done | sed '/null$/d'Length of output: 3971 React 19 도입 기반 검증 — @types/react 버전 일관성 확보 필요 이 PR에서 • apps/extension/package.json • 루트 package.json • 필요 시, 다른 패키지(특히 devDependencies)에 위 조치 후, React 19 기준의 ref prop 사용이 전체 코드베이스에서 안전하게 작동함을 보장할 수 있습니다. 🤖 Prompt for AI Agents |
||
| isError?: boolean; | ||
| helperText?: string; | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. story 중앙배치~~! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import Popup from './Popup'; | ||
|
|
||
| const meta: Meta<typeof Popup> = { | ||
| title: 'Components/Popup', | ||
| component: Popup, | ||
| tags: ['autodocs'], | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| argTypes: { | ||
| type: { | ||
| control: 'radio', | ||
| options: ['input', 'subtext', 'default'], | ||
| description: '팝업의 내용 타입', | ||
| }, | ||
| title: { | ||
| control: 'text', | ||
| description: '팝업 제목', | ||
| }, | ||
| subtext: { | ||
| control: 'text', | ||
| description: 'subtext 타입일 때 보여줄 보조 문구', | ||
| }, | ||
| placeholder: { | ||
| control: 'text', | ||
| description: 'input 타입일 때 placeholder', | ||
| }, | ||
| left: { | ||
| control: 'text', | ||
| description: '왼쪽 버튼 텍스트', | ||
| }, | ||
| right: { | ||
| control: 'text', | ||
| description: '오른쪽 버튼 텍스트', | ||
| }, | ||
| isError: { | ||
| control: 'boolean', | ||
| description: 'input 타입일 때 에러 상태 여부', | ||
| }, | ||
| helperText: { | ||
| control: 'text', | ||
| description: '에러 상태일 때 표시할 도움말', | ||
| }, | ||
| }, | ||
| }; | ||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof Popup>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| type: 'default', | ||
| title: '기본 팝업', | ||
| left: '취소', | ||
| right: '확인', | ||
| }, | ||
| }; | ||
|
|
||
| export const WithInput: Story = { | ||
| args: { | ||
| type: 'input', | ||
| title: '카테고리 입력', | ||
| placeholder: '카테고리 제목을 입력해주세요', | ||
| left: '취소', | ||
| right: '저장', | ||
| }, | ||
| }; | ||
|
|
||
| export const WithErrorInput: Story = { | ||
| args: { | ||
| type: 'input', | ||
| title: '카테고리 입력', | ||
| placeholder: '카테고리 제목을 입력해주세요', | ||
| isError: true, | ||
| helperText: '10자 이내로 입력해주세요', | ||
| left: '취소', | ||
| right: '저장', | ||
| }, | ||
| }; | ||
|
|
||
| export const WithSubtext: Story = { | ||
| args: { | ||
| type: 'subtext', | ||
| title: '알림', | ||
| subtext: '카테고리가 정상적으로 저장되었습니다.', | ||
| left: '닫기', | ||
| right: '확인', | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import Input from '../input/Input'; | ||
|
|
||
| type PopupType = 'input' | 'subtext' | 'default'; | ||
|
|
||
| interface BasePopupProps { | ||
| type: PopupType; | ||
| title: string; | ||
| left: string; | ||
| right: string; | ||
| subtext?: string; | ||
| placeholder?: string; | ||
| isError?: boolean; | ||
| helperText?: string; | ||
| onLeftClick?: () => void; | ||
| onRightClick?: () => void; | ||
| } | ||
| const Popup = ({ | ||
| type, | ||
| subtext, | ||
| placeholder, | ||
| title, | ||
| left, | ||
| right, | ||
| helperText, | ||
| isError, | ||
| onLeftClick, | ||
| onRightClick, | ||
| }: BasePopupProps) => { | ||
|
Comment on lines
+17
to
+28
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 디자인을 보니 설계하기가 어려우셨을 것 같습니다! 꽤 많은 타입의
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추후 계속 리팩토링 요소 보이는대로 작업해보겠습니다! 감사합니다 |
||
| return ( | ||
| <div className="bg-white-bg flex w-[26rem] cursor-pointer flex-col items-center justify-center rounded-[1.2rem] bg-white px-[1.6rem] py-[2.4rem] shadow-[0_0_32px_0_rgba(0,0,0,0.10)]"> | ||
| <div className="sub2-sb text-font-black-1 pb-[0.8rem]">{title}</div> | ||
| {type === 'input' && ( | ||
| <div className="w-full py-[0.8rem]"> | ||
| <Input | ||
| placeholder={placeholder} | ||
| helperText={helperText} | ||
| isError={isError} | ||
| /> | ||
| </div> | ||
| )} | ||
| {type === 'subtext' && ( | ||
| <div className="body3-r text-font-gray-2 w-full py-[0.8rem] text-center"> | ||
| {subtext} | ||
| </div> | ||
| )} | ||
| {/* type===default일 떄는 아무것도 없음 */} | ||
| <div className="flex flex-row items-center justify-center gap-[1.2rem] pt-[0.8rem]"> | ||
| <button | ||
| className="border-gray200 sub5-sb bg-white-bg text-font-black-1 w-[10.8rem] rounded-[0.4rem] border py-[0.85rem]" | ||
| onClick={onLeftClick} | ||
| > | ||
| {left} | ||
| </button> | ||
| <button | ||
| className="sub5-sb bg-gray900 text-white-bg w-[10.8rem] rounded-[0.4rem] py-[0.85rem]" | ||
| onClick={onRightClick} | ||
| > | ||
| {right} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Popup; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||||||
| import { useState } from 'react'; | ||||||
| import PopupContainer from './PopupContainer'; | ||||||
|
|
||||||
| const PopupContainerWrapper = ( | ||||||
| props: React.ComponentProps<typeof PopupContainer> | ||||||
| ) => <PopupContainer {...props} />; | ||||||
|
|
||||||
| const meta: Meta<typeof PopupContainerWrapper> = { | ||||||
| title: 'Components/PopupContainer', | ||||||
| component: PopupContainerWrapper, | ||||||
| tags: ['autodocs'], | ||||||
| parameters: { | ||||||
| layout: 'centered', | ||||||
| }, | ||||||
| }; | ||||||
| export default meta; | ||||||
|
|
||||||
| type Story = StoryObj<typeof PopupContainer>; | ||||||
|
|
||||||
|
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Story 제네릭이 meta의 component와 불일치
-type Story = StoryObj<typeof PopupContainer>;
+type Story = StoryObj<typeof PopupContainerWrapper>;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| // 인터랙션 가능한 예시 | ||||||
| const WithTriggerButtonComponent = () => { | ||||||
| const [open, setOpen] = useState(false); | ||||||
|
|
||||||
| return ( | ||||||
| <div className="flex flex-col items-center gap-4"> | ||||||
| <button | ||||||
| className="rounded bg-blue-500 px-4 py-2 text-white" | ||||||
| onClick={() => setOpen(true)} | ||||||
| > | ||||||
| 팝업 열기 | ||||||
| </button> | ||||||
|
|
||||||
| <PopupContainer | ||||||
| isOpen={open} | ||||||
| onClose={() => setOpen(false)} | ||||||
| type="input" | ||||||
| title="카테고리 입력" | ||||||
| left="취소" | ||||||
| right="저장" | ||||||
| placeholder="카테고리 제목을 입력해주세요" | ||||||
| /> | ||||||
| </div> | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| export const WithTriggerButton: Story = { | ||||||
| render: () => <WithTriggerButtonComponent />, | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,50 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createPortal } from 'react-dom'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Popup from './Popup'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type PopupType = 'input' | 'subtext' | 'default'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface BasePopupProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: PopupType; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| left: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| right: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subtext?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isError?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| helperText?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onLeftClick?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onRightClick?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface PopupContainerProps extends BasePopupProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const PopupContainer = ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...popupProps | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: PopupContainerProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ESC 키로 닫는 것 정도 (외부 클릭은 안되게! : 어차피 x박스나 취소버튼이 있음) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그렇다면 모달/팝업은 요소 외부 클릭 시 닫히는 경우가 없나요? 기/디 분들과 상의는 필요 없는지 궁금합니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넹! 확인해보겠습니다!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확인해보니! 닫기나 x버튼 이외의 외부요소 클릭 제어는 필요없다고 하십니다! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isOpen) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleEsc = (e: KeyboardEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Escape') onClose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener('keydown', handleEsc); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => window.removeEventListener('keydown', handleEsc); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [isOpen, onClose]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+35
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 재사용이 다른 곳에서도 된다면 Hook으로 분리해도 좋겠네요 👍 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isOpen) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return createPortal( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="fixed inset-0 z-10 flex items-center justify-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="absolute inset-0 bg-[#00000099]" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Popup {...popupProps} onLeftClick={onClose} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.body // body 위에서 렌더링 되게 함! | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion SSR 안전성 보강 및 포털 대상 가드 필요
또한 현재 -import { useEffect } from 'react';
+import { useEffect, useRef, useState } from 'react';
...
-const PopupContainer = ({
+const PopupContainer = ({
isOpen,
onClose,
...popupProps
}: PopupContainerProps) => {
+ const [mounted, setMounted] = useState(false);
+ const dialogRef = useRef<HTMLDivElement>(null);
+ const { onLeftClick, ...restPopupProps } = popupProps;
+
+ // 클라이언트 마운트 확인
+ useEffect(() => {
+ setMounted(true);
+ return () => setMounted(false);
+ }, []);
// ESC 키로 닫는 것 정도 (외부 클릭은 안되게! : 어차피 x박스나 취소버튼이 있음)
useEffect(() => {
if (!isOpen) return;
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [isOpen, onClose]);
- if (!isOpen) return null;
+ if (!isOpen || !mounted) return null;
- return createPortal(
+ const portalTarget =
+ typeof document !== 'undefined' ? document.body : null;
+
+ return portalTarget ? createPortal(
<div className="fixed inset-0 z-10 flex items-center justify-center">
- <div className="absolute inset-0 bg-[#00000099]" />
- <div className="relative">
- <Popup {...popupProps} onLeftClick={onClose} />
+ <div className="absolute inset-0 bg-black/60" />
+ <div
+ className="relative outline-none"
+ ref={dialogRef}
+ role="dialog"
+ aria-modal="true"
+ aria-label={popupProps.title}
+ tabIndex={-1}
+ >
+ <Popup
+ {...restPopupProps}
+ onLeftClick={() => {
+ onLeftClick?.();
+ onClose();
+ }}
+ />
</div>
</div>,
- document.body // body 위에서 렌더링 되게 함!
- );
+ portalTarget // body 위에서 렌더링 되게 함!
+ ) : null;
추가로, 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default PopupContainer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
react-router-dom v7는 자체 타입을 포함합니다 — @types/react-router-dom 추가는 불필요하며 타입 충돌 위험이 큽니다.
react-router-dom은 v6부터 타입 정의를 내장합니다. 현재 deps 조합(
react-router-dom@^7+@types/react-router-dom@^5)은 API/타입이 상이해 오류를 유발할 수 있습니다. 루트에 불필요한 타입 패키지는 제거해 주세요.아래 최소 수정안을 제안합니다.
"dependencies": { - "@types/react-router-dom": "^5.3.3", "react-router-dom": "^7.8.2" }📝 Committable suggestion
🤖 Prompt for AI Agents
💡 Verification agent
🧩 Analysis chain
루트 의존성 배치는 PNPM 워크스페이스 격리 원칙과 어긋날 수 있습니다. 실제 사용 패키지로 옮기는 것을 권장합니다.
design-system의 팝업 작업과 직접 관련 없는 라우터를 루트에 두면, 하위 패키지가 암묵적으로 루트에 의존하는(hoist 전제) 안티패턴이 생길 수 있습니다. react-router-dom을 사용하는 앱(예: packages/web-app)이나 스토리북을 실행하는 특정 워크스페이스의 package.json으로 이동해 주세요. 사용 위치가 불명확하면 아래 스크립트로 참조 지점을 확인해 보세요.
🏁 Script executed:
Length of output: 198
🏁 Script executed:
Length of output: 2353
루트 package.json에서 react-router-dom 제거
루트(
pinback-client) 패키지는 해당 의존성을 직접 사용하지 않으므로 삭제하고, 실제 사용하는 워크스페이스(apps/client등)에만 선언하세요.🤖 Prompt for AI Agents