Repository: 9oormthon-univ/2024_DANPOONG_TEAM_22_FE
Pull request #142: feat/DismissKeyboardView 에 고정가능한 footer 추가
Author: login: salmonco
Created: 2025-04-14T15:58:12Z | Merged: 2025-04-22T15:17:01Z

## 작업 분류

- [x] 기능 추가
- [ ] QA 반영
- [ ] 리팩토링

## 요구사항

- 공통 > 온보딩 > 키보드 올랐다가 내리면 밑에 플로팅버튼이 사라지는 문제 해결

## 작업 내용

- close #107 

## 테스트

<!-- 시연영상, 복잡한 로직의 경우 테스트 코드 등 -->


Linked issue #107: [ios 대응] 공통 > 온보딩 > 키보드 올랐다가 내리면 밑에 플로팅버튼이 사라지는 문제
story point: 3

Diff:
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 45f0c345..8e00d7b1 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -24,12 +24,14 @@
       android:theme="@style/AppTheme"
       android:usesCleartextTraffic="true"
       android:supportsRtl="true">
+      <!-- android:windowSoftInputMode="adjustNothing" 설정함으로써 -->
+      <!-- 키보드가 올라올 때 화면을 조정하지 않도록 함. 컴포넌트가 키보드 위로 올라오는 문제 해결을 위함 -->
       <activity
         android:name=".MainActivity"
         android:label="내일모래"
         android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
-        android:launchMode="singleTask"
-        android:windowSoftInputMode="adjustResize"
+        android:launchMode="singleTask"        
+        android:windowSoftInputMode="adjustNothing"
         android:exported="true"
         android:networkSecurityConfig="@xml/network_security_config">
         <intent-filter>
diff --git a/src/components/AnimatedView/index.tsx b/src/components/AnimatedView/index.tsx
index aad1ea96..5c5bfb8b 100644
--- a/src/components/AnimatedView/index.tsx
+++ b/src/components/AnimatedView/index.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
 import { Animated, type ViewStyle } from 'react-native';
 
 type AnimatedViewProps = {
@@ -14,11 +14,15 @@ export const AnimatedView: React.FC<AnimatedViewProps> = ({
   className,
   style,
 }) => {
+  const [shouldRender, setShouldRender] = useState(visible);
+
   const fadeAnim = useRef(new Animated.Value(0)).current;
   const translateYAnim = useRef(new Animated.Value(50)).current;
 
   useEffect(() => {
     if (visible) {
+      setShouldRender(true); // 먼저 보여주고 애니메이션 시작
+
       Animated.parallel([
         Animated.timing(fadeAnim, {
           toValue: 1,
@@ -43,10 +47,14 @@ export const AnimatedView: React.FC<AnimatedViewProps> = ({
           duration: 200,
           useNativeDriver: true,
         }),
-      ]).start();
+      ]).start(() => {
+        setShouldRender(false); // 애니메이션 끝난 후 숨기기
+      });
     }
   }, [visible]);
 
+  if (!shouldRender) return null;
+
   return (
     <Animated.View
       style={[
diff --git a/src/components/BottomMenu/index.tsx b/src/components/BottomMenu/index.tsx
index fd77774b..fe2ebeae 100644
--- a/src/components/BottomMenu/index.tsx
+++ b/src/components/BottomMenu/index.tsx
@@ -9,7 +9,7 @@ type BottomMenuProps = {
 
 export const BottomMenu = ({ title, data }: Readonly<BottomMenuProps>) => {
   return (
-    <>
+    <View style={{ borderRadius: 10 }} className="bg-blue500">
       {title && (
         <View className="h-[43] justify-center items-center">
           <CustomText type="caption1" text={title} className="text-gray300" />
@@ -26,6 +26,6 @@ export const BottomMenu = ({ title, data }: Readonly<BottomMenuProps>) => {
           {index !== data.length - 1 && <View className="bg-blue600 h-[1]" />}
         </View>
       ))}
-    </>
+    </View>
   );
 };
diff --git a/src/components/DismissKeyboardView/index.tsx b/src/components/DismissKeyboardView/index.tsx
index cb684018..3e274d6e 100644
--- a/src/components/DismissKeyboardView/index.tsx
+++ b/src/components/DismissKeyboardView/index.tsx
@@ -1,23 +1,92 @@
+import { Children, isValidElement, type ReactNode } from 'react';
 import {
   Keyboard,
+  Platform,
   type StyleProp,
   TouchableWithoutFeedback,
+  View,
   type ViewStyle,
 } from 'react-native';
 import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
 
-type Component = {
+import { useKeyboardHeight } from '@hooks/useKeyboardHeight';
+
+type FooterProps = { children: ReactNode };
+
+type Props = {
+  children: ReactNode;
+  /** 고정시킬 푸터가 있고, 키보드가 올라오면서 키보드 위 간격을 띄우고 싶을 경우, 키보드 위 간격 (android-only) */
+  extraHeight?: number;
+  /** 키보드 위에 올릴 컴포넌트가 있을 경우, 컴포넌트를 띄울 키보드 위 간격 */
+  extraScrollHeight?: number;
   style?: StyleProp<ViewStyle>;
-  children: JSX.Element[] | JSX.Element;
 };
 
+/** default extra offset when focusing the TextInputs. */
+const DEFAULT_EXTRA_HEIGHT = 75;
+
+// TODO: 안드로이드에서는 footer 고정이 안 되는 문제
 export const DismissKeyboardView = ({
   children,
+  extraHeight = 0,
+  extraScrollHeight = 0,
   ...props
-}: Readonly<Component>) => (
-  <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
-    <KeyboardAwareScrollView {...props} style={props.style}>
-      {children}
-    </KeyboardAwareScrollView>
-  </TouchableWithoutFeedback>
-);
+}: Props) => {
+  const { maxKeyboardHeight } = useKeyboardHeight();
+
+  // 푸터로 렌더링될 React 노드를 저장할 변수
+  let footer: ReactNode = null;
+
+  /** 푸터를 제외한 나머지 children */
+  // 자식 요소들을 순회하며 DismissKeyboardView.Footer 컴포넌트를 찾고,
+  // 찾은 경우 해당 컴포넌트의 자식(푸터 내용)을 footer 변수에 저장하고,
+  // 원래 자식 목록에서는 제거합니다.
+  const filteredChildren = Children.map(children, child => {
+    if (
+      isValidElement(child) &&
+      child.type === DismissKeyboardView.Footer // 자식 요소의 타입이 DismissKeyboardView.Footer인지 확인
+    ) {
+      // DismissKeyboardView.Footer 컴포넌트의 자식 요소를 footer 변수에 할당
+      footer = (child as React.ReactElement<FooterProps>).props.children;
+
+      // 해당 자식(DismissKeyboardView.Footer)은 메인 컨텐츠 영역에서 렌더링하지 않도록 null 반환
+      return null;
+    }
+
+    // DismissKeyboardView.Footer가 아닌 다른 자식 요소는 그대로 반환
+    return child;
+  });
+
+  return (
+    <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
+      <View style={{ flex: 1 }}>
+        <KeyboardAwareScrollView
+          enableOnAndroid
+          extraHeight={extraHeight}
+          // KeyboardAwareScrollView 안에 있는 컴포넌트 중 키보드에 가려지는 컴포넌트의 경우, 키보드 위로 올라오도록 함
+          extraScrollHeight={
+            extraScrollHeight &&
+            (Platform.OS === 'ios'
+              ? extraScrollHeight
+              : maxKeyboardHeight - DEFAULT_EXTRA_HEIGHT + extraScrollHeight)
+          }
+          // 사용자가 키보드가 열린 상태에서 아무 뷰나 터치하면, 그 터치 이벤트가 keyboard dismiss도 하고, 터치도 정상 처리
+          keyboardShouldPersistTaps="handled"
+          contentContainerStyle={{ flexGrow: 1 }}
+          {...props}
+          style={props.style}>
+          {filteredChildren}
+        </KeyboardAwareScrollView>
+
+        {/* 키보드와 관계없이 항상 고정 */}
+        {footer}
+      </View>
+    </TouchableWithoutFeedback>
+  );
+};
+
+const Footer = ({ children }: FooterProps) => <>{children}</>;
+
+Footer.displayName = 'DismissKeyboardView.Footer';
+
+DismissKeyboardView.Footer = Footer;
diff --git a/src/components/TimeSelectBottomSheet/index.tsx b/src/components/TimeSelectBottomSheet/index.tsx
index 6c83fe08..a75046d2 100644
--- a/src/components/TimeSelectBottomSheet/index.tsx
+++ b/src/components/TimeSelectBottomSheet/index.tsx
@@ -1,32 +1,66 @@
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import { Animated, Platform, Pressable, ScrollView, View } from 'react-native';
 
 import { CustomText } from '@components/CustomText';
 
 import SelectCheckIcon from '@assets/svgs/selectCheck.svg';
 
+type Props = {
+  type: 'hour' | 'minute';
+  value: string;
+  isShow: boolean;
+  setValue: React.Dispatch<React.SetStateAction<string>>;
+  onClose: () => void;
+  onSelect?: () => void;
+};
+
 export const TimeSelectBottomSheet = ({
   type,
   value,
+  isShow,
   setValue,
   onClose,
   onSelect,
-}: Readonly<{
-  type: 'hour' | 'minute';
-  value: string;
-  setValue: React.Dispatch<React.SetStateAction<string>>;
-  onClose: () => void;
-  onSelect?: () => void;
-}>) => {
-  const slideAnim = useRef(new Animated.Value(0)).current;
+}: Props) => {
+  const [shouldRender, setShouldRender] = useState(isShow);
 
+  const slideAnim = useRef(new Animated.Value(0)).current; // 바텀시트
+  const opacityAnim = useRef(new Animated.Value(0)).current; // 딤드
+
+  // TODO: isShow 가 true 로 바뀔 때 애니메이션 적용 안 되는 문제. 후순위
   useEffect(() => {
-    Animated.timing(slideAnim, {
-      toValue: 1,
-      duration: 200,
-      useNativeDriver: true,
-    }).start();
-  }, []);
+    if (isShow) {
+      setShouldRender(true);
+
+      Animated.parallel([
+        Animated.timing(slideAnim, {
+          toValue: 1,
+          duration: 200,
+          useNativeDriver: true,
+        }),
+        Animated.timing(opacityAnim, {
+          toValue: 1,
+          duration: 200,
+          useNativeDriver: true,
+        }),
+      ]).start();
+    } else {
+      Animated.parallel([
+        Animated.timing(slideAnim, {
+          toValue: 0,
+          duration: 200,
+          useNativeDriver: true,
+        }),
+        Animated.timing(opacityAnim, {
+          toValue: 0,
+          duration: 200,
+          useNativeDriver: true,
+        }),
+      ]).start(() => {
+        setShouldRender(false);
+      });
+    }
+  }, [isShow]);
 
   const hourOptions = Array.from({ length: 12 * 2 }, (_, i) => {
     const period = i < 12 ? '오전' : '오후';
@@ -44,11 +78,16 @@ export const TimeSelectBottomSheet = ({
     setValue(option);
     onClose();
 
-    if (onSelect) onSelect();
+    if (onSelect) {
+      onSelect();
+    }
   };
 
+  if (!shouldRender) return null;
+
   return (
-    <View
+    <Animated.View
+      style={{ opacity: opacityAnim }}
       className={`absolute top-0 left-0 right-0 bottom-0 bg-black/50 justify-end px-[30] ${
         Platform.OS === 'ios' ? 'pb-[51]' : 'pb-[27]'
       }`}>
@@ -57,54 +96,58 @@ export const TimeSelectBottomSheet = ({
         onPress={onClose}
       />
       <Animated.View
-        className={`bg-blue500 overflow-hidden ${
-          type === 'hour' ? 'h-[474]' : 'h-[220]'
-        }`}
         style={{
-          borderRadius: 10,
           transform: [
             {
               translateY: slideAnim.interpolate({
                 inputRange: [0, 1],
-                outputRange: [300, 0],
+                outputRange: [300, 0], // 아래 -> 위
               }),
             },
           ],
         }}>
-        <View className="h-[13]" />
+        <View
+          className={`bg-blue500 overflow-hidden ${
+            type === 'hour' ? 'h-[474]' : 'h-[220]'
+          }`}
+          style={{ borderRadius: 10 }}>
+          <View className="h-[13]" />
 
-        <View className="items-center">
-          <View
-            className="h-[4] w-[52] bg-blue300"
-            style={{ borderRadius: 100 }}
-          />
-        </View>
+          <View className="items-center">
+            <View
+              className="h-[4] w-[52] bg-blue300"
+              style={{ borderRadius: 100 }}
+            />
+          </View>
+
+          <View className="h-[25]" />
+
+          <View className="flex-1 px-[30]">
+            <CustomText type="title3" text={titleText} className="text-white" />
+            <View className="h-[21]" />
 
-        <View className="h-[25]" />
-
-        <View className="flex-1 px-[30]">
-          <CustomText type="title3" text={titleText} className="text-white" />
-          <View className="h-[21]" />
-
-          <ScrollView className="pb-[30]" showsVerticalScrollIndicator={false}>
-            {options.map(option => (
-              <Pressable
-                key={option}
-                className="h-[56] flex-row items-center justify-between"
-                onPress={() => handleOptionClick(option)}>
-                <CustomText
-                  type="body3"
-                  text={option}
-                  className={`${
-                    value === option ? 'text-yellowPrimary' : 'text-white'
-                  }`}
-                />
-                {value === option && <SelectCheckIcon />}
-              </Pressable>
-            ))}
-          </ScrollView>
+            <ScrollView
+              className="pb-[30]"
+              showsVerticalScrollIndicator={false}>
+              {options.map(option => (
+                <Pressable
+                  key={option}
+                  className="h-[56] flex-row items-center justify-between"
+                  onPress={() => handleOptionClick(option)}>
+                  <CustomText
+                    type="body3"
+                    text={option}
+                    className={`${
+                      value === option ? 'text-yellowPrimary' : 'text-white'
+                    }`}
+                  />
+                  {value === option && <SelectCheckIcon />}
+                </Pressable>
+              ))}
+            </ScrollView>
+          </View>
         </View>
       </Animated.View>
-    </View>
+    </Animated.View>
   );
 };
diff --git a/src/libs/hooks/useKeyboardHeight.ts b/src/libs/hooks/useKeyboardHeight.ts
new file mode 100644
index 00000000..695631ab
--- /dev/null
+++ b/src/libs/hooks/useKeyboardHeight.ts
@@ -0,0 +1,41 @@
+import { useEffect, useRef, useState } from 'react';
+import { Keyboard, type KeyboardEvent } from 'react-native';
+
+const DEFAULT_KEYBOARD_HEIGHT = 290;
+
+export const useKeyboardHeight = () => {
+  const [keyboardHeight, setKeyboardHeight] = useState(0);
+  const maxHeightRef = useRef(0);
+
+  useEffect(() => {
+    const onKeyboardDidShow = (e: KeyboardEvent) => {
+      const height = e.endCoordinates.height;
+
+      if (height > maxHeightRef.current) {
+        maxHeightRef.current = height;
+      }
+
+      setKeyboardHeight(height);
+    };
+
+    const onKeyboardDidHide = () => {
+      setKeyboardHeight(0);
+    };
+
+    const showSub = Keyboard.addListener('keyboardDidShow', onKeyboardDidShow);
+    const hideSub = Keyboard.addListener('keyboardDidHide', onKeyboardDidHide);
+
+    return () => {
+      showSub.remove();
+      hideSub.remove();
+    };
+  }, []);
+
+  return {
+    keyboardHeight,
+    maxKeyboardHeight:
+      maxHeightRef.current === 0
+        ? DEFAULT_KEYBOARD_HEIGHT
+        : maxHeightRef.current,
+  };
+};
diff --git a/src/screens/Letter/LetterHome/index.tsx b/src/screens/Letter/LetterHome/index.tsx
index 28e380d3..174cfaf7 100644
--- a/src/screens/Letter/LetterHome/index.tsx
+++ b/src/screens/Letter/LetterHome/index.tsx
@@ -335,13 +335,10 @@ export const LetterHomeScreen = ({ navigation }: Readonly<LetterProps>) => {
           } justify-end ${clickedMoreDot ? '' : 'hidden'}`}>
           {/* 내부 컴포넌트에는 상위 onPress 이벤트가 전파되지 않도록 함 */}
           <Pressable onPress={() => {}} className="w-full">
-            <AnimatedView
-              visible={clickedMoreDot}
-              style={{ borderRadius: 10 }}
-              className="bg-blue500 mb-[24]">
+            <AnimatedView visible={clickedMoreDot}>
               <BottomMenu data={bottomMenuData} />
             </AnimatedView>
-
+            <View className="h-[24]" />
             <Button text="취소" onPress={() => setClickedMoreDot(false)} />
           </Pressable>
         </Pressable>
diff --git a/src/screens/Login/MemberInfoWrite/index.tsx b/src/screens/Login/MemberInfoWrite/index.tsx
index ec35ac3f..46e10f3f 100644
--- a/src/screens/Login/MemberInfoWrite/index.tsx
+++ b/src/screens/Login/MemberInfoWrite/index.tsx
@@ -1,12 +1,5 @@
-import { useEffect, useState } from 'react';
-import {
-  Alert,
-  Image,
-  Keyboard,
-  Platform,
-  Pressable,
-  View,
-} from 'react-native';
+import { useState } from 'react';
+import { Alert, Image, Platform, Pressable, View } from 'react-native';
 import DatePicker from 'react-native-date-picker';
 
 import { uploadImageToS3 } from '@apis/util';
@@ -16,7 +9,6 @@ import { Button } from '@components/Button';
 import { CustomText } from '@components/CustomText';
 import { DismissKeyboardView } from '@components/DismissKeyboardView';
 import { Modal } from '@components/Modal';
-import { KEYBOARD_DELAY_MS } from '@constants/common';
 import { usePostMember } from '@hooks/auth/usePostMember';
 import { useLoading } from '@hooks/useLoading';
 import { useModal } from '@hooks/useModal';
@@ -38,7 +30,6 @@ export const MemberInfoWriteScreen = ({
   route,
   navigation,
 }: Readonly<AuthProps>) => {
-  const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
   const { nickname, imageUri, role } = route.params;
   const [birthday, setBirthday] = useState<Date | null>(null);
   const [gender, setGender] = useState<Gender | null>(null);
@@ -48,22 +39,6 @@ export const MemberInfoWriteScreen = ({
   const [visible2, setVisible2] = useState(false);
   const { mutate: postMember } = usePostMember();
 
-  useEffect(() => {
-    const showSubscription = Keyboard.addListener('keyboardDidShow', () =>
-      setIsKeyboardVisible(true),
-    );
-    const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
-      setTimeout(() => {
-        setIsKeyboardVisible(false);
-      }, KEYBOARD_DELAY_MS);
-    });
-
-    return () => {
-      showSubscription.remove();
-      hideSubscription.remove();
-    };
-  }, []);
-
   const handleNext = async () => {
     if (!gender || !birthday) return;
 
@@ -132,7 +107,7 @@ export const MemberInfoWriteScreen = ({
 
   return (
     <BG type="main">
-      <DismissKeyboardView>
+      <DismissKeyboardView extraHeight={80}>
         <AppBar
           goBackCallbackFn={openModal}
           className="absolute top-[0] w-full"
@@ -208,19 +183,21 @@ export const MemberInfoWriteScreen = ({
           source={require('@assets/pngs/background/signup2.png')}
           className="w-full h-auto flex-1 mt-[54]"
         />
-      </DismissKeyboardView>
 
-      <View
-        className={`absolute left-0 w-full px-[30] ${
-          isKeyboardVisible ? 'hidden' : ''
-        } ${Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'}`}>
-        <Button
-          text="다음"
-          onPress={handleNext}
-          disabled={!birthday || !gender || isLoading}
-          isLoading={isLoading}
-        />
-      </View>
+        <DismissKeyboardView.Footer>
+          <View
+            className={`absolute left-0 ${
+              Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
+            } w-full px-[30]`}>
+            <Button
+              text="다음"
+              onPress={handleNext}
+              disabled={!birthday || !gender || isLoading}
+              isLoading={isLoading}
+            />
+          </View>
+        </DismissKeyboardView.Footer>
+      </DismissKeyboardView>
 
       <DatePicker
         modal
diff --git a/src/screens/Login/NicknameWrite/index.tsx b/src/screens/Login/NicknameWrite/index.tsx
index f19665bc..a141577e 100644
--- a/src/screens/Login/NicknameWrite/index.tsx
+++ b/src/screens/Login/NicknameWrite/index.tsx
@@ -1,5 +1,5 @@
-import { useEffect, useState } from 'react';
-import { Image, Keyboard, Platform, Pressable, View } from 'react-native';
+import { useState } from 'react';
+import { Image, Platform, Pressable, View } from 'react-native';
 import {
   type ImageLibraryOptions,
   type ImagePickerResponse,
@@ -15,7 +15,6 @@ import { CustomText } from '@components/CustomText';
 import { DismissKeyboardView } from '@components/DismissKeyboardView';
 import { Modal } from '@components/Modal';
 import { TextInput } from '@components/TextInput';
-import { KEYBOARD_DELAY_MS } from '@constants/common';
 import { useModal } from '@hooks/useModal';
 import { useValidateInput } from '@hooks/useValidateInput';
 import { type NativeStackScreenProps } from '@react-navigation/native-stack';
@@ -48,25 +47,8 @@ export const NicknameWriteScreen = ({
     message: nicknameMessage,
   } = useValidateInput({ type: 'nickname' });
   const [clickedUpload, setClickedUpload] = useState(false);
-  const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
   const { visible, openModal, closeModal } = useModal();
 
-  useEffect(() => {
-    const showSubscription = Keyboard.addListener('keyboardDidShow', () =>
-      setIsKeyboardVisible(true),
-    );
-    const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
-      setTimeout(() => {
-        setIsKeyboardVisible(false);
-      }, KEYBOARD_DELAY_MS);
-    });
-
-    return () => {
-      showSubscription.remove();
-      hideSubscription.remove();
-    };
-  }, []);
-
   const selectImage = () => {
     const options: ImageLibraryOptions = {
       mediaType: 'photo', // 'photo', 'video', 또는 'mixed' 중 선택 가능
@@ -124,7 +106,7 @@ export const NicknameWriteScreen = ({
 
   return (
     <BG type="main">
-      <DismissKeyboardView>
+      <DismissKeyboardView extraHeight={80}>
         <AppBar
           goBackCallbackFn={openModal}
           className="absolute top-[0] w-full"
@@ -178,38 +160,35 @@ export const NicknameWriteScreen = ({
           source={require('@assets/pngs/background/signup2.png')}
           className="w-full h-auto flex-1 mt-[80]"
         />
+
+        <DismissKeyboardView.Footer>
+          <View
+            className={`absolute left-0 ${
+              Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
+            } w-full px-[30]`}>
+            <Button
+              text="다음"
+              onPress={handleNext}
+              disabled={!isValidNickname}
+            />
+          </View>
+        </DismissKeyboardView.Footer>
       </DismissKeyboardView>
 
-      {clickedUpload ? (
-        <Pressable
-          onPress={() => setClickedUpload(false)}
-          className={`absolute left-0 bottom-0 w-full h-full bg-black/50 px-[30] pb-[55] justify-end ${
-            isKeyboardVisible ? 'hidden' : ''
-          }`}>
-          {/* 내부 컴포넌트에는 상위 onPress 이벤트가 전파되지 않도록 함 */}
-          <Pressable onPress={() => {}} className="w-full">
-            <AnimatedView
-              visible={clickedUpload}
-              style={{ borderRadius: 10 }}
-              className="bg-blue500 mb-[24]">
-              <BottomMenu title="프로필 사진 설정" data={bottomMenuData} />
-            </AnimatedView>
-
-            <Button text="취소" onPress={() => setClickedUpload(false)} />
-          </Pressable>
+      <Pressable
+        onPress={() => setClickedUpload(false)}
+        className={`absolute left-0 bottom-0 w-full h-full bg-black/50 px-[30] pb-[55] justify-end ${
+          clickedUpload ? '' : 'hidden'
+        }`}>
+        {/* 내부 컴포넌트에는 상위 onPress 이벤트가 전파되지 않도록 함 */}
+        <Pressable onPress={() => {}} className="w-full">
+          <AnimatedView visible={clickedUpload}>
+            <BottomMenu title="프로필 사진 설정" data={bottomMenuData} />
+          </AnimatedView>
+          <View className="h-[24]" />
+          <Button text="취소" onPress={() => setClickedUpload(false)} />
         </Pressable>
-      ) : (
-        <View
-          className={`absolute left-0 ${
-            Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
-          } w-full px-[30] ${isKeyboardVisible ? 'hidden' : ''}`}>
-          <Button
-            text="다음"
-            onPress={handleNext}
-            disabled={!isValidNickname}
-          />
-        </View>
-      )}
+      </Pressable>
 
       <Modal
         type="info"
diff --git a/src/screens/Login/YouthEat/index.tsx b/src/screens/Login/YouthEat/index.tsx
index f3a12a1f..c6aa66e9 100644
--- a/src/screens/Login/YouthEat/index.tsx
+++ b/src/screens/Login/YouthEat/index.tsx
@@ -72,191 +72,183 @@ const YouthEatScreen = ({ route, navigation }: Readonly<AuthProps>) => {
 
   return (
     <BG type="main">
-      <>
-        <AppBar
-          goBackCallbackFn={() => {
-            navigation.goBack();
-          }}
-          className="absolute top-[0] w-full"
-        />
-        <View className="w-full h-[3] bg-white/5 absolute top-[60]" />
-        <View className="w-[66%] h-[3] bg-yellowPrimary absolute top-[60]" />
+      <AppBar
+        goBackCallbackFn={() => {
+          navigation.goBack();
+        }}
+        className="absolute top-[0] w-full"
+      />
+      <View className="w-full h-[3] bg-white/5 absolute top-[60]" />
+      <View className="w-[66%] h-[3] bg-yellowPrimary absolute top-[60]" />
 
-        <FlexableMargin flexGrow={120} />
+      <FlexableMargin flexGrow={120} />
 
-        <CustomText
-          type="title2"
-          text="식사 시간을 알려주세요"
-          className="text-white text-center"
-        />
-        <FlexableMargin flexGrow={9} />
-        <CustomText
-          type="body3"
-          text="식사 알림을 받고 싶은 시간을 입력해주세요"
-          className="text-gray300 text-center"
-        />
+      <CustomText
+        type="title2"
+        text="식사 시간을 알려주세요"
+        className="text-white text-center"
+      />
+      <FlexableMargin flexGrow={9} />
+      <CustomText
+        type="body3"
+        text="식사 알림을 받고 싶은 시간을 입력해주세요"
+        className="text-gray300 text-center"
+      />
 
-        <FlexableMargin flexGrow={60} />
+      <FlexableMargin flexGrow={60} />
 
-        {/* 아침 */}
-        <View className="px-[50]">
-          <CustomText type="button" text="아침" className="text-gray300" />
-          <FlexableMargin flexGrow={11} />
-          <View className="flex-row items-center justify-between">
-            <Pressable
-              onPress={() => setShowBreakfastHourBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText
-                type="title2"
-                text={
-                  breakfastHour.includes('자정') ? '오전 12시' : breakfastHour
-                }
-                className="text-white"
-              />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-            <View className="w-[17]" />
-            <Pressable
-              onPress={() => setShowBreakfastMinuteBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText
-                type="title2"
-                text={breakfastMinute}
-                className="text-white"
-              />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-          </View>
+      {/* 아침 */}
+      <View className="px-[50]">
+        <CustomText type="button" text="아침" className="text-gray300" />
+        <FlexableMargin flexGrow={11} />
+        <View className="flex-row items-center justify-between">
+          <Pressable
+            onPress={() => setShowBreakfastHourBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText
+              type="title2"
+              text={
+                breakfastHour.includes('자정') ? '오전 12시' : breakfastHour
+              }
+              className="text-white"
+            />
+            <ChevronBottomGrayIcon />
+          </Pressable>
+          <View className="w-[17]" />
+          <Pressable
+            onPress={() => setShowBreakfastMinuteBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText
+              type="title2"
+              text={breakfastMinute}
+              className="text-white"
+            />
+            <ChevronBottomGrayIcon />
+          </Pressable>
         </View>
+      </View>
 
-        <FlexableMargin flexGrow={59} />
+      <FlexableMargin flexGrow={59} />
 
-        {/* 점심 */}
-        <View className="px-[50]">
-          <CustomText type="button" text="점심" className="text-gray300" />
-          <FlexableMargin flexGrow={11} />
-          <View className="flex-row items-center justify-between">
-            <Pressable
-              onPress={() => setShowLunchHourBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText
-                type="title2"
-                text={lunchHour.includes('자정') ? '오전 12시' : lunchHour}
-                className="text-white"
-              />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-            <View className="w-[17]" />
-            <Pressable
-              onPress={() => setShowLunchMinuteBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText
-                type="title2"
-                text={lunchMinute}
-                className="text-white"
-              />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-          </View>
+      {/* 점심 */}
+      <View className="px-[50]">
+        <CustomText type="button" text="점심" className="text-gray300" />
+        <FlexableMargin flexGrow={11} />
+        <View className="flex-row items-center justify-between">
+          <Pressable
+            onPress={() => setShowLunchHourBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText
+              type="title2"
+              text={lunchHour.includes('자정') ? '오전 12시' : lunchHour}
+              className="text-white"
+            />
+            <ChevronBottomGrayIcon />
+          </Pressable>
+          <View className="w-[17]" />
+          <Pressable
+            onPress={() => setShowLunchMinuteBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText
+              type="title2"
+              text={lunchMinute}
+              className="text-white"
+            />
+            <ChevronBottomGrayIcon />
+          </Pressable>
         </View>
+      </View>
 
-        <FlexableMargin flexGrow={59} />
+      <FlexableMargin flexGrow={59} />
 
-        {/* 저녁 */}
-        <View className="px-[50]">
-          <CustomText type="button" text="저녁" className="text-gray300" />
-          <FlexableMargin flexGrow={11} />
-          <View className="flex-row items-center justify-between">
-            <Pressable
-              onPress={() => setShowDinnerHourBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText
-                type="title2"
-                text={dinnerHour.includes('자정') ? '오전 12시' : dinnerHour}
-                className="text-white"
-              />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-            <View className="w-[17]" />
-            <Pressable
-              onPress={() => setShowDinnerMinuteBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText
-                type="title2"
-                text={dinnerMinute}
-                className="text-white"
-              />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-          </View>
+      {/* 저녁 */}
+      <View className="px-[50]">
+        <CustomText type="button" text="저녁" className="text-gray300" />
+        <FlexableMargin flexGrow={11} />
+        <View className="flex-row items-center justify-between">
+          <Pressable
+            onPress={() => setShowDinnerHourBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText
+              type="title2"
+              text={dinnerHour.includes('자정') ? '오전 12시' : dinnerHour}
+              className="text-white"
+            />
+            <ChevronBottomGrayIcon />
+          </Pressable>
+          <View className="w-[17]" />
+          <Pressable
+            onPress={() => setShowDinnerMinuteBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText
+              type="title2"
+              text={dinnerMinute}
+              className="text-white"
+            />
+            <ChevronBottomGrayIcon />
+          </Pressable>
         </View>
+      </View>
 
-        <FlexableMargin flexGrow={140} />
+      <FlexableMargin flexGrow={140} />
 
-        <View
-          className={`absolute left-0 ${
-            Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
-          } w-full px-[30]`}>
-          <Button text="다음" onPress={handleNext} />
-        </View>
+      <View
+        className={`absolute left-0 ${
+          Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
+        } w-full px-[30]`}>
+        <Button text="다음" onPress={handleNext} />
+      </View>
 
-        {/* 아침 */}
-        {showBreakfastHourBottomSheet && (
-          <TimeSelectBottomSheet
-            type="hour"
-            value={breakfastHour}
-            setValue={setBreakfastHour}
-            onClose={() => setShowBreakfastHourBottomSheet(false)}
-            onSelect={() => setShowBreakfastMinuteBottomSheet(true)}
-          />
-        )}
-        {showBreakfastMinuteBottomSheet && (
-          <TimeSelectBottomSheet
-            type="minute"
-            value={breakfastMinute}
-            setValue={setBreakfastMinute}
-            onClose={() => setShowBreakfastMinuteBottomSheet(false)}
-          />
-        )}
+      {/* 아침 */}
+      <TimeSelectBottomSheet
+        type="hour"
+        value={breakfastHour}
+        setValue={setBreakfastHour}
+        onClose={() => setShowBreakfastHourBottomSheet(false)}
+        onSelect={() => setShowBreakfastMinuteBottomSheet(true)}
+        isShow={showBreakfastHourBottomSheet}
+      />
+      <TimeSelectBottomSheet
+        type="minute"
+        value={breakfastMinute}
+        setValue={setBreakfastMinute}
+        onClose={() => setShowBreakfastMinuteBottomSheet(false)}
+        isShow={showBreakfastMinuteBottomSheet}
+      />
 
-        {/* 점심 */}
-        {showLunchHourBottomSheet && (
-          <TimeSelectBottomSheet
-            type="hour"
-            value={lunchHour}
-            setValue={setLunchHour}
-            onClose={() => setShowLunchHourBottomSheet(false)}
-            onSelect={() => setShowLunchMinuteBottomSheet(true)}
-          />
-        )}
-        {showLunchMinuteBottomSheet && (
-          <TimeSelectBottomSheet
-            type="minute"
-            value={lunchMinute}
-            setValue={setLunchMinute}
-            onClose={() => setShowLunchMinuteBottomSheet(false)}
-          />
-        )}
+      {/* 점심 */}
+      <TimeSelectBottomSheet
+        type="hour"
+        value={lunchHour}
+        setValue={setLunchHour}
+        onClose={() => setShowLunchHourBottomSheet(false)}
+        onSelect={() => setShowLunchMinuteBottomSheet(true)}
+        isShow={showLunchHourBottomSheet}
+      />
+      <TimeSelectBottomSheet
+        type="minute"
+        value={lunchMinute}
+        setValue={setLunchMinute}
+        onClose={() => setShowLunchMinuteBottomSheet(false)}
+        isShow={showLunchMinuteBottomSheet}
+      />
 
-        {/* 저녁 */}
-        {showDinnerHourBottomSheet && (
-          <TimeSelectBottomSheet
-            type="hour"
-            value={dinnerHour}
-            setValue={setDinnerHour}
-            onClose={() => setShowDinnerHourBottomSheet(false)}
-            onSelect={() => setShowDinnerMinuteBottomSheet(true)}
-          />
-        )}
-        {showDinnerMinuteBottomSheet && (
-          <TimeSelectBottomSheet
-            type="minute"
-            value={dinnerMinute}
-            setValue={setDinnerMinute}
-            onClose={() => setShowDinnerMinuteBottomSheet(false)}
-          />
-        )}
-      </>
+      {/* 저녁 */}
+      <TimeSelectBottomSheet
+        type="hour"
+        value={dinnerHour}
+        setValue={setDinnerHour}
+        onClose={() => setShowDinnerHourBottomSheet(false)}
+        onSelect={() => setShowDinnerMinuteBottomSheet(true)}
+        isShow={showDinnerHourBottomSheet}
+      />
+      <TimeSelectBottomSheet
+        type="minute"
+        value={dinnerMinute}
+        setValue={setDinnerMinute}
+        onClose={() => setShowDinnerMinuteBottomSheet(false)}
+        isShow={showDinnerMinuteBottomSheet}
+      />
     </BG>
   );
 };
diff --git a/src/screens/Login/YouthSleepTime/index.tsx b/src/screens/Login/YouthSleepTime/index.tsx
index 37647526..bf22ef40 100644
--- a/src/screens/Login/YouthSleepTime/index.tsx
+++ b/src/screens/Login/YouthSleepTime/index.tsx
@@ -55,79 +55,74 @@ const YouthSleepTimeScreen = ({ route, navigation }: Readonly<AuthProps>) => {
 
   return (
     <BG type="main">
-      <>
-        <AppBar
-          goBackCallbackFn={() => {
-            navigation.goBack();
-          }}
-          className="absolute top-[0] w-full"
+      <AppBar
+        goBackCallbackFn={() => {
+          navigation.goBack();
+        }}
+        className="absolute top-[0] w-full"
+      />
+      <View className="w-full h-[3] bg-yellowPrimary absolute top-[60]" />
+
+      <View className="h-[180]" />
+
+      <View className="items-center flex-1">
+        <CustomText
+          type="title2"
+          text="취침 시간을 알려주세요"
+          className="text-white text-center"
         />
-        <View className="w-full h-[3] bg-yellowPrimary absolute top-[60]" />
-
-        <View className="h-[180]" />
-
-        <View className="items-center flex-1">
-          <CustomText
-            type="title2"
-            text="취침 시간을 알려주세요"
-            className="text-white text-center"
-          />
-          <View className="h-[9]" />
-          <CustomText
-            type="body3"
-            text="취침 알림을 받고 싶은 시간을 입력해주세요"
-            className="text-gray300 text-center"
-          />
-
-          <View className="h-[100]" />
-
-          <View className="px-[50] flex-row items-center justify-between">
-            <Pressable
-              onPress={() => setShowHourBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText
-                type="title2"
-                text={hour.includes('자정') ? '오전 12시' : hour}
-                className="text-white"
-              />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-            <View className="w-[17]" />
-            <Pressable
-              onPress={() => setShowMinuteBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText type="title2" text={minute} className="text-white" />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-          </View>
-
-          <View
-            className={`absolute left-0 ${
-              Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
-            } w-full px-[30]`}>
-            <Button text="다음" onPress={handleNext} />
-          </View>
-
-          {showHourBottomSheet && (
-            <TimeSelectBottomSheet
-              type="hour"
-              value={hour}
-              setValue={setHour}
-              onClose={() => setShowHourBottomSheet(false)}
-              onSelect={() => setShowMinuteBottomSheet(true)}
-            />
-          )}
-
-          {showMinuteBottomSheet && (
-            <TimeSelectBottomSheet
-              type="minute"
-              value={minute}
-              setValue={setMinute}
-              onClose={() => setShowMinuteBottomSheet(false)}
+        <View className="h-[9]" />
+        <CustomText
+          type="body3"
+          text="취침 알림을 받고 싶은 시간을 입력해주세요"
+          className="text-gray300 text-center"
+        />
+
+        <View className="h-[100]" />
+
+        <View className="px-[50] flex-row items-center justify-between">
+          <Pressable
+            onPress={() => setShowHourBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText
+              type="title2"
+              text={hour.includes('자정') ? '오전 12시' : hour}
+              className="text-white"
             />
-          )}
+            <ChevronBottomGrayIcon />
+          </Pressable>
+          <View className="w-[17]" />
+          <Pressable
+            onPress={() => setShowMinuteBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText type="title2" text={minute} className="text-white" />
+            <ChevronBottomGrayIcon />
+          </Pressable>
         </View>
-      </>
+
+        <View
+          className={`absolute left-0 ${
+            Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
+          } w-full px-[30]`}>
+          <Button text="다음" onPress={handleNext} />
+        </View>
+
+        <TimeSelectBottomSheet
+          type="hour"
+          value={hour}
+          setValue={setHour}
+          onClose={() => setShowHourBottomSheet(false)}
+          onSelect={() => setShowMinuteBottomSheet(true)}
+          isShow={showHourBottomSheet}
+        />
+        <TimeSelectBottomSheet
+          type="minute"
+          value={minute}
+          setValue={setMinute}
+          onClose={() => setShowMinuteBottomSheet(false)}
+          isShow={showMinuteBottomSheet}
+        />
+      </View>
     </BG>
   );
 };
diff --git a/src/screens/Login/YouthWakeUpTime/index.tsx b/src/screens/Login/YouthWakeUpTime/index.tsx
index adbd1daa..83f295f5 100644
--- a/src/screens/Login/YouthWakeUpTime/index.tsx
+++ b/src/screens/Login/YouthWakeUpTime/index.tsx
@@ -55,80 +55,75 @@ const YouthWakeUpTimeScreen = ({ navigation }: Readonly<AuthProps>) => {
 
   return (
     <BG type="main">
-      <>
-        <AppBar
-          goBackCallbackFn={() => {
-            navigation.goBack();
-          }}
-          className="absolute top-[0] w-full"
+      <AppBar
+        goBackCallbackFn={() => {
+          navigation.goBack();
+        }}
+        className="absolute top-[0] w-full"
+      />
+      <View className="w-full h-[3] bg-white/5 absolute top-[60]" />
+      <View className="w-[33%] h-[3] bg-yellowPrimary absolute top-[60]" />
+
+      <View className="h-[180]" />
+
+      <View className="items-center flex-1">
+        <CustomText
+          type="title2"
+          text="기상 시간을 알려주세요"
+          className="text-white text-center"
+        />
+        <View className="h-[9]" />
+        <CustomText
+          type="body3"
+          text="기상 알림을 받고 싶은 시간을 입력해주세요"
+          className="text-gray300 text-center"
         />
-        <View className="w-full h-[3] bg-white/5 absolute top-[60]" />
-        <View className="w-[33%] h-[3] bg-yellowPrimary absolute top-[60]" />
-
-        <View className="h-[180]" />
-
-        <View className="items-center flex-1">
-          <CustomText
-            type="title2"
-            text="기상 시간을 알려주세요"
-            className="text-white text-center"
-          />
-          <View className="h-[9]" />
-          <CustomText
-            type="body3"
-            text="기상 알림을 받고 싶은 시간을 입력해주세요"
-            className="text-gray300 text-center"
-          />
-
-          <View className="h-[100]" />
-
-          <View className="px-[50] flex-row items-center justify-between">
-            <Pressable
-              onPress={() => setShowHourBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText
-                type="title2"
-                text={hour.includes('자정') ? '오전 12시' : hour}
-                className="text-white"
-              />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-            <View className="w-[17]" />
-            <Pressable
-              onPress={() => setShowMinuteBottomSheet(true)}
-              className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
-              <CustomText type="title2" text={minute} className="text-white" />
-              <ChevronBottomGrayIcon />
-            </Pressable>
-          </View>
-        </View>
 
-        <View
-          className={`absolute left-0 ${
-            Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
-          } w-full px-[30]`}>
-          <Button text="다음" onPress={handleNext} />
+        <View className="h-[100]" />
+
+        <View className="px-[50] flex-row items-center justify-between">
+          <Pressable
+            onPress={() => setShowHourBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText
+              type="title2"
+              text={hour.includes('자정') ? '오전 12시' : hour}
+              className="text-white"
+            />
+            <ChevronBottomGrayIcon />
+          </Pressable>
+          <View className="w-[17]" />
+          <Pressable
+            onPress={() => setShowMinuteBottomSheet(true)}
+            className="border-b border-b-gray300 flex-row items-center justify-between w-full shrink">
+            <CustomText type="title2" text={minute} className="text-white" />
+            <ChevronBottomGrayIcon />
+          </Pressable>
         </View>
-
-        {showHourBottomSheet && (
-          <TimeSelectBottomSheet
-            type="hour"
-            value={hour}
-            setValue={setHour}
-            onClose={() => setShowHourBottomSheet(false)}
-            onSelect={() => setShowMinuteBottomSheet(true)}
-          />
-        )}
-
-        {showMinuteBottomSheet && (
-          <TimeSelectBottomSheet
-            type="minute"
-            value={minute}
-            setValue={setMinute}
-            onClose={() => setShowMinuteBottomSheet(false)}
-          />
-        )}
-      </>
+      </View>
+
+      <View
+        className={`absolute left-0 ${
+          Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
+        } w-full px-[30]`}>
+        <Button text="다음" onPress={handleNext} />
+      </View>
+
+      <TimeSelectBottomSheet
+        type="hour"
+        value={hour}
+        setValue={setHour}
+        onClose={() => setShowHourBottomSheet(false)}
+        onSelect={() => setShowMinuteBottomSheet(true)}
+        isShow={showHourBottomSheet}
+      />
+      <TimeSelectBottomSheet
+        type="minute"
+        value={minute}
+        setValue={setMinute}
+        onClose={() => setShowMinuteBottomSheet(false)}
+        isShow={showMinuteBottomSheet}
+      />
     </BG>
   );
 };
diff --git a/src/screens/System/ModifyInfo/index.tsx b/src/screens/System/ModifyInfo/index.tsx
index cd8e6021..68dba3e7 100644
--- a/src/screens/System/ModifyInfo/index.tsx
+++ b/src/screens/System/ModifyInfo/index.tsx
@@ -14,6 +14,7 @@ import { AppBar } from '@components/AppBar';
 import { BG } from '@components/BG';
 import { Button } from '@components/Button';
 import { CustomText } from '@components/CustomText';
+import { DismissKeyboardView } from '@components/DismissKeyboardView';
 import { Modal } from '@components/Modal';
 import { TextInput } from '@components/TextInput';
 import { useModal } from '@hooks/useModal';
@@ -182,58 +183,60 @@ export const ModifyInfoScreen = () => {
 
   return (
     <BG type="solid">
-      <AppBar
-        title="내 정보 수정"
-        goBackCallbackFn={goBackCallbackFn}
-        confirmCallbackFn={confirmCallbackFn}
-        isLoading={isLoading}
-      />
-      {/* 프로필 이미지 영역 */}
-      <View className="flex-1 items-center pt-[38]">
-        <Pressable onPress={() => !isLoading && setClickedUpload(true)}>
-          <View className="relative w-[107] h-[107]">
-            {imageUri ? (
-              <Image
-                source={{ uri: imageUri }}
-                style={{
-                  width: '100%',
-                  height: '100%',
-                  borderRadius: 53.5,
-                }}
-              />
-            ) : (
-              <Image
-                source={defaultImageUri}
-                style={{
-                  width: '100%',
-                  height: '100%',
-                  borderRadius: 53.5,
-                }}
-              />
-            )}
-            <ProfileCameraIcon className="absolute right-[-8] bottom-[0]" />
+      <DismissKeyboardView>
+        <AppBar
+          title="내 정보 수정"
+          goBackCallbackFn={goBackCallbackFn}
+          confirmCallbackFn={confirmCallbackFn}
+          isLoading={isLoading}
+        />
+        {/* 프로필 이미지 영역 */}
+        <View className="flex-1 items-center pt-[38]">
+          <Pressable onPress={() => !isLoading && setClickedUpload(true)}>
+            <View className="relative w-[107] h-[107]">
+              {imageUri ? (
+                <Image
+                  source={{ uri: imageUri }}
+                  style={{
+                    width: '100%',
+                    height: '100%',
+                    borderRadius: 53.5,
+                  }}
+                />
+              ) : (
+                <Image
+                  source={defaultImageUri}
+                  style={{
+                    width: '100%',
+                    height: '100%',
+                    borderRadius: 53.5,
+                  }}
+                />
+              )}
+              <ProfileCameraIcon className="absolute right-[-8] bottom-[0]" />
+            </View>
+          </Pressable>
+          {/* 공백백*/}
+          <View className="h-[39]" />
+          {/* 닉네임 수정 Section */}
+          <View className="w-full px-px gap-y-[10]">
+            <CustomText
+              type="caption1"
+              text="닉네임"
+              className="ml-[9] mb-[8px] text-gray200"
+            />
+            <TextInput
+              value={nickname}
+              onChangeText={handleNicknameChange}
+              isError={isErrorNickname}
+              isSuccess={isSuccessNickname}
+              placeholder="닉네임을 입력해주세요"
+              message={nicknameMessage}
+              maxLength={10}
+            />
           </View>
-        </Pressable>
-        {/* 공백백*/}
-        <View className="h-[39]" />
-        {/* 닉네임 수정 Section */}
-        <View className="w-full px-px gap-y-[10]">
-          <CustomText
-            type="caption1"
-            text="닉네임"
-            className="ml-[9] mb-[8px] text-gray200"
-          />
-          <TextInput
-            value={nickname}
-            onChangeText={handleNicknameChange}
-            isError={isErrorNickname}
-            isSuccess={isSuccessNickname}
-            placeholder="닉네임을 입력해주세요"
-            message={nicknameMessage}
-            maxLength={10}
-          />
         </View>
-      </View>
+      </DismissKeyboardView>
 
       {/* 이미지 수정 모달 (앨범에서 사진 선택 / 기본 이미지 적용) */}
       <>
diff --git a/src/screens/System/MyAccount/LeaveAccount/LeaveAccount2/index.tsx b/src/screens/System/MyAccount/LeaveAccount/LeaveAccount2/index.tsx
index c337e1df..4bdd9e5b 100644
--- a/src/screens/System/MyAccount/LeaveAccount/LeaveAccount2/index.tsx
+++ b/src/screens/System/MyAccount/LeaveAccount/LeaveAccount2/index.tsx
@@ -114,7 +114,7 @@ export const LeaveAccount2Screen = ({
           navigation.goBack();
         }}
       />
-      <View className="px-px" style={{ height: windowHeight - 64 }}>
+      <View className="px-px" style={{ height: windowHeight - 196 }}>
         {/* 상단 영역 */}
         {role == 'HELPER' ? (
           <>
@@ -127,7 +127,7 @@ export const LeaveAccount2Screen = ({
             />
             <FlexableMargin flexGrow={21} />
             <View className="w-full flex-row">
-              <CustomText type="title4" text="정말" className="text-white" />
+              <CustomText type="title4" text="정말 " className="text-white" />
               <CustomText
                 type="title4"
                 text="내일모래"
@@ -227,38 +227,42 @@ export const LeaveAccount2Screen = ({
             <FlexableMargin flexGrow={479} />
           </>
         )}
+        <FlexableMargin flexGrow={196} />
+      </View>
 
-        {/* 버튼 영역 */}
-        <View className="w-full ">
-          {/* 회원 탈퇴 확인완료 버튼 */}
+      {/* 버튼 영역 */}
+      <View
+        className={`absolute left-0 ${
+          Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
+        } w-full px-[30]`}>
+        {/* 회원 탈퇴 확인완료 버튼 */}
+        <View
+          className="flex-row items-center justify-center py-[18] space-x-[10px]"
+          onTouchEnd={() => setIsConfirmed(!isConfirmed)}>
+          {/* 체크박스 원 */}
           <View
-            className="flex-row items-center justify-center py-[18] space-x-[10px]"
-            onTouchEnd={() => setIsConfirmed(!isConfirmed)}>
-            {/* 체크박스 원 */}
-            <View
-              className={`w-[20px] h-[20px] rounded-full ${
-                isConfirmed ? 'bg-yellowPrimary' : 'bg-blue600'
-              } justify-center items-center`}>
-              {isConfirmed && (
-                <View className="w-[6px] h-[10px] border-r-2 border-b-2 border-blue700 rotate-45" />
-              )}
-            </View>
-            {/* 텍스트 */}
-            <CustomText
-              type="body4"
-              text="회원 탈퇴 유의사항을 확인했습니다"
-              className="text-gray200"
-            />
+            className={`w-[20px] h-[20px] rounded-full ${
+              isConfirmed ? 'bg-yellowPrimary' : 'bg-blue600'
+            } justify-center items-center`}>
+            {isConfirmed && (
+              <View className="w-[6px] h-[10px] border-r-2 border-b-2 border-blue700 rotate-45" />
+            )}
           </View>
-          {/* 회원 탈퇴 버튼 */}
-          <Button
-            text="회원 탈퇴하고 계정 삭제하기"
-            onPress={handleDeleteMember}
-            disabled={!isConfirmed}
+          {/* 텍스트 */}
+          <CustomText
+            type="body4"
+            text="회원 탈퇴 유의사항을 확인했습니다"
+            className="text-gray200"
           />
         </View>
-        <FlexableMargin flexGrow={Platform.OS === 'ios' ? 79 : 55} />
+        {/* 회원 탈퇴 버튼 */}
+        <Button
+          text="회원 탈퇴하고 계정 삭제하기"
+          onPress={handleDeleteMember}
+          disabled={!isConfirmed}
+        />
       </View>
+      <FlexableMargin flexGrow={Platform.OS === 'ios' ? 79 : 55} />
     </BG>
   );
 };
diff --git a/src/screens/System/MyAccount/LeaveAccount/index.tsx b/src/screens/System/MyAccount/LeaveAccount/index.tsx
index 104d5c89..4cc86c36 100644
--- a/src/screens/System/MyAccount/LeaveAccount/index.tsx
+++ b/src/screens/System/MyAccount/LeaveAccount/index.tsx
@@ -1,16 +1,11 @@
 import { useState } from 'react';
-import {
-  KeyboardAvoidingView,
-  Platform,
-  Pressable,
-  ScrollView,
-  View,
-} from 'react-native';
+import { Platform, Pressable, ScrollView, View } from 'react-native';
 
 import { AppBar } from '@components/AppBar';
 import { BG } from '@components/BG';
 import { Button } from '@components/Button';
 import { CustomText } from '@components/CustomText';
+import { DismissKeyboardView } from '@components/DismissKeyboardView';
 import { TextInput } from '@components/TextInput';
 import { type NavigationProp, useNavigation } from '@react-navigation/native';
 import { type SystemStackParamList } from '@type/nav/SystemStackParamList';
@@ -38,17 +33,15 @@ export const LeaveAccountScreen = () => {
   };
 
   return (
-    <KeyboardAvoidingView
-      style={{ flex: 1 }}
-      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
-      <BG type="solid">
+    <BG type="solid">
+      <DismissKeyboardView>
         <AppBar
           title="회원 탈퇴"
           goBackCallbackFn={() => navigation.goBack()}
         />
         <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
           {/* 전체 컨테이너 */}
-          <View className="flex-1 items-center">
+          <View className="flex-1 items-center pb-[174]">
             {/* 안내 문구 */}
             <View className="py-[41] px-px gap-[3] w-full">
               <CustomText
@@ -73,35 +66,73 @@ export const LeaveAccountScreen = () => {
                 onPress={() => toggleReason(reason)}
               />
             ))}
-            <>
+            <ReasonItem
+              reason="기타"
+              isSelected={selectedReasons.includes('기타')}
+              onPress={() => {
+                toggleReason('기타');
+                setIsDetailTyping(true);
+              }}
+            />
+            {selectedReasons.includes('기타') && (
+              <View
+                className="w-full px-px"
+                onTouchStart={() => {
+                  setIsDetailTyping(true);
+                }}>
+                <TextInput
+                  autoFocus
+                  value={detailReason}
+                  onChangeText={setDetailReason}
+                  placeholder="내용을 입력해주세요"
+                  isError={false}
+                  maxLength={160}
+                />
+              </View>
+            )}
+          </View>
+        </ScrollView>
+        {isDetailTyping && (
+          <View className="absolute top-0 right-0 h-full w-full z-[9]">
+            <BG type="solid">
               <ReasonItem
                 reason="기타"
                 isSelected={selectedReasons.includes('기타')}
                 onPress={() => {
                   toggleReason('기타');
-                  setIsDetailTyping(true);
+                  setIsDetailTyping(false);
+                }}
+              />
+              <View className="w-full px-px">
+                <TextInput
+                  autoFocus
+                  value={detailReason}
+                  onChangeText={setDetailReason}
+                  placeholder="내용을 입력해주세요"
+                  isError={false}
+                  maxLength={160}
+                />
+              </View>
+              <View
+                className="flex-1"
+                onTouchStart={() => {
+                  setIsDetailTyping(false);
                 }}
               />
-              {selectedReasons.includes('기타') && (
-                <View
-                  className="w-full px-px"
-                  onTouchStart={() => {
-                    setIsDetailTyping(true);
-                  }}>
-                  <TextInput
-                    autoFocus
-                    value={detailReason}
-                    onChangeText={setDetailReason}
-                    placeholder="내용을 입력해주세요"
-                    isError={false}
-                    maxLength={160}
-                  />
-                </View>
-              )}
-            </>
+            </BG>
+          </View>
+        )}
+
+        <DismissKeyboardView.Footer>
+          <>
             <View
-              className={`w-full px-px mt-[29] ${
-                Platform.OS === 'ios' ? 'mb-[79]' : 'mb-[55]'
+              className={`absolute left-0 bottom-0 w-full bg-blue700 ${
+                Platform.OS === 'ios' ? 'h-[163]' : 'h-[139]'
+              }`}
+            />
+            <View
+              className={`absolute left-0 w-full px-[30] ${
+                Platform.OS === 'ios' ? 'bottom-[79]' : 'bottom-[55]'
               }`}>
               <Button
                 text="다음"
@@ -116,42 +147,10 @@ export const LeaveAccountScreen = () => {
                 disabled={selectedReasons.length === 0}
               />
             </View>
-          </View>
-        </ScrollView>
-        {isDetailTyping && (
-          <View className="absolute top-0 right-0 h-full w-full z-[9]">
-            <BG type="solid">
-              <View className="mt-[64]">
-                <ReasonItem
-                  reason="기타"
-                  isSelected={selectedReasons.includes('기타')}
-                  onPress={() => {
-                    toggleReason('기타');
-                    setIsDetailTyping(false);
-                  }}
-                />
-                <View className="w-full px-px">
-                  <TextInput
-                    autoFocus
-                    value={detailReason}
-                    onChangeText={setDetailReason}
-                    placeholder="내용을 입력해주세요"
-                    isError={false}
-                    maxLength={160}
-                  />
-                </View>
-              </View>
-              <View
-                className="flex-1"
-                onTouchStart={() => {
-                  setIsDetailTyping(false);
-                }}
-              />
-            </BG>
-          </View>
-        )}
-      </BG>
-    </KeyboardAvoidingView>
+          </>
+        </DismissKeyboardView.Footer>
+      </DismissKeyboardView>
+    </BG>
   );
 };
 
@@ -167,7 +166,7 @@ const ReasonItem = ({
   return (
     <Pressable
       onPress={onPress}
-      className="flex-row gap-[21] items-center w-full px-px py-[32]">
+      className="flex-row gap-[21] items-center w-full px-px py-[24]">
       <View
         className={`w-[20px] h-[20px] ${
           isSelected ? 'bg-yellowPrimary' : 'bg-blue500'
diff --git a/src/screens/System/NotificationSetting/NotificationSettingYouth.tsx b/src/screens/System/NotificationSetting/NotificationSettingYouth.tsx
index f6fe0cac..9a326273 100644
--- a/src/screens/System/NotificationSetting/NotificationSettingYouth.tsx
+++ b/src/screens/System/NotificationSetting/NotificationSettingYouth.tsx
@@ -244,24 +244,21 @@ export const NotificationSettingYouth = () => {
         />
       ))}
 
-      {showHourBottomSheet && (
-        <TimeSelectBottomSheet
-          type="hour"
-          value={notifications[selectedIndex]?.hour || '오전 00시'}
-          setValue={setTempHour}
-          onClose={() => setShowHourBottomSheet(false)}
-          onSelect={() => setShowMinuteBottomSheet(true)}
-        />
-      )}
-
-      {showMinuteBottomSheet && (
-        <TimeSelectBottomSheet
-          type="minute"
-          value={notifications[selectedIndex]?.minute || '00분'}
-          setValue={setTempMinute}
-          onClose={() => setShowMinuteBottomSheet(false)}
-        />
-      )}
+      <TimeSelectBottomSheet
+        type="hour"
+        value={notifications[selectedIndex]?.hour || '오전 00시'}
+        setValue={setTempHour}
+        onClose={() => setShowHourBottomSheet(false)}
+        onSelect={() => setShowMinuteBottomSheet(true)}
+        isShow={showHourBottomSheet}
+      />
+      <TimeSelectBottomSheet
+        type="minute"
+        value={notifications[selectedIndex]?.minute || '00분'}
+        setValue={setTempMinute}
+        onClose={() => setShowMinuteBottomSheet(false)}
+        isShow={showMinuteBottomSheet}
+      />
     </BG>
   );
 };
diff --git a/src/screens/Youth/YouthListen/index.tsx b/src/screens/Youth/YouthListen/index.tsx
index c40c8183..a201863b 100644
--- a/src/screens/Youth/YouthListen/index.tsx
+++ b/src/screens/Youth/YouthListen/index.tsx
@@ -4,6 +4,7 @@ import {
   Alert,
   Image,
   Keyboard,
+  Platform,
   Pressable,
   ScrollView,
   TextInput,
@@ -16,6 +17,7 @@ import { postProvidedfileCommentByProvidedFileId } from '@apis/YouthListenToVoic
 import { AppBar } from '@components/AppBar';
 import { BG } from '@components/BG';
 import { CustomText } from '@components/CustomText';
+import { DismissKeyboardView } from '@components/DismissKeyboardView';
 import { COLORS } from '@constants/Colors';
 import { KEYBOARD_DELAY_MS } from '@constants/common';
 import { EMOTION_OPTIONS_YOUTH } from '@constants/letter';
@@ -288,23 +290,23 @@ export const YouthListenScreen = ({
   // 메인 UI 렌더링
   return (
     <BG type="main">
-      <View
-        className={`absolute left-0 bottom-[40] w-full h-full ${
-          isKeyboardVisible ? 'hidden' : ''
-        }`}
-        style={{ transform: [{ scale: 1.1 }] }}>
-        <LottieView
-          ref={animation}
-          style={{
-            flex: 1,
-          }}
-          source={require('@assets/lottie/voice.json')}
-          autoPlay
-          loop
-        />
-      </View>
+      <DismissKeyboardView extraScrollHeight={24}>
+        <View
+          className={`absolute left-0 bottom-[40] w-full h-full ${
+            isKeyboardVisible ? 'hidden' : ''
+          }`}
+          style={{ transform: [{ scale: 1.1 }] }}>
+          <LottieView
+            ref={animation}
+            style={{
+              flex: 1,
+            }}
+            source={require('@assets/lottie/voice.json')}
+            autoPlay
+            loop
+          />
+        </View>
 
-      <View className="flex-1">
         <AppBar
           exitCallbackFn={() => navigation.goBack()}
           className="absolute top-[0] w-full"
@@ -395,8 +397,13 @@ export const YouthListenScreen = ({
                 ))}
               </ScrollView>
             )}
-            {/* 메시지 입력 영역 */}
-            {/* TODO: 플로팅 버튼 pr 합쳐지면 바텀 간격도 ios, android 별도 다르게 설정 */}
+          </View>
+        </View>
+
+        {/* 메시지 입력 영역 */}
+        {/* MEMO: absolute 스타일을 가진 View 상위에 View 로 감싸야 키보드가 올라갔을 때 키보드 위로 컴포넌트가 올라감 (이유 조사 필요) */}
+        <View>
+          <View className="absolute left-0 bottom-0 w-full">
             <View className="h-[86] px-[25] bg-blue500 flex-row items-center">
               <View className="flex-[7.5] relative">
                 <TextInput
@@ -426,9 +433,10 @@ export const YouthListenScreen = ({
                 {isClickedEmotion ? <SmileGrayIcon /> : <SmileIcon />}
               </Pressable>
             </View>
+            {Platform.OS === 'ios' && <View className="h-[24] bg-blue500" />}
           </View>
         </View>
-      </View>
+      </DismissKeyboardView>
     </BG>
   );
 };


Reviews:
user: login: salmonco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T16:02:30Z
user: login: salmonco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T16:17:05Z
user: login: salmonco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T16:17:23Z
user: login: salmonco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T17:00:44Z
user: login: obb8923
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-22T06:38:33Z
user: login: salmonco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T15:14:45Z
user: login: salmonco
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T15:16:06Z

Review comments:
diff_hunk: @@ -2,22 +2,34 @@ import {
   Keyboard,
   type StyleProp,
   TouchableWithoutFeedback,
+  View,
   type ViewStyle,
 } from 'react-native';
 import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
 
 type Component = {
-  style?: StyleProp<ViewStyle>;
   children: JSX.Element[] | JSX.Element;
+  footer?: JSX.Element;
+  style?: StyleProp<ViewStyle>;
 };
 
 export const DismissKeyboardView = ({
   children,
+  footer,
   ...props
 }: Readonly<Component>) => (
   <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
-    <KeyboardAwareScrollView {...props} style={props.style}>
-      {children}
-    </KeyboardAwareScrollView>
+    <View style={{ flex: 1 }}>
+      <KeyboardAwareScrollView
+        {...props}
+        style={props.style}
+        // 사용자가 키보드가 열린 상태에서 아무 뷰나 터치하면, 그 터치 이벤트가 keyboard dismiss도 하고, 터치도 정상 처리
+        keyboardShouldPersistTaps="handled">
+        {children}
+      </KeyboardAwareScrollView>
+
+      {/* 키보드와 관계없이 항상 고정 */}
+      {footer}
path: src/components/DismissKeyboardView/index.tsx
user: login: salmonco
body: 키보드가 올라오면 absolute 로 플로팅한 버튼이 키보드 위로 올라오는 문제가 있었습니다.
해결을 위해 기존엔 keyboard visible 상태를 관리해서 visible 이면 버튼 숨기는 식으로 했었는데요,
DismissKeyboardView 안에 키보드가 올라와도 고정가능한 footer 옵션을 추가하는 식으로 방법을 바꿨습니다.

---

인풋창이 있어서 키보드를 쓰고, 키보드와 관계없이 고정시키고픈 컴포넌트 -> DismissKeyboardView 으로 감싸고 footer 속성 사용
created_at: 2025-04-14T16:01:38Z
author_association: COLLABORATOR
diff_hunk: @@ -125,7 +107,17 @@ export const MemberInfoWriteScreen = ({
 
   return (
     <BG type="main">
-      <DismissKeyboardView>
+      <DismissKeyboardView
+        footer={
+          <View className={`absolute left-0 bottom-[55] w-full px-[30]`}>
+            <Button
+              text="다음"
+              onPress={handleNext}
+              disabled={!birthday || !gender || isLoading}
+              isLoading={isLoading}
+            />
+          </View>
+        }>
path: src/screens/Login/MemberInfoWrite/index.tsx
user: login: salmonco
body: footer 에 들어가는 컴포넌트는 키보드가 올라와도 키보드 위로 같이 올라가지 않고 고정됩니다.
created_at: 2025-04-14T16:02:22Z
author_association: COLLABORATOR
diff_hunk: @@ -2,22 +2,34 @@ import {
   Keyboard,
   type StyleProp,
   TouchableWithoutFeedback,
+  View,
   type ViewStyle,
 } from 'react-native';
 import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
 
 type Component = {
-  style?: StyleProp<ViewStyle>;
   children: JSX.Element[] | JSX.Element;
+  footer?: JSX.Element;
+  style?: StyleProp<ViewStyle>;
 };
 
 export const DismissKeyboardView = ({
   children,
+  footer,
   ...props
 }: Readonly<Component>) => (
   <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
-    <KeyboardAwareScrollView {...props} style={props.style}>
-      {children}
-    </KeyboardAwareScrollView>
+    <View style={{ flex: 1 }}>
+      <KeyboardAwareScrollView
+        {...props}
+        style={props.style}
+        // 사용자가 키보드가 열린 상태에서 아무 뷰나 터치하면, 그 터치 이벤트가 keyboard dismiss도 하고, 터치도 정상 처리
+        keyboardShouldPersistTaps="handled">
+        {children}
+      </KeyboardAwareScrollView>
+
+      {/* 키보드와 관계없이 항상 고정 */}
+      {footer}
path: src/components/DismissKeyboardView/index.tsx
user: login: obb8923
body: KeyboardAwareScrollView  를 도입한 이유가
1. 사용자가 키보드가 열린 상태에서 아무 뷰나 터치하면, 그 터치 이벤트가 keyboard dismiss도 하고, 터치도 정상 처리
2.키보드가 올라오면 absolute 로 플로팅한 버튼이 키보드 위로 올라오는 문제해결
이 두 이유인가요?
created_at: 2025-04-15T00:47:08Z
author_association: COLLABORATOR
diff_hunk: @@ -125,7 +107,17 @@ export const MemberInfoWriteScreen = ({
 
   return (
     <BG type="main">
-      <DismissKeyboardView>
+      <DismissKeyboardView
+        footer={
+          <View className={`absolute left-0 bottom-[55] w-full px-[30]`}>
+            <Button
+              text="다음"
+              onPress={handleNext}
+              disabled={!birthday || !gender || isLoading}
+              isLoading={isLoading}
+            />
+          </View>
+        }>
path: src/screens/Login/MemberInfoWrite/index.tsx
user: login: salmonco
body: -> 합성 컴포넌트 구조로 변경 b4c6726555d788910a136a45625870efd1edea9d
created_at: 2025-04-20T16:17:05Z
author_association: COLLABORATOR
diff_hunk: @@ -2,22 +2,34 @@ import {
   Keyboard,
   type StyleProp,
   TouchableWithoutFeedback,
+  View,
   type ViewStyle,
 } from 'react-native';
 import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
 
 type Component = {
-  style?: StyleProp<ViewStyle>;
   children: JSX.Element[] | JSX.Element;
+  footer?: JSX.Element;
+  style?: StyleProp<ViewStyle>;
 };
 
 export const DismissKeyboardView = ({
   children,
+  footer,
   ...props
 }: Readonly<Component>) => (
   <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
-    <KeyboardAwareScrollView {...props} style={props.style}>
-      {children}
-    </KeyboardAwareScrollView>
+    <View style={{ flex: 1 }}>
+      <KeyboardAwareScrollView
+        {...props}
+        style={props.style}
+        // 사용자가 키보드가 열린 상태에서 아무 뷰나 터치하면, 그 터치 이벤트가 keyboard dismiss도 하고, 터치도 정상 처리
+        keyboardShouldPersistTaps="handled">
+        {children}
+      </KeyboardAwareScrollView>
+
+      {/* 키보드와 관계없이 항상 고정 */}
+      {footer}
path: src/components/DismissKeyboardView/index.tsx
user: login: salmonco
body: -> 합성 컴포넌트 구조로 변경 b4c6726555d788910a136a45625870efd1edea9d
created_at: 2025-04-20T16:17:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+import { useEffect, useRef, useState } from 'react';
+import { Keyboard, type KeyboardEvent } from 'react-native';
+
+const DEFAULT_KEYBOARD_HEIGHT = 290;
+
+export const useKeyboardHeight = () => {
+  const [keyboardHeight, setKeyboardHeight] = useState(0);
+  const maxHeightRef = useRef(0);
+
+  useEffect(() => {
+    const onKeyboardDidShow = (e: KeyboardEvent) => {
+      const height = e.endCoordinates.height;
+
+      if (height > maxHeightRef.current) {
+        maxHeightRef.current = height;
+      }
+
+      setKeyboardHeight(height);
+    };
path: src/libs/hooks/useKeyboardHeight.ts
user: login: salmonco
body: 키보드가 올라왔을 때 키보드 위에 컴포넌트를 띄우고 싶은 경우, 띄울 컴포넌트의 위치를 조정하기 위해 키보드 높이 계산하는 훅 추가했습니다.
created_at: 2025-04-21T16:51:05Z
author_association: COLLABORATOR
diff_hunk: @@ -107,7 +107,7 @@ export const MemberInfoWriteScreen = ({
 
   return (
     <BG type="main">
-      <DismissKeyboardView>
+      <DismissKeyboardView extraHeight={80}>
path: src/screens/Login/MemberInfoWrite/index.tsx
user: login: salmonco
body: 고정시킬 푸터가 있고, 키보드가 올라왔을 때 키보드 위에 간격을 주고 싶은 경우, extraHeight 사용했습니다.
created_at: 2025-04-21T16:51:49Z
author_association: COLLABORATOR
diff_hunk: @@ -288,23 +289,23 @@ export const YouthListenScreen = ({
   // 메인 UI 렌더링
   return (
     <BG type="main">
-      <View
-        className={`absolute left-0 bottom-[40] w-full h-full ${
-          isKeyboardVisible ? 'hidden' : ''
-        }`}
-        style={{ transform: [{ scale: 1.1 }] }}>
-        <LottieView
-          ref={animation}
-          style={{
-            flex: 1,
-          }}
-          source={require('@assets/lottie/voice.json')}
-          autoPlay
-          loop
-        />
-      </View>
+      <DismissKeyboardView extraScrollHeight={23}>
+        <View
+          className={`absolute left-0 bottom-[40] w-full h-full ${
+            isKeyboardVisible ? 'hidden' : ''
+          }`}
+          style={{ transform: [{ scale: 1.1 }] }}>
+          <LottieView
+            ref={animation}
+            style={{
+              flex: 1,
+            }}
+            source={require('@assets/lottie/voice.json')}
+            autoPlay
+            loop
+          />
+        </View>
path: src/screens/Youth/YouthListen/index.tsx
user: login: salmonco
body: 키보드가 올라왔을 때 가려지는 컴포넌트를 키보드 위로 띄우고 싶을 경우, 키보드와 컴포넌트 사이의 간격을 조정하기 위해 extraScrollHeight 를 설정했습니다. (android-only)

ios -> android 와 달리 키보드와 컴포넌트 사이 간격 조정할 상황이 지금까진 없었는데, 생기면 별도로 프로퍼티 추가해야 될 거 같아요.
created_at: 2025-04-21T16:53:14Z
author_association: COLLABORATOR
diff_hunk: @@ -395,39 +396,43 @@ export const YouthListenScreen = ({
                 ))}
               </ScrollView>
             )}
-            {/* 메시지 입력 영역 */}
-            <View className="h-[86] px-[25] bg-blue500 flex-row items-center">
-              <View className="flex-[7.5] relative">
-                <TextInput
-                  value={message}
-                  onChangeText={setMessage}
-                  placeholder="감사의 말을 전해보세요"
-                  placeholderTextColor={COLORS.gray300}
-                  className={`h-[40] text-gray100 py-[8] pl-[27] pr-[45] font-r bg-blue400 border ${
-                    isKeyboardVisible ? 'border-gray200' : 'border-blue400'
-                  }`}
-                  style={{ fontSize: 15, borderRadius: 100 }}
-                  onSubmitEditing={() => handleMessageSend()}
-                  maxLength={160}
-                />
-                {!!message && (
-                  <Pressable
-                    className={`absolute right-[6] top-[5]`}
-                    onPress={() => handleMessageSend()}>
-                    <SendIcon />
-                  </Pressable>
-                )}
-              </View>
-              <View className="w-[15]" />
-              <Pressable
-                className="flex-[1]"
-                onPress={() => setIsClickedEmotion(prev => !prev)}>
-                {isClickedEmotion ? <SmileGrayIcon /> : <SmileIcon />}
-              </Pressable>
+          </View>
+        </View>
+
+        {/* 메시지 입력 영역 */}
+        {/* MEMO: absolute 스타일을 가진 View 상위에 View 로 감싸야 키보드가 올라갔을 때 키보드 위로 컴포넌트가 올라감 (이유 조사 필요) */}
+        <View>
+          <View className="absolute left-0 bottom-0 w-full h-[86] px-[25] bg-blue500 flex-row items-center">
+            <View className="flex-[7.5] relative">
path: src/screens/Youth/YouthListen/index.tsx
user: login: salmonco
body: [메모]
absolute 스타일을 가진 View 상위에 View 로 감싸야 키보드가 올라갔을 때 키보드 위로 컴포넌트가 올라감 (이유 조사 필요)
created_at: 2025-04-21T16:56:23Z
author_association: COLLABORATOR
diff_hunk: @@ -43,10 +64,20 @@ export const DismissKeyboardView = ({ children, ...props }: Props) => {
     <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
       <View style={{ flex: 1 }}>
         <KeyboardAwareScrollView
-          {...props}
-          style={props.style}
+          enableOnAndroid
+          extraHeight={extraHeight}
+          // KeyboardAwareScrollView 안에 있는 컴포넌트 중 키보드에 가려지는 컴포넌트의 경우, 키보드 위로 올라오도록 함
+          extraScrollHeight={
+            extraScrollHeight &&
+            (Platform.OS === 'ios'
+              ? -DEFAULT_IOS_EXTRA_HEIGHT
+              : maxKeyboardHeight - DEFAULT_EXTRA_HEIGHT + extraScrollHeight)
+          }
path: src/components/DismissKeyboardView/index.tsx
user: login: salmonco
body: extraScrollHeight 는 키보드 위에 띄울 컴포넌트가 있을 경우 사용했습니다.

- android -> extraScrollHeight 로 키보드와 컴포넌트 사이 간격 조정
- ios -> extraScrollHeight 설정 안 해줘도 키보드와 컴포넌트 사이 간격을 임의로 조정(DEFAULT_IOS_EXTRA_HEIGHT)하도록 설정했습니다. 추후 간격 조정 필요한 경우 생기면 수정 필요.
created_at: 2025-04-21T17:00:34Z
author_association: COLLABORATOR
diff_hunk: @@ -43,10 +64,20 @@ export const DismissKeyboardView = ({ children, ...props }: Props) => {
     <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
       <View style={{ flex: 1 }}>
         <KeyboardAwareScrollView
-          {...props}
-          style={props.style}
+          enableOnAndroid
+          extraHeight={extraHeight}
+          // KeyboardAwareScrollView 안에 있는 컴포넌트 중 키보드에 가려지는 컴포넌트의 경우, 키보드 위로 올라오도록 함
+          extraScrollHeight={
+            extraScrollHeight &&
+            (Platform.OS === 'ios'
+              ? -DEFAULT_IOS_EXTRA_HEIGHT
+              : maxKeyboardHeight - DEFAULT_EXTRA_HEIGHT + extraScrollHeight)
+          }
path: src/components/DismissKeyboardView/index.tsx
user: login: salmonco
body: -> extraScrollHeight 를 ios 에서도 적용. 324c8a12a684edfce242f14b16bcaacb73976a63
created_at: 2025-04-22T15:14:45Z
author_association: COLLABORATOR
diff_hunk: @@ -288,23 +289,23 @@ export const YouthListenScreen = ({
   // 메인 UI 렌더링
   return (
     <BG type="main">
-      <View
-        className={`absolute left-0 bottom-[40] w-full h-full ${
-          isKeyboardVisible ? 'hidden' : ''
-        }`}
-        style={{ transform: [{ scale: 1.1 }] }}>
-        <LottieView
-          ref={animation}
-          style={{
-            flex: 1,
-          }}
-          source={require('@assets/lottie/voice.json')}
-          autoPlay
-          loop
-        />
-      </View>
+      <DismissKeyboardView extraScrollHeight={23}>
+        <View
+          className={`absolute left-0 bottom-[40] w-full h-full ${
+            isKeyboardVisible ? 'hidden' : ''
+          }`}
+          style={{ transform: [{ scale: 1.1 }] }}>
+          <LottieView
+            ref={animation}
+            style={{
+              flex: 1,
+            }}
+            source={require('@assets/lottie/voice.json')}
+            autoPlay
+            loop
+          />
+        </View>
path: src/screens/Youth/YouthListen/index.tsx
user: login: salmonco
body: -> extraScrollHeight 를 ios 에서도 적용. 324c8a12a684edfce242f14b16bcaacb73976a63
created_at: 2025-04-22T15:16:06Z
author_association: COLLABORATOR

PR comments:
user: login: obb8923
created_at: 2025-04-20T14:12:35Z
body: KeyboardAwareScrollView 를 도입한 이유가

1.사용자가 키보드가 열린 상태에서 아무 뷰나 터치하면, 그 터치 이벤트가 keyboard dismiss도 하고, 터치도 정상 처리
2.키보드가 올라오면 absolute 로 플로팅한 버튼이 키보드 위로 올라오는 문제해결
이 두 이유인가요?
author_association: COLLABORATOR
user: login: salmonco
created_at: 2025-04-20T14:16:25Z
body: > KeyboardAwareScrollView 를 도입한 이유가
> 
> 1.사용자가 키보드가 열린 상태에서 아무 뷰나 터치하면, 그 터치 이벤트가 keyboard dismiss도 하고, 터치도 정상 처리
> 2.키보드가 올라오면 absolute 로 플로팅한 버튼이 키보드 위로 올라오는 문제해결 이 두 이유인가요?

1. 네 맞아요!
2. KeyboardAwareScrollView 만으론 플로팅한 버튼이 키보드 위로 올라오는 문제해결이 안 되어서, 커스텀 컴포넌트인 DismissKeyboardView 에 footer 속성 추가함으로써 플로팅한 버튼이 키보드 위로 올라오지 않도록 했습니다.
author_association: COLLABORATOR
user: login: obb8923
created_at: 2025-04-20T14:37:56Z
body: https://www.notion.so/compound-pattern-1db0173187fb80a6b8a7f7d06d413909

다른 방법을 제안해드립니다. 읽어보시고 결정부탁드립니다.
author_association: COLLABORATOR
user: login: salmonco
created_at: 2025-04-20T15:48:35Z
body: > https://www.notion.so/compound-pattern-1db0173187fb80a6b8a7f7d06d413909
> 
> 다른 방법을 제안해드립니다. 읽어보시고 결정부탁드립니다.

제안 감사합니다! 말씀주신 것처럼 가독성이 합성 컴포넌트 구조가 나을 것 같아요. b4c6726555d788910a136a45625870efd1edea9d 반영했습니다!

다시한번 테스트해보니 안드에선 푸터 고정이 안 되는 문제가 있어서, 안드 해결 마저 하고 머지하면 좋을 것 같아요.
author_association: COLLABORATOR
user: login: salmonco
created_at: 2025-04-20T16:15:56Z
body: 요거 죄송한데 다른 pr 베이스 설정 잘못해서 diff 가 섞여가지고, 아래 컴포넌트만 봐주시면 될 것 같아요.
DismissKeyboardView, MemberInfoWrite, NicknameWrite, LeaveAccount
author_association: COLLABORATOR
user: login: obb8923
created_at: 2025-04-21T05:19:24Z
body: 네, 안드에선 푸터 고정이 안 되는 문제 해결하시면 알려주세요!

author_association: COLLABORATOR