Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/react": "19",
"@types/react-dom": "19",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"playwright": "^1.55.0",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"storybook": "^9.1.3",
"turbo": "^2.5.6",
"typescript": "5.9.2",
"vite": "7.1.2",
"vitest": "^3.2.4",
"@vitest/browser": "^3.2.4",
"playwright": "^1.55.0",
"@vitest/coverage-v8": "^3.2.4"
"vitest": "^3.2.4"
},
"pnpm": {
"overrides": {
Expand All @@ -40,5 +40,9 @@
"packageManager": "pnpm@9.0.0",
"engines": {
"node": ">=18"
},
"dependencies": {
"@types/react-router-dom": "^5.3.3",
"react-router-dom": "^7.8.2"
Comment on lines +44 to +46
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"dependencies": {
"@types/react-router-dom": "^5.3.3",
"react-router-dom": "^7.8.2"
"dependencies": {
"react-router-dom": "^7.8.2"
🤖 Prompt for AI Agents
In package.json around lines 44-46, the project lists "@types/react-router-dom"
alongside "react-router-dom@^7", which causes type conflicts because
react-router-dom v7 already includes its own TypeScript definitions; remove the
"@types/react-router-dom" entry from dependencies (or devDependencies if
present), run yarn/npm install to update lockfile, search codebase for any
direct references to the removed package and update CI/containers to use the
refreshed lockfile and rebuild types to ensure no remaining type errors.

💡 Verification agent

🧩 Analysis chain

루트 의존성 배치는 PNPM 워크스페이스 격리 원칙과 어긋날 수 있습니다. 실제 사용 패키지로 옮기는 것을 권장합니다.

design-system의 팝업 작업과 직접 관련 없는 라우터를 루트에 두면, 하위 패키지가 암묵적으로 루트에 의존하는(hoist 전제) 안티패턴이 생길 수 있습니다. react-router-dom을 사용하는 앱(예: packages/web-app)이나 스토리북을 실행하는 특정 워크스페이스의 package.json으로 이동해 주세요. 사용 위치가 불명확하면 아래 스크립트로 참조 지점을 확인해 보세요.


🏁 Script executed:

#!/bin/bash
# repo 루트에서 실행: react-router-dom 사용/선언 위치를 확인
set -euo pipefail

echo "== import/require 사용 위치 조회 =="
rg -nP -C2 --type=ts --type=tsx '\bfrom\s+[\'"]react-router-dom[\'"]|\brequire\([\'"]react-router-dom[\'"]\)' || true

echo "== 워크스페이스들의 package.json 의존성 선언 조회 =="
fd package.json -t f | xargs -I{} jq -r '."name" as $n | .dependencies["react-router-dom"]? as $v | select($v!=null) | "\($n): \($v)"' {} \
  | sort || true

Length of output: 198


🏁 Script executed:

#!/bin/bash
# repo 루트에서 실행: react-router-dom 사용/선언 위치를 확인
set -euo pipefail

echo "== import/require 사용 위치 조회 =="
rg -n -C2 --type=ts --type=tsx 'from.*react-router-dom|require.*react-router-dom' || echo "No matches found"

echo "== 워크스페이스들의 package.json 의존성 선언 조회 =="
fd package.json -t f | while read file; do
    if jq -e '.dependencies["react-router-dom"]' "$file" >/dev/null 2>&1; then
        name=$(jq -r '.name // "unknown"' "$file")
        version=$(jq -r '.dependencies["react-router-dom"]' "$file")
        echo "$name: $version (in $file)"
    fi
done

echo "== devDependencies도 확인 =="
fd package.json -t f | while read file; do
    if jq -e '.devDependencies["react-router-dom"]' "$file" >/dev/null 2>&1; then
        name=$(jq -r '.name // "unknown"' "$file")
        version=$(jq -r '.devDependencies["react-router-dom"]' "$file")
        echo "$name: $version (devDep in $file)"
    fi
done

Length of output: 2353


루트 package.json에서 react-router-dom 제거
루트(pinback-client) 패키지는 해당 의존성을 직접 사용하지 않으므로 삭제하고, 실제 사용하는 워크스페이스(apps/client 등)에만 선언하세요.

🤖 Prompt for AI Agents
In package.json around lines 44-46, the root package declares react-router-dom
and @types/react-router-dom even though pinback-client doesn't use them; remove
these entries from the root "dependencies" block and instead add/ensure
react-router-dom (and @types/react-router-dom if needed for TypeScript) are
declared in the workspace package(s) that actually use them (e.g., apps/client)
and run the workspace package manager install (npm/yarn/pnpm) in those
workspaces to update lockfiles; also verify no imports in the root reference
react-router-dom before committing.

}
}
2 changes: 2 additions & 0 deletions packages/design-system/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export { default as AutoDismissToast } from './toast/hooks/uesFadeOut';
export { default as Toast } from './toast/Toast';
export { WheelPicker, WheelPickerWrapper } from './wheelPicker/WheelPicker';
export type { WheelPickerOption } from './wheelPicker/WheelPicker';
export { default as Popup } from './popup/Popup';
export { default as PopupContainer } from './popup/PopupContainer';
2 changes: 1 addition & 1 deletion packages/design-system/src/components/input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InputHTMLAttributes, Ref } from 'react';
import { cn } from '../../lib';

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
ref: Ref<HTMLInputElement>;
ref?: Ref<HTMLInputElement>;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

ref를 선택적(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에서 ref를 선택적 prop으로 전환한 것은 React 19의 컨벤션에 부합하나, 워크스페이스 전반의 타입 일관성을 위해 다음 위치의 @types/react 버전을 React 19.x와 맞춰주세요.

• apps/extension/package.json
– 현재: @types/react: ^18.3.5
– 수정: React 19.x 타입(^19.1.10 등)으로 업데이트

• 루트 package.json
– 현재: @types/react: 19
– 수정: 명확한 SemVer(^19.1.x) 형식으로 변경

• 필요 시, 다른 패키지(특히 devDependencies)에 @types/react가 있다면 동일하게 점검 및 업데이트

위 조치 후, React 19 기준의 ref prop 사용이 전체 코드베이스에서 안전하게 작동함을 보장할 수 있습니다.

🤖 Prompt for AI Agents
packages/design-system/src/components/input/Input.tsx around line 6: the ref
prop was made optional to align with React 19 conventions but workspace
@types/react versions are inconsistent; update apps/extension/package.json to
use a React 19.x types range (e.g. "@types/react": "^19.1.10"), change the root
package.json entry from "19" to a clear semver range like "^19.1.x", and audit
other package.json/devDependencies across the monorepo to ensure all
@types/react entries are aligned to the same React 19.x range so the optional
ref typing is safe throughout the codebase.

isError?: boolean;
helperText?: string;
}
Expand Down
90 changes: 90 additions & 0 deletions packages/design-system/src/components/popup/Popup.stories.tsx
Copy link
Member

Choose a reason for hiding this comment

The 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: '확인',
},
};
65 changes: 65 additions & 0 deletions packages/design-system/src/components/popup/Popup.tsx
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
Copy link
Member

Choose a reason for hiding this comment

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

디자인을 보니 설계하기가 어려우셨을 것 같습니다! 꽤 많은 타입의 popup이 있어서 힘드셨을텐데 잘 해주셨네요.
다만 확실히 인터페이스가 많아지는 것은 사실이어서 이후에 더 좋은 설계가 있다면 리팩토링하는 것도 좋을 것 같아요 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Story 제네릭이 meta의 component와 불일치

metaPopupContainerWrapper를 component로 사용하지만, Story 타입은 typeof PopupContainer를 제네릭으로 사용하고 있습니다. 타입 안전성과 자동 추론을 위해 일치시키는 편이 좋습니다.

-type Story = StoryObj<typeof PopupContainer>;
+type Story = StoryObj<typeof PopupContainerWrapper>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type Story = StoryObj<typeof PopupContainer>;
type Story = StoryObj<typeof PopupContainerWrapper>;
🤖 Prompt for AI Agents
In packages/design-system/src/components/popup/PopupContainer.stories.tsx around
lines 19-20, the Story generic uses typeof PopupContainer while the meta uses
PopupContainerWrapper as the component; change the Story type to use typeof
PopupContainerWrapper so the generic matches the meta component, and ensure
PopupContainerWrapper is imported/available in this file to preserve type safety
and correct prop inference.

// 인터랙션 가능한 예시
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 />,
};
50 changes: 50 additions & 0 deletions packages/design-system/src/components/popup/PopupContainer.tsx
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박스나 취소버튼이 있음)
Copy link
Member

Choose a reason for hiding this comment

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

그렇다면 모달/팝업은 요소 외부 클릭 시 닫히는 경우가 없나요? 기/디 분들과 상의는 필요 없는지 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넹! 확인해보겠습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

SSR 안전성 보강 및 포털 대상 가드 필요

isOpen === true 상태로 SSR 렌더가 발생하면 document가 없어 크래시가 납니다. 마운트 여부 가드와 document 존재 확인 후 포털을 생성하도록 방어 코드를 추가해주세요.

또한 현재 onLeftClick를 무조건 onClose로 덮어써 상위가 전달한 핸들러가 무시됩니다. “취소 후 닫기”라는 기본 UX를 유지하면서도 상위 핸들러와 합성하도록 바꾸는 편이 안전합니다.

-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;
  • SSR 크래시 방지: mounted + portalTarget 가드
  • a11y 기본값: role="dialog", aria-modal, aria-label, tabIndex={-1} 및 포커스 가능한 컨테이너
  • 핸들러 합성: 상위 onLeftClick → 실행 후 onClose 호출
  • 시각적: 투명도는 Tailwind 표기(bg-black/60) 사용

추가로, isOpen이 true가 될 때 dialogRef.current?.focus() 하는 포커스 진입 로직을 넣으면 키보드 접근성이 더 좋아집니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 위에서 렌더링 되게 함!
);
// don't render on server or when closed
if (!isOpen || !mounted) return null;
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-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>,
portalTarget // body 위에서 렌더링 되게 함!
) : null;
🤖 Prompt for AI Agents
packages/design-system/src/components/popup/PopupContainer.tsx around lines 23
to 31: guard portal creation with a mounted flag and a portalTarget check (only
call createPortal when mounted && typeof document !== "undefined" &&
document.body exists), change backdrop class to Tailwind shorthand bg-black/60,
make the popup container focusable (tabIndex={-1}) and add accessibility
attributes role="dialog" aria-modal="true" and an aria-label prop fallback, on
open call dialogRef.current?.focus() to move focus into the dialog, and compose
the onLeftClick handler so it first calls any provided popupProps.onLeftClick
and then calls onClose (ensuring both run) instead of unconditionally overriding
it.

};

export default PopupContainer;
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading