Repository: WillYouMerryMe/getting-married
Pull request #80: feat(mobile): 참석 여부 바텀 시트 컴포넌트 퍼블리싱
Author: login: junghongseop
Created: 2025-05-15T23:55:42Z | Merged: 2025-05-17T16:23:41Z

## 📄 Summary

> 참석 여부 바텀 시트 컴포넌트를 퍼블리싱했습니다.

<br>

## 🔨 Tasks

- 컴포넌트 개발
- 스크롤 방지, 스와이프로 닫기 훅 개발
- 추가 반응형을 위한 스타일 수정

<br>

## 🙋🏻 More
<img width="388" alt="스크린샷 2025-05-16 오전 8 55 30" src="https://github.com/user-attachments/assets/3c8638e0-f812-41a5-9786-df2a23ddc7b9" />


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **신규 기능**
    - 데스크톱 환경에서 버튼 그룹을 표시하는 DesktopButtonGroup 컴포넌트가 추가되었습니다.
    - 하단 시트에서 참석 의사를 입력할 수 있는 AttendBottomSheet 컴포넌트가 추가되었습니다.
    - AttendFloatButton 클릭 시 참석 의사 입력 하단 시트가 오버레이로 표시됩니다.
    - 스와이프 다운 제스처 및 스크롤 잠금 기능을 위한 커스텀 훅(useSwipeDown, useBodyScrollLock)이 도입되었습니다.
    - 숫자 입력 관리를 위한 useInput 훅이 추가되었습니다.

- **기능 개선**
    - BottomSheet의 드래그 및 스와이프 동작이 개선되고, 시트 높이가 동적으로 변경되며, 시각적 효과(박스 섀도우)가 추가되었습니다.
    - ButtonGroup에서 제목(title)과 pointColor가 선택적으로 적용되고, 버튼 스타일 및 레이아웃이 개선되었습니다.
    - DesktopButton에 pointColor 속성이 추가되어 강조 색상 지정이 가능합니다.

- **스타일**
    - GiftPickerBottomSheet의 상단 마진이 소폭 조정되었습니다.
    - 버튼(box-sizing)이 border-box로 변경되어 레이아웃이 일관되게 표시됩니다.
    - Input 컴포넌트의 플레이스홀더 텍스트 스타일이 개선되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/mobile/src/components/common/BottomSheet/BottomSheet.tsx b/apps/mobile/src/components/common/BottomSheet/BottomSheet.tsx
index 90fde6f5..3c8dba89 100644
--- a/apps/mobile/src/components/common/BottomSheet/BottomSheet.tsx
+++ b/apps/mobile/src/components/common/BottomSheet/BottomSheet.tsx
@@ -1,7 +1,9 @@
-import React, { useState, useRef, useEffect, ReactNode } from 'react';
+import React, { ReactNode } from 'react';
 import { color } from '@merried/design-system';
 import { flex } from '@merried/utils';
 import styled from 'styled-components';
+import useBodyScrollLock from '@/hooks/useBodyScrollLock';
+import useSwipeDown from '@/hooks/useSwipeDown';
 
 interface BottomSheetProps {
   isOpen: boolean;
@@ -9,36 +11,9 @@ interface BottomSheetProps {
   children: ReactNode;
 }
 
-const SWIPE_THRESHOLD = 120;
-
 const BottomSheet = ({ isOpen, onClose, children }: BottomSheetProps) => {
-  const [dragY, setDragY] = useState(0);
-  const [dragging, setDragging] = useState(false);
-  const startYRef = useRef(0);
-
-  useEffect(() => {
-    if (isOpen) setDragY(0);
-  }, [isOpen]);
-
-  const handlePointerDown = (e: React.PointerEvent) => {
-    startYRef.current = e.clientY;
-    setDragging(true);
-  };
-
-  const handlePointerMove = (e: React.PointerEvent) => {
-    if (!dragging) return;
-    const delta = e.clientY - startYRef.current;
-    if (delta > 0) setDragY(delta);
-  };
-
-  const handlePointerUp = () => {
-    setDragging(false);
-    if (dragY > SWIPE_THRESHOLD) {
-      onClose();
-    } else {
-      setDragY(0);
-    }
-  };
+  useBodyScrollLock(isOpen);
+  const { dragY, dragging, handlers } = useSwipeDown(isOpen, onClose);
 
   return (
     <Background $isOpen={isOpen}>
@@ -46,9 +21,7 @@ const BottomSheet = ({ isOpen, onClose, children }: BottomSheetProps) => {
         $isOpen={isOpen}
         $dragY={dragY}
         $dragging={dragging}
-        onPointerDown={handlePointerDown}
-        onPointerMove={handlePointerMove}
-        onPointerUp={handlePointerUp}
+        {...handlers}
         onClick={(e) => e.stopPropagation()}
       >
         <Line />
@@ -66,7 +39,6 @@ const Background = styled.div<{ $isOpen: boolean }>`
   display: flex;
   align-items: flex-end;
   justify-content: center;
-  background: rgba(0, 0, 0, 0.3);
   pointer-events: ${(p) => (p.$isOpen ? 'auto' : 'none')};
   opacity: ${(p) => (p.$isOpen ? 1 : 0)};
   transition: opacity 0.3s ease;
@@ -80,13 +52,16 @@ const StyledBottomSheet = styled.div<{
 }>`
   ${flex({ flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start' })}
   width: 100%;
-  height: 439px;
+  height: auto;
   background-color: ${color.G0};
   border-radius: 24px 24px 0 0;
   padding: 24px 12px 49px;
   touch-action: none;
   transform: ${(p) => (p.$isOpen ? `translateY(${p.$dragY}px)` : 'translateY(100%)')};
   transition: ${(p) => (p.$dragging ? 'none' : 'transform 0.3s ease-out')};
+  box-shadow:
+    -5px -15px 9px 0px rgba(199, 199, 199, 0.05),
+    -2px -7px 7px 0px rgba(199, 199, 199, 0.09);
 `;
 
 const Line = styled.div`
diff --git a/apps/mobile/src/components/common/ButtonGroup/ButtonGroup.tsx b/apps/mobile/src/components/common/ButtonGroup/ButtonGroup.tsx
index 771a146c..d8338a43 100644
--- a/apps/mobile/src/components/common/ButtonGroup/ButtonGroup.tsx
+++ b/apps/mobile/src/components/common/ButtonGroup/ButtonGroup.tsx
@@ -10,25 +10,30 @@ interface ButtonItem {
 }
 
 interface ButtonGroupProps {
-  title: string;
+  title?: string;
   buttons: ButtonItem[];
+  pointColor?: string;
 }
 
-const ButtonGroup = ({ title, buttons }: ButtonGroupProps) => {
+const ButtonGroup = ({ title, buttons, pointColor }: ButtonGroupProps) => {
   const [selectedButton, setSelectedButton] = useState<number>(0);
 
   return (
     <StyledButtonGroup>
-      <Text fontType="P2" color={color.G900}>
-        {title}
-      </Text>
+      {title && (
+        <Text fontType="P2" color={color.G900}>
+          {title}
+        </Text>
+      )}
       <Row alignItems="center" gap={19} width="100%">
         {buttons.map((button, index) => (
           <Button
             key={index}
-            styleType={selectedButton === index ? 'DEFAULT' : 'SELECT'}
-            size='SMALL'
+            styleType={selectedButton === index ? 'DEFAULT' : 'SECOND'}
+            pointColor={pointColor}
+            size="SMALL"
             width="100%"
+            style={{ flex: 1, minWidth: 0 }}
             onClick={() => {
               setSelectedButton(index);
               button.onClick();
diff --git a/apps/mobile/src/components/common/ButtonGroup/DesktopButtonGroup.tsx b/apps/mobile/src/components/common/ButtonGroup/DesktopButtonGroup.tsx
new file mode 100644
index 00000000..e834bf0a
--- /dev/null
+++ b/apps/mobile/src/components/common/ButtonGroup/DesktopButtonGroup.tsx
@@ -0,0 +1,56 @@
+import { color } from '@merried/design-system';
+import { DesktopButton, Row, Text } from '@merried/ui';
+import { flex } from '@merried/utils';
+import { useState } from 'react';
+import styled from 'styled-components';
+
+interface ButtonItem {
+  label: string;
+  onClick: () => void;
+}
+
+interface DesktopButtonGroupProps {
+  title?: string;
+  buttons: ButtonItem[];
+  pointColor?: string;
+}
+
+const DesktopButtonGroup = ({ title, buttons, pointColor }: DesktopButtonGroupProps) => {
+  const [selectedButton, setSelectedButton] = useState<number>(0);
+
+  return (
+    <StyledButtonGroup>
+      {title && (
+        <Text fontType="P3" color={color.G900}>
+          {title}
+        </Text>
+      )}
+      <Row alignItems="center" gap={19} width="100%">
+        {buttons.map((button, index) => (
+          <DesktopButton
+            key={index}
+            styleType={selectedButton === index ? 'DEFAULT' : 'SECOND'}
+            pointColor={pointColor}
+            size="SMALL"
+            width="100%"
+            style={{ flex: 1, minWidth: 0 }}
+            onClick={() => {
+              setSelectedButton(index);
+              button.onClick();
+            }}
+          >
+            <Text fontType="Button3" color={color.G900}>{button.label}</Text>
+          </DesktopButton>
+        ))}
+      </Row>
+    </StyledButtonGroup>
+  );
+};
+
+export default DesktopButtonGroup;
+
+const StyledButtonGroup = styled.div`
+  ${flex({ flexDirection: 'column', alignItems: 'flex-start' })}
+  width: 100%;
+  gap: 10px;
+`;
diff --git a/apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.hooks.ts b/apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.hooks.ts
new file mode 100644
index 00000000..bd35237c
--- /dev/null
+++ b/apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.hooks.ts
@@ -0,0 +1,23 @@
+import { useState } from 'react';
+
+export const useInput = () => {
+  const [count, setCount] = useState('1');
+
+  const handleCountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const raw = e.target.value;
+    // 빈값 허용, 아니면 숫자만 허용
+    if (raw === '' || /^\d+$/.test(raw)) {
+      setCount(raw);
+    }
+  };
+
+  const handleCountBlur = () => {
+    // blur 시 빈값이거나 1 미만이면 1로 복구
+    const num = parseInt(count, 10);
+    if (isNaN(num) || num < 1) {
+      setCount('1');
+    }
+  };
+
+  return { handleCountChange, handleCountBlur, count };
+};
diff --git a/apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx b/apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx
new file mode 100644
index 00000000..1c406676
--- /dev/null
+++ b/apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx
@@ -0,0 +1,91 @@
+import BottomSheet from '@/components/common/BottomSheet/BottomSheet';
+import ButtonGroup from '@/components/common/ButtonGroup/ButtonGroup';
+import DesktopButtonGroup from '@/components/common/ButtonGroup/DesktopButtonGroup';
+import { color } from '@merried/design-system';
+import { Button, Column, Input, Text } from '@merried/ui';
+import { flex } from '@merried/utils';
+import { styled } from 'styled-components';
+import { useInput } from './AttendBottomSheet.hooks';
+
+interface AttendBottomSheetProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+const AttendBottomSheet = ({ isOpen, onClose }: AttendBottomSheetProps) => {
+  const { handleCountChange, handleCountBlur, count } = useInput();
+
+  return (
+    <BottomSheet isOpen={isOpen} onClose={onClose}>
+      <StyledAttendBottomSheet>
+        <Column gap={40}>
+          <Column gap={24}>
+            <Column>
+              <Text fontType="H2" color={color.G900}>
+                결혼식 참석 의사
+              </Text>
+              <Text fontType="P2" color={color.G80}>
+                솔직한 답은 신랑•신부에게 도움이 됩니다
+              </Text>
+            </Column>
+            <ButtonGroup
+              buttons={[
+                { label: '신랑측', onClick: () => {} },
+                { label: '신부측', onClick: () => {} },
+              ]}
+              pointColor={color.pointYellow}
+            />
+            <Column gap={18}>
+              <Input
+                label="이름"
+                placeholder="이름을 입력해주세요"
+                size="LARGE"
+                width="100%"
+                onChange={() => {}}
+              />
+              <Input
+                label="참석인원"
+                size="LARGE"
+                type="number"
+                inputMode="numeric"
+                pattern="[0-9]*"
+                min={1}
+                step={1}
+                value={count}
+                onChange={handleCountChange}
+                onBlur={handleCountBlur}
+              />
+              <DesktopButtonGroup
+                title="식사 여부"
+                buttons={[
+                  { label: '예정', onClick: () => {} },
+                  { label: '안함', onClick: () => {} },
+                  { label: '미정', onClick: () => {} },
+                ]}
+                pointColor={color.pointYellow}
+              />
+            </Column>
+          </Column>
+          <Button
+            size="VERY_LARGE"
+            onClick={() => {}}
+            pointColor={color.pointYellow}
+            width="100%"
+          >
+            <Text fontType="Button3" color={color.G900}>
+              참석 의사 전달하기
+            </Text>
+          </Button>
+        </Column>
+      </StyledAttendBottomSheet>
+    </BottomSheet>
+  );
+};
+
+export default AttendBottomSheet;
+
+const StyledAttendBottomSheet = styled.div`
+  ${flex({ flexDirection: 'column', alignItems: 'stretch' })}
+  width: 100%;
+  margin-top: 40px;
+`;
diff --git a/apps/mobile/src/components/invitation/AttendFloatButton/AttendFloatButton.tsx b/apps/mobile/src/components/invitation/AttendFloatButton/AttendFloatButton.tsx
index 3ce5fdef..064fe4d0 100644
--- a/apps/mobile/src/components/invitation/AttendFloatButton/AttendFloatButton.tsx
+++ b/apps/mobile/src/components/invitation/AttendFloatButton/AttendFloatButton.tsx
@@ -1,11 +1,21 @@
 import { color } from '@merried/design-system';
 import { Text } from '@merried/ui';
 import { flex } from '@merried/utils';
+import { useOverlay } from '@toss/use-overlay';
 import styled from 'styled-components';
+import AttendBottomSheet from '../AttendBottomSheet/AttendBottomSheet';
 
 const AttendFloatButton = () => {
+  const overlay = useOverlay();
+
+  const handleOpenBottomSheet = () => {
+    overlay.open(({ isOpen, close }) => (
+      <AttendBottomSheet isOpen={isOpen} onClose={close} />
+    ));
+  };
+
   return (
-    <StyledAttendFloatButton onClick={() => {}}>
+    <StyledAttendFloatButton onClick={handleOpenBottomSheet}>
       <ActionButton>
         <Text fontType="Button3" color={color.G900}>
           참석 의사 전달하기
diff --git a/apps/mobile/src/components/shop/GiftPickerBottomSheet/GiftPickerBottomSheet.tsx b/apps/mobile/src/components/shop/GiftPickerBottomSheet/GiftPickerBottomSheet.tsx
index 34311f51..85ab2cc5 100644
--- a/apps/mobile/src/components/shop/GiftPickerBottomSheet/GiftPickerBottomSheet.tsx
+++ b/apps/mobile/src/components/shop/GiftPickerBottomSheet/GiftPickerBottomSheet.tsx
@@ -54,6 +54,6 @@ const StyledGiftPickerBottomSheet = styled.div`
     alignItems: 'flex-start',
   })}
   width: 100%;
-  margin-top: 32px;
+  margin-top: 36px;
   gap: 56px;
 `;
diff --git a/apps/mobile/src/hooks/useBodyScrollLock.ts b/apps/mobile/src/hooks/useBodyScrollLock.ts
new file mode 100644
index 00000000..97cc2f10
--- /dev/null
+++ b/apps/mobile/src/hooks/useBodyScrollLock.ts
@@ -0,0 +1,10 @@
+import { useEffect } from 'react';
+
+export default function useBodyScrollLock(lock: boolean) {
+  useEffect(() => {
+    document.body.style.overflow = lock ? 'hidden' : '';
+    return () => {
+      document.body.style.overflow = '';
+    };
+  }, [lock]);
+}
diff --git a/apps/mobile/src/hooks/useSwipeDown.ts b/apps/mobile/src/hooks/useSwipeDown.ts
new file mode 100644
index 00000000..5f0edcae
--- /dev/null
+++ b/apps/mobile/src/hooks/useSwipeDown.ts
@@ -0,0 +1,50 @@
+import { useState, useRef, useEffect } from 'react';
+
+interface SwipeHandlers {
+  onPointerDown: React.PointerEventHandler;
+  onPointerMove: React.PointerEventHandler;
+  onPointerUp: React.PointerEventHandler;
+  onPointerLeave: React.PointerEventHandler;
+}
+
+export default function useSwipeDown(
+  isOpen: boolean,
+  onClose: () => void,
+  threshold = 120
+): { dragY: number; dragging: boolean; handlers: SwipeHandlers } {
+  const startY = useRef(0);
+  const [dragY, setDragY] = useState(0);
+  const [dragging, setDragging] = useState(false);
+
+  useEffect(() => {
+    if (isOpen) setDragY(0);
+  }, [isOpen]);
+
+  const onPointerDown: SwipeHandlers['onPointerDown'] = (e) => {
+    startY.current = e.clientY;
+    setDragging(true);
+  };
+
+  const onPointerMove: SwipeHandlers['onPointerMove'] = (e) => {
+    if (!dragging) return;
+    const delta = e.clientY - startY.current;
+    if (delta > 0) setDragY(delta);
+  };
+
+  const onPointerUp: SwipeHandlers['onPointerUp'] = () => {
+    setDragging(false);
+    if (dragY > threshold) onClose();
+    else setDragY(0);
+  };
+
+  const onPointerLeave: SwipeHandlers['onPointerLeave'] = () => {
+    setDragging(false);
+    setDragY(0);
+  };
+
+  return {
+    dragY,
+    dragging,
+    handlers: { onPointerDown, onPointerMove, onPointerUp, onPointerLeave },
+  };
+}
diff --git a/packages/ui/src/Button/Button.tsx b/packages/ui/src/Button/Button.tsx
index c7cd7e43..009e2f7e 100644
--- a/packages/ui/src/Button/Button.tsx
+++ b/packages/ui/src/Button/Button.tsx
@@ -46,6 +46,7 @@ const StyledButton = styled.button<{
   ${flex({ alignItems: 'center', justifyContent: 'center' })}
   border-radius: 8px;
   word-break: keep-all;
+  box-sizing: border-box;
 
   ${(props) => props && getButtonStyle[props.styleType]};
   ${(props) => props && getButtonSize[props.size]};
diff --git a/packages/ui/src/Button/DesktopButton.tsx b/packages/ui/src/Button/DesktopButton.tsx
index bf15500d..3d48e530 100644
--- a/packages/ui/src/Button/DesktopButton.tsx
+++ b/packages/ui/src/Button/DesktopButton.tsx
@@ -2,13 +2,14 @@ import { ButtonHTMLAttributes, ReactNode, CSSProperties } from 'react';
 import { ButtonStyleType, DesktopButtonSize } from './button.type';
 import styled from 'styled-components';
 import { flex } from '@merried/utils';
-import { getDesktopButtonSize, getButtonStyle } from './button.style';
+import { getDesktopButtonSize, getButtonStyle, getPointColorStyle } from './button.style';
 
 type Props = {
   children: ReactNode;
   styleType?: ButtonStyleType;
   size?: DesktopButtonSize;
   width?: CSSProperties['width'];
+  pointColor?: string;
 } & ButtonHTMLAttributes<HTMLButtonElement>;
 
 const DesktopButton = ({
@@ -19,6 +20,7 @@ const DesktopButton = ({
   width,
   style,
   disabled,
+  pointColor,
 }: Props) => {
   return (
     <StyledDesktopButton
@@ -26,6 +28,7 @@ const DesktopButton = ({
       onClick={onClick}
       $styleType={styleType}
       $size={size}
+      pointColor={pointColor}
       disabled={disabled || styleType === 'DISABLED'}
     >
       {children}
@@ -38,6 +41,7 @@ export default DesktopButton;
 const StyledDesktopButton = styled.button<{
   $styleType: ButtonStyleType;
   $size: DesktopButtonSize;
+  pointColor?: string;
 }>`
   ${flex({ alignItems: 'center', justifyContent: 'center' })}
   border-radius: 8px;
@@ -45,4 +49,7 @@ const StyledDesktopButton = styled.button<{
 
   ${(props) => props && getButtonStyle[props.$styleType]};
   ${(props) => props && getDesktopButtonSize[props.$size]};
+
+  ${({ $styleType, pointColor }) =>
+    $styleType === 'DEFAULT' && getPointColorStyle(pointColor)}
 `;
diff --git a/packages/ui/src/Input/Input.tsx b/packages/ui/src/Input/Input.tsx
index 65ed3266..0a869d43 100644
--- a/packages/ui/src/Input/Input.tsx
+++ b/packages/ui/src/Input/Input.tsx
@@ -73,6 +73,7 @@ const StyledInput = styled.input<{
   background: ${color.G0};
 
   &::placeholder {
+    ${font.P3}
     color: ${color.G80};
   }
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>apps/mobile/src/hooks/useBodyScrollLock.ts (1)</summary><blockquote>

`1-10`: **새로운 훅이 만들어졌어요!**

`useBodyScrollLock` 훅은 바텀 시트가 열렸을 때 배경 스크롤을 막아주는 아주 유용한 기능이에요. 

작동 방식을 살펴볼까요?
1. `lock` 값이 `true`면 `document.body.style.overflow = 'hidden'`으로 설정해서 스크롤을 막아요.
2. `lock` 값이 `false`면 원래대로 돌려놓아요.
3. 컴포넌트가 사라질 때 (클린업 함수) 항상 스크롤을 원래대로 복구해요.

이렇게 하면 바텀 시트를 열었을 때 뒤의 콘텐츠가 스크롤되지 않아 사용자 경험이 더 좋아질 거예요! 특히 모바일에서는 이런 디테일이 매우 중요하답니다. 😄

다만, 조금 더 개선할 수 있는 부분도 있어요:
- iOS Safari에서는 `overflow: hidden`만으로는 스크롤을 완전히 막기 어려울 수 있어요. 
- 접근성 관점에서는 스크롤을 막더라도 키보드 네비게이션은 유지하는 것이 좋답니다.


나중에 시간이 된다면, 이렇게 개선해볼 수도 있을 것 같아요:

```diff
import { useEffect } from 'react';

export default function useBodyScrollLock(lock: boolean) {
  useEffect(() => {
-   document.body.style.overflow = lock ? 'hidden' : '';
+   if (lock) {
+     // 현재 스크롤 위치 저장
+     const scrollY = window.scrollY;
+     document.body.style.position = 'fixed';
+     document.body.style.top = `-${scrollY}px`;
+     document.body.style.width = '100%';
+     document.body.style.overflow = 'hidden';
+   } else {
+     const scrollY = document.body.style.top;
+     document.body.style.position = '';
+     document.body.style.top = '';
+     document.body.style.width = '';
+     document.body.style.overflow = '';
+     // 스크롤 위치 복원
+     window.scrollTo(0, parseInt(scrollY || '0', 10) * -1);
+   }
    return () => {
-     document.body.style.overflow = '';
+     document.body.style.position = '';
+     document.body.style.top = '';
+     document.body.style.width = '';
+     document.body.style.overflow = '';
    };
  }, [lock]);
}
```

이 방식은 iOS Safari에서도 잘 작동하고, 바텀 시트를 닫으면 이전 스크롤 위치로 돌아가는 기능도 있어요! 하지만 지금 구현도 충분히 잘 작동할 거예요. 🌈

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8387f28a63fdf1ba66f8ee3c4e8a87a299744832 and 5475e3c7953b7225045b92e8b1f6a1d11be688cd.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `apps/mobile/src/components/common/BottomSheet/BottomSheet.tsx` (2 hunks)
* `apps/mobile/src/components/common/ButtonGroup/ButtonGroup.tsx` (1 hunks)
* `apps/mobile/src/components/common/ButtonGroup/DesktopButtonGroup.tsx` (1 hunks)
* `apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx` (1 hunks)
* `apps/mobile/src/components/invitation/AttendFloatButton/AttendFloatButton.tsx` (1 hunks)
* `apps/mobile/src/components/shop/GiftPickerBottomSheet/GiftPickerBottomSheet.tsx` (1 hunks)
* `apps/mobile/src/hooks/useBodyScrollLock.ts` (1 hunks)
* `apps/mobile/src/hooks/useSwipeDown.ts` (1 hunks)
* `packages/ui/src/Button/Button.tsx` (1 hunks)
* `packages/ui/src/Button/DesktopButton.tsx` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>apps/mobile/src/components/common/BottomSheet/BottomSheet.tsx (2)</summary><blockquote>

<details>
<summary>apps/mobile/src/hooks/useBodyScrollLock.ts (1)</summary>

* `useBodyScrollLock` (3-10)

</details>
<details>
<summary>apps/mobile/src/hooks/useSwipeDown.ts (1)</summary>

* `useSwipeDown` (9-38)

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/Button/DesktopButton.tsx (2)</summary><blockquote>

<details>
<summary>packages/ui/src/Button/button.type.ts (2)</summary>

* `ButtonStyleType` (1-1)
* `DesktopButtonSize` (5-5)

</details>
<details>
<summary>packages/ui/src/Button/button.style.ts (3)</summary>

* `getButtonStyle` (5-40)
* `getDesktopButtonSize` (65-76)
* `getPointColorStyle` (78-89)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (35)</summary><blockquote>

<details>
<summary>apps/mobile/src/components/shop/GiftPickerBottomSheet/GiftPickerBottomSheet.tsx (1)</summary>

`57-57`: **마진 값이 변경되었네요!**

상단 마진이 이전보다 4px 증가했어요. 이 작은 변화는 아마도 다른 바텀 시트 컴포넌트들과의 일관성을 위한 것으로 보여요. 시각적으로 더 여유로운 공간을 제공해서 사용자 경험을 향상시키는 좋은 변화예요! 💕

</details>
<details>
<summary>packages/ui/src/Button/Button.tsx (1)</summary>

`49-49`: **상자 크기 설정이 추가되었어요!**

`box-sizing: border-box`를 추가한 것은 정말 좋은 선택이에요! 이렇게 하면 버튼의 패딩과 테두리가 전체 너비와 높이에 포함되어, 더 예측 가능한 레이아웃을 만들 수 있어요. 특히 반응형 디자인에서는 이런 설정이 중요하답니다. 다른 UI 컴포넌트들도 이렇게 설정하면 일관된 레이아웃을 유지할 수 있을 거예요! 🌟

</details>
<details>
<summary>apps/mobile/src/components/invitation/AttendFloatButton/AttendFloatButton.tsx (3)</summary>

`4-6`: **새로운 임포트가 추가되었네요!**

`@toss/use-overlay` 라이브러리에서 `useOverlay` 훅을 가져오고, 새로 만든 `AttendBottomSheet` 컴포넌트를 임포트했어요. 이렇게 하면 플로팅 버튼을 클릭했을 때 바텀 시트를 열 수 있게 된답니다. 아주 깔끔하게 필요한 것들만 가져왔어요! 👏

---

`9-15`: **바텀 시트를 열기 위한 로직이 추가되었어요!**

`useOverlay` 훅을 사용해서 바텀 시트를 열고 닫는 기능을 구현했네요. 이런 방식은 모달이나 오버레이를 관리하는 아주 좋은 방법이에요! 특히 `overlay.open` 메서드를 사용해서 컴포넌트에 `isOpen`과 `close` props를 자동으로 전달하는 패턴은 정말 깔끔해요. 

이제 사용자가 버튼을 클릭하면 참석 의사를 전달할 수 있는 바텀 시트가 나타나게 될 거예요! 🎉

---

`18-18`: **버튼 클릭 이벤트가 연결되었어요!**

이제 플로팅 버튼을 클릭하면 `handleOpenBottomSheet` 함수가 호출되어 바텀 시트가 열리게 됐어요. 이전에는 아무 동작도 하지 않았지만, 이제 실제로 기능하는 버튼이 되었네요! 사용자 경험이 훨씬 좋아질 거예요! 😊

</details>
<details>
<summary>apps/mobile/src/components/common/ButtonGroup/ButtonGroup.tsx (4)</summary>

`13-15`: **ButtonGroup 컴포넌트의 프롭스가 업데이트되었네요!**

props 타입에 새로운 `pointColor` 옵션을 추가하고 `title`을 선택적으로 만든 것은 정말 좋은 변화예요. 이렇게 하면 컴포넌트가 더 다양한 상황에서 유연하게 사용될 수 있어요. 특히 제목이 필요하지 않은 경우에도 사용할 수 있게 됐어요! 👍

---

`18-18`: **함수 매개변수 업데이트도 잘했어요!**

컴포넌트 함수 매개변수에 새로 추가된 `pointColor`를 포함시켜서 타입 정의와 일치시켰네요. 아주 좋아요! 👏

---

`23-27`: **조건부 렌더링 처리 잘했어요!**

title이 있을 때만 Text 컴포넌트를 렌더링하도록 조건부 렌더링을 추가했네요. 이렇게 하면 title이 없을 때 불필요한 요소가 렌더링되지 않아 성능 최적화에도 도움이 돼요. 정말 세심한 부분까지 생각하셨군요! 💖

---

`32-36`: **버튼 스타일 개선이 훌륭해요!**

1. 선택되지 않은 버튼의 스타일을 'SELECT'에서 'SECOND'로 변경한 것은 디자인 일관성 측면에서 좋은 결정이에요.
2. `pointColor` 속성을 각 버튼에 전달하여 맞춤형 색상을 지원하게 했어요.
3. 버튼에 `flex: 1, minWidth: 0` 스타일을 추가해서 모든 버튼이 동일한 너비를 가지고 적절히 줄어들 수 있게 했네요.

이러한 변경으로 버튼 그룹의 레이아웃과 스타일링이 더 일관되고 유연해졌어요! 🌈

</details>
<details>
<summary>packages/ui/src/Button/DesktopButton.tsx (6)</summary>

`5-5`: **import 구문 업데이트 잘했어요!**

`getPointColorStyle` 함수를 추가로 가져와서 버튼 스타일링에 사용할 준비를 했네요. 새로운 기능을 위해 필요한 의존성을 추가하는 것은 아주 중요해요! 👍

---

`12-12`: **Props 타입에 pointColor 추가 좋아요!**

Props 타입에 `pointColor` 속성을 추가해서 타입 안전성을 유지했네요. 이렇게 하면 컴포넌트 사용자가 어떤 속성을 사용할 수 있는지 명확하게 알 수 있어요. 타입스크립트의 장점을 잘 활용했어요! 🌟

---

`23-23`: **매개변수 구조분해에 pointColor 추가 잘했어요!**

컴포넌트 함수의 구조분해 매개변수에 `pointColor`를 추가했네요. Props 타입과 함수 매개변수를 일치시키는 것은 일관성을 유지하는 데 매우 중요해요. 꼼꼼하게 처리했어요! 😊

---

`31-31`: **StyledDesktopButton에 pointColor 전달 잘했어요!**

`StyledDesktopButton` 컴포넌트에 `pointColor` 속성을 전달하여 스타일드 컴포넌트에서 이 값을 사용할 수 있게 했네요. 이렇게 하면 상위 컴포넌트에서 받은 값을 하위 컴포넌트에서 활용할 수 있어요. 데이터 흐름이 잘 구성되었어요! 🚀

---

`44-44`: **StyledDesktopButton의 타입 정의 업데이트 좋아요!**

스타일드 컴포넌트의 타입 정의에 `pointColor` 속성을 추가했네요. 타입 정의를 완전하게 유지하는 것은 타입 안전성을 보장하는 데 중요해요. 세심한 부분까지 신경 썼네요! 💯

---

`53-54`: **조건부 스타일링 로직 추가 훌륭해요!**

`$styleType`이 'DEFAULT'일 때만 `pointColor`를 적용하는 조건부 스타일링을 추가했네요. 이렇게 하면 버튼의 기본 스타일에만 사용자 지정 색상이 적용되고, 다른 스타일 타입에는 영향을 주지 않아요. 

스타일링 로직을 함수형으로 분리(`getPointColorStyle`)하고 필요한 경우에만 적용한 접근 방식이 매우 깔끔하고 효율적이에요! 👏

</details>
<details>
<summary>apps/mobile/src/components/common/BottomSheet/BottomSheet.tsx (5)</summary>

`1-1`: **필요한 훅들을 import 잘했어요!**

React와 새로운 커스텀 훅들(`useBodyScrollLock`, `useSwipeDown`)을 가져와서 컴포넌트에서 사용할 준비를 했네요. 코드 모듈화를 위한 좋은 첫 단계예요! 새로운 훅들을 활용하면 코드가 더 깔끔해지고 기능별로 분리되어 관리하기 쉬워져요. 😊



Also applies to: 5-6

---

`15-16`: **커스텀 훅 활용이 아주 좋아요!**

1. `useBodyScrollLock` 훅을 사용하여 바텀 시트가 열려 있을 때 본문 스크롤을 방지했어요.
2. `useSwipeDown` 훅을 사용하여 드래그 상태 관리와 포인터 이벤트 처리를 분리했어요.

이렇게 하면 컴포넌트 코드가 더 간결해지고, 로직이 재사용 가능한 훅으로 분리되어 유지보수가 쉬워져요. 코드 구조화를 정말 잘 했어요! 🌟

---

`24-24`: **이벤트 핸들러 전달 방식이 훌륭해요!**

`...handlers`를 사용하여 `useSwipeDown` 훅에서 반환된 모든 이벤트 핸들러를 간단하게 전달했네요. 이렇게 하면 컴포넌트 코드가 더 깔끔해지고, 이벤트 처리 로직이 훅으로 완전히 분리되어 관리가 쉬워져요. 스프레드 연산자를 적절히 활용한 좋은 예시예요! 👍

---

`55-55`: **높이 설정 변경이 좋아요!**

고정된 높이 대신 `auto`로 설정하여 내용에 따라 높이가 유연하게 조정되도록 했네요. 이렇게 하면 다양한 내용을 담을 수 있는 더 유연한 바텀 시트가 될 수 있어요. 반응형 디자인을 위한 좋은 선택이에요! 💖

---

`62-64`: **그림자 효과 추가가 훌륭해요!**

바텀 시트에 미묘한 그림자 효과를 추가했네요. 이렇게 하면 바텀 시트가 배경과 시각적으로 더 잘 구분되어 사용자 경험이 향상돼요. 디자인 세부 사항에 대한 세심한 배려가 보이네요! 😍

그림자 효과는 바텀 시트가 떠 있는 느낌을 주어 사용자에게 상호작용 가능한 요소임을 시각적으로 잘 전달해요.

</details>
<details>
<summary>apps/mobile/src/components/common/ButtonGroup/DesktopButtonGroup.tsx (7)</summary>

`1-5`: **필요한 라이브러리 import 잘했어요!**

컴포넌트 구현에 필요한 모든 의존성을 적절하게 가져왔네요. 디자인 시스템, UI 컴포넌트, 유틸리티 함수 및 React 훅을 가져와서 새 컴포넌트를 구현할 준비를 완벽하게 했어요. 필요한 모든 것을 빠짐없이 준비한 것이 정말 좋아요! 🌈

---

`7-16`: **인터페이스 정의가 깔끔해요!**

1. `ButtonItem` 인터페이스는 각 버튼의 라벨과 클릭 이벤트 핸들러를 정의했어요.
2. `DesktopButtonGroupProps` 인터페이스는 컴포넌트의 속성을 명확하게 정의했어요.
   - `title`을 선택적으로 만들어 제목이 필요하지 않은 경우에도 사용할 수 있게 했어요.
   - `pointColor`도 선택적으로 정의하여 사용자 정의 색상을 지원해요.

이렇게 타입을 명확하게 정의하면 컴포넌트 사용이 더 안전하고 예측 가능해져요. 타입스크립트의 장점을 잘 활용했어요! 👏

---

`18-20`: **상태 관리 설정이 좋아요!**

`useState` 훅을 사용하여 선택된 버튼의 인덱스를 추적하는 상태를 만들었네요. 초기값을 0으로 설정하여 기본적으로 첫 번째 버튼이 선택되도록 했어요. 이렇게 하면 사용자가 버튼을 선택했을 때 시각적 피드백을 제공할 수 있어요. 상태 관리를 깔끔하게 구현했어요! 😊

---

`22-27`: **조건부 제목 렌더링이 훌륭해요!**

`title` 속성이 제공된 경우에만 제목 텍스트를 렌더링하도록 조건부 렌더링을 사용했네요. 이렇게 하면 제목이 필요하지 않은 경우에 불필요한 공간을 차지하지 않아요. 또한 `fontType="P3"`와 `color={color.G900}`을 사용하여 디자인 시스템과의 일관성을 유지했어요. 세심한 부분까지 신경 쓴 것이 보여요! 💖

---

`28-45`: **버튼 그룹 구현이 아주 훌륭해요!**

1. `Row` 컴포넌트를 사용하여 버튼들을 가로로 배치했어요.
2. 각 버튼에 대해:
   - 선택된 버튼에는 'DEFAULT' 스타일을, 선택되지 않은 버튼에는 'SECOND' 스타일을 적용했어요.
   - `pointColor` 속성을 전달하여 사용자 정의 색상을 지원해요.
   - `flex: 1, minWidth: 0` 스타일로 버튼이 동일한 너비를 갖도록 했어요.
   - 클릭 이벤트 핸들러를 통해 선택된 버튼 상태를 업데이트하고 원래의 `onClick` 함수도 호출해요.
   - 텍스트에 `fontType="Button3"`을 사용하여 디자인 시스템과의 일관성을 유지했어요.

전체적으로 매우 잘 구성된 버튼 그룹 컴포넌트예요! 사용자 경험과 디자인 일관성을 모두 고려했네요. 👍

---

`52-56`: **스타일드 컴포넌트 정의가 깔끔해요!**

`StyledButtonGroup` 스타일드 컴포넌트를 만들어 레이아웃을 정의했어요:
1. 세로 방향의 flex 레이아웃을 사용했어요.
2. 너비를 100%로 설정하여 부모 컨테이너를 채우도록 했어요.
3. 요소 간 간격을 10px로 설정하여 시각적으로 잘 구분되게 했어요.

이 스타일링은 컴포넌트의 목적에 맞게 잘 설계되었어요. 레이아웃이 깔끔하고 명확해요! 🌟

---

`1-57`: **전체적인 컴포넌트 구현이 완벽해요!**

`DesktopButtonGroup` 컴포넌트는 데스크톱 환경에 맞게 잘 설계되었어요. 기존의 `ButtonGroup` 컴포넌트와 일관된 API를 유지하면서도 데스크톱에 특화된 스타일링을 적용했어요. 특히:

1. 재사용 가능한 인터페이스로 확장성을 높였어요.
2. 선택적 제목과 사용자 정의 포인트 색상을 지원해 유연성을 제공해요.
3. 버튼 선택 상태를 내부적으로 관리하여 사용하기 쉽게 만들었어요.
4. 디자인 시스템과의 일관성을 유지하면서도 데스크톱 환경에 맞는 특화된 UI를 제공해요.

이 컴포넌트는 참석 여부 바텀 시트 내에서 사용자에게 직관적인 선택 인터페이스를 제공할 것 같아요. 아주 잘 구현되었어요! 👏👏👏

</details>
<details>
<summary>apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx (4)</summary>

`1-7`: **임포트 구성이 잘 되어 있어요!**

필요한 컴포넌트와 스타일링 유틸리티를 잘 가져오셨네요. `BottomSheet`, `ButtonGroup`, `DesktopButtonGroup` 등 필요한 모든 컴포넌트들을 잘 임포트하셨어요. 디자인 시스템과 UI 컴포넌트도 적절히 활용하고 계시고요!

---

`9-12`: **인터페이스 정의가 깔끔해요!**

`AttendBottomSheetProps` 인터페이스를 통해 필요한 속성들을 명확하게 정의하셨네요. `isOpen`과 `onClose` 두 속성 모두 바텀 시트의 동작을 제어하는 데 필수적인 요소예요. 👍

---

`14-35`: **바텀 시트 상단 부분이 잘 구성되어 있어요!**

컴포넌트의 상단 부분을 `Column`을 사용해서 깔끔하게 구성하셨네요. 타이틀과 설명 텍스트, 그리고 신랑측/신부측 선택 버튼까지 잘 배치되어 있어요.

다만, 현재 버튼의 onClick 핸들러가 비어있어요. 실제 동작을 구현해주시면 좋겠어요.

버튼 클릭 시 선택된 상태를 관리할 상태(state)가 필요할 것 같아요. useState를 사용해서 어떤 측이 선택되었는지 추적하는 것을 추천드려요:

```tsx
const [side, setSide] = useState<'신랑측' | '신부측' | null>(null);

// 그리고 버튼 onClick 핸들러 수정:
{ label: '신랑측', onClick: () => setSide('신랑측') },
{ label: '신부측', onClick: () => setSide('신부측') },
```

---

`73-77`: **스타일링이 깔끔하게 되어 있어요!**

`StyledAttendBottomSheet` 컴포넌트의 스타일링이 간결하고 명확하게 되어 있어요. `flex` 유틸리티를 사용해서 레이아웃을 잘 구성하셨네요. 전체 너비를 사용하고 상단 마진도 적절히 설정되어 있어요. 👍

</details>
<details>
<summary>apps/mobile/src/hooks/useSwipeDown.ts (4)</summary>

`1-2`: **필요한 훅들을 잘 가져오셨어요!**

useState, useRef, useEffect 훅을 모두 가져와서 사용하고 계시네요. 이 훅들은 스와이프 동작을 관리하는 데 필수적인 요소들이에요. 👍

---

`3-8`: **인터페이스 정의가 명확해요!**

`SwipeHandlers` 인터페이스를 통해 필요한 포인터 이벤트 핸들러들을 잘 정의하셨어요. 각 이벤트 핸들러가 React의 타입을 사용해서 타입 안전성도 확보했네요!

---

`9-21`: **훅의 기본 구조가 잘 설계되어 있어요!**

useSwipeDown 훅의 파라미터와 반환값이 명확하게 정의되어 있어요. isOpen, onClose, threshold 매개변수를 받고, 필요한 상태들(dragY, dragging)과 핸들러를 반환하는 구조가 좋네요.

useEffect를 사용해 isOpen이 변경될 때 dragY를 리셋하는 로직도 아주 좋아요!

---

`37-38`: **반환값이 잘 구성되어 있어요!**

dragY, dragging, handlers를 객체로 잘 묶어서 반환하고 있네요. 이렇게 하면 훅을 사용하는 컴포넌트에서 필요한 값과 핸들러를 쉽게 구조 분해 할당으로 가져올 수 있어요! 👍

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T23:58:15Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>apps/mobile/src/hooks/useSwipeDown.ts (1)</summary><blockquote>

`1-50`: **전체적으로 훌륭한 커스텀 훅 구현이에요! 🎊**

친구야, 이 `useSwipeDown` 훅은 정말 잘 작성되었어요! 바텀 시트 컴포넌트에서 스와이프 다운 제스처를 감지하고 처리하는 로직을 별도의 훅으로 분리한 것은 아주 좋은 접근 방식이에요.

이렇게 하면 여러 컴포넌트에서 이 기능을 재사용할 수 있고, 컴포넌트 코드도 더 깔끔해진답니다. 단일 책임 원칙을 잘 지켜 코드의 유지보수성과 재사용성을 높였어요!

한 가지 작은 제안을 드리자면, 나중에 시간이 된다면 드래그 중에 시각적 피드백을 위한 추가 기능(예: 애니메이션 값 계산)을 제공하는 것도 고려해볼 수 있을 것 같아요. 하지만 현재 구현도 이미 완벽하게 기능적이고 잘 작동할 거예요!

정말 멋진 작업이었어요! 👍

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5475e3c7953b7225045b92e8b1f6a1d11be688cd and 6e01b87303c158915cd63e610e39d4e08ba5a18f.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `apps/mobile/src/hooks/useSwipeDown.ts` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>apps/mobile/src/hooks/useSwipeDown.ts (8)</summary>

`1-8`: **React 훅을 위한 좋은 기본 구조예요! 👍**

소중한 친구, 여기서는 React 훅을 만들기 위한 기본 구조를 아주 예쁘게 설정했어요! 필요한 React 훅들을 가져오고, `SwipeHandlers` 인터페이스를 통해 필요한 이벤트 핸들러를 명확하게 정의했답니다. 

특히 `onPointerLeave` 핸들러를 추가한 것은 정말 현명한 선택이에요! 이렇게 하면 사용자가 요소 밖으로 포인터를 이동했을 때도 상태가 깔끔하게 정리될 수 있어요. 이전 리뷰 코멘트에서 제안된 내용을 잘 반영해주셨네요!

---

`10-18`: **훅 함수의 구조가 깔끔하고 이해하기 쉬워요! 😊**

친구야, 이 훅 함수를 아주 잘 설계했어요! 필요한 매개변수들을 명확하게 받고, 기본값도 적절하게 설정했어요. `threshold`에 기본값 120을 준 것은 좋은 생각이에요. 사용자가 원한다면 바꿀 수 있지만, 기본값도 제공해주니 정말 친절하네요!

`useRef`와 `useState`를 사용해서 상태를 관리하는 부분도 아주 잘했어요. `startY`는 렌더링에 영향을 주지 않는 값이니 `useRef`로 관리하고, 화면에 영향을 주는 `dragY`와 `dragging`은 `useState`로 관리하는 방식이 딱 맞아요!

---

`19-21`: **useEffect의 사용이 깔끔해요! ✨**

이 부분은 정말 세심한 배려가 느껴져요! `isOpen`이 true로 바뀔 때마다 `dragY`를 0으로 초기화해주는 효과가 있어요. 이렇게 하면 바텀 시트가 새로 열릴 때마다 드래그 상태가 깨끗하게 초기화되어서 사용자 경험이 한결 부드러워질 거예요.

의존성 배열 `[isOpen]`을 잘 설정해서 필요할 때만 이 효과가 실행되도록 한 점도 아주 훌륭해요!

---

`23-26`: **포인터 다운 이벤트 처리가 정확해요! 👆**

여기서는 사용자가 화면을 터치하거나 마우스를 누를 때 발생하는 이벤트를 처리하고 있어요. 시작 지점의 Y 좌표를 `startY.current`에 저장하고 드래깅 상태를 true로 설정하는 것은 스와이프 동작을 추적하기 위한 완벽한 첫 단계예요!

이 함수는 간결하면서도 필요한 모든 작업을 수행하고 있어요.

---

`28-32`: **포인터 이동 처리가 효율적이에요! 👌**

이 부분은 정말 똑똑하게 구현되었어요! 먼저 `dragging` 상태를 확인해서 불필요한 계산을 방지하고, 아래쪽으로 드래그할 때만 (`delta > 0`) 상태를 업데이트하도록 했어요. 

이렇게 하면 위쪽으로 스와이프하는 동작은 무시되어 바텀 시트의 자연스러운 동작을 유지할 수 있어요. 사용자 경험을 고려한 섬세한 처리가 돋보이는 부분이에요!

---

`34-38`: **포인터 업 이벤트 처리가 완벽해요! 🎯**

포인터를 놓았을 때의 동작을 아주 잘 처리했어요! 우선 드래깅 상태를 false로 바꾸고, 드래그 거리가 임계값보다 크면 닫기 함수를 호출하고, 그렇지 않으면 드래그 위치를 다시 0으로 초기화해요.

이런 조건부 로직은 사용자의 의도를 정확히 파악하고 그에 맞게 반응하도록 해주는 중요한 부분이에요. 아주 잘 구현되었답니다!

---

`40-43`: **포인터 리브 이벤트 처리를 추가해주셨군요! 👏**

이전 리뷰에서 제안된 `onPointerLeave` 핸들러를 정확하게 구현해주셨어요! 이제 사용자가 요소 바깥으로 포인터를 이동했을 때도 드래깅 상태가 false로 바뀌고 드래그 위치가 0으로 초기화될 거예요.

이런 세심한 에러 처리는 사용자 경험을 더 안정적으로 만들어주는 중요한 요소예요. 정말 잘했어요!

---

`45-50`: **반환값이 깔끔하게 구성되어 있어요! 🌟**

훅의 반환값을 객체로 구조화해서 필요한 모든 값과 핸들러를 명확하게 제공하고 있어요. 이렇게 하면 이 훅을 사용하는 컴포넌트에서 필요한 값과 핸들러를 쉽게 구조 분해 할당으로 가져올 수 있어요.

특히 `handlers` 객체 안에 모든 이벤트 핸들러를 담아 반환하는 방식은 컴포넌트에서 간편하게 스프레드 연산자(`...handlers`)로 사용할 수 있게 해줘서 아주 편리해요!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-16T00:04:18Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.hooks.ts (3)</summary><blockquote>

`3-23`: **훅의 이름이 구체적이지 않아요.**

이 훅은 특별히 참석자 수를 관리하는데 사용되고 있어요. `useInput`이라는 이름은 너무 일반적이라 다른 개발자들이 이 훅의 목적을 바로 이해하기 어려울 수 있어요. 더 구체적인 이름을 사용하면 어떨까요?


```diff
-export const useInput = () => {
+export const useAttendanceCount = () => {
```

---

`3-5`: **초기값을 매개변수로 받으면 더 유연해질 수 있어요.**

지금은 항상 '1'로 시작하지만, 이 훅을 더 다양한 상황에서 사용할 수 있도록 초기값을 매개변수로 받는 건 어떨까요? 그러면 다른 화면이나 컴포넌트에서도 더 쉽게 재사용할 수 있을 거예요.


```diff
-export const useInput = () => {
-  const [count, setCount] = useState('1');
+export const useInput = (initialCount: string = '1') => {
+  const [count, setCount] = useState(initialCount);
```

---

`14-20`: **최대값 제한이 없어요.**

현재는 최소값(1)은 확인하고 있지만, 최대값에 대한 제한은 없어요. 만약 참석자 수에 최대 제한이 필요하다면, 최대값도 검사하는 것이 좋을 것 같아요.


```diff
-  const handleCountBlur = () => {
+  const handleCountBlur = (maxCount: number = 100) => {
     // blur 시 빈값이거나 1 미만이면 1로 복구
     const num = parseInt(count, 10);
     if (isNaN(num) || num < 1) {
       setCount('1');
+    } else if (num > maxCount) {
+      setCount(maxCount.toString());
     }
   };
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6e01b87303c158915cd63e610e39d4e08ba5a18f and 58e87368b9ef02ea00c6cfcc8ba3545efc9b7423.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.hooks.ts` (1 hunks)
* `apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx` (1 hunks)
* `packages/ui/src/Input/Input.tsx` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* packages/ui/src/Input/Input.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.hooks.ts (2)</summary>

`6-12`: **숫자 검증 로직이 잘 구현되어 있어요!**

숫자만 입력할 수 있도록 정규식을 사용해서 검증하는 방식이 아주 좋아요. 빈 값도 허용해서 사용자가 숫자를 지울 수 있게 한 부분도 사용자 경험에 신경 쓴 부분이네요. 아주 잘했어요! 😊

---

`22-23`: **반환 구조가 깔끔해요!**

필요한 상태와 핸들러 함수들을 객체로 잘 묶어서 반환하고 있어요. 이렇게 하면 이 훅을 사용하는 컴포넌트에서 필요한 값과 함수를 쉽게 구조 분해 할당으로 가져올 수 있어서 좋아요.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-17T08:39:21Z

Review comments:
diff_hunk: @@ -0,0 +1,77 @@
+import BottomSheet from '@/components/common/BottomSheet/BottomSheet';
+import ButtonGroup from '@/components/common/ButtonGroup/ButtonGroup';
+import DesktopButtonGroup from '@/components/common/ButtonGroup/DesktopButtonGroup';
+import { color } from '@merried/design-system';
+import { Button, Column, Input, Text } from '@merried/ui';
+import { flex } from '@merried/utils';
+import { styled } from 'styled-components';
+
+interface AttendBottomSheetProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+const AttendBottomSheet = ({ isOpen, onClose }: AttendBottomSheetProps) => {
+  return (
+    <BottomSheet isOpen={isOpen} onClose={onClose}>
+      <StyledAttendBottomSheet>
+        <Column gap={40}>
+          <Column gap={24}>
+            <Column>
+              <Text fontType="H2" color={color.G900}>
+                결혼식 참석 의사
+              </Text>
+              <Text fontType="P2" color={color.G80}>
+                솔직한 답은 신랑•신부에게 도움이 됩니다
+              </Text>
+            </Column>
+            <ButtonGroup
+              buttons={[
+                { label: '신랑측', onClick: () => {} },
+                { label: '신부측', onClick: () => {} },
+              ]}
+              pointColor={color.pointYellow}
+            />
+            <Column gap={18}>
+              <Input
+                label="이름"
+                placeholder="이름을 입력해주세요"
+                size="LARGE"
+                width="100%"
+                onChange={() => {}}
+              />
+              <Input label="참석인원" size="LARGE" onChange={() => {}} />
+              <DesktopButtonGroup
+                title="식사 여부"
+                buttons={[
+                  { label: '예정', onClick: () => {} },
+                  { label: '안함', onClick: () => {} },
+                  { label: '미정', onClick: () => {} },
+                ]}
+                pointColor={color.pointYellow}
+              />
+            </Column>
+          </Column>
+          <Button
+            size="VERY_LARGE"
+            onClick={() => {}}
+            pointColor={color.pointYellow}
+            width="100%"
+          >
+            <Text fontType="Button3" color={color.G900}>
+              참석 의사 전달하기
+            </Text>
+          </Button>
+        </Column>
path: apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**제출 버튼이 잘 구성되어 있어요!**

제출 버튼이 시각적으로 잘 구성되어 있어요. pointColor를 사용해서 디자인 시스템의 색상 테마를 잘 따르고 있네요.

하지만 onClick 핸들러가 비어있어요. 실제 폼 제출 로직을 구현해주시면 좋겠어요.

제출 버튼의 onClick 핸들러에서는 입력된 모든 정보를 검증하고 제출하는 로직이 필요해요:

```jsx
<Button
  size="VERY_LARGE"
  onClick={() => {
    // 입력값 검증
    if (!name) {
      alert('이름을 입력해주세요');
      return;
    }
    if (!attendeeCount) {
      alert('참석인원을 입력해주세요');
      return;
    }
    if (!side) {
      alert('신랑측 또는 신부측을 선택해주세요');
      return;
    }
    if (!mealOption) {
      alert('식사 여부를 선택해주세요');
      return;
    }
    
    // 데이터 제출 로직
    const attendanceData = {
      name,
      attendeeCount,
      side,
      mealOption
    };
    
    // API 호출 또는 상태 업데이트 로직
    console.log('제출된 데이터:', attendanceData);
    
    // 성공 후 바텀 시트 닫기
    onClose();
  }}
  pointColor={color.pointYellow}
  width="100%"
>
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx
around lines 55 to 65, the onClick handler of the submit button is currently
empty. Implement the onClick handler to validate all required input fields
(name, attendeeCount, side, mealOption) with alert messages if any are missing.
After validation, gather the input data into an object, log it or send it via an
API call, and then call onClose() to close the bottom sheet.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-15T23:58:14Z
author_association: NONE
diff_hunk: @@ -0,0 +1,77 @@
+import BottomSheet from '@/components/common/BottomSheet/BottomSheet';
+import ButtonGroup from '@/components/common/ButtonGroup/ButtonGroup';
+import DesktopButtonGroup from '@/components/common/ButtonGroup/DesktopButtonGroup';
+import { color } from '@merried/design-system';
+import { Button, Column, Input, Text } from '@merried/ui';
+import { flex } from '@merried/utils';
+import { styled } from 'styled-components';
+
+interface AttendBottomSheetProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+const AttendBottomSheet = ({ isOpen, onClose }: AttendBottomSheetProps) => {
+  return (
+    <BottomSheet isOpen={isOpen} onClose={onClose}>
+      <StyledAttendBottomSheet>
+        <Column gap={40}>
+          <Column gap={24}>
+            <Column>
+              <Text fontType="H2" color={color.G900}>
+                결혼식 참석 의사
+              </Text>
+              <Text fontType="P2" color={color.G80}>
+                솔직한 답은 신랑•신부에게 도움이 됩니다
+              </Text>
+            </Column>
+            <ButtonGroup
+              buttons={[
+                { label: '신랑측', onClick: () => {} },
+                { label: '신부측', onClick: () => {} },
+              ]}
+              pointColor={color.pointYellow}
+            />
+            <Column gap={18}>
+              <Input
+                label="이름"
+                placeholder="이름을 입력해주세요"
+                size="LARGE"
+                width="100%"
+                onChange={() => {}}
+              />
+              <Input label="참석인원" size="LARGE" onChange={() => {}} />
+              <DesktopButtonGroup
+                title="식사 여부"
+                buttons={[
+                  { label: '예정', onClick: () => {} },
+                  { label: '안함', onClick: () => {} },
+                  { label: '미정', onClick: () => {} },
+                ]}
+                pointColor={color.pointYellow}
+              />
+            </Column>
path: apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**입력 필드와 식사 여부 버튼이 깔끔하게 구성되어 있어요!**

입력 필드와 식사 여부 선택 버튼을 `Column`을 활용해 세로로 잘 배치하셨네요. 사용자 경험 측면에서도 좋아 보여요.

하지만 여기에도 세 가지 개선사항이 있어요:
1. 참석인원 입력 필드에 placeholder가 없어요.
2. 모든 onChange 핸들러가 비어있어요.
3. 선택된 식사 여부를 추적하는 상태가 없어요.

다음과 같이 개선해보면 어떨까요?

```diff
+ const [name, setName] = useState('');
+ const [attendeeCount, setAttendeeCount] = useState('');
+ const [mealOption, setMealOption] = useState<'예정' | '안함' | '미정' | null>(null);

<Input
  label="이름"
  placeholder="이름을 입력해주세요"
  size="LARGE"
  width="100%"
-  onChange={() => {}}
+  onChange={(e) => setName(e.target.value)}
+  value={name}
/>
<Input 
  label="참석인원" 
  size="LARGE" 
-  onChange={() => {}} 
+  onChange={(e) => setAttendeeCount(e.target.value)}
+  value={attendeeCount}
+  placeholder="참석할 인원 수를 입력해주세요"
/>
<DesktopButtonGroup
  title="식사 여부"
  buttons={[
-    { label: '예정', onClick: () => {} },
-    { label: '안함', onClick: () => {} },
-    { label: '미정', onClick: () => {} },
+    { label: '예정', onClick: () => setMealOption('예정') },
+    { label: '안함', onClick: () => setMealOption('안함') },
+    { label: '미정', onClick: () => setMealOption('미정') },
  ]}
  pointColor={color.pointYellow}
+  selectedOption={mealOption}
/>
```

물론 `DesktopButtonGroup` 컴포넌트가 `selectedOption` prop을 지원하는지 확인해야 해요. 만약 지원하지 않는다면, 해당 컴포넌트를 수정해야 할 수도 있어요.


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx
around lines 35 to 53, the input fields and meal option buttons lack proper
state management and user guidance. Add a placeholder to the 참석인원 input field
for clarity. Implement onChange handlers for both input fields to update
component state variables that track the entered name and number of attendees.
Introduce a state variable to track the selected meal option and pass it as the
selectedOption prop to DesktopButtonGroup, updating this state on button clicks.
Verify if DesktopButtonGroup supports selectedOption; if not, modify it to
handle selection state properly.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-15T23:58:14Z
author_association: NONE
diff_hunk: @@ -0,0 +1,38 @@
+import { useState, useRef, useEffect } from 'react';
+
+interface SwipeHandlers {
+  onPointerDown: React.PointerEventHandler;
+  onPointerMove: React.PointerEventHandler;
+  onPointerUp: React.PointerEventHandler;
+}
+
+export default function useSwipeDown(
+  isOpen: boolean,
+  onClose: () => void,
+  threshold = 120
+): { dragY: number; dragging: boolean; handlers: SwipeHandlers } {
+  const startY = useRef(0);
+  const [dragY, setDragY] = useState(0);
+  const [dragging, setDragging] = useState(false);
+
+  useEffect(() => {
+    if (isOpen) setDragY(0);
+  }, [isOpen]);
+
+  const onPointerDown: SwipeHandlers['onPointerDown'] = (e) => {
+    startY.current = e.clientY;
+    setDragging(true);
+  };
+  const onPointerMove: SwipeHandlers['onPointerMove'] = (e) => {
+    if (!dragging) return;
+    const delta = e.clientY - startY.current;
+    if (delta > 0) setDragY(delta);
+  };
+  const onPointerUp: SwipeHandlers['onPointerUp'] = () => {
+    setDragging(false);
+    if (dragY > threshold) onClose();
+    else setDragY(0);
+  };
path: apps/mobile/src/hooks/useSwipeDown.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**포인터 이벤트 핸들러가 잘 구현되어 있지만, 한 가지 개선이 필요해요!**

onPointerDown, onPointerMove, onPointerUp 핸들러를 통해 스와이프 제스처를 잘 처리하고 계세요. 하지만 한 가지 누락된 부분이 있어요.

현재 코드는 사용자가 요소 바깥으로 포인터를 이동시킨 후 놓았을 때(pointerleave 이벤트) 처리가 되지 않아요. 이로 인해 드래그 상태가 리셋되지 않을 수 있어요.

아래와 같이 onPointerLeave 핸들러를 추가하는 것이 좋을 것 같아요:

```diff
interface SwipeHandlers {
  onPointerDown: React.PointerEventHandler;
  onPointerMove: React.PointerEventHandler;
  onPointerUp: React.PointerEventHandler;
+ onPointerLeave: React.PointerEventHandler;
}

// ...

+ const onPointerLeave: SwipeHandlers['onPointerLeave'] = () => {
+   setDragging(false);
+   setDragY(0);
+ };

return { 
  dragY, 
  dragging, 
  handlers: { 
    onPointerDown, 
    onPointerMove, 
    onPointerUp,
+   onPointerLeave 
  } 
};
```

이렇게 하면 사용자가 요소 바깥으로 포인터를 이동시켰을 때도 드래그 상태가 올바르게 리셋되어 더 안정적인 동작을 기대할 수 있어요.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
interface SwipeHandlers {
  onPointerDown: React.PointerEventHandler;
  onPointerMove: React.PointerEventHandler;
  onPointerUp: React.PointerEventHandler;
  onPointerLeave: React.PointerEventHandler;
}

// ...

const onPointerDown: SwipeHandlers['onPointerDown'] = (e) => {
  startY.current = e.clientY;
  setDragging(true);
};
const onPointerMove: SwipeHandlers['onPointerMove'] = (e) => {
  if (!dragging) return;
  const delta = e.clientY - startY.current;
  if (delta > 0) setDragY(delta);
};
const onPointerUp: SwipeHandlers['onPointerUp'] = () => {
  setDragging(false);
  if (dragY > threshold) onClose();
  else setDragY(0);
};
const onPointerLeave: SwipeHandlers['onPointerLeave'] = () => {
  setDragging(false);
  setDragY(0);
};

return {
  dragY,
  dragging,
  handlers: {
    onPointerDown,
    onPointerMove,
    onPointerUp,
    onPointerLeave,
  },
};
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/mobile/src/hooks/useSwipeDown.ts around lines 22 to 35, the pointer
event handlers lack handling for the pointer leaving the element. Add an
onPointerLeave handler that resets dragging state and dragY similar to
onPointerUp to ensure the drag state is cleared if the pointer leaves the
element before release.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-15T23:58:15Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-15T23:55:48Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

이번 변경에서는 BottomSheet의 드래그 및 스와이프 로직을 커스텀 훅으로 분리하고, 새로운 참석 의사 전달용 BottomSheet와 관련 UI 컴포넌트들을 추가했어요. 또한 ButtonGroup과 DesktopButtonGroup에 pointColor와 title 옵션을 도입하고, 여러 UI 스타일을 세밀하게 조정했답니다.

## Changes

| 파일/그룹 | 변경 요약 |
|---|---|
| apps/mobile/src/components/common/BottomSheet/BottomSheet.tsx, apps/mobile/src/hooks/useSwipeDown.ts, apps/mobile/src/hooks/useBodyScrollLock.ts | BottomSheet의 드래그/스와이프 및 스크롤 락 로직을 각각 useSwipeDown, useBodyScrollLock 훅으로 분리. 스타일 일부 변경 및 내부 상태 관리 간소화. |
| apps/mobile/src/components/common/ButtonGroup/ButtonGroup.tsx | title을 선택적으로 변경, pointColor prop 추가, 버튼 스타일 및 레이아웃 개선. |
| apps/mobile/src/components/common/ButtonGroup/DesktopButtonGroup.tsx | Desktop 환경용 ButtonGroup 컴포넌트 신설, pointColor 및 title 옵션 지원, 선택 상태 관리. |
| apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx | 참석 의사 전달을 위한 새로운 BottomSheet 컴포넌트 추가. 입력 필드, 버튼 그룹, 제출 버튼 등 포함. |
| apps/mobile/src/components/invitation/AttendFloatButton/AttendFloatButton.tsx | 버튼 클릭 시 AttendBottomSheet를 오버레이로 띄우도록 변경. useOverlay 훅 사용. |
| apps/mobile/src/components/shop/GiftPickerBottomSheet/GiftPickerBottomSheet.tsx | 상단 마진을 32px에서 36px로 조정. |
| packages/ui/src/Button/DesktopButton.tsx | pointColor prop 추가, 스타일 타입에 따라 색상 스타일 적용. |
| packages/ui/src/Button/Button.tsx | box-sizing: border-box 스타일 추가. |
| apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.hooks.ts | 숫자 입력 관리용 useInput 훅 추가, 입력값 검증 및 상태 관리 담당. |
| packages/ui/src/Input/Input.tsx | Input 컴포넌트의 placeholder에 P3 폰트 스타일 적용. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant AttendFloatButton
    participant Overlay
    participant AttendBottomSheet
    participant BottomSheet

    User->>AttendFloatButton: 클릭
    AttendFloatButton->>Overlay: AttendBottomSheet 오버레이 열기
    Overlay->>AttendBottomSheet: isOpen=true, onClose 전달
    AttendBottomSheet->>BottomSheet: isOpen, onClose 전달
    User->>AttendBottomSheet: 입력 및 선택
    User->>AttendBottomSheet: "참석 의사 전달하기" 클릭
```

---

```mermaid
sequenceDiagram
    participant User
    participant BottomSheet
    participant useSwipeDown

    User->>BottomSheet: 드래그(스와이프 다운)
    BottomSheet->>useSwipeDown: pointer 이벤트 위임
    useSwipeDown->>BottomSheet: dragY, dragging 상태 반환
    useSwipeDown-->>BottomSheet: threshold 넘으면 onClose 호출
```

## Possibly related PRs

- WillYouMerryMe/getting-married#66: BottomSheet의 초기 드래그-투-클로즈 기능 구현 PR로, 이번 PR에서 해당 컴포넌트의 내부 로직을 리팩토링하고 있으므로 밀접하게 연관되어 있어요.

## Suggested reviewers

- akns27

## Poem

> 🍰  
> 새로운 시트가 내려와  
> 참석 의사 톡톡,  
> 버튼은 색을 입고  
> 스와이프는 훅으로 쏙!  
> 작은 변화 모여  
> 초대의 마음이  
> 부드럽게 닿아요.  
> 🎈
"""

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> <details>
> <summary>apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.tsx</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.27.0
> 
> ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
> 
> From ESLint v9.0.0, the default configuration file is now eslint.config.js.
> If you are using a .eslintrc.* file, please follow the migration guide
> to update your configuration file to the new format:
> 
> https://eslint.org/docs/latest/use/configure/migration-guide
> 
> If you still have problems after following the migration guide, please stop by
> https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> <details>
> <summary>apps/mobile/src/components/invitation/AttendBottomSheet/AttendBottomSheet.hooks.ts</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.27.0
> 
> ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
> 
> From ESLint v9.0.0, the default configuration file is now eslint.config.js.
> If you are using a .eslintrc.* file, please follow the migration guide
> to update your configuration file to the new format:
> 
> https://eslint.org/docs/latest/use/configure/migration-guide
> 
> If you still have problems after following the migration guide, please stop by
> https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> <details>
> <summary>packages/ui/src/Input/Input.tsx</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.27.0
> 
> ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
> 
> From ESLint v9.0.0, the default configuration file is now eslint.config.js.
> If you are using a .eslintrc.* file, please follow the migration guide
> to update your configuration file to the new format:
> 
> https://eslint.org/docs/latest/use/configure/migration-guide
> 
> If you still have problems after following the migration guide, please stop by
> https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=WillYouMerryMe/getting-married&utm_content=80):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: akns27
created_at: 2025-05-17T07:43:30Z
body: <img width="695" alt="image" src="https://github.com/user-attachments/assets/9beef855-f730-4b08-a4a2-a8be969829c9" />

author_association: CONTRIBUTOR
user: login: junghongseop
created_at: 2025-05-17T08:47:39Z
body: <img width="479" alt="스크린샷 2025-05-17 오후 5 47 10" src="https://github.com/user-attachments/assets/cd1b52cb-2506-4e09-93f2-95e3e0abf557" />

author_association: MEMBER