diff --git a/apps/native/src/components/common/Header.tsx b/apps/native/src/components/common/Header.tsx
new file mode 100644
index 000000000..3a4935ba5
--- /dev/null
+++ b/apps/native/src/components/common/Header.tsx
@@ -0,0 +1,131 @@
+import React, { type FC, type ReactNode } from 'react';
+import { View, Text } from 'react-native';
+import { ChevronLeft, type LucideIcon } from 'lucide-react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import { colors } from '@theme/tokens';
+
+import ContentInset from './ContentInset';
+import AnimatedPressable from './AnimatedPressable';
+
+type HeaderBadge = 'correct' | 'incorrect';
+
+type HeaderProps = {
+ title?: string;
+ subtitle?: string;
+ badge?: HeaderBadge;
+ showBackButton?: boolean;
+ onPressBack?: () => void;
+ right?: ReactNode;
+};
+
+const badgeConfig = {
+ correct: { bg: 'bg-primary-200', text: 'text-primary-600', label: '정답' },
+ incorrect: { bg: 'bg-red-100', text: 'text-red-500', label: '오답' },
+} as const;
+
+const Badge = ({ variant }: { variant: HeaderBadge }) => {
+ const config = badgeConfig[variant];
+ return (
+
+ {config.label}
+
+ );
+};
+
+const HeaderTextButton = ({
+ children,
+ onPress,
+ color = colors['gray-700'],
+}: {
+ children: ReactNode;
+ onPress?: () => void;
+ color?: string;
+}) => (
+
+
+ {children}
+
+
+);
+
+const HeaderIconButton = ({
+ icon: Icon,
+ onPress,
+ color,
+}: {
+ icon: LucideIcon;
+ onPress?: () => void;
+ color?: string;
+}) => (
+
+
+
+);
+
+const getRightGap = (children: ReactNode): number => {
+ let hasTextButton = false;
+ const check = (node: ReactNode) => {
+ React.Children.forEach(node, (child) => {
+ if (!React.isValidElement(child)) return;
+ if (child.type === HeaderTextButton) hasTextButton = true;
+ if (child.type === React.Fragment) check((child.props as { children?: ReactNode }).children);
+ });
+ };
+ check(children);
+ return hasTextButton ? 8 : 4;
+};
+
+const HeaderRoot = ({
+ title,
+ subtitle,
+ badge,
+ showBackButton,
+ onPressBack,
+ right,
+}: HeaderProps) => {
+ const navigation = useNavigation();
+
+ const handleBack = () => {
+ if (onPressBack) {
+ onPressBack();
+ return;
+ }
+ if (navigation.canGoBack()) {
+ navigation.goBack();
+ }
+ };
+
+ return (
+
+
+
+ {showBackButton && }
+ {title && (
+
+ {title}
+ {subtitle && {subtitle}}
+ {badge && }
+
+ )}
+
+ {right && (
+
+ {right}
+
+ )}
+
+
+ );
+};
+
+type HeaderComponent = FC & {
+ TextButton: typeof HeaderTextButton;
+ IconButton: typeof HeaderIconButton;
+};
+
+const Header = HeaderRoot as HeaderComponent;
+Header.TextButton = HeaderTextButton;
+Header.IconButton = HeaderIconButton;
+
+export default Header;
diff --git a/apps/native/src/components/common/index.ts b/apps/native/src/components/common/index.ts
index c86f30e39..b62046181 100644
--- a/apps/native/src/components/common/index.ts
+++ b/apps/native/src/components/common/index.ts
@@ -1,3 +1,4 @@
+import Header from './Header';
import AnimatedPressable from './AnimatedPressable';
import ContentInset from './ContentInset';
import LoadingScreen from './LoadingScreen';
@@ -7,6 +8,7 @@ import SegmentedControl from './SegmentedControl';
import { ImageWithSkeleton } from './ImageWithSkeleton';
export {
+ Header,
AnimatedPressable,
ContentInset,
LoadingScreen,
diff --git a/apps/native/src/components/system/icons/AlertBellButtonIcon.tsx b/apps/native/src/components/system/icons/AlertBellButtonIcon.tsx
index 422992289..0c36e12f3 100644
--- a/apps/native/src/components/system/icons/AlertBellButtonIcon.tsx
+++ b/apps/native/src/components/system/icons/AlertBellButtonIcon.tsx
@@ -1,36 +1,36 @@
import React from 'react';
-import { Path, Circle, Svg } from 'react-native-svg';
+import { Path, Svg } from 'react-native-svg';
import type { LucideIcon, LucideProps } from 'lucide-react-native';
const AlertButtonIcon = React.forwardRef, LucideProps>(
- ({ color = 'black', size = 48, strokeWidth = 2, ...rest }, ref) => {
- const resolvedStrokeWidth = Number(strokeWidth);
-
+ ({ size = 24, ...rest }, ref) => {
return (
-
+ <>
+
+ >
);
}
) as LucideIcon;