Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5b38095
fix(native): refactor sortScrapData to use a unified timestamp retrie…
b0nsu Feb 5, 2026
e881e15
refactor(native): replace deprecated client-side sorting with updated…
b0nsu Feb 5, 2026
150ef00
refactor(native): reorganize imports and restore sortedData calculati…
b0nsu Feb 5, 2026
ab016bd
feat(native): add useScrapStoreSync hook for synchronizing scrap stor…
b0nsu Feb 5, 2026
16141cf
refactor(native): enhance ScrapScreen by integrating useScrapStoreSyn…
b0nsu Feb 5, 2026
389e76a
refactor(native): remove unused imports and cleanup delete handling i…
b0nsu Feb 5, 2026
d62a607
refactor(native): remove unused hooks and cleanup delete handling in …
b0nsu Feb 5, 2026
ae153da
refactor(native): simplify RecentScrapStore by replacing scrap detail…
b0nsu Feb 5, 2026
e5ef4ee
refactor(native): update ScrapDetailScreen to use addScrapId for rece…
b0nsu Feb 5, 2026
a5857b7
refactor(native): update SortDropdown to improve sorting options and …
b0nsu Feb 5, 2026
cee9453
refactor(native): enhance optimistic updates for scrap movement and d…
b0nsu Feb 5, 2026
a9985a7
refactor(native): update ChevronUpFilledIcon and ScrapDefaultIcon for…
b0nsu Feb 5, 2026
42e501e
refactor(native): update RecentScrapCard to use ScrapListItemResp for…
b0nsu Feb 5, 2026
ad7b41f
refactor(native): remove border styling from TooltipPopover for clean…
b0nsu Feb 5, 2026
756995b
refactor(native): update header components for improved styling and c…
b0nsu Feb 5, 2026
3721079
refactor(native): integrate LoadQnaImageModal into ScrapModalsContext…
b0nsu Feb 5, 2026
c389481
refactor(native): integrate LoadQnaImageModal into ScrapModalsContext…
b0nsu Feb 5, 2026
3211816
Merge branch 'refactor/native/scrap-#190' of https://github.com/team-…
b0nsu Feb 5, 2026
d64e390
refactor(native): enhance ScrapScreen with refresh control and improv…
b0nsu Feb 5, 2026
73b481c
refactor(native): remove unused folder data fetching in FolderScrapSc…
b0nsu Feb 5, 2026
569ba42
refactor(native): add useScrapStoreSync hook for improved state synch…
b0nsu Feb 5, 2026
8aa7005
feat(native): add AnalysisSection component and integrate it into Scr…
b0nsu Feb 6, 2026
94eaeac
refactor(native): update DRAG_HANDLE_WIDTH constant for improved UI c…
b0nsu Feb 8, 2026
11ecf42
refactor(native): enhance ScrapDetailScreen with recent scrap ID hand…
b0nsu Feb 8, 2026
dd6b275
refactor(native): enhance shouldShowAnalysisSection logic to include …
b0nsu Feb 8, 2026
20b37fe
refactor(native): update ApiSortKey and UISortKey types for enhanced …
b0nsu Feb 8, 2026
0470fcd
refactor(native): update SearchScrapHeader and CreateFolderModal for …
b0nsu Feb 8, 2026
992fede
refactor(native): update CreateFolderModal and LoadQnaImageModal to s…
b0nsu Feb 8, 2026
f7e5672
refactor(native): reset selectedId state when LoadQnaImageModal is cl…
b0nsu Feb 8, 2026
f1ce98e
refactor(native): integrate refetchScraps in AddScrapTooltip after su…
b0nsu Feb 8, 2026
d238708
refactor(native): improving state management for title updates
b0nsu Feb 8, 2026
b16a7a6
feat(native): add useGetEntireProblem and useGetEntireProblemPointing…
b0nsu Feb 11, 2026
aae4c16
refactor(native): enhance ScrapDetailScreen with additional data fetc…
b0nsu Feb 11, 2026
9ff64b0
refactor(native): update convertScrapToGroup function to handle entir…
b0nsu Feb 11, 2026
ce6a50e
refactor(native): integrate folder data fetching and update folder se…
b0nsu Feb 11, 2026
e95e5c1
refactor(native): improve ScrapCard, ScrapHeadCard, and TrashCard com…
b0nsu Feb 11, 2026
e59e6b5
refactor(native): simplify ScrapGrid components by removing itemHeigh…
b0nsu Feb 11, 2026
43b256a
refactor(native): remove unused useRecentScrapStore import from Scrap…
b0nsu Feb 11, 2026
6e7eb72
refactor(native): adjust padding in ProblemViewer component within Po…
b0nsu Feb 11, 2026
a813aab
refactor(native): enhance formatToMinute function to support Korean l…
b0nsu Feb 11, 2026
a3d951c
feat(native): add isHovered prop to ImageWithSkeleton component for h…
b0nsu Feb 11, 2026
0b65940
feat(native): add isHovered prop to ScrapFolderDefaultIcon for dynami…
b0nsu Feb 11, 2026
009ad84
feat(native): implement isHovered state for TrashCard and ScrapCard c…
b0nsu Feb 11, 2026
c70c595
feat(native): add onOpenChange prop to TooltipPopover for handling vi…
b0nsu Feb 11, 2026
d7f8b8d
refactor(native): update layout in SearchResultCard component for imp…
b0nsu Feb 11, 2026
839bc6b
feat(native): add new API endpoints
b0nsu Feb 11, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export const useMoveScraps = () => {
});
return data as MoveScrapsResponse;
},
// 낙관적 업데이트: 이동된 항목을 현재 폴더에서 즉시 제거
// 낙관적 업데이트: 이동된 스크랩의 folderId 변경
onMutate: async (request) => {
// scrapIds를 items 형태로 변환 (타입은 항상 SCRAP)
const items = request.scrapIds.map((id) => ({ id, type: 'SCRAP' as const }));
return await optimisticMoveScrap(queryClient, items);
return await optimisticMoveScrap(queryClient, items, request.targetFolderId);
},
// 에러 발생 시 롤백
onError: (error, request, context) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,25 @@ export const optimisticDeleteScrap = async (
// 이전 데이터 백업
const previousQueries = queryClient.getQueriesData(searchQueryFilters);

// 낙관적 업데이트: 삭제된 항목을 즉시 제거
// 낙관적 업데이트: 삭제된 항목을 즉시 제거 + 폴더 정보 업데이트
queryClient.setQueriesData<ScrapSearchResponse>(searchQueryFilters, (old) => {
if (!old) return old;

// 삭제되는 스크랩들의 folderId별 개수 계산
const folderCountDeltas = new Map<number, number>();
old.scraps?.forEach((scrap) => {
if (deletedIds.has(`SCRAP-${scrap.id}`) && scrap.folderId != null) {
folderCountDeltas.set(scrap.folderId, (folderCountDeltas.get(scrap.folderId) ?? 0) + 1);
}
});

return {
folders: old.folders?.filter((folder) => !deletedIds.has(`FOLDER-${folder.id}`)),
folders: old.folders
?.filter((folder) => !deletedIds.has(`FOLDER-${folder.id}`))
.map((folder) => ({
...folder,
scrapCount: (folder.scrapCount ?? 0) - (folderCountDeltas.get(folder.id) ?? 0),
})),
scraps: old.scraps?.filter((scrap) => !deletedIds.has(`SCRAP-${scrap.id}`)),
};
});
Expand All @@ -48,11 +61,13 @@ export const optimisticDeleteScrap = async (

/**
* 스크랩 이동 낙관적 업데이트
* @param targetFolderId 이동할 폴더 ID (undefined면 전체 스크랩으로 이동)
* @returns 롤백을 위한 이전 데이터
*/
export const optimisticMoveScrap = async (
queryClient: QueryClient,
items: Array<{ id: number; type: string }>
items: Array<{ id: number; type: string }>,
targetFolderId?: number
) => {
const movedIds = createDeletedIdsSet(items);
const searchQueryFilters = createSearchQueryFilters();
Expand All @@ -63,13 +78,32 @@ export const optimisticMoveScrap = async (
// 이전 데이터 백업
const previousQueries = queryClient.getQueriesData(searchQueryFilters);

// 낙관적 업데이트: 이동된 항목을 현재 폴더에서 제거
// 낙관적 업데이트: 이동된 스크랩의 folderId 변경
// 스크랩 이동 시 폴더 정보도 업데이트
queryClient.setQueriesData<ScrapSearchResponse>(searchQueryFilters, (old) => {
if (!old) return old;

// 이동되는 스크랩들의 원래 folderId 수집
const sourceFolderCounts = new Map<number, number>();
old.scraps?.forEach((scrap) => {
if (movedIds.has(`SCRAP-${scrap.id}`) && scrap.folderId != null) {
sourceFolderCounts.set(scrap.folderId, (sourceFolderCounts.get(scrap.folderId) ?? 0) + 1);
}
});

return {
folders: old.folders?.filter((folder) => !movedIds.has(`FOLDER-${folder.id}`)),
scraps: old.scraps?.filter((scrap) => !movedIds.has(`SCRAP-${scrap.id}`)),
folders: old.folders?.map((folder) => {
const removedCount = sourceFolderCounts.get(folder.id) ?? 0;
const addedCount = folder.id === targetFolderId ? items.length : 0;

return {
...folder,
scrapCount: (folder.scrapCount ?? 0) - removedCount + addedCount,
};
}),
scraps: old.scraps?.map((scrap) =>
movedIds.has(`SCRAP-${scrap.id}`) ? { ...scrap, folderId: targetFolderId } : scrap
),
};
});

Expand Down
4 changes: 4 additions & 0 deletions apps/native/src/apis/controller/student/study/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import useGetProblem from './useGetProblem';
import useGetPublishDetail from './useGetPublishDetail';
import useGetWeeklyProgress from './useGetWeeklyProgress';
import useGetWeeklyPublish from './useGetWeeklyPublish';
import useGetEntireProblemPointing from './useGetEntireProblemPointing';
import useGetEntireProblem from './useGetEntireProblem';

export {
getPublishDetailById,
Expand All @@ -16,4 +18,6 @@ export {
useGetPublishDetail,
useGetWeeklyProgress,
useGetWeeklyPublish,
useGetEntireProblem,
useGetEntireProblemPointing,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { TanstackQueryClient } from '@/apis/client';

const useGetEntireProblem = (problemId: number, enabled = true) => {
return TanstackQueryClient.useQuery(
'get',
'/api/student/study/problem/entire/{problemId}',
{
params: {
path: {
problemId: problemId,
},
},
},
{
enabled: enabled,
}
);
};

export default useGetEntireProblem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { TanstackQueryClient } from '@/apis/client';

const useGetEntireProblemPointing = (problemId: number, enabled = true) => {
return TanstackQueryClient.useQuery(
'get',
'/api/student/study/pointing/entire/{problemId}',
{
params: {
path: {
problemId: problemId,
},
},
},
{
enabled: enabled,
}
);
};

export default useGetEntireProblemPointing;
15 changes: 14 additions & 1 deletion apps/native/src/components/common/ImageWithSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type ImageWithSkeletonProps = {
fallback?: React.ReactNode;
/** 대각선 레이아웃 사용 여부 (true면 대각선 배치, false면 전체 영역에 표시) */
isDiagonalLayout?: boolean;
/** 선택된 이미지인지 여부 */
isHovered?: boolean;
};

// 스켈레톤 컴포넌트
Expand Down Expand Up @@ -79,6 +81,7 @@ const ImageWithSkeletonComponent = ({
uniqueId = 'default',
fallback,
isDiagonalLayout = false,
isHovered = false,
}: ImageWithSkeletonProps) => {
// 이미지 로딩 상태 관리
const [isLoading, setIsLoading] = useState(true);
Expand Down Expand Up @@ -176,6 +179,8 @@ const ImageWithSkeletonComponent = ({
width: '80%',
height: '80%',
borderRadius: borderRadius,
borderWidth: 4,
borderColor: isHovered ? colors['gray-300'] : colors['gray-100'],
}}>
<Image
source={{ uri: imageUrls[0] }}
Expand All @@ -186,6 +191,7 @@ const ImageWithSkeletonComponent = ({
width: '100%',
height: '100%',
borderRadius: borderRadius,
overflow: 'hidden',
}}
/>
</View>
Expand All @@ -198,6 +204,9 @@ const ImageWithSkeletonComponent = ({
height: '80%',
backgroundColor: colors['gray-400'],
borderRadius: borderRadius,
overflow: 'hidden',
borderWidth: 4,
borderColor: isHovered ? colors['gray-300'] : colors['gray-100'],
}}
/>
)}
Expand All @@ -210,6 +219,8 @@ const ImageWithSkeletonComponent = ({
width: '80%',
height: '80%',
borderRadius: borderRadius,
borderWidth: 4,
borderColor: isHovered ? colors['gray-300'] : colors['gray-100'],
}}>
<Image
source={{ uri: imageToShow }}
Expand All @@ -220,6 +231,7 @@ const ImageWithSkeletonComponent = ({
width: '100%',
height: '100%',
borderRadius: borderRadius,
overflow: 'hidden',
}}
/>
</View>
Expand Down Expand Up @@ -302,6 +314,7 @@ export const ImageWithSkeleton = React.memo(ImageWithSkeletonComponent, (prevPro
prevProps.height === nextProps.height &&
prevProps.aspectRatio === nextProps.aspectRatio &&
prevProps.borderRadius === nextProps.borderRadius &&
prevProps.resizeMode === nextProps.resizeMode
prevProps.resizeMode === nextProps.resizeMode &&
prevProps.isHovered === nextProps.isHovered
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Path, Svg } from 'react-native-svg';
const ChevronUpFilledIcon = React.forwardRef<React.ComponentRef<typeof Svg>, LucideProps>(
({ color = '#1E1E21', size = 20, ...rest }, ref) => (
<Svg ref={ref} width={size} height={size} viewBox='0 0 20 20' fill='none' {...rest}>
<Path d='M6 15L12 9L18 15' fill={color} />
<Path d='M5 12.5L10 7.5L15 12.5' fill={color} />
</Svg>
)
) as LucideIcon;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const ScrapDefaultIcon = React.forwardRef<React.ComponentRef<typeof Svg>, Lucide
const numericSize = typeof size === 'number' ? size : Number(size) || 48;
const svgProps = hasStyleSize
? { ...rest, style }
: { width: numericSize, height: (numericSize * 116) / 99, ...rest, style };
: { width: numericSize, height: numericSize, ...rest, style };

return (
<Svg ref={ref} viewBox='0 0 99 116' fill='none' {...svgProps}>
Expand Down
47 changes: 25 additions & 22 deletions apps/native/src/components/system/icons/ScrapFolderDefalutIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,31 @@ import { Svg, Rect, Path } from 'react-native-svg';
import type { LucideIcon, LucideProps } from 'lucide-react-native';
import { StyleSheet } from 'react-native';

const ScrapFolderDefaultIcon = React.forwardRef<React.ComponentRef<typeof Svg>, LucideProps>(
({ size = 124, style, ...rest }, ref) => {
// style에 width나 height가 있으면 size를 무시
const flattenedStyle = StyleSheet.flatten(style);
const hasStyleSize = !!flattenedStyle && (flattenedStyle.width || flattenedStyle.height);
const svgProps = hasStyleSize
? { ...rest, style }
: { width: size, height: size, ...rest, style };
const ScrapFolderDefaultIcon = React.forwardRef<
React.ComponentRef<typeof Svg>,
LucideProps & { isHovered?: boolean }
>(({ size = 124, style, isHovered = false, ...rest }, ref) => {
// style에 width나 height가 있으면 size를 무시
const flattenedStyle = StyleSheet.flatten(style);
const hasStyleSize = !!flattenedStyle && (flattenedStyle.width || flattenedStyle.height);
const svgProps = hasStyleSize
? { ...rest, style }
: { width: size, height: size, ...rest, style };

return (
<Svg ref={ref} viewBox='0 0 124 124' fill='none' {...svgProps}>
<Rect x={2} y={2} width={94} height={94} rx={8} fill='#C5CEFF' />
<Rect x={2} y={2} width={94} height={94} rx={8} stroke='#F8F9FC' strokeWidth={4} />
<Rect x={28} y={28} width={94} height={94} rx={8} fill='#C5CEFF' />
<Rect x={28} y={28} width={94} height={94} rx={8} stroke='#F8F9FC' strokeWidth={4} />
<Path
d='M90.334 90.3335C91.3506 90.3335 92.3257 89.9296 93.0446 89.2108C93.7634 88.4919 94.1673 87.5168 94.1673 86.5002V67.3335C94.1673 66.3169 93.7634 65.3418 93.0446 64.6229C92.3257 63.9041 91.3506 63.5002 90.334 63.5002H75.1923C74.5512 63.5065 73.9188 63.3519 73.3529 63.0505C72.787 62.7491 72.3057 62.3107 71.9531 61.7752L70.4007 59.4752C70.0516 58.9452 69.5764 58.5101 69.0178 58.209C68.4591 57.908 67.8344 57.7503 67.1998 57.7502H59.6673C58.6507 57.7502 57.6756 58.1541 56.9567 58.8729C56.2379 59.5918 55.834 60.5669 55.834 61.5835V86.5002C55.834 87.5168 56.2379 88.4919 56.9567 89.2108C57.6756 89.9296 58.6507 90.3335 59.6673 90.3335H90.334Z'
fill='#617AF9'
/>
</Svg>
);
}
) as LucideIcon;
const fillColor = isHovered ? '#EDEEF2' : '#F8F9FC';

return (
<Svg ref={ref} viewBox='0 0 124 124' fill='none' {...svgProps}>
<Rect x={2} y={2} width={94} height={94} rx={8} fill='#C5CEFF' />
<Rect x={2} y={2} width={94} height={94} rx={8} stroke={fillColor} strokeWidth={4} />
<Rect x={28} y={28} width={94} height={94} rx={8} fill='#C5CEFF' />
<Rect x={28} y={28} width={94} height={94} rx={8} stroke={fillColor} strokeWidth={4} />
<Path
d='M90.334 90.3335C91.3506 90.3335 92.3257 89.9296 93.0446 89.2108C93.7634 88.4919 94.1673 87.5168 94.1673 86.5002V67.3335C94.1673 66.3169 93.7634 65.3418 93.0446 64.6229C92.3257 63.9041 91.3506 63.5002 90.334 63.5002H75.1923C74.5512 63.5065 73.9188 63.3519 73.3529 63.0505C72.787 62.7491 72.3057 62.3107 71.9531 61.7752L70.4007 59.4752C70.0516 58.9452 69.5764 58.5101 69.0178 58.209C68.4591 57.908 67.8344 57.7503 67.1998 57.7502H59.6673C58.6507 57.7502 57.6756 58.1541 56.9567 58.8729C56.2379 59.5918 55.834 60.5669 55.834 61.5835V86.5002C55.834 87.5168 56.2379 88.4919 56.9567 89.2108C57.6756 89.9296 58.6507 90.3335 59.6673 90.3335H90.334Z'
fill='#617AF9'
/>
</Svg>
);
});

export default ScrapFolderDefaultIcon;
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface ScrapGridProps {
}
export const ScrapGrid = ({ data, reducerState, dispatch }: ScrapGridProps) => {
const [containerWidth, setContainerWidth] = useState(0);
const { numColumns, gap, itemWidth, itemHeight } = useGridLayout(containerWidth);
const { numColumns, gap, itemWidth } = useGridLayout(containerWidth);
const finalData = addPlaceholders(data as ScrapItem[], numColumns);

return (
Expand Down Expand Up @@ -86,8 +86,9 @@ export const ScrapGrid = ({ data, reducerState, dispatch }: ScrapGridProps) => {

const spacingStyle = {
width: itemWidth,
height: itemHeight,
marginRight: isLastColumn ? 0 : gap,

maxHeight: 216,
};

// Check for placeholder first
Expand Down Expand Up @@ -155,7 +156,7 @@ interface SearchScrapGridProps {

export const SearchScrapGrid = ({ data, searchQuery }: SearchScrapGridProps) => {
const [containerWidth, setContainerWidth] = useState(0);
const { numColumns, gap, itemWidth, itemHeight } = useGridLayout(containerWidth);
const { numColumns, gap, itemWidth } = useGridLayout(containerWidth);
const finalData = addPlaceholders(data, numColumns);

return (
Expand Down Expand Up @@ -196,8 +197,9 @@ export const SearchScrapGrid = ({ data, searchQuery }: SearchScrapGridProps) =>

const spacingStyle = {
width: itemWidth,
height: itemHeight,
marginRight: isLastColumn ? 0 : gap,

maxHeight: 216,
};

if ('placeholder' in item && item.placeholder) {
Expand Down Expand Up @@ -255,7 +257,7 @@ interface TrashScrapGridProps {

export const TrashScrapGrid = ({ data, reducerState, dispatch }: TrashScrapGridProps) => {
const [containerWidth, setContainerWidth] = useState(0);
const { numColumns, gap, itemWidth, itemHeight } = useGridLayout(containerWidth);
const { numColumns, gap, itemWidth } = useGridLayout(containerWidth);
const finalData = addPlaceholders(data, numColumns);

return (
Expand Down Expand Up @@ -296,8 +298,9 @@ export const TrashScrapGrid = ({ data, reducerState, dispatch }: TrashScrapGridP

const spacingStyle = {
width: itemWidth,
height: itemHeight,
marginRight: isLastColumn ? 0 : gap,

maxHeight: 216,
};

// Check for placeholder first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { Pressable, View, Text, ImageBackground } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { StudentRootStackParamList } from '@/navigation/student/types';
import type { ScrapDetailResp } from '@/features/student/scrap/utils/types';
import type { ScrapListItemResp } from '@/features/student/scrap/utils/types';
import { useNoteStore } from '@/features/student/scrap/stores/scrapNoteStore';
import { useRecentScrapStore } from '@/features/student/scrap/stores/recentScrapStore';
import { formatToMinute } from '../../../utils/formatters/formatToMinute';

type RecentScrapCardProps = {
scrap: ScrapDetailResp & { type: 'SCRAP' };
scrap: ScrapListItemResp & { type: 'SCRAP' };
};

export const RecentScrapCard = ({ scrap }: RecentScrapCardProps) => {
Expand Down
Loading