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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions apps/native/src/components/common/Header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View className={`rounded px-[6px] py-[2px] ${config.bg}`}>
<Text className={`typo-heading-2-semibold ${config.text}`}>{config.label}</Text>
</View>
);
};

const HeaderTextButton = ({
children,
onPress,
color = colors['gray-700'],
}: {
children: ReactNode;
onPress?: () => void;
color?: string;
}) => (
<AnimatedPressable className='h-[48px] items-center justify-center px-[12px]' onPress={onPress}>
<Text className='typo-heading-2-semibold' style={{ color }}>
{children}
</Text>
</AnimatedPressable>
);

const HeaderIconButton = ({
icon: Icon,
onPress,
color,
}: {
icon: LucideIcon;
onPress?: () => void;
color?: string;
}) => (
<AnimatedPressable className='p-[12px]' onPress={onPress}>
<Icon size={24} {...(color && { color })} />
</AnimatedPressable>
);

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 (
<View className='h-[56px] w-full'>
<ContentInset className='h-full flex-row items-center justify-between'>
<View className='flex-row items-center gap-[4px]'>
{showBackButton && <HeaderIconButton icon={ChevronLeft} onPress={handleBack} />}
{title && (
<View className='flex-row items-center gap-[12px]'>
<Text className='typo-title-1-bold text-gray-900'>{title}</Text>
{subtitle && <Text className='typo-title-1-semibold text-gray-700'>{subtitle}</Text>}
{badge && <Badge variant={badge} />}
Comment on lines +105 to +108
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Textcolor-gray-900/color-gray-700 클래스가 사용되고 있는데, 이 프로젝트의 다른 컴포넌트들은 text-gray-900/text-gray-700 형태로 색상을 지정하고 있습니다(예: student/problem/components/Header.tsx). 현재 클래스명은 Tailwind/NativeWind에서 매칭되지 않아 타이틀/서브타이틀 색상이 적용되지 않을 가능성이 큽니다. text-gray-*로 수정하거나, 토큰 색상을 style={{ color: ... }}로 일관되게 적용해 주세요.

Copilot uses AI. Check for mistakes.
</View>
)}
</View>
{right && (
<View className='flex-row items-center' style={{ gap: getRightGap(right) }}>
{right}
</View>
)}
</ContentInset>
</View>
);
};

type HeaderComponent = FC<HeaderProps> & {
TextButton: typeof HeaderTextButton;
IconButton: typeof HeaderIconButton;
};

const Header = HeaderRoot as HeaderComponent;
Header.TextButton = HeaderTextButton;
Header.IconButton = HeaderIconButton;

export default Header;
2 changes: 2 additions & 0 deletions apps/native/src/components/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Header from './Header';
import AnimatedPressable from './AnimatedPressable';
import ContentInset from './ContentInset';
import LoadingScreen from './LoadingScreen';
Expand All @@ -7,6 +8,7 @@ import SegmentedControl from './SegmentedControl';
import { ImageWithSkeleton } from './ImageWithSkeleton';

export {
Header,
AnimatedPressable,
ContentInset,
LoadingScreen,
Expand Down
56 changes: 28 additions & 28 deletions apps/native/src/components/system/icons/AlertBellButtonIcon.tsx
Original file line number Diff line number Diff line change
@@ -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<React.ComponentRef<typeof Svg>, LucideProps>(
({ color = 'black', size = 48, strokeWidth = 2, ...rest }, ref) => {
const resolvedStrokeWidth = Number(strokeWidth);

({ size = 24, ...rest }, ref) => {
return (
<Svg ref={ref} width={size} height={size} viewBox='0 0 48 48' fill='none' {...rest}>
<Path
d='M22.2681 33C22.4436 33.304 22.6961 33.5565 23.0001 33.732C23.3041 33.9075 23.649 33.9999 24.0001 33.9999C24.3511 33.9999 24.696 33.9075 25 33.732C25.3041 33.5565 25.5565 33.304 25.7321 33'
stroke={color}
strokeWidth={resolvedStrokeWidth}
strokeLinecap='round'
strokeLinejoin='round'
/>
<Path
d='M25.9161 14.3141C25.0136 14.01 24.0516 13.9251 23.1098 14.0664C22.168 14.2077 21.2733 14.5712 20.4998 15.1269C19.7263 15.6825 19.0961 16.4142 18.6614 17.2616C18.2268 18.1091 18.0001 19.0478 18.0001 20.0001C18.0001 24.4991 16.5891 25.9561 15.2601 27.3271C15.1297 27.4705 15.0437 27.6486 15.0127 27.8399C14.9816 28.0312 15.0068 28.2274 15.0852 28.4047C15.1636 28.5819 15.2918 28.7325 15.4542 28.8383C15.6167 28.944 15.8063 29.0002 16.0001 29.0001H32.0001C32.1939 29.0002 32.3836 28.944 32.546 28.8383C32.7084 28.7325 32.8366 28.5819 32.915 28.4047C32.9934 28.2274 33.0186 28.0312 32.9876 27.8399C32.9565 27.6486 32.8705 27.4705 32.7401 27.3271C32.5343 27.1152 32.3391 26.8933 32.1551 26.6621'
stroke={color}
strokeWidth={resolvedStrokeWidth}
strokeLinecap='round'
strokeLinejoin='round'
/>
<Circle
cx={30}
cy={20}
r={3}
fill='#E57A00'
stroke='#E57A00'
strokeWidth={resolvedStrokeWidth}
/>
</Svg>
<>
<Svg ref={ref} width={size} height={size} viewBox='0 0 24 24' fill='none' {...rest}>
<Path
d='M10.2681 21C10.4436 21.304 10.6961 21.5565 11.0001 21.732C11.3041 21.9075 11.649 21.9999 12.0001 21.9999C12.3511 21.9999 12.696 21.9075 13 21.732C13.3041 21.5565 13.5565 21.304 13.7321 21'
stroke='#1E1E21'
strokeWidth={2}
Comment on lines 5 to +13
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

AlertBellButtonIconLucideProps를 받도록 되어 있지만, 변경 후에는 color/strokeWidth를 더 이상 적용하지 않고 Path에 stroke/strokeWidth를 하드코딩하고 있습니다. 이 상태면 소비자가 color/strokeWidth를 넘겨도 반영되지 않아(타입상 지원하는 것처럼 보이는데) API가 혼란스러울 수 있습니다. 고정 색상이 의도라면 props 타입을 축소하거나, 아니면 이전처럼 color/strokeWidth를 받아 Path에 반영해 주세요.

Copilot uses AI. Check for mistakes.
strokeLinecap='round'
strokeLinejoin='round'
/>
<Path
d='M13.9159 2.31415C13.0134 2.01002 12.0514 1.9251 11.1096 2.06642C10.1677 2.20774 9.27304 2.57124 8.49953 3.12685C7.72601 3.68246 7.09586 4.41423 6.6612 5.26164C6.22654 6.10905 5.99985 7.04776 5.99987 8.00015C5.99987 12.4991 4.58887 13.9561 3.25987 15.3271C3.12944 15.4705 3.04348 15.6486 3.01243 15.8399C2.98138 16.0312 3.00659 16.2274 3.08498 16.4047C3.16338 16.5819 3.29158 16.7325 3.454 16.8383C3.61643 16.944 3.80607 17.0002 3.99987 17.0001H19.9999C20.1937 17.0002 20.3833 16.944 20.5457 16.8383C20.7082 16.7325 20.8364 16.5819 20.9148 16.4047C20.9932 16.2274 21.0184 16.0312 20.9873 15.8399C20.9563 15.6486 20.8703 15.4705 20.7399 15.3271C20.5341 15.1152 20.3388 14.8933 20.1549 14.6621'
stroke='#1E1E21'
strokeWidth={2}
strokeLinecap='round'
strokeLinejoin='round'
/>
<Path
d='M18 11C19.6569 11 21 9.65685 21 8C21 6.34315 19.6569 5 18 5C16.3431 5 15 6.34315 15 8C15 9.65685 16.3431 11 18 11Z'
fill='#E57A00'
stroke='#E57A00'
strokeWidth={2}
strokeLinecap='round'
strokeLinejoin='round'
/>
</Svg>
</>
);
}
) as LucideIcon;
Expand Down
Loading