(undefined);
+
+// Backward compatibility
+const ScrapModalContext = ScrapModalsContext;
+
+export const useScrapModals = () => {
+ const context = useContext(ScrapModalsContext);
+ if (!context) {
+ throw new Error('useScrapModals must be used within ScrapModalsProvider');
+ }
+ return context;
+};
+
+// Backward compatibility
+export const useScrapModal = useScrapModals;
+
+interface ScrapModalsProviderProps {
+ children: ReactNode;
+}
+
+export const ScrapModalsProvider = ({ children }: ScrapModalsProviderProps) => {
+ const [isCreateFolderModalVisible, setIsCreateFolderModalVisible] = useState(false);
+ const [isMoveScrapModalVisible, setIsMoveScrapModalVisible] = useState(false);
+ const [moveScrapModalProps, setMoveScrapModalProps] = useState<{
+ currentFolderId?: number;
+ selectedItems: SelectedItem[];
+ }>({
+ selectedItems: [],
+ });
+ const [refetchFolders, setRefetchFoldersState] = useState<(() => void) | undefined>(undefined);
+ const [refetchScraps, setRefetchScrapsState] = useState<(() => void) | undefined>(undefined);
+
+ const openCreateFolderModal = useCallback(() => {
+ setIsCreateFolderModalVisible(true);
+ }, []);
+
+ const closeCreateFolderModal = useCallback(() => {
+ setIsCreateFolderModalVisible(false);
+ }, []);
+
+ const openMoveScrapModal = useCallback(
+ (props: { currentFolderId?: number; selectedItems: SelectedItem[] }) => {
+ setMoveScrapModalProps(props);
+ setIsMoveScrapModalVisible(true);
+ },
+ []
+ );
+
+ const closeMoveScrapModal = useCallback(() => {
+ setIsMoveScrapModalVisible(false);
+ }, []);
+
+ const setRefetchFolders = useCallback((refetch: () => void) => {
+ setRefetchFoldersState(() => refetch);
+ }, []);
+
+ const setRefetchScraps = useCallback((refetch: () => void) => {
+ setRefetchScrapsState(() => refetch);
+ }, []);
+
+ const value: ScrapModalContextValue = {
+ isCreateFolderModalVisible,
+ openCreateFolderModal,
+ closeCreateFolderModal,
+ isMoveScrapModalVisible,
+ moveScrapModalProps,
+ openMoveScrapModal,
+ closeMoveScrapModal,
+ refetchFolders,
+ setRefetchFolders,
+ refetchScraps,
+ setRefetchScraps,
+ };
+
+ return {children};
+};
+
+// Backward compatibility
+export const ScrapModalProvider = ScrapModalsProvider;
diff --git a/apps/native/src/features/student/scrap/hoc/index.ts b/apps/native/src/features/student/scrap/hoc/index.ts
new file mode 100644
index 00000000..db79ab13
--- /dev/null
+++ b/apps/native/src/features/student/scrap/hoc/index.ts
@@ -0,0 +1 @@
+export { withScrapModals } from './withScrapModals';
diff --git a/apps/native/src/features/student/scrap/hoc/withScrapModals.tsx b/apps/native/src/features/student/scrap/hoc/withScrapModals.tsx
new file mode 100644
index 00000000..16a65766
--- /dev/null
+++ b/apps/native/src/features/student/scrap/hoc/withScrapModals.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { ScrapModalProvider } from '../contexts/ScrapModalsContext';
+import { MoveScrapModal } from '../components/Modal/MoveScrapModal';
+import { CreateFolderModal } from '../components/Modal/CreateFolderModal';
+
+/**
+ * 스크랩 모달들을 자동으로 추가하는 HOC (Higher-Order Component)
+ *
+ * ScrapModalProvider와 CreateFolderModal, MoveScrapModal을 자동으로 감싸줍니다.
+ *
+ * @param Component - 감쌀 컴포넌트
+ * @returns 모달이 추가된 컴포넌트
+ *
+ * @example
+ * const ScrapScreen = () => {
+ * return ;
+ * };
+ *
+ * export default withScrapModals(ScrapScreen);
+ */
+export const withScrapModals = (Component: React.ComponentType
) => {
+ const WrappedComponent = (props: P) => {
+ return (
+
+
+
+
+
+ );
+ };
+
+ WrappedComponent.displayName = `withScrapModals(${Component.displayName || Component.name || 'Component'})`;
+
+ return WrappedComponent;
+};
diff --git a/apps/native/src/features/student/scrap/hooks/index.ts b/apps/native/src/features/student/scrap/hooks/index.ts
new file mode 100644
index 00000000..8688f017
--- /dev/null
+++ b/apps/native/src/features/student/scrap/hooks/index.ts
@@ -0,0 +1,3 @@
+export { usePreSignedUrlAdapter } from './usePreSignedUrlAdapter';
+export { useCardImageSources } from './useCardImageSources';
+export { useScrapSelection } from './useScrapSelection';
diff --git a/apps/native/src/features/student/scrap/hooks/useCardImageSources.ts b/apps/native/src/features/student/scrap/hooks/useCardImageSources.ts
new file mode 100644
index 00000000..180198c7
--- /dev/null
+++ b/apps/native/src/features/student/scrap/hooks/useCardImageSources.ts
@@ -0,0 +1,48 @@
+import { useMemo } from 'react';
+import { ImageSourcePropType } from 'react-native';
+
+interface CardImageSourcesResult {
+ imageSources: ImageSourcePropType[] | undefined;
+ isDiagonalLayout: boolean;
+}
+
+/**
+ * 카드의 이미지 소스와 레이아웃을 계산하는 훅
+ *
+ * @param thumbnailUrl - 썸네일 URL (스크랩 또는 폴더)
+ * @param folderTop2Thumbnail - 폴더의 상위 2개 스크랩 썸네일 (폴더인 경우)
+ * @returns imageSources와 isDiagonalLayout
+ *
+ * @example
+ * // 폴더 카드
+ * const folderTop2 = props.type === 'FOLDER' ? props.top2ScrapThumbnail : undefined;
+ * const { imageSources, isDiagonalLayout } = useCardImageSources(props.thumbnailUrl, folderTop2);
+ *
+ * // 스크랩 카드
+ * const { imageSources, isDiagonalLayout } = useCardImageSources(props.thumbnailUrl);
+ */
+export const useCardImageSources = (
+ thumbnailUrl?: string,
+ folderTop2Thumbnail?: string[]
+): CardImageSourcesResult => {
+ return useMemo(() => {
+ if (thumbnailUrl) {
+ return {
+ imageSources: [{ uri: thumbnailUrl }],
+ isDiagonalLayout: false,
+ };
+ }
+
+ if (folderTop2Thumbnail && folderTop2Thumbnail.length > 0 && !thumbnailUrl) {
+ return {
+ imageSources: folderTop2Thumbnail.slice(0, 2).map((url) => ({ uri: url })),
+ isDiagonalLayout: true,
+ };
+ }
+
+ return {
+ imageSources: undefined,
+ isDiagonalLayout: false,
+ };
+ }, [thumbnailUrl, folderTop2Thumbnail]);
+};
diff --git a/apps/native/src/features/student/scrap/hooks/usePreSignedUrlAdapter.ts b/apps/native/src/features/student/scrap/hooks/usePreSignedUrlAdapter.ts
new file mode 100644
index 00000000..7a85355c
--- /dev/null
+++ b/apps/native/src/features/student/scrap/hooks/usePreSignedUrlAdapter.ts
@@ -0,0 +1,45 @@
+import { useGetPreSignedUrl } from '@/apis/controller/common/';
+
+/**
+ * PreSignedURL mutation을 래핑하여 uploadImageToS3가 기대하는 형식으로 변환하는 훅
+ *
+ * @returns getPreSignedUrl 함수를 반환
+ *
+ * @example
+ * const getPreSignedUrl = usePreSignedUrlAdapter();
+ *
+ * await uploadImageToS3(
+ * image,
+ * getPreSignedUrl,
+ * async (result) => { ... },
+ * (error) => { ... }
+ * );
+ */
+export const usePreSignedUrlAdapter = () => {
+ const { mutate: getPreSignedUrlMutate } = useGetPreSignedUrl();
+
+ const getPreSignedUrl = (
+ params: { fileName: string; fileType?: 'IMAGE' | 'DOCUMENT' | 'OTHER' },
+ callbacks: {
+ onSuccess: (data: {
+ uploadUrl: string;
+ contentDisposition: string;
+ file: { id: number };
+ }) => void;
+ onError: (error: any) => void;
+ }
+ ) => {
+ getPreSignedUrlMutate(params, {
+ onSuccess: (data) => {
+ callbacks.onSuccess({
+ uploadUrl: data.uploadUrl,
+ contentDisposition: data.contentDisposition,
+ file: { id: data.file.id },
+ });
+ },
+ onError: callbacks.onError,
+ });
+ };
+
+ return getPreSignedUrl;
+};
diff --git a/apps/native/src/features/student/scrap/hooks/useScrapSelection.ts b/apps/native/src/features/student/scrap/hooks/useScrapSelection.ts
new file mode 100644
index 00000000..b65245ca
--- /dev/null
+++ b/apps/native/src/features/student/scrap/hooks/useScrapSelection.ts
@@ -0,0 +1,23 @@
+import { useReducer } from 'react';
+import { reducer, initialSelectionState, State, Action } from '../utils/reducer';
+
+/**
+ * 스크랩 아이템 선택 상태를 관리하는 커스텀 훅
+ *
+ * @returns [reducerState, dispatch] - 선택 상태와 디스패치 함수
+ *
+ * @example
+ * const [reducerState, dispatch] = useScrapSelection();
+ *
+ * // 선택 모드 진입
+ * dispatch({ type: 'ENTER_SELECTION' });
+ *
+ * // 아이템 선택/해제
+ * dispatch({ type: 'SELECTING_ITEM', id: 1, itemType: 'SCRAP' });
+ *
+ * // 선택 모드 종료
+ * dispatch({ type: 'EXIT_SELECTION' });
+ */
+export const useScrapSelection = (): [State, React.Dispatch] => {
+ return useReducer(reducer, initialSelectionState);
+};
diff --git a/apps/native/src/features/student/scrap/index.ts b/apps/native/src/features/student/scrap/index.ts
index 6dcbdcde..0c67dca7 100644
--- a/apps/native/src/features/student/scrap/index.ts
+++ b/apps/native/src/features/student/scrap/index.ts
@@ -1,3 +1,6 @@
import ScrapScreen from './screens/ScrapScreen';
+import FolderScrapScreen from './screens/FolderScrapScreen';
+import DeletedScrapScreen from './screens/DeletedScrapScreen';
+import SearchScrapScreen from './screens/SearchScrapScreen';
-export { ScrapScreen };
+export { ScrapScreen, FolderScrapScreen, DeletedScrapScreen, SearchScrapScreen };
diff --git a/apps/native/src/features/student/scrap/screens/DeletedScrapScreen.tsx b/apps/native/src/features/student/scrap/screens/DeletedScrapScreen.tsx
new file mode 100644
index 00000000..09551fb8
--- /dev/null
+++ b/apps/native/src/features/student/scrap/screens/DeletedScrapScreen.tsx
@@ -0,0 +1,182 @@
+import React, { useMemo, useState } from 'react';
+import { Pressable, Text, View } from 'react-native';
+import DeletedScrapHeader from '../components/Header/DeletedScrapHeader';
+import { useNavigation } from '@react-navigation/native';
+import { StudentRootStackParamList } from '@/navigation/student/types';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { Container, LoadingScreen } from '@/components/common';
+import { TrashScrapGrid } from '../components/Card/ScrapCardGrid';
+import SortDropdown from '../components/Dropdown/SortDropdown';
+import { sortScrapData } from '../utils/formatters/sortScrap';
+import type { UISortKey, SortOrder } from '../utils/types';
+import { PopUpModal } from '../components/Dialog';
+import { showToast } from '../components/Notification/Toast';
+import { useGetTrash, useRestoreTrash, usePermanentDeleteTrash } from '@/apis';
+import { useScrapModal } from '../contexts/ScrapModalsContext';
+import { useScrapSelection } from '../hooks';
+import { validateOnlyScrapCanMove } from '../utils/validation';
+import { withScrapModals } from '../hoc';
+
+const DeletedScrapScreenContent = () => {
+ const [reducerState, dispatch] = useScrapSelection();
+ const [sortKey, setSortKey] = useState('TYPE');
+ const [sortOrder, setSortOrder] = useState('DESC');
+ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
+
+ const navigation = useNavigation>();
+ const { openMoveScrapModal } = useScrapModal();
+
+ // API 호출
+ const { data: trashData, isLoading } = useGetTrash();
+ const { mutateAsync: restoreTrash } = useRestoreTrash();
+ const { mutateAsync: permanentDelete } = usePermanentDeleteTrash();
+
+ const trashItems = trashData?.data || [];
+
+ const sortedData = useMemo(() => {
+ const itemsWithCreatedAt = trashItems.map((item) => ({
+ ...item,
+ createdAt: item.deletedAt,
+ }));
+
+ return sortScrapData(itemsWithCreatedAt, sortKey, sortOrder);
+ }, [trashItems, sortKey, sortOrder]);
+
+ const handlePermanentDelete = async () => {
+ try {
+ const items = reducerState.selectedItems;
+ await permanentDelete({ items });
+ dispatch({ type: 'CLEAR_SELECTION' });
+ setIsDeleteModalVisible(false);
+ showToast('success', '영구 삭제되었습니다.');
+ } catch (error) {
+ showToast('error', '삭제 중 오류가 발생했습니다.');
+ }
+ };
+
+ return (
+
+ {
+ const allItems = sortedData.map((item) => ({ id: item.id, type: item.type }));
+ const isAllSelected =
+ reducerState.selectedItems.length === sortedData.length && sortedData.length > 0;
+ dispatch({ type: 'SELECT_ALL', allItems: isAllSelected ? [] : allItems });
+ },
+ onEnterSelection: () => dispatch({ type: 'ENTER_SELECTION' }),
+ onExitSelection: () => dispatch({ type: 'EXIT_SELECTION' }),
+ onDelete: () => {
+ if (reducerState.selectedItems.length > 0) {
+ setIsDeleteModalVisible(true);
+ }
+ },
+ onMove: () => {
+ if (validateOnlyScrapCanMove(reducerState.selectedItems)) {
+ return;
+ }
+ if (reducerState.selectedItems.length === 0) {
+ showToast('error', '이동할 스크랩을 선택해주세요.');
+ return;
+ }
+ openMoveScrapModal({
+ selectedItems: reducerState.selectedItems,
+ });
+ dispatch({ type: 'CLEAR_SELECTION' });
+ },
+ onRestore: async () => {
+ try {
+ const items = reducerState.selectedItems;
+
+ await restoreTrash({ items });
+ dispatch({ type: 'CLEAR_SELECTION' });
+ showToast('success', '선택된 파일들이 복구되었습니다.');
+ } catch (error) {
+ showToast('error', '복구 중 오류가 발생했습니다.');
+ }
+ },
+ }}
+ />
+
+
+
+ 휴지통의 스크랩은 30일 이후에 영구적으로 삭제됩니다.
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ setIsDeleteModalVisible(false)}
+ selectedCount={reducerState.selectedItems.length}
+ onConfirm={handlePermanentDelete}
+ />
+
+ );
+};
+
+const DeletedScrapScreen = () => {
+ return ;
+};
+
+export default withScrapModals(DeletedScrapScreen);
+
+interface PermanentDeleteModalProps {
+ visible: boolean;
+ onClose: () => void;
+ selectedCount: number;
+ onConfirm: () => void;
+}
+
+const PermanentDeleteModal = ({
+ visible,
+ onClose,
+ selectedCount,
+ onConfirm,
+}: PermanentDeleteModalProps) => {
+ return (
+
+
+
+
+ {selectedCount === 1
+ ? '스크랩을 영구적으로 삭제합니다.'
+ : `${selectedCount}개의 스크랩을 영구적으로 삭제합니다.`}
+
+
+ {selectedCount === 1
+ ? '되돌릴 수 없는 작업입니다.'
+ : '선택하신 스크랩이 영구적으로 삭제되며\n돌릴 수 없는 작업입니다.'}
+
+
+
+
+ 취소
+
+
+ 삭제하기
+
+
+
+
+ );
+};
diff --git a/apps/native/src/features/student/scrap/screens/FolderScrapScreen.tsx b/apps/native/src/features/student/scrap/screens/FolderScrapScreen.tsx
new file mode 100644
index 00000000..cfbcb1b4
--- /dev/null
+++ b/apps/native/src/features/student/scrap/screens/FolderScrapScreen.tsx
@@ -0,0 +1,138 @@
+import { View } from 'react-native';
+import ScrapHeader from '../components/Header/ScrapHeader';
+import { useMemo, useState, useEffect } from 'react';
+import { sortScrapData, mapUIKeyToAPIKey } from '../utils/formatters/sortScrap';
+import type { UISortKey, SortOrder } from '../utils/types';
+import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { StudentRootStackParamList } from '@/navigation/student/types';
+import { Container, LoadingScreen } from '@/components/common';
+import SortDropdown from '../components/Dropdown/SortDropdown';
+import { ScrapGrid } from '../components/Card/ScrapCardGrid';
+import { showToast } from '../components/Notification/Toast';
+import { useGetScrapsByFolder, useDeleteScrap, useGetFolders } from '@/apis';
+import { useScrapModal } from '../contexts/ScrapModalsContext';
+import { useScrapSelection } from '../hooks';
+import { validateOnlyScrapCanMove } from '../utils/validation';
+import { withScrapModals } from '../hoc';
+
+type FolderScrapRouteProp = RouteProp;
+
+const FolderScrapScreenContent = () => {
+ const route = useRoute();
+ const { id } = route.params;
+
+ const [reducerState, dispatch] = useScrapSelection();
+ const [sortKey, setSortKey] = useState('TITLE');
+ const [sortOrder, setSortOrder] = useState('ASC');
+ const navigation = useNavigation>();
+ const { openMoveScrapModal, setRefetchScraps, setRefetchFolders } = useScrapModal();
+
+ // API 호출
+ const { data: foldersData, refetch: refetchFolders } = useGetFolders();
+ const { data: contentsData, isLoading, refetch } = useGetScrapsByFolder(id);
+ const { mutateAsync: deleteScrap } = useDeleteScrap();
+
+ // refetch를 context에 등록
+ useEffect(() => {
+ if (refetch) {
+ setRefetchScraps(() => refetch);
+ }
+ }, [refetch, setRefetchScraps]);
+ useEffect(() => {
+ if (refetchFolders) {
+ setRefetchFolders(refetchFolders);
+ }
+ }, [refetchFolders, setRefetchFolders]);
+
+ // 폴더 정보 가져오기
+ const folder = foldersData?.data?.find((f) => f.id === Number(id));
+ const contents = contentsData?.data || [];
+
+ // 정렬된 데이터
+ const sortedData = useMemo(
+ () => sortScrapData(contents, sortKey, sortOrder),
+ [contents, sortKey, sortOrder]
+ );
+
+ const isAllSelected =
+ reducerState.selectedItems.length === contents.length && contents.length > 0;
+
+ return (
+ <>
+
+ navigation.push('SearchScrap'),
+ onTrashPress: () => navigation.push('DeletedScrap'),
+ onEnterSelection: () => dispatch({ type: 'ENTER_SELECTION' }),
+ onExitSelection: () => dispatch({ type: 'EXIT_SELECTION' }),
+ onSelectAll: () => {
+ const allItems = contents.map((item) => ({ id: item.id, type: item.type }));
+ dispatch({ type: 'SELECT_ALL', allItems: isAllSelected ? [] : allItems });
+ },
+ onMove: () => {
+ if (validateOnlyScrapCanMove(reducerState.selectedItems)) {
+ return;
+ }
+ if (reducerState.selectedItems.length === 0) {
+ showToast('error', '이동할 스크랩을 선택해주세요.');
+ return;
+ }
+ openMoveScrapModal({
+ currentFolderId: Number(id),
+ selectedItems: reducerState.selectedItems,
+ });
+ dispatch({ type: 'CLEAR_SELECTION' });
+ },
+ onDelete: async () => {
+ if (reducerState.selectedItems.length === 0) {
+ showToast('error', '삭제할 항목을 선택해주세요.');
+ return;
+ }
+
+ try {
+ const items = reducerState.selectedItems;
+
+ await deleteScrap({ items });
+
+ dispatch({ type: 'CLEAR_SELECTION' });
+ showToast('success', '휴지통으로 이동해 한 달 후 영구 삭제됩니다.');
+ } catch (error: any) {
+ showToast('error', '삭제 중 오류가 발생했습니다.');
+ }
+ },
+ }}
+ />
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
+ );
+};
+
+const FolderScrapScreen = () => {
+ return ;
+};
+
+export default withScrapModals(FolderScrapScreen);
diff --git a/apps/native/src/features/student/scrap/screens/ScrapDetailScreen.tsx b/apps/native/src/features/student/scrap/screens/ScrapDetailScreen.tsx
new file mode 100644
index 00000000..2763bdb9
--- /dev/null
+++ b/apps/native/src/features/student/scrap/screens/ScrapDetailScreen.tsx
@@ -0,0 +1,877 @@
+import React, { useEffect, useState, useMemo, useCallback, useRef } from 'react';
+import {
+ View,
+ Text,
+ Image,
+ ScrollView,
+ Pressable,
+ LayoutChangeEvent,
+ Modal,
+ Dimensions,
+ ActivityIndicator,
+ Alert,
+} from 'react-native';
+import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { ChevronLeft, X, ChevronDown, ChevronUp, Maximize2, Save } from 'lucide-react-native';
+import { Gesture, GestureDetector } from 'react-native-gesture-handler';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withSpring,
+ SharedValue,
+ runOnJS,
+} from 'react-native-reanimated';
+import { Container, SegmentedControl, TextButton } from '@/components/common';
+import { StudentRootStackParamList } from '@/navigation/student/types';
+import { useGetHandwriting, useGetScrapDetail, useUpdateHandwriting } from '@/apis';
+import { LoadingScreen } from '@/components/common';
+import ProblemViewer from '../../problem/components/ProblemViewer';
+import { useNoteStore, Note } from '@/stores/scrapNoteStore';
+import { toAlphabetSequence } from '../utils/formatters/toAlphabetSequence';
+import { components } from '@/types/api/schema';
+import DrawingCanvas, { DrawingCanvasRef, Stroke, TextItem } from '../utils/skia/drawing';
+import { colors } from '@/theme/tokens';
+
+type ScrapDetailRouteProp = RouteProp;
+
+const ScrapDetailScreen = () => {
+ const route = useRoute();
+ const navigation = useNavigation>();
+ const { id } = route.params;
+ const scrapId = Number(id);
+
+ const { data: scrapDetail, isLoading } = useGetScrapDetail(scrapId, !!id);
+ const { data: handwritingData } = useGetHandwriting(scrapId, !!id);
+ const { mutate: updateHandwriting, isPending: isSaving } = useUpdateHandwriting();
+
+ const canvasRef = useRef(null);
+ const [isEraserMode, setIsEraserMode] = useState(false);
+ const [isTextMode, setIsTextMode] = useState(false);
+ const [strokeWidth, setStrokeWidth] = useState(1.5);
+ const [eraserSize, setEraserSize] = useState(6);
+ const [canUndo, setCanUndo] = useState(false);
+ const [canRedo, setCanRedo] = useState(false);
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
+ const [showSave, setShowSave] = useState(false);
+ const lastSavedDataRef = useRef('');
+
+ const { openNotes, activeNoteId, setActiveNote, closeNote, reorderNotes } = useNoteStore();
+ const [tabLayouts, setTabLayouts] = useState>({});
+ const scrollViewRef = useRef(null);
+ const scrollX = useSharedValue(0);
+ const screenWidth = Dimensions.get('window').width;
+ const [expandedSections, setExpandedSections] = useState>(
+ {}
+ );
+ const [selectedFilter, setSelectedFilter] = useState(0); // 0: 전체, 1: 문제, 2+: 포인팅 인덱스
+ const [isProblemExpanded, setIsProblemExpanded] = useState(false);
+ const [isHoveringProblem, setIsHoveringProblem] = useState(false);
+
+ useEffect(() => {
+ if (activeNoteId && activeNoteId !== scrapId) {
+ navigation.setParams({ id: String(activeNoteId) });
+ }
+ }, [activeNoteId, scrapId, navigation]);
+
+ // undo/redo 상태 변경 핸들러
+ const handleHistoryChange = useCallback((canUndoValue: boolean, canRedoValue: boolean) => {
+ setCanUndo(canUndoValue);
+ setCanRedo(canRedoValue);
+ // 변경사항이 있음을 표시
+ setHasUnsavedChanges(true);
+ }, []);
+
+ // handwriting 데이터를 로드하여 canvas에 설정
+ useEffect(() => {
+ if (handwritingData?.data && canvasRef.current) {
+ try {
+ // Base64 디코딩 (React Native에서는 atob 사용)
+ const decodedData = decodeURIComponent(escape(atob(handwritingData.data)));
+ const data = JSON.parse(decodedData);
+
+ // 이전 형식 호환성: strokes만 있는 경우와 { strokes, texts } 형식 모두 지원
+ if (Array.isArray(data)) {
+ // 이전 형식: strokes 배열만
+ canvasRef.current.setStrokes(data);
+ canvasRef.current.setTexts([]);
+ } else {
+ // 새 형식: { strokes, texts } 객체
+ canvasRef.current.setStrokes(data.strokes || []);
+ canvasRef.current.setTexts(data.texts || []);
+ }
+
+ // 초기 데이터를 저장된 데이터로 설정
+ lastSavedDataRef.current = handwritingData.data;
+ setHasUnsavedChanges(false);
+ } catch (error) {
+ console.error('필기 데이터 로드 실패:', error);
+ }
+ }
+ }, [handwritingData]);
+
+ // 저장하기 버튼 핸들러
+ const handleSave = useCallback(
+ (isAutoSave = false) => {
+ const strokes = canvasRef.current?.getStrokes();
+ const texts = canvasRef.current?.getTexts();
+
+ if ((!strokes || strokes.length === 0) && (!texts || texts.length === 0)) {
+ if (!isAutoSave) {
+ Alert.alert('알림', '저장할 필기 내용이 없습니다.');
+ }
+ return;
+ }
+
+ try {
+ // strokes와 texts를 함께 저장
+ const data = {
+ strokes: strokes || [],
+ texts: texts || [],
+ };
+ const jsonString = JSON.stringify(data);
+ const base64Data = btoa(unescape(encodeURIComponent(jsonString)));
+
+ // 변경사항이 없으면 저장하지 않음
+ if (base64Data === lastSavedDataRef.current) {
+ if (!isAutoSave) {
+ Alert.alert('알림', '변경사항이 없습니다.');
+ }
+ return;
+ }
+
+ updateHandwriting(
+ {
+ scrapId,
+ request: {
+ data: base64Data,
+ },
+ },
+ {
+ onSuccess: () => {
+ lastSavedDataRef.current = base64Data;
+ setHasUnsavedChanges(false);
+ if (!isAutoSave) {
+ Alert.alert('성공', '필기가 저장되었습니다.');
+ setShowSave(true);
+ setTimeout(() => setShowSave(false), 2000);
+ }
+ },
+ onError: (error) => {
+ console.error('필기 저장 실패:', error);
+ if (!isAutoSave) {
+ Alert.alert('오류', '필기 저장에 실패했습니다.');
+ }
+ },
+ }
+ );
+ } catch (error) {
+ console.error('필기 데이터 변환 실패:', error);
+ if (!isAutoSave) {
+ Alert.alert('오류', '필기 데이터 변환에 실패했습니다.');
+ }
+ }
+ },
+ [scrapId, updateHandwriting]
+ );
+
+ // 10초마다 자동 저장
+ useEffect(() => {
+ const autoSaveInterval = setInterval(() => {
+ if (hasUnsavedChanges && !isSaving) {
+ handleSave(true);
+ }
+ }, 10000); // 10초마다 실행
+
+ return () => clearInterval(autoSaveInterval);
+ }, [hasUnsavedChanges, isSaving, handleSave]);
+
+ // 포인팅 데이터에 알파벳 레이블 추가 (메모이제이션)
+ const pointingsWithLabels = useMemo(() => {
+ if (!scrapDetail?.pointings) return [];
+ return scrapDetail.pointings.map((pointing, idx) => ({
+ ...pointing,
+ label: toAlphabetSequence(idx),
+ }));
+ }, [scrapDetail?.pointings]);
+
+ // 필터 옵션 생성 (scrapDetail이 없어도 Hook은 항상 호출되어야 함)
+ const filterOptions = useMemo(() => {
+ if (!scrapDetail) return ['전체', '문제'];
+ const options = ['전체', '문제'];
+ if (pointingsWithLabels.length > 0) {
+ pointingsWithLabels.forEach((pointing) => {
+ options.push(`포인팅 ${pointing.label}`);
+ });
+ }
+ return options;
+ }, [scrapDetail, pointingsWithLabels]);
+
+ // 필터에 따른 표시 여부 결정
+ const shouldShowProblem = selectedFilter === 0 || selectedFilter === 1;
+ const shouldShowPointing = (pointingIndex: number) => {
+ if (selectedFilter === 0) return true; // 전체
+ if (selectedFilter === 1) return false; // 문제만
+ return selectedFilter === pointingIndex + 2; // 특정 포인팅만
+ };
+
+ // 표시할 포인팅이 있는지 확인
+ const hasVisiblePointings = useMemo(() => {
+ if (!scrapDetail?.pointings || scrapDetail.pointings.length === 0) return false;
+ if (selectedFilter === 1) return false; // 문제만 선택 시 포인팅 숨김
+ if (selectedFilter === 0) return true; // 전체 선택 시 포인팅 표시
+ // 특정 포인팅 선택 시 해당 포인팅이 존재하는지 확인
+ const pointingIndex = selectedFilter - 2;
+ return pointingIndex >= 0 && pointingIndex < scrapDetail.pointings.length;
+ }, [scrapDetail?.pointings, selectedFilter]);
+
+ // 스크랩 데이터를 AllPointings에 전달할 형식으로 변환
+ const convertScrapToGroup = useCallback(():
+ | components['schemas']['PublishProblemGroupResp']
+ | null => {
+ if (!scrapDetail?.problem) return null;
+
+ // PointingResp를 PointingWithFeedbackResp로 변환
+ const pointingsWithFeedback: components['schemas']['PointingWithFeedbackResp'][] =
+ scrapDetail.pointings?.map((pointing) => ({
+ id: pointing.id,
+ no: pointing.no,
+ questionContent: pointing.questionContent,
+ commentContent: pointing.commentContent,
+ concepts: pointing.concepts,
+ isUnderstood: undefined, // 스크랩에서는 피드백 정보가 없음
+ })) || [];
+
+ // ProblemExtendResp를 ProblemWithStudyInfoResp로 변환
+ const problemWithStudyInfo: components['schemas']['ProblemWithStudyInfoResp'] = {
+ id: scrapDetail.problem.id,
+ problemType: scrapDetail.problem.problemType,
+ parentProblem: scrapDetail.problem.parentProblem,
+ parentProblemTitle: scrapDetail.problem.parentProblemTitle,
+ customId: scrapDetail.problem.customId,
+ createType: scrapDetail.problem.createType,
+ practiceTest: scrapDetail.problem.practiceTest,
+ practiceTestNo: scrapDetail.problem.practiceTestNo,
+ problemContent: scrapDetail.problem.problemContent,
+ title: scrapDetail.problem.title,
+ answerType: scrapDetail.problem.answerType,
+ answer: scrapDetail.problem.answer,
+ difficulty: scrapDetail.problem.difficulty,
+ recommendedTimeSec: scrapDetail.problem.recommendedTimeSec,
+ memo: scrapDetail.problem.memo,
+ concepts: scrapDetail.problem.concepts,
+ mainAnalysisImage: scrapDetail.problem.mainAnalysisImage,
+ mainHandAnalysisImage: scrapDetail.problem.mainHandAnalysisImage,
+ readingTipContent: scrapDetail.problem.readingTipContent,
+ oneStepMoreContent: scrapDetail.problem.oneStepMoreContent,
+ pointings: pointingsWithFeedback,
+ progress: 'NONE', // 스크랩에서는 진행 상태가 없음
+ submitAnswer: 0, // 스크랩에서는 제출 답안이 없음
+ isCorrect: false, // 스크랩에서는 정답 여부가 없음
+ isDone: false, // 스크랩에서는 완료 여부가 없음
+ childProblems: [], // 스크랩에는 childProblems가 없음
+ };
+
+ return {
+ no: 1, // 스크랩에서는 번호가 없으므로 1로 설정
+ problemId: scrapDetail.problem.id,
+ progress: 'DONE', // 스크랩된 문제는 완료된 것으로 간주
+ problem: problemWithStudyInfo,
+ childProblems: [],
+ };
+ }, [scrapDetail]);
+
+ // 전체보기 버튼 클릭 핸들러
+ const handleViewAllPointings = useCallback(() => {
+ const group = convertScrapToGroup();
+ if (!group) return;
+
+ navigation.navigate('AllPointings', {
+ group,
+ problemSetTitle: scrapDetail?.name || '스크랩',
+ });
+ }, [convertScrapToGroup, navigation, scrapDetail?.name]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!scrapDetail) {
+ return (
+
+ 스크랩을 찾을 수 없습니다.
+
+ );
+ }
+
+ const scrap = scrapDetail;
+
+ return (
+
+
+
+ {navigation.canGoBack() && (
+ navigation.goBack()}
+ className='p-2 md:right-[48px] lg:right-[96px]'>
+
+
+
+
+ )}
+ {showSave && }
+ {scrap.name || '스크랩 상세'}
+ {handwritingData?.updatedAt}
+
+ {openNotes.length > 1 && (
+
+ {
+ const offsetX = event.nativeEvent.contentOffset.x;
+ scrollX.value = offsetX; // 실시간 업데이트
+ }}
+ scrollEventThrottle={1}>
+ {openNotes.map((note, index) => (
+ setActiveNote(note.id)}
+ onClose={() => closeNote(note.id)}
+ onLayout={(event: LayoutChangeEvent) => {
+ const { x, width } = event.nativeEvent.layout;
+ setTabLayouts((prev) => ({ ...prev, [note.id]: { x, width } }));
+ }}
+ onDragEnd={(fromIndex, toIndex) => {
+ reorderNotes(fromIndex, toIndex);
+ }}
+ tabLayouts={tabLayouts}
+ scrollViewRef={scrollViewRef as React.RefObject}
+ scrollX={scrollX}
+ screenWidth={screenWidth}
+ />
+ ))}
+
+
+ )}
+
+
+
+
+ {/* 필터 버튼 및 전체보기 */}
+ {filterOptions.length > 0 && (
+
+
+ {scrap.pointings && scrap.pointings.length > 0 && scrap.problem && (
+
+
+ 전체보기
+
+
+ )}
+
+ )}
+ {shouldShowProblem &&
+ !(scrap.problem && scrap.problem.problemContent) &&
+ scrap.thumbnailUrl && (
+
+ 문제 내용
+ {
+ setIsHoveringProblem(true);
+
+ setTimeout(() => {
+ setIsHoveringProblem(false);
+ }, 2000);
+ }}>
+
+ {isHoveringProblem && (
+ setIsProblemExpanded(true)}
+ className='absolute right-2 top-2 z-10 rounded-full bg-black/50 p-2'>
+
+
+ )}
+
+
+ )}
+ {/* 문제 내용 */}
+ {shouldShowProblem && scrap.problem && scrap.problem.problemContent && (
+
+ 문제 내용
+ {
+ setIsHoveringProblem(true);
+
+ setTimeout(() => {
+ setIsHoveringProblem(false);
+ }, 2000);
+ }}>
+
+ {isHoveringProblem && (
+ setIsProblemExpanded(true)}
+ className='absolute right-2 top-2 z-10 rounded-full bg-black/50 p-2'>
+
+
+ )}
+
+
+ )}
+ {/* 포인팅 */}
+ {hasVisiblePointings && (
+
+ 포인팅
+
+ {pointingsWithLabels.map((pointing, idx) => {
+ if (!shouldShowPointing(idx)) return null;
+ const sectionKey = `pointing-${pointing.id}`;
+ const isCommentExpanded = expandedSections[sectionKey]?.comment ?? false;
+
+ return (
+
+
+ 포인팅 {pointing.label}
+ 포인팅 질문
+
+ {pointing.questionContent && (
+
+
+
+ )}
+ {pointing.concepts && pointing.concepts.length > 0 && (
+
+ {pointing.concepts.map((concept) => (
+
+ {concept.name}
+
+ ))}
+
+ )}
+ {pointing.commentContent && (
+
+ {
+ setExpandedSections((prev) => ({
+ ...prev,
+ [sectionKey]: {
+ ...prev[sectionKey],
+ comment: !isCommentExpanded,
+ },
+ }));
+ }}
+ className='flex-row items-end'>
+ {isCommentExpanded ? (
+
+ ) : (
+
+ )}
+
+ {isCommentExpanded && (
+
+ )}
+
+ )}
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+
+
+
+ {/* 하단 제어 버튼 */}
+
+
+ {
+ setIsTextMode(false);
+ setIsEraserMode(false);
+ }}
+ className={`flex-1 items-center justify-center rounded-lg py-3 ${
+ !isTextMode && !isEraserMode ? 'bg-blue-500' : 'bg-gray-200'
+ }`}>
+
+ 필기
+
+
+ {
+ setIsTextMode(true);
+ setIsEraserMode(false);
+ }}
+ className={`flex-1 items-center justify-center rounded-lg py-3 ${
+ isTextMode && !isEraserMode ? 'bg-blue-500' : 'bg-gray-200'
+ }`}>
+
+ 텍스트
+
+
+ {
+ setIsEraserMode((prev) => !prev);
+ if (!isEraserMode) {
+ setIsTextMode(false);
+ }
+ }}
+ className={`flex-1 items-center justify-center rounded-lg py-3 ${
+ isEraserMode ? 'bg-red-500' : 'bg-gray-200'
+ }`}>
+ 지우개
+
+
+
+ {/* 그리기 크기 선택 (필기 모드일 때만 표시) */}
+ {!isTextMode && !isEraserMode && (
+
+ 그리기 크기
+
+ {[1.5, 2, 4].map((size) => (
+ setStrokeWidth(size)}
+ className={`flex-1 items-center justify-center rounded-lg py-2 ${
+ strokeWidth === size ? 'bg-blue-500' : 'bg-gray-200'
+ }`}>
+
+ {size}
+
+
+ ))}
+
+
+ )}
+
+ {/* 지우개 크기 선택 (지우개 모드일 때만 표시) */}
+ {isEraserMode && (
+
+ 지우개 크기
+
+ {[6, 12, 20].map((size) => (
+ setEraserSize(size)}
+ className={`flex-1 items-center justify-center rounded-lg py-2 ${
+ eraserSize === size ? 'bg-red-500' : 'bg-gray-200'
+ }`}>
+
+ {size}
+
+
+ ))}
+
+
+ )}
+
+
+ canvasRef.current?.undo()}
+ disabled={!canUndo}
+ className={`flex-1 items-center justify-center rounded-lg py-3 ${
+ canUndo ? 'bg-gray-200' : 'bg-gray-100'
+ }`}>
+ undo
+
+ canvasRef.current?.redo()}
+ disabled={!canRedo}
+ className={`flex-1 items-center justify-center rounded-lg py-3 ${
+ canRedo ? 'bg-gray-200' : 'bg-gray-100'
+ }`}>
+ redo
+
+ handleSave(false)}
+ disabled={isSaving}
+ className={`flex-1 items-center justify-center rounded-lg py-3 ${
+ isSaving ? 'bg-gray-400' : 'bg-blue-600'
+ }`}>
+ {isSaving ? '저장 중...' : '저장하기'}
+
+
+
+
+
+
+ {/* 문제 확대 모달 */}
+
+ setIsProblemExpanded(false)}>
+ setIsProblemExpanded(false)}>
+ e.stopPropagation()}>
+
+ 문제 내용
+ {
+ setIsProblemExpanded(false);
+ setIsHoveringProblem(false);
+ }}
+ className='rounded-full bg-gray-200 p-2'>
+
+
+
+
+ {scrap.problem && scrap.problem.problemContent && (
+
+ )}
+ {!(scrap.problem && scrap.problem.problemContent) && scrap.thumbnailUrl && (
+
+ )}
+
+
+
+
+
+ );
+};
+
+// DraggableTabProps 인터페이스 수정
+interface DraggableTabProps {
+ note: Note;
+ index: number;
+ isActive: boolean;
+ onPress: () => void;
+ onClose: () => void;
+ onLayout: (event: LayoutChangeEvent) => void;
+ onDragEnd: (fromIndex: number, toIndex: number) => void;
+ tabLayouts: Record;
+ scrollViewRef: React.RefObject;
+ scrollX: SharedValue;
+ screenWidth: number;
+}
+
+// DraggableTab 컴포넌트 수정
+const DraggableTab = ({
+ note,
+ index,
+ isActive,
+ onPress,
+ onClose,
+ onLayout,
+ onDragEnd,
+ tabLayouts,
+ scrollViewRef,
+ scrollX,
+ screenWidth,
+}: DraggableTabProps) => {
+ const translateX = useSharedValue(0);
+ const startX = useSharedValue(0);
+ const [isDragging, setIsDragging] = useState(false);
+ const { openNotes } = useNoteStore();
+
+ const autoScroll = useCallback(
+ (currentX: number) => {
+ if (!scrollViewRef.current) return;
+
+ const currentLayout = tabLayouts[note.id];
+ if (!currentLayout) return;
+
+ // 탭의 실제 화면상 위치 (스크롤 오프셋 고려)
+ const absoluteX = currentLayout.x - scrollX.value + currentX;
+ const tabRight = absoluteX + currentLayout.width;
+ const tabLeft = absoluteX;
+
+ const visibleLeft = 0;
+ const visibleRight = screenWidth;
+ const scrollThreshold = 100;
+ const scrollSpeed = 0.4;
+
+ let newScrollX = scrollX.value;
+ let shouldScroll = false;
+
+ // 오른쪽으로 스크롤
+ if (tabRight > visibleRight - scrollThreshold) {
+ const distance = tabRight - (visibleRight - scrollThreshold);
+ newScrollX = scrollX.value + distance * scrollSpeed;
+ shouldScroll = true;
+ }
+ // 왼쪽으로 스크롤
+ else if (tabLeft < visibleLeft + scrollThreshold) {
+ const distance = visibleLeft + scrollThreshold - tabLeft;
+ newScrollX = scrollX.value - distance * scrollSpeed;
+ shouldScroll = true;
+ }
+
+ if (shouldScroll && Math.abs(newScrollX - scrollX.value) > 0.5) {
+ scrollViewRef.current.scrollTo({
+ x: Math.max(0, newScrollX),
+ animated: false,
+ });
+ }
+ },
+ [tabLayouts, note.id, scrollViewRef, scrollX, screenWidth]
+ );
+
+ const panGesture = Gesture.Pan()
+ .enabled(isActive) // 활성 탭일 때만 드래그 가능
+ .onStart(() => {
+ // 활성 탭이 아니면 드래그 시작하지 않음
+ if (!isActive) return;
+
+ startX.value = translateX.value;
+ runOnJS(setIsDragging)(true);
+ })
+ .onUpdate((e) => {
+ // 활성 탭이 아니면 업데이트하지 않음
+ if (!isActive) return;
+
+ translateX.value = startX.value + e.translationX;
+
+ // 드래그 중 자동 스크롤
+ const currentX = startX.value + e.translationX;
+ runOnJS(autoScroll)(currentX);
+ })
+ .onEnd((e) => {
+ // 활성 탭이 아니면 종료하지 않음
+ if (!isActive) {
+ translateX.value = withSpring(0);
+ runOnJS(setIsDragging)(false);
+ return;
+ }
+
+ const finalX = startX.value + e.translationX;
+ const currentLayout = tabLayouts[note.id];
+
+ if (!currentLayout) {
+ translateX.value = withSpring(0);
+ runOnJS(setIsDragging)(false);
+ return;
+ }
+
+ // 드래그된 위치를 기반으로 새로운 인덱스 계산
+ const newPosition = currentLayout.x + finalX;
+ let newIndex = index;
+
+ // 다른 탭들의 위치와 비교하여 새로운 인덱스 결정
+ openNotes.forEach((otherNote, otherIndex) => {
+ const otherLayout = tabLayouts[otherNote.id];
+ if (otherLayout && otherIndex !== index) {
+ const otherCenter = otherLayout.x + otherLayout.width / 2;
+ const currentCenter = currentLayout.x + currentLayout.width / 2 + finalX;
+
+ if (otherIndex < index && currentCenter < otherCenter) {
+ newIndex = Math.min(newIndex, otherIndex);
+ } else if (otherIndex > index && currentCenter > otherCenter) {
+ newIndex = Math.max(newIndex, otherIndex + 1);
+ }
+ }
+ });
+
+ newIndex = Math.max(0, Math.min(newIndex, openNotes.length - 1));
+
+ if (newIndex !== index) {
+ runOnJS(onDragEnd)(index, newIndex);
+ }
+
+ translateX.value = withSpring(0);
+ runOnJS(setIsDragging)(false);
+ });
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ translateX: translateX.value }],
+ zIndex: isDragging ? 10 : 1,
+ opacity: isDragging ? 0.8 : 1,
+ }));
+
+ return (
+
+
+
+
+ {note.title}
+
+
+
+ {
+ e.stopPropagation();
+ onClose();
+ }}
+ className='items-center justify-center rounded-full bg-gray-300 p-0.5'>
+
+
+
+
+ );
+};
+
+export default ScrapDetailScreen;
diff --git a/apps/native/src/features/student/scrap/screens/ScrapScreen.tsx b/apps/native/src/features/student/scrap/screens/ScrapScreen.tsx
index 098adabc..b3a19b8d 100644
--- a/apps/native/src/features/student/scrap/screens/ScrapScreen.tsx
+++ b/apps/native/src/features/student/scrap/screens/ScrapScreen.tsx
@@ -1,12 +1,188 @@
-import React from 'react';
-import { Text, View } from 'react-native';
+import { Container, LoadingScreen } from '@/components/common';
+import { StudentRootStackParamList } from '@/navigation/student/types';
+import { useNavigation } from '@react-navigation/native';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import React, { useMemo, useState } from 'react';
+import { View, Text, ScrollView, Pressable, ImageBackground } from 'react-native';
+import ScrapHeader from '../components/Header/ScrapHeader';
+import { ScrapGrid } from '../components/Card/ScrapCardGrid';
+import SortDropdown from '../components/Dropdown/SortDropdown';
+import { useRecentScrapStore } from '@/stores/recentScrapStore';
+import { sortScrapData, mapUIKeyToAPIKey } from '../utils/formatters/sortScrap';
+import type { UISortKey, SortOrder, ScrapSearchResponse } from '../utils/types';
+import { showToast } from '../components/Notification/Toast';
+import { useSearchScraps, useDeleteScrap } from '@/apis';
+import { validateOnlyScrapCanMove } from '../utils/validation';
+import { useQueries } from '@tanstack/react-query';
+import { TanstackQueryClient } from '@/apis';
+import { RecentScrapCard } from '../components/Card/cards/RecentScrapCard';
+import { useScrapModal } from '../contexts/ScrapModalsContext';
+import { useScrapSelection } from '../hooks';
+import { withScrapModals } from '../hoc';
+
+const ScrapScreenContent = () => {
+ const [reducerState, dispatch] = useScrapSelection();
+ const [sortKey, setSortKey] = useState('DATE');
+ const [sortOrder, setSortOrder] = useState('DESC');
+ const navigation = useNavigation>();
+ const recentScraps = useRecentScrapStore((state) => state.scrapIds);
+ const { openMoveScrapModal, setRefetchScraps } = useScrapModal();
+
+ const {
+ data: searchData,
+ isLoading,
+ refetch,
+ } = useSearchScraps({
+ sort: mapUIKeyToAPIKey(sortKey),
+ order: sortOrder,
+ });
+ const { mutateAsync: deleteScrap } = useDeleteScrap();
+
+ // refetch를 context에 등록
+ React.useEffect(() => {
+ if (refetch) {
+ setRefetchScraps(() => refetch);
+ }
+ }, [refetch, setRefetchScraps]);
+
+ const recentScrapsQueries = useQueries({
+ queries:
+ recentScraps.length > 0
+ ? recentScraps.map((scrapId) => ({
+ ...TanstackQueryClient.queryOptions('get', '/api/student/scrap/{id}', {
+ params: {
+ path: { id: scrapId },
+ },
+ }),
+ enabled: scrapId > 0 && recentScraps.length > 0,
+ }))
+ : [],
+ });
+
+ const recentScrapsData = useMemo(() => {
+ if (recentScraps.length === 0) return [];
+
+ return recentScrapsQueries
+ .map((query) => {
+ const scrapDetail = query.data;
+ if (!scrapDetail) return null;
+
+ return {
+ ...scrapDetail,
+ type: 'SCRAP' as const,
+ };
+ })
+ .filter((scrap): scrap is NonNullable => scrap !== null);
+ }, [recentScrapsQueries, recentScraps.length]);
+
+ // ScrapSearchResponse는 folders와 scraps를 각각 반환하므로 합쳐야 함
+ const data = useMemo(() => {
+ if (!searchData) return [];
+ const typedSearchData = searchData as ScrapSearchResponse;
+ const folders = (typedSearchData.folders || []).map((folder) => ({
+ ...folder,
+ type: 'FOLDER' as const,
+ }));
+ const scraps = (typedSearchData.scraps || []).filter((scrap) => scrap.folderId == null);
+ return [...folders, ...scraps];
+ }, [searchData]);
+
+ // 클라이언트 사이드 정렬 (TYPE 정렬 등 추가 정렬 로직 적용)
+ const sortedData = useMemo(
+ () => sortScrapData(data, sortKey, sortOrder),
+ [data, sortKey, sortOrder]
+ );
+
+ const isAllSelected = data.length > 0 && reducerState.selectedItems.length === data.length;
-const ScrapScreen = () => {
return (
-
- Scrap
-
+ <>
+
+ navigation.push('SearchScrap'),
+ onTrashPress: () => navigation.push('DeletedScrap'),
+ onEnterSelection: () => dispatch({ type: 'ENTER_SELECTION' }),
+ onExitSelection: () => dispatch({ type: 'EXIT_SELECTION' }),
+ onSelectAll: () => {
+ const allItems = data.map((item) => ({ id: item.id, type: item.type }));
+ dispatch({ type: 'SELECT_ALL', allItems: isAllSelected ? [] : allItems });
+ },
+ onMove: () => {
+ if (validateOnlyScrapCanMove(reducerState.selectedItems)) {
+ return;
+ }
+ if (reducerState.selectedItems.length === 0) {
+ showToast('error', '이동할 스크랩을 선택해주세요.');
+ return;
+ }
+ openMoveScrapModal({
+ selectedItems: reducerState.selectedItems,
+ });
+ dispatch({ type: 'CLEAR_SELECTION' });
+ },
+ onDelete: async () => {
+ if (reducerState.selectedItems.length === 0) {
+ showToast('error', '삭제할 항목을 선택해주세요.');
+ return;
+ }
+
+ const items = reducerState.selectedItems;
+
+ dispatch({ type: 'CLEAR_SELECTION' });
+
+ try {
+ await deleteScrap({ items });
+ showToast('success', '휴지통으로 이동해 한 달 후 영구 삭제됩니다.');
+ } catch (error: any) {
+ // 에러 발생 시 롤백은 mutation의 onError에서 처리됨
+ showToast('error', '삭제 중 오류가 발생했습니다.');
+ }
+ },
+ }}
+ />
+
+ {recentScrapsData.length > 0 && (
+
+ 최근 본
+
+ {recentScrapsData.map((scrap) => (
+
+ ))}
+
+
+ )}
+
+ 전체 스크랩
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
);
};
-export default ScrapScreen;
+const ScrapScreen = () => {
+ return ;
+};
+
+export default withScrapModals(ScrapScreen);
diff --git a/apps/native/src/features/student/scrap/screens/SearchScrapScreen.tsx b/apps/native/src/features/student/scrap/screens/SearchScrapScreen.tsx
new file mode 100644
index 00000000..8f6cdac3
--- /dev/null
+++ b/apps/native/src/features/student/scrap/screens/SearchScrapScreen.tsx
@@ -0,0 +1,117 @@
+import Container from '@/components/common/Container';
+import { StudentRootStackParamList } from '@/navigation/student/types';
+import { colors } from '@/theme/tokens';
+import { useNavigation } from '@react-navigation/native';
+import { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import { ChevronLeft, X } from 'lucide-react-native';
+import React, { useEffect, useState } from 'react';
+import { Pressable, ScrollView, Text, View } from 'react-native';
+import { SearchScrapGrid } from '../components/Card/ScrapCardGrid';
+import { useSearchHistoryStore } from '@/stores/searchHistoryStore';
+import SearchScrapHeader from '../components/Header/SearchScrapHeader';
+import { useSearchScraps } from '@/apis';
+
+const SearchScrapScreen = () => {
+ const navigation = useNavigation>();
+ const [query, setQuery] = useState('');
+ const [debouncedQuery, setDebouncedQuery] = useState('');
+ const { keywords, addKeyword, removeKeyword, clear } = useSearchHistoryStore();
+
+ // 디바운스: 입력이 멈춘 후 300ms 후에 검색어 업데이트
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedQuery(query.trim());
+ }, 300);
+
+ return () => clearTimeout(timer);
+ }, [query]);
+
+ // API 검색 (디바운스된 쿼리 사용)
+ const { data: searchData } = useSearchScraps(
+ {
+ query: debouncedQuery,
+ sort: 'CREATED_AT',
+ order: 'DESC',
+ },
+ // 쿼리가 있을 때만 검색
+ debouncedQuery.length > 0
+ );
+
+ // ScrapSearchResp는 folders와 scraps를 각각 반환하므로 분리해서 표시
+ const folders = React.useMemo(() => {
+ if (!searchData) return [];
+ return (searchData.folders || []).map((folder) => ({
+ ...folder,
+ type: 'FOLDER' as const,
+ }));
+ }, [searchData]);
+
+ const scraps = React.useMemo(() => {
+ if (!searchData) return [];
+ return searchData.scraps || [];
+ }, [searchData]);
+
+ const onSearch = () => {
+ if (!query.trim()) return;
+ addKeyword(query);
+ // 검색 버튼을 누르면 즉시 검색
+ setDebouncedQuery(query.trim());
+ };
+
+ return (
+
+
+
+ {query.length === 0 && (
+
+ 최근 검색어
+ clear()}>
+ 전체 지우기
+
+
+ )}
+ {query.length === 0 && (
+
+ {keywords.map((item, i) => (
+
+ setQuery(item)}>
+ {item}
+
+ removeKeyword(item)}>
+
+
+
+ ))}
+
+ )}
+
+
+ {query.length > 0 && folders.length > 0 && (
+
+ 폴더
+
+
+ )}
+
+ {query.length > 0 && scraps.length > 0 && (
+
+ 스크랩
+
+
+ )}
+
+
+ );
+};
+
+export default SearchScrapScreen;
diff --git a/apps/native/src/features/student/scrap/utils/constants.ts b/apps/native/src/features/student/scrap/utils/constants.ts
new file mode 100644
index 00000000..867ecd26
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/constants.ts
@@ -0,0 +1,39 @@
+/**
+ * Scrap 기능 전역 상수
+ */
+
+/**
+ * 그리드 레이아웃 설정
+ */
+export const GRID_CONFIG = {
+ /** 그리드 아이템 간 간격 (px) */
+ GAP: 22,
+ /** 그리드 아이템 최소 너비 (px) */
+ MIN_ITEM_WIDTH: 136,
+ /** 그리드 아이템 너비:높이 비율 */
+ ITEM_HEIGHT_RATIO: 1.15,
+ /** 최소 컬럼 수 */
+ MIN_COLUMNS: 2,
+} as const;
+
+/**
+ * 애니메이션 및 UI 딜레이 설정 (ms)
+ */
+export const ANIMATION_DELAYS = {
+ /** 팝오버 닫기 딜레이 */
+ POPOVER_CLOSE: 100,
+ /** 모달 닫기 딜레이 */
+ MODAL_CLOSE: 200,
+ /** 즉시 실행 (다음 이벤트 루프) */
+ IMMEDIATE: 0,
+} as const;
+
+/**
+ * 정렬 기본값
+ */
+export const DEFAULT_SORT = {
+ /** 기본 정렬 키 */
+ KEY: 'DATE' as const,
+ /** 기본 정렬 순서 */
+ ORDER: 'DESC' as const,
+} as const;
diff --git a/apps/native/src/features/student/scrap/utils/formatters/formatToMinute.ts b/apps/native/src/features/student/scrap/utils/formatters/formatToMinute.ts
new file mode 100644
index 00000000..380c91bf
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/formatters/formatToMinute.ts
@@ -0,0 +1,10 @@
+export function formatToMinute(date: Date, locale: string = 'ko-KR'): string {
+ return date.toLocaleString(locale, {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true,
+ });
+}
diff --git a/apps/native/src/features/student/scrap/utils/formatters/index.ts b/apps/native/src/features/student/scrap/utils/formatters/index.ts
new file mode 100644
index 00000000..11522288
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/formatters/index.ts
@@ -0,0 +1,3 @@
+export * from './formatToMinute';
+export * from './sortScrap';
+export * from './toAlphabetSequence';
diff --git a/apps/native/src/features/student/scrap/utils/formatters/sortScrap.ts b/apps/native/src/features/student/scrap/utils/formatters/sortScrap.ts
new file mode 100644
index 00000000..3b6422c3
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/formatters/sortScrap.ts
@@ -0,0 +1,82 @@
+import {
+ type ScrapItem,
+ type TrashItem,
+ type UISortKey,
+ type SortOrder,
+ parseTimestamp,
+} from '@/features/student/scrap/utils/types';
+
+/**
+ * 스크랩 데이터 정렬 함수
+ *
+ * @param list - 정렬할 스크랩/휴지통 아이템 목록
+ * @param key - 정렬 키 (TYPE, TITLE, DATE)
+ * @param order - 정렬 방향 (ASC, DESC)
+ * @returns 정렬된 아이템 목록
+ *
+ * @description
+ * - TYPE: 폴더가 스크랩보다 먼저 표시, 같은 타입 내에서는 생성일시 기준
+ * - TITLE: 이름 기준 (한글 로케일 지원)
+ * - DATE: 생성일시 기준
+ */
+export const sortScrapData = (
+ list: T[],
+ key: UISortKey,
+ order: SortOrder
+): T[] => {
+ const mul = order === 'ASC' ? 1 : -1;
+
+ const sorted = [...list].sort((a, b) => {
+ switch (key) {
+ case 'TYPE': {
+ // 타입 우선 정렬 (FOLDER가 SCRAP보다 먼저)
+ if (a.type !== b.type) {
+ return (a.type === 'FOLDER' ? -1 : 1) * mul;
+ }
+ // 같은 타입: 생성일시 기준
+ const timestampA = parseTimestamp(a.createdAt);
+ const timestampB = parseTimestamp(b.createdAt);
+ return (timestampA - timestampB) * mul;
+ }
+
+ case 'TITLE': {
+ // 이름 기준 (한글 로케일 지원)
+ return a.name.localeCompare(b.name, 'ko', { numeric: true }) * mul;
+ }
+
+ case 'DATE': {
+ // 생성일시 기준
+ const timestampA = parseTimestamp(a.createdAt);
+ const timestampB = parseTimestamp(b.createdAt);
+ return (timestampA - timestampB) * mul;
+ }
+
+ default:
+ return 0;
+ }
+ });
+
+ return sorted;
+};
+
+/**
+ * UI 정렬 키를 API 정렬 키로 변환
+ *
+ * @param uiKey - UI 정렬 키 (TYPE, TITLE, DATE)
+ * @returns API 정렬 키 (CREATED_AT, NAME)
+ *
+ * @description
+ * - TYPE, DATE → CREATED_AT (서버에서 생성일시 기준 정렬)
+ * - TITLE → NAME (서버에서 이름 기준 정렬)
+ */
+export const mapUIKeyToAPIKey = (uiKey: UISortKey): 'CREATED_AT' | 'NAME' => {
+ switch (uiKey) {
+ case 'TYPE':
+ case 'DATE':
+ return 'CREATED_AT';
+ case 'TITLE':
+ return 'NAME';
+ default:
+ return 'CREATED_AT';
+ }
+};
diff --git a/apps/native/src/features/student/scrap/utils/formatters/toAlphabetSequence.ts b/apps/native/src/features/student/scrap/utils/formatters/toAlphabetSequence.ts
new file mode 100644
index 00000000..dfb0a938
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/formatters/toAlphabetSequence.ts
@@ -0,0 +1,11 @@
+export const toAlphabetSequence = (num: number) => {
+ let result = '';
+ let n = num;
+
+ while (n >= 0) {
+ result = String.fromCharCode((n % 26) + 65) + result;
+ n = Math.floor(n / 26) - 1;
+ }
+
+ return result;
+};
diff --git a/apps/native/src/features/student/scrap/utils/images/imagePicker.ts b/apps/native/src/features/student/scrap/utils/images/imagePicker.ts
new file mode 100644
index 00000000..6595657b
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/images/imagePicker.ts
@@ -0,0 +1,72 @@
+import * as ImagePicker from 'expo-image-picker';
+import { Alert } from 'react-native';
+
+// 에러 핸들러 타입 정의
+type ErrorHandler = (error: Error) => void;
+
+// 기본 에러 핸들러 (Alert 사용)
+const defaultErrorHandler: ErrorHandler = (error: Error) => {
+ if (error.message?.includes('permission')) {
+ const permissionType = error.message.includes('Camera') ? '카메라' : '갤러리';
+ Alert.alert('권한 필요', `${permissionType} 권한이 필요합니다.`);
+ } else {
+ console.error('이미지 선택 오류:', error);
+ }
+};
+
+// 카메라 열기 (에러 처리 포함)
+export const openCameraWithErrorHandling = async (
+ onError?: ErrorHandler
+): Promise => {
+ try {
+ return await openCamera();
+ } catch (error: any) {
+ const handler = onError || defaultErrorHandler;
+ handler(error);
+ return null;
+ }
+};
+
+// 갤러리 열기 (에러 처리 포함)
+export const openImageLibraryWithErrorHandling = async (
+ onError?: ErrorHandler
+): Promise => {
+ try {
+ return await openImageLibrary();
+ } catch (error: any) {
+ const handler = onError || defaultErrorHandler;
+ handler(error);
+ return null;
+ }
+};
+
+// 기존 함수들 (하위 호환성 유지)
+export const openCamera = async () => {
+ const { status } = await ImagePicker.requestCameraPermissionsAsync();
+ if (status !== 'granted') {
+ throw new Error('Camera permission denied');
+ }
+
+ const result = await ImagePicker.launchCameraAsync({
+ mediaTypes: ['images'],
+ quality: 1,
+ });
+
+ if (result.canceled) return null;
+ return result.assets[0];
+};
+
+export const openImageLibrary = async () => {
+ const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
+ if (status !== 'granted') {
+ throw new Error('Gallery permission denied');
+ }
+
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ['images'],
+ quality: 1,
+ });
+
+ if (result.canceled) return null;
+ return result.assets[0];
+};
diff --git a/apps/native/src/features/student/scrap/utils/images/imageUpload.ts b/apps/native/src/features/student/scrap/utils/images/imageUpload.ts
new file mode 100644
index 00000000..48bf9a5e
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/images/imageUpload.ts
@@ -0,0 +1,102 @@
+import { uploadFileToS3 } from './s3Upload';
+import * as ImagePicker from 'expo-image-picker';
+
+/**
+ * 이미지 업로드 결과
+ */
+export interface ImageUploadResult {
+ fileId: number;
+ uploadUrl: string;
+ contentDisposition: string;
+}
+
+/**
+ * Pre-signed URL 요청 함수 타입
+ */
+type GetPreSignedUrlFn = (
+ params: { fileName: string; fileType?: 'IMAGE' | 'DOCUMENT' | 'OTHER' },
+ callbacks: {
+ onSuccess: (data: {
+ uploadUrl: string;
+ contentDisposition: string;
+ file: { id: number };
+ }) => void;
+ onError: (error: any) => void;
+ }
+) => void;
+
+/**
+ * 이미지를 S3에 업로드하는 공통 함수
+ * @param image - 업로드할 이미지
+ * @param getPreSignedUrl - Pre-signed URL을 요청하는 함수
+ * @param onSuccess - 업로드 성공 후 실행할 콜백 (fileId를 받음)
+ * @param onError - 에러 발생 시 실행할 콜백
+ * @returns Promise - 업로드 성공 여부
+ */
+export const uploadImageToS3 = async (
+ image: ImagePicker.ImagePickerAsset,
+ getPreSignedUrl: GetPreSignedUrlFn,
+ onSuccess: (result: ImageUploadResult) => Promise | void,
+ onError?: (error: string) => void
+): Promise => {
+ if (!image || !image.uri) {
+ onError?.('이미지가 없습니다.');
+ return false;
+ }
+
+ try {
+ // 파일명 추출 (없으면 기본값 사용)
+ const fileName = image.fileName || `${Date.now()}.jpg`;
+
+ // Promise로 변환하여 await 가능하게 함
+ return await new Promise((resolve) => {
+ getPreSignedUrl(
+ { fileName, fileType: 'IMAGE' },
+ {
+ onSuccess: async (data) => {
+ try {
+ const { uploadUrl, contentDisposition, file } = data;
+
+ if (!uploadUrl) {
+ onError?.('업로드 URL을 받아오지 못했습니다.');
+ resolve(false);
+ return;
+ }
+
+ // S3에 파일 업로드
+ const uploadSuccess = await uploadFileToS3(uploadUrl, image.uri, contentDisposition);
+
+ if (!uploadSuccess) {
+ onError?.('파일 업로드에 실패했습니다.');
+ resolve(false);
+ return;
+ }
+
+ // 성공 후 처리
+ await onSuccess({
+ fileId: file.id,
+ uploadUrl,
+ contentDisposition,
+ });
+
+ resolve(true);
+ } catch (error) {
+ console.error('이미지 업로드 후 처리 실패:', error);
+ onError?.('이미지 처리 중 오류가 발생했습니다.');
+ resolve(false);
+ }
+ },
+ onError: (error) => {
+ console.error('Pre-signed URL 요청 실패:', error);
+ onError?.('파일 업로드 준비에 실패했습니다.');
+ resolve(false);
+ },
+ }
+ );
+ });
+ } catch (error) {
+ console.error('이미지 업로드 실패:', error);
+ onError?.('이미지 업로드 중 오류가 발생했습니다.');
+ return false;
+ }
+};
diff --git a/apps/native/src/features/student/scrap/utils/images/index.ts b/apps/native/src/features/student/scrap/utils/images/index.ts
new file mode 100644
index 00000000..3f996b19
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/images/index.ts
@@ -0,0 +1,3 @@
+export * from './imagePicker';
+export * from './imageUpload';
+export * from './s3Upload';
diff --git a/apps/native/src/features/student/scrap/utils/images/s3Upload.ts b/apps/native/src/features/student/scrap/utils/images/s3Upload.ts
new file mode 100644
index 00000000..dbedced4
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/images/s3Upload.ts
@@ -0,0 +1,32 @@
+/**
+ * S3에 파일을 업로드하는 유틸리티 함수
+ * @param uploadUrl - Pre-signed 업로드 URL
+ * @param fileUri - 업로드할 파일의 URI (React Native)
+ * @param contentDisposition - Content-Disposition 헤더 값
+ * @returns 업로드 성공 여부
+ */
+export const uploadFileToS3 = async (
+ uploadUrl: string,
+ fileUri: string,
+ contentDisposition: string
+): Promise => {
+ try {
+ // React Native에서 파일 읽기
+ const response = await fetch(fileUri);
+ const blob = await response.blob();
+
+ // S3에 PUT 요청
+ const uploadResponse = await fetch(uploadUrl, {
+ method: 'PUT',
+ headers: {
+ 'content-disposition': contentDisposition,
+ },
+ body: blob,
+ });
+
+ return uploadResponse.ok;
+ } catch (error) {
+ console.error('S3 업로드 실패:', error);
+ return false;
+ }
+};
diff --git a/apps/native/src/features/student/scrap/utils/layout/gridLayout.ts b/apps/native/src/features/student/scrap/utils/layout/gridLayout.ts
new file mode 100644
index 00000000..66517679
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/layout/gridLayout.ts
@@ -0,0 +1,28 @@
+import { GRID_CONFIG } from '../constants';
+
+/**
+ * Calculates grid layout parameters based on window dimensions
+ * @returns Object containing numColumns, gap, and itemWidth
+ */
+export const useGridLayout = (containerWidth: number) => {
+ const { GAP, MIN_ITEM_WIDTH, ITEM_HEIGHT_RATIO, MIN_COLUMNS } = GRID_CONFIG;
+
+ // 컬럼 수 계산
+ let numColumns = Math.floor((containerWidth + GAP) / (MIN_ITEM_WIDTH + GAP));
+
+ // 최소 컬럼 수 적용
+ numColumns = Math.max(MIN_COLUMNS, numColumns);
+
+ // item width
+ const itemWidth = (containerWidth - GAP * (numColumns - 1)) / numColumns;
+
+ // 비율 기반 height
+ const itemHeight = itemWidth * ITEM_HEIGHT_RATIO;
+
+ return {
+ numColumns,
+ gap: GAP,
+ itemWidth,
+ itemHeight,
+ };
+};
diff --git a/apps/native/src/features/student/scrap/utils/layout/index.ts b/apps/native/src/features/student/scrap/utils/layout/index.ts
new file mode 100644
index 00000000..c391dba5
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/layout/index.ts
@@ -0,0 +1 @@
+export * from './gridLayout';
diff --git a/apps/native/src/features/student/scrap/utils/reducer.ts b/apps/native/src/features/student/scrap/utils/reducer.ts
new file mode 100644
index 00000000..fe8c7e87
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/reducer.ts
@@ -0,0 +1,84 @@
+/**
+ * 스크랩 아이템 선택 상태 관리
+ * id와 type을 함께 저장하여 같은 id를 가진 FOLDER와 SCRAP을 구분
+ */
+export type SelectedItem = {
+ id: number;
+ type: 'FOLDER' | 'SCRAP';
+};
+
+export interface State {
+ /** 선택 모드 활성화 여부 */
+ isSelecting: boolean;
+ /** 선택된 아이템 목록 (id와 type을 함께 저장) */
+ selectedItems: SelectedItem[];
+}
+
+/**
+ * 선택 액션 타입
+ */
+export type Action =
+ | { type: 'ENTER_SELECTION' }
+ | { type: 'EXIT_SELECTION' }
+ | { type: 'SELECTING_ITEM'; id: number; itemType: 'FOLDER' | 'SCRAP' }
+ | { type: 'SELECT_ALL'; allItems: SelectedItem[] }
+ | { type: 'CLEAR_SELECTION' };
+
+/**
+ * 초기 선택 상태
+ */
+export const initialSelectionState: State = {
+ isSelecting: false,
+ selectedItems: [],
+};
+
+/**
+ * 선택된 아이템인지 확인하는 헬퍼 함수
+ */
+export function isItemSelected(
+ selectedItems: SelectedItem[],
+ id: number,
+ type: 'FOLDER' | 'SCRAP'
+): boolean {
+ return selectedItems.some((item) => item.id === id && item.type === type);
+}
+
+/**
+ * 선택 상태 리듀서
+ * @param state - 현재 선택 상태
+ * @param action - 수행할 액션
+ * @returns 새로운 선택 상태
+ */
+export function reducer(state: State, action: Action): State {
+ switch (action.type) {
+ case 'ENTER_SELECTION':
+ return { ...state, isSelecting: true };
+
+ case 'EXIT_SELECTION':
+ return { ...state, isSelecting: false, selectedItems: [] };
+
+ case 'SELECTING_ITEM': {
+ const { id, itemType } = action;
+ const exists = isItemSelected(state.selectedItems, id, itemType);
+
+ return {
+ ...state,
+ selectedItems: exists
+ ? state.selectedItems.filter((item) => !(item.id === id && item.type === itemType))
+ : [...state.selectedItems, { id, type: itemType }],
+ };
+ }
+
+ case 'SELECT_ALL':
+ return { ...state, selectedItems: action.allItems };
+
+ case 'CLEAR_SELECTION':
+ return { ...state, selectedItems: [] };
+
+ default: {
+ // Exhaustive check: 모든 액션 타입이 처리되었는지 확인
+ const _exhaustive: never = action;
+ return state;
+ }
+ }
+}
diff --git a/apps/native/src/features/student/scrap/utils/skia/drawing.tsx b/apps/native/src/features/student/scrap/utils/skia/drawing.tsx
new file mode 100644
index 00000000..97641274
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/skia/drawing.tsx
@@ -0,0 +1,1096 @@
+import React, {
+ forwardRef,
+ useImperativeHandle,
+ useRef,
+ useState,
+ useCallback,
+ useMemo,
+ useEffect,
+} from 'react';
+import {
+ View,
+ StyleSheet,
+ TextInput,
+ Dimensions,
+ Pressable,
+ Text as RNText,
+ ScrollView,
+ Keyboard,
+ Platform,
+} from 'react-native';
+import {
+ Canvas,
+ Path,
+ SkPath,
+ Skia,
+ Text,
+ useFont,
+ Circle,
+ Group,
+} from '@shopify/react-native-skia';
+import { Gesture, GestureDetector, PointerType } from 'react-native-gesture-handler';
+import { runOnJS, useSharedValue, useDerivedValue } from 'react-native-reanimated';
+import { buildSmoothPath } from '../../utils/skia/smoothing';
+
+export type Point = { x: number; y: number };
+export type Stroke = { points: Point[]; color: string; width: number };
+export type TextItem = {
+ id: string;
+ text: string;
+ x: number;
+ y: number;
+ fontSize: number;
+ color: string;
+};
+export type DrawingCanvasRef = {
+ clear: () => void;
+ undo: () => void;
+ redo: () => void;
+ canUndo: () => boolean;
+ canRedo: () => boolean;
+ getStrokes: () => Stroke[];
+ setStrokes: (strokes: Stroke[]) => void;
+ getTexts: () => TextItem[];
+ setTexts: (texts: TextItem[]) => void;
+};
+
+type Props = {
+ strokeColor?: string;
+ strokeWidth?: number;
+ onChange?: (strokes: Stroke[]) => void;
+ onHistoryChange?: (canUndo: boolean, canRedo: boolean) => void;
+ eraserMode?: boolean;
+ eraserSize?: number;
+ textMode?: boolean;
+ textFontSize?: number;
+};
+
+const DrawingCanvas = forwardRef(
+ (
+ {
+ strokeColor = 'black',
+ strokeWidth = 3,
+ onChange,
+ onHistoryChange,
+ eraserMode = false,
+ eraserSize = 20,
+ textMode = false,
+ textFontSize = 32,
+ },
+ ref
+ ) => {
+ const [paths, setPaths] = useState([]);
+ const [strokes, setStrokes] = useState([]);
+ const [texts, setTexts] = useState([]);
+ const [, setTick] = useState(0);
+ const [activeTextInput, setActiveTextInput] = useState<{
+ id: string;
+ x: number;
+ y: number;
+ value: string;
+ } | null>(null);
+ const textInputRef = useRef(null);
+ const scrollViewRef = useRef(null);
+ const containerLayout = useRef<{ x: number; y: number; width: number; height: number } | null>(
+ null
+ );
+ const canvasHeight = useRef(800); // 기본 캔버스 높이
+ const maxY = useRef(0); // 그려진 내용의 최대 Y 좌표
+ const keyboardHeight = useRef(0); // 키보드 높이
+
+ // 호버 좌표를 저장할 SharedValue (성능을 위해 스레드 분리)
+ const hoverX = useSharedValue(0);
+ const hoverY = useSharedValue(0);
+ const showHover = useSharedValue(false);
+
+ const livePath = useRef(Skia.Path.Make());
+ const currentPoints = useRef([]);
+ const strokesRef = useRef([]);
+ const textsRef = useRef([]);
+ const eraserPoints = useRef([]);
+ const lastEraserTime = useRef(0);
+ const ERASER_THROTTLE_MS = 16; // ~60fps
+
+ // 히스토리 관리 (모든 동작에 대한 undo 지원)
+ type HistoryState = { strokes: Stroke[]; texts: TextItem[] };
+ const historyRef = useRef([]);
+ const historyIndexRef = useRef(-1);
+
+ // 히스토리 상태 변경 알림
+ const notifyHistoryChange = useCallback(() => {
+ if (!onHistoryChange) return;
+
+ const canUndo =
+ activeTextInput !== null ||
+ historyIndexRef.current > 0 ||
+ (historyIndexRef.current === 0 && historyRef.current.length > 1);
+
+ const canRedo =
+ activeTextInput === null && historyIndexRef.current + 1 < historyRef.current.length;
+
+ onHistoryChange(canUndo, canRedo);
+ }, [onHistoryChange, activeTextInput]);
+
+ // 현재 상태를 히스토리에 저장
+ const saveToHistory = useCallback(() => {
+ const currentState: HistoryState = {
+ strokes: JSON.parse(JSON.stringify(strokesRef.current)), // deep copy
+ texts: JSON.parse(JSON.stringify(textsRef.current)), // deep copy
+ };
+
+ // 현재 인덱스 이후의 히스토리 제거 (새 동작이 발생하면 redo 불가)
+ historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1);
+
+ // 새 상태 추가
+ historyRef.current.push(currentState);
+ historyIndexRef.current = historyRef.current.length - 1;
+
+ // 히스토리 크기 제한 (메모리 관리)
+ if (historyRef.current.length > 50) {
+ historyRef.current.shift();
+ historyIndexRef.current--;
+ }
+
+ notifyHistoryChange();
+ }, [notifyHistoryChange]);
+
+ // 히스토리에서 상태 복원
+ const restoreFromHistory = useCallback(
+ (index: number) => {
+ if (index < 0 || index >= historyRef.current.length) return;
+
+ const state = historyRef.current[index];
+ const newPaths = state.strokes.map((stroke) => buildSmoothPath(stroke.points));
+
+ setStrokes(state.strokes);
+ setPaths(newPaths);
+ setTexts(state.texts);
+ strokesRef.current = state.strokes;
+ textsRef.current = state.texts;
+
+ // 최대 Y 좌표 재계산
+ let maxYValue = 0;
+ if (state.strokes.length > 0) {
+ const strokesMaxY = Math.max(
+ ...state.strokes.flatMap((stroke) => stroke.points.map((p) => p.y))
+ );
+ maxYValue = Math.max(maxYValue, strokesMaxY);
+ }
+ if (state.texts.length > 0) {
+ const textsMaxY = Math.max(...state.texts.map((text) => text.y));
+ maxYValue = Math.max(maxYValue, textsMaxY);
+ }
+
+ if (maxYValue > 0) {
+ maxY.current = maxYValue;
+ canvasHeight.current = Math.max(800, maxY.current + 200);
+ } else {
+ maxY.current = 0;
+ canvasHeight.current = 800;
+ }
+
+ onChange?.(state.strokes);
+ setTick((t) => t + 1);
+ notifyHistoryChange();
+ },
+ [onChange, notifyHistoryChange]
+ );
+
+ // 폰트 로드
+ const font = useFont(require('@assets/fonts/PretendardVariable.ttf'), textFontSize);
+
+ // 텍스트의 실제 줄 수 계산 (자동 줄바꿈 포함)
+ const calculateTextLineCount = useCallback(
+ (text: string): number => {
+ if (!font) return 1;
+
+ const maxWidth = Dimensions.get('window').width * 0.5 - 40;
+ let totalLines = 0;
+
+ const paragraphs = text.split('\n');
+
+ paragraphs.forEach((paragraph) => {
+ if (!paragraph) {
+ totalLines += 1;
+ return;
+ }
+
+ const words = paragraph.split(' ');
+ let currentLine = '';
+ let paragraphLines = 0;
+
+ words.forEach((word, idx) => {
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
+ const textWidth = font.measureText(testLine).width;
+
+ if (textWidth > maxWidth && currentLine) {
+ paragraphLines += 1;
+ currentLine = word;
+ } else {
+ currentLine = testLine;
+ }
+
+ if (idx === words.length - 1) {
+ paragraphLines += 1;
+ }
+ });
+
+ totalLines += paragraphLines;
+ });
+
+ return totalLines;
+ },
+ [font]
+ );
+
+ const loadStrokes = useCallback(
+ (newStrokes: Stroke[]) => {
+ // strokes와 paths를 함께 업데이트
+ const newPaths = newStrokes.map((stroke) => buildSmoothPath(stroke.points));
+ setStrokes(newStrokes);
+ setPaths(newPaths);
+ strokesRef.current = newStrokes;
+
+ // 최대 Y 좌표 계산
+ if (newStrokes.length > 0) {
+ const maxYValue = Math.max(
+ ...newStrokes.flatMap((stroke) => stroke.points.map((p) => p.y))
+ );
+ maxY.current = maxYValue;
+ canvasHeight.current = Math.max(800, maxYValue + 200);
+ } else {
+ maxY.current = 0;
+ canvasHeight.current = 800;
+ }
+
+ setTick((t) => t + 1);
+ onChange?.(newStrokes);
+
+ // 히스토리 초기화 및 초기 상태 저장 (외부에서 로드한 경우)
+ historyRef.current = [];
+ historyIndexRef.current = -1;
+ // 초기 상태를 히스토리에 저장
+ setTimeout(() => {
+ const initialState: HistoryState = {
+ strokes: JSON.parse(JSON.stringify(newStrokes)),
+ texts: JSON.parse(JSON.stringify(textsRef.current)),
+ };
+ historyRef.current = [initialState];
+ historyIndexRef.current = 0;
+ }, 0);
+ },
+ [onChange]
+ );
+
+ const loadTexts = useCallback((newTexts: TextItem[]) => {
+ setTexts(newTexts);
+ textsRef.current = newTexts;
+
+ // 최대 Y 좌표 계산 (strokes와 texts 모두 고려)
+ let maxYValue = 0;
+
+ // strokes의 최대 Y 계산
+ if (strokesRef.current.length > 0) {
+ const strokesMaxY = Math.max(
+ ...strokesRef.current.flatMap((stroke) => stroke.points.map((p) => p.y))
+ );
+ maxYValue = Math.max(maxYValue, strokesMaxY);
+ }
+
+ // texts의 최대 Y 계산
+ if (newTexts.length > 0) {
+ const textsMaxY = Math.max(...newTexts.map((text) => text.y));
+ maxYValue = Math.max(maxYValue, textsMaxY);
+ }
+
+ if (maxYValue > 0) {
+ maxY.current = maxYValue;
+ canvasHeight.current = Math.max(800, maxY.current + 200);
+ } else {
+ maxY.current = 0;
+ canvasHeight.current = 800;
+ }
+
+ setTick((t) => t + 1);
+ }, []);
+
+ // 텍스트 영역과 충돌하는지 확인 (32px 여백 포함, 캔버스 전체 너비, 멀티라인 고려)
+ const isNearExistingText = useCallback(
+ (y: number): boolean => {
+ const safeDistance = 32; // 텍스트 주변 32px 보호 영역
+
+ for (const textItem of texts) {
+ // 실제 줄 수 계산
+ const lineCount = calculateTextLineCount(textItem.text);
+ const totalTextHeight = lineCount * textFontSize;
+
+ // 텍스트 영역의 Y 범위 (32px 여백 포함, X는 캔버스 전체 너비)
+ const textTop = textItem.y - textFontSize - safeDistance;
+ const textBottom = textItem.y + totalTextHeight - textFontSize + safeDistance;
+
+ // Y 좌표가 텍스트 영역 내에 있으면 캔버스 전체 너비에서 필기 차단
+ if (y >= textTop && y <= textBottom) {
+ return true;
+ }
+ }
+ return false;
+ },
+ [texts, textFontSize, calculateTextLineCount]
+ );
+
+ const addPoint = useCallback(
+ (x: number, y: number) => {
+ // 텍스트 영역에서는 필기 차단
+ if (isNearExistingText(y)) {
+ return;
+ }
+
+ currentPoints.current.push({ x, y });
+ // 최대 Y 좌표 업데이트
+ if (y > maxY.current) {
+ maxY.current = y;
+ // 여유 공간을 위해 200px 추가
+ canvasHeight.current = Math.max(800, maxY.current + 200);
+ setTick((t) => t + 1);
+ }
+ // 경로는 매번 재생성하되, 렌더링은 최적화
+ livePath.current = buildSmoothPath(currentPoints.current);
+ setTick((t) => t + 1);
+ },
+ [isNearExistingText]
+ );
+
+ const startStroke = useCallback(
+ (x: number, y: number) => {
+ // 텍스트 영역에서는 필기 시작 차단
+ if (isNearExistingText(y)) {
+ return;
+ }
+
+ currentPoints.current = [{ x, y }];
+ livePath.current = buildSmoothPath(currentPoints.current);
+ setTick((t) => t + 1);
+ },
+ [isNearExistingText]
+ );
+
+ const finalizeStroke = useCallback(() => {
+ if (currentPoints.current.length === 0) {
+ livePath.current = Skia.Path.Make();
+ setTick((t) => t + 1);
+ return;
+ }
+
+ const pointsToFinalize = [...currentPoints.current];
+ // 최대 Y 좌표 업데이트
+ const strokeMaxY = Math.max(...pointsToFinalize.map((p) => p.y));
+ if (strokeMaxY > maxY.current) {
+ maxY.current = strokeMaxY;
+ canvasHeight.current = Math.max(800, maxY.current + 200);
+ }
+
+ const newPath = buildSmoothPath(pointsToFinalize);
+ const strokeData: Stroke = {
+ points: pointsToFinalize,
+ color: strokeColor,
+ width: strokeWidth,
+ };
+
+ // 배치 업데이트: paths와 strokes를 함께 업데이트
+ setStrokes((prev) => {
+ const next = [...prev, strokeData];
+ setPaths((prevPaths) => [...prevPaths, newPath]);
+ strokesRef.current = next;
+ onChange?.(next);
+ setTick((t) => t + 1);
+
+ // 히스토리에 저장
+ setTimeout(() => saveToHistory(), 0);
+
+ return next;
+ });
+
+ currentPoints.current = [];
+ livePath.current = Skia.Path.Make();
+ }, [strokeColor, strokeWidth, onChange, saveToHistory]);
+
+ // 지우개: 터치한 위치에서 가까운 점들을 제거
+ const eraseAtPoint = useCallback(
+ (x: number, y: number) => {
+ const now = Date.now();
+ // Throttle: 너무 자주 호출되지 않도록 제한
+ if (now - lastEraserTime.current < ERASER_THROTTLE_MS) {
+ return;
+ }
+ lastEraserTime.current = now;
+
+ const thresholdSquared = eraserSize * eraserSize; // 제곱 비교로 sqrt 제거
+
+ setStrokes((prevStrokes) => {
+ // 1. 지울 선들을 걸러냅니다. (선 위의 점 중 하나라도 지우개 반경에 닿으면 삭제)
+ const nextStrokes = prevStrokes.filter((stroke) => {
+ // 최적화: 제곱 거리 비교 (sqrt 제거)
+ const isTouched = stroke.points.some((point) => {
+ const dx = point.x - x;
+ const dy = point.y - y;
+ const distanceSquared = dx * dx + dy * dy;
+ return distanceSquared < thresholdSquared;
+ });
+ return !isTouched; // 닿지 않은 선들만 남김
+ });
+
+ // 2. 만약 지워진 선이 있다면 Path 배열도 업데이트
+ if (nextStrokes.length !== prevStrokes.length) {
+ // 경로를 한 번에 생성
+ const newPaths = nextStrokes.map((s) => buildSmoothPath(s.points));
+ setPaths(newPaths);
+ strokesRef.current = nextStrokes;
+ onChange?.(nextStrokes);
+ setTick((t) => t + 1);
+
+ // 히스토리에 저장 (지우개 동작)
+ setTimeout(() => saveToHistory(), 0);
+
+ return nextStrokes;
+ }
+
+ return prevStrokes;
+ });
+ },
+ [eraserSize, onChange, saveToHistory]
+ );
+
+ const addEraserPoint = useCallback(
+ (x: number, y: number) => {
+ eraserPoints.current.push({ x, y });
+ eraseAtPoint(x, y);
+ },
+ [eraseAtPoint]
+ );
+
+ const startEraser = useCallback(
+ (x: number, y: number) => {
+ eraserPoints.current = [{ x, y }];
+ eraseAtPoint(x, y);
+ },
+ [eraseAtPoint]
+ );
+
+ const finalizeEraser = useCallback(() => {
+ eraserPoints.current = [];
+ }, []);
+
+ const deleteText = useCallback((textId: string) => {
+ setTexts((prev) => {
+ const next = prev.filter((t) => t.id !== textId);
+ textsRef.current = next;
+ return next;
+ });
+ setTick((t) => t + 1);
+ }, []);
+
+ // 필기(stroke) 위에 텍스트 박스를 생성할 수 있는지 확인
+ // 필기 위는 차단 (캔버스 전체 너비), 필기 아래 16px부터는 허용
+ const canAddTextAtPosition = useCallback(
+ (y: number): boolean => {
+ const minGap = 32; // 필기 아래 최소 16px 간격
+
+ for (const stroke of strokes) {
+ // stroke의 최대 Y 좌표 (가장 아래)
+ const strokeMaxY = Math.max(...stroke.points.map((p) => p.y));
+
+ // Y 좌표가 stroke보다 위에 있으면 차단 (X 좌표 무관, 캔버스 전체 너비)
+ if (y < strokeMaxY + minGap) {
+ return false; // 필기 위 또는 너무 가까움
+ }
+ }
+ return true; // 텍스트 생성 가능
+ },
+ [strokes]
+ );
+
+ const addText = useCallback(
+ (x: number, y: number) => {
+ const padding = 16;
+ const minGap = 32; // 필기 아래 32px
+
+ // 가장 아래쪽 stroke 찾기
+ let textY = padding; // 기본값: 캔버스 상단 16px
+
+ if (strokes.length > 0) {
+ // 모든 stroke의 최대 Y 좌표 찾기
+ const maxStrokeY = Math.max(
+ ...strokes.flatMap((stroke) => stroke.points.map((p) => p.y))
+ );
+ textY = maxStrokeY + minGap; // 가장 아래 필기 + 32px
+ }
+
+ // 기존 텍스트가 있으면 그 아래로 (멀티라인 고려)
+ if (texts.length > 0) {
+ const textBottoms = texts.map((text) => {
+ const lineCount = calculateTextLineCount(text.text);
+ const totalHeight = lineCount * textFontSize;
+ return text.y + totalHeight - textFontSize;
+ });
+ const maxTextBottom = Math.max(...textBottoms);
+ textY = Math.max(textY, maxTextBottom + minGap);
+ }
+
+ const textId = Date.now().toString();
+ setActiveTextInput({
+ id: textId,
+ x: padding, // 항상 캔버스 왼쪽 끝(16px)에서 시작
+ y: textY, // 가장 아래 필기/텍스트 아래 32px
+ value: '',
+ });
+
+ // TextInput 포커스 및 키보드 처리
+ setTimeout(() => {
+ textInputRef.current?.focus();
+
+ // 키보드가 올라올 때 텍스트 입력 위치로 스크롤
+ const showListener = Keyboard.addListener(
+ Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
+ (e) => {
+ keyboardHeight.current = e.endCoordinates.height;
+
+ // 약간의 지연 후 스크롤 (레이아웃 업데이트 대기)
+ setTimeout(() => {
+ if (!containerLayout.current) return;
+
+ // 텍스트 입력 위치 계산 (ScrollView 내부 기준)
+ const textInputY = textY;
+ const screenHeight = Dimensions.get('window').height;
+ const keyboardTop = screenHeight - e.endCoordinates.height;
+
+ // 컨테이너의 절대 위치 계산
+ const containerY = containerLayout.current.y;
+ const textInputAbsoluteY = containerY + textInputY;
+ const textInputBottom = textInputAbsoluteY + textFontSize + 40; // 텍스트 높이 + 여백
+
+ // 키보드가 텍스트 입력을 가릴 수 있는지 확인
+ if (textInputBottom > keyboardTop) {
+ // 스크롤할 거리 계산 (키보드 위로 여유 공간 확보)
+ const scrollOffset = textInputBottom - keyboardTop + 20;
+
+ // ScrollView를 스크롤
+ scrollViewRef.current?.scrollTo({
+ y: Math.max(0, scrollOffset),
+ animated: true,
+ });
+ }
+ }, 100);
+ }
+ );
+
+ // 키보드가 내려갈 때 리스너 제거
+ const hideListener = Keyboard.addListener(
+ Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
+ () => {
+ keyboardHeight.current = 0;
+ showListener.remove();
+ hideListener.remove();
+ }
+ );
+ }, 100);
+ },
+ [
+ isNearExistingText,
+ canAddTextAtPosition,
+ strokes,
+ texts,
+ textFontSize,
+ calculateTextLineCount,
+ ]
+ );
+
+ const confirmTextInput = useCallback(() => {
+ if (activeTextInput && activeTextInput.value.trim()) {
+ const newText: TextItem = {
+ id: activeTextInput.id,
+ text: activeTextInput.value,
+ x: activeTextInput.x,
+ y: activeTextInput.y,
+ fontSize: textFontSize,
+ color: strokeColor,
+ };
+ // 최대 Y 좌표 업데이트
+ if (activeTextInput.y > maxY.current) {
+ maxY.current = activeTextInput.y;
+ canvasHeight.current = Math.max(800, maxY.current + 200);
+ }
+ setTexts((prev) => {
+ const next = [...prev, newText];
+ textsRef.current = next;
+
+ // 히스토리에 저장 (텍스트 추가)
+ setTimeout(() => saveToHistory(), 0);
+
+ return next;
+ });
+ setTick((t) => t + 1);
+ }
+ setActiveTextInput(null);
+ }, [activeTextInput, textFontSize, strokeColor, saveToHistory]);
+
+ const handleTextInputBlur = useCallback(() => {
+ if (activeTextInput) {
+ confirmTextInput();
+ }
+ }, [activeTextInput, confirmTextInput]);
+
+ const handleTextInputChange = useCallback(
+ (text: string) => {
+ if (activeTextInput) {
+ setActiveTextInput((prev) => (prev ? { ...prev, value: text } : null));
+ }
+ },
+ [activeTextInput]
+ );
+
+ const undo = useCallback(() => {
+ // 활성 텍스트 입력이 있으면 먼저 취소
+ if (activeTextInput) {
+ setActiveTextInput(null);
+ return;
+ }
+
+ // 히스토리에서 이전 상태로 복원
+ if (historyIndexRef.current > 0) {
+ historyIndexRef.current--;
+ restoreFromHistory(historyIndexRef.current);
+ } else if (historyIndexRef.current === 0) {
+ // 첫 번째 상태로 복원 (빈 상태)
+ historyIndexRef.current = -1;
+ restoreFromHistory(0);
+ }
+ // historyIndexRef.current === -1이면 undo할 히스토리가 없음
+ }, [activeTextInput, restoreFromHistory]);
+
+ const redo = useCallback(() => {
+ // 활성 텍스트 입력이 있으면 redo 불가
+ if (activeTextInput) {
+ return;
+ }
+
+ // 히스토리에서 다음 상태로 복원
+ const nextIndex = historyIndexRef.current + 1;
+ if (nextIndex < historyRef.current.length) {
+ historyIndexRef.current = nextIndex;
+ restoreFromHistory(nextIndex);
+ }
+ // nextIndex >= historyRef.current.length이면 redo할 히스토리가 없음
+ }, [activeTextInput, restoreFromHistory]);
+
+ // 초기 상태를 히스토리에 저장
+ useEffect(() => {
+ if (historyRef.current.length === 0) {
+ const initialState: HistoryState = {
+ strokes: [],
+ texts: [],
+ };
+ historyRef.current = [initialState];
+ historyIndexRef.current = 0;
+ notifyHistoryChange();
+ }
+ }, [notifyHistoryChange]);
+
+ // activeTextInput 상태 변경 시 히스토리 상태 알림
+ useEffect(() => {
+ notifyHistoryChange();
+ }, [activeTextInput, notifyHistoryChange]);
+
+ useImperativeHandle(ref, () => ({
+ clear() {
+ setPaths([]);
+ setStrokes([]);
+ setTexts([]);
+ setActiveTextInput(null);
+ strokesRef.current = [];
+ textsRef.current = [];
+ livePath.current = Skia.Path.Make();
+ maxY.current = 0;
+ canvasHeight.current = 800;
+ setTick((t) => t + 1);
+ onChange?.([]);
+
+ // 히스토리 초기화
+ historyRef.current = [];
+ historyIndexRef.current = -1;
+ },
+ undo,
+ redo,
+ canUndo: () => {
+ // 활성 텍스트 입력이 있으면 undo 가능
+ if (activeTextInput) return true;
+ // 히스토리 인덱스가 0보다 크면 undo 가능 (이전 상태가 있음)
+ if (historyIndexRef.current > 0) return true;
+ // 초기 상태만 있고 실제 변경이 없으면 undo 불가능
+ // 히스토리가 1개만 있으면 (초기 상태만) undo 불가능
+ if (historyRef.current.length === 1) return false;
+ // 히스토리가 2개 이상이면 undo 가능
+ return historyIndexRef.current === 0 && historyRef.current.length > 1;
+ },
+ canRedo: () => {
+ // 활성 텍스트 입력이 있으면 redo 불가
+ if (activeTextInput) return false;
+ // 다음 히스토리가 있으면 redo 가능
+ return historyIndexRef.current + 1 < historyRef.current.length;
+ },
+ getStrokes: () => strokesRef.current,
+ setStrokes: loadStrokes,
+ getTexts: () => textsRef.current,
+ setTexts: loadTexts,
+ }));
+
+ const tap = useMemo(
+ () =>
+ Gesture.Tap().onEnd((e) => {
+ 'worklet';
+ // 텍스트 입력은 손가락도 허용 (모든 입력 타입 허용)
+ if (textMode && !eraserMode) {
+ runOnJS(addText)(e.x, e.y);
+ }
+ }),
+ [textMode, eraserMode, addText]
+ );
+
+ const pan = useMemo(
+ () =>
+ Gesture.Pan()
+ .minPointers(1)
+ .maxPointers(1) // 한 손가락만 허용 (두 손가락은 스크롤)
+ .onBegin((e) => {
+ 'worklet';
+ // 펜슬만 허용 (제스처 이벤트에서 직접 pointerType 확인)
+ const pointerType = e.pointerType;
+ if (pointerType !== PointerType.STYLUS && pointerType !== PointerType.MOUSE) {
+ return;
+ }
+ showHover.value = false; // 그리기 시작 시 호버 숨김
+ if (textMode) return; // 텍스트 모드에서는 그리기 비활성화
+ if (eraserMode) {
+ runOnJS(startEraser)(e.x, e.y);
+ } else {
+ runOnJS(startStroke)(e.x, e.y);
+ }
+ })
+ .onUpdate((e) => {
+ 'worklet';
+ // 펜슬만 허용 (제스처 이벤트에서 직접 pointerType 확인)
+ const pointerType = e.pointerType;
+ if (pointerType !== PointerType.STYLUS && pointerType !== PointerType.MOUSE) {
+ return;
+ }
+ if (textMode) return;
+ if (eraserMode) {
+ runOnJS(addEraserPoint)(e.x, e.y);
+ } else {
+ runOnJS(addPoint)(e.x, e.y);
+ }
+ })
+ .onEnd(() => {
+ 'worklet';
+ if (textMode) return;
+ if (eraserMode) {
+ runOnJS(finalizeEraser)();
+ } else {
+ runOnJS(finalizeStroke)();
+ }
+ })
+ .minDistance(1),
+ [
+ textMode,
+ eraserMode,
+ startStroke,
+ addPoint,
+ finalizeStroke,
+ startEraser,
+ addEraserPoint,
+ finalizeEraser,
+ ]
+ );
+
+ // 호버 제스처 (펜슬/마우스에서만 작동)
+ const hoverGesture = useMemo(
+ () =>
+ Gesture.Hover()
+ .onBegin((e) => {
+ 'worklet';
+ // 펜슬/마우스에서만 호버 표시
+ const pointerType = e.pointerType;
+ if (pointerType === PointerType.STYLUS || pointerType === PointerType.MOUSE) {
+ hoverX.value = e.x;
+ hoverY.value = e.y;
+ showHover.value = true;
+ }
+ })
+ .onUpdate((e) => {
+ 'worklet';
+ // 펜슬/마우스에서만 호버 표시
+ const pointerType = e.pointerType;
+ if (pointerType === PointerType.STYLUS || pointerType === PointerType.MOUSE) {
+ hoverX.value = e.x;
+ hoverY.value = e.y;
+ showHover.value = true;
+ } else {
+ showHover.value = false;
+ }
+ })
+ .onEnd(() => {
+ 'worklet';
+ showHover.value = false;
+ })
+ .onFinalize(() => {
+ 'worklet';
+ showHover.value = false;
+ }),
+ []
+ );
+
+ // 호버 opacity를 위한 derived value
+ const hoverOpacity = useDerivedValue(() => {
+ return showHover.value ? 0.6 : 0;
+ }, [showHover]);
+
+ const composedGesture = useMemo(
+ () => Gesture.Simultaneous(Gesture.Race(tap, pan), hoverGesture),
+ [tap, pan, hoverGesture]
+ );
+
+ // 경로 렌더링 최적화: paths 배열이 변경될 때만 재렌더링
+ // 각 stroke는 저장된 width와 color를 사용
+ const renderedPaths = useMemo(
+ () =>
+ paths.map((p, i) => {
+ const stroke = strokes[i];
+ return (
+
+ );
+ }),
+ [paths, strokes, strokeWidth, strokeColor]
+ );
+
+ // 텍스트 렌더링 최적화 (멀티라인 지원 + 자동 줄바꿈)
+ const renderedTexts = useMemo(
+ () =>
+ font
+ ? texts.flatMap((textItem) => {
+ const maxWidth = Dimensions.get('window').width * 0.5 - 40;
+ const allLines: string[] = [];
+
+ // 먼저 명시적 줄바꿈으로 분할
+ const paragraphs = textItem.text.split('\n');
+
+ // 각 문단을 너비 기준으로 추가 분할
+ paragraphs.forEach((paragraph) => {
+ if (!paragraph) {
+ allLines.push(''); // 빈 줄 유지
+ return;
+ }
+
+ const words = paragraph.split(' ');
+ let currentLine = '';
+
+ words.forEach((word, idx) => {
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
+ const textWidth = font.measureText(testLine).width;
+
+ if (textWidth > maxWidth && currentLine) {
+ // 현재 줄이 최대 너비를 초과하면 줄바꿈
+ allLines.push(currentLine);
+ currentLine = word;
+ } else {
+ currentLine = testLine;
+ }
+
+ // 마지막 단어인 경우 현재 줄 추가
+ if (idx === words.length - 1) {
+ allLines.push(currentLine);
+ }
+ });
+ });
+
+ return allLines.map((line, lineIndex) => (
+
+ ));
+ })
+ : null,
+ [texts, font, textFontSize]
+ );
+
+ // 텍스트 삭제 버튼 렌더링 (텍스트 모드일 때만, 텍스트 시작 위치에 배치)
+ const renderedTextDeleteButtons = useMemo(() => {
+ if (!textMode || eraserMode) return null;
+
+ return texts.map((textItem) => {
+ const buttonSize = 20;
+ const buttonX = textItem.x - buttonSize + 10; // 텍스트 시작 왼쪽에 배치
+ const buttonY = textItem.y - textFontSize + (textFontSize - buttonSize) / 2 + 10;
+
+ return (
+ deleteText(textItem.id)}>
+ ×
+
+ );
+ });
+ }, [texts, textMode, eraserMode, textFontSize, deleteText]);
+
+ return (
+
+
+ {
+ const { x, y, width, height } = e.nativeEvent.layout;
+ containerLayout.current = { x, y, width, height };
+ }}>
+
+
+ {/* 인라인 텍스트 입력 박스 */}
+ {activeTextInput && (
+ {}}>
+
+
+ )}
+
+ {/* 텍스트 삭제 버튼 */}
+ {renderedTextDeleteButtons}
+
+
+
+ );
+ }
+);
+
+const styles = StyleSheet.create({
+ scrollView: {
+ flex: 1,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ },
+ container: { minHeight: 400, position: 'relative' },
+ canvas: { width: '100%', backgroundColor: 'white' },
+ textInputWrapper: {
+ position: 'absolute',
+ backgroundColor: 'transparent',
+ maxWidth: Dimensions.get('window').width * 0.5 - 40,
+ },
+ inlineTextInput: {
+ backgroundColor: 'transparent',
+ borderWidth: 0,
+ padding: 0,
+ margin: 0,
+ textAlignVertical: 'top',
+ width: '100%', // wrapper의 maxWidth에 맞춰서 자동 줄바꿈
+ },
+ deleteButton: {
+ position: 'absolute',
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
+ borderRadius: 10,
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 10,
+ },
+ deleteButtonText: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: 'bold',
+ lineHeight: 16,
+ },
+});
+
+export default React.memo(DrawingCanvas);
diff --git a/apps/native/src/features/student/scrap/utils/skia/index.ts b/apps/native/src/features/student/scrap/utils/skia/index.ts
new file mode 100644
index 00000000..03830ce1
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/skia/index.ts
@@ -0,0 +1,3 @@
+export * from './smoothing';
+export { default as DrawingCanvas } from './drawing';
+export type { Point, Stroke, TextItem, DrawingCanvasRef } from './drawing';
diff --git a/apps/native/src/features/student/scrap/utils/skia/smoothing.ts b/apps/native/src/features/student/scrap/utils/skia/smoothing.ts
new file mode 100644
index 00000000..032ea4ab
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/skia/smoothing.ts
@@ -0,0 +1,40 @@
+import { Skia, SkPath } from '@shopify/react-native-skia';
+
+type Point = { x: number; y: number };
+
+export function buildSmoothPath(points: Point[]): SkPath {
+ const path = Skia.Path.Make();
+ if (points.length === 0) return path;
+
+ // 첫 번째 점으로 이동
+ path.moveTo(points[0].x, points[0].y);
+
+ if (points.length < 3) {
+ // 점이 2개뿐일 때는 단순 직선 연결
+ if (points.length === 2) {
+ path.lineTo(points[1].x, points[1].y);
+ }
+ return path;
+ }
+
+ // 쿼드라틱 베지에 곡선(Quadratic Bezier)을 이용한 부드러운 경로 생성
+ for (let i = 1; i < points.length - 1; i++) {
+ const p0 = points[i];
+ const p1 = points[i + 1];
+
+ // 좌표값이 유효한지 확인 (NaN 방지)
+ if (isNaN(p0.x) || isNaN(p0.y) || isNaN(p1.x) || isNaN(p1.y)) continue;
+
+ const midX = (p0.x + p1.x) / 2;
+ const midY = (p0.y + p1.y) / 2;
+
+ // p0를 제어점으로 사용하고 mid point를 종착점으로 사용하여 부드럽게 연결
+ path.quadTo(p0.x, p0.y, midX, midY);
+ }
+
+ // 마지막 점까지 연결
+ const lastPoint = points[points.length - 1];
+ path.lineTo(lastPoint.x, lastPoint.y);
+
+ return path;
+}
diff --git a/apps/native/src/features/student/scrap/utils/types.ts b/apps/native/src/features/student/scrap/utils/types.ts
new file mode 100644
index 00000000..ababb94d
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/types.ts
@@ -0,0 +1,79 @@
+import { paths, components } from '@/types/api/schema';
+
+/**
+ * API 응답 타입 추출
+ */
+export type ScrapSearchResponse =
+ paths['/api/student/scrap/search']['get']['responses']['200']['content']['*/*'];
+export type TrashResponse =
+ paths['/api/student/scrap/trash']['get']['responses']['200']['content']['*/*'];
+
+/**
+ * API 스키마 기반 기본 타입
+ */
+export type ScrapListItemResp = components['schemas']['ScrapListItemResp'];
+export type ScrapDetailResp = components['schemas']['ScrapDetailResp'];
+export type ScrapFolderResp = components['schemas']['ScrapFolderResp'];
+
+/**
+ * 스크랩 아이템 유니언 타입
+ * - API의 ScrapListItemResp를 기반으로 함
+ * - type 필드로 FOLDER/SCRAP 구분
+ */
+export type ScrapItem = ScrapListItemResp;
+
+/**
+ * 휴지통 아이템 타입 (API 스키마)
+ * API의 TrashItemResp를 직접 사용
+ */
+export type TrashItemResp = components['schemas']['TrashItemResp'];
+
+/**
+ * 휴지통 아이템 (정렬을 위한 확장 타입)
+ * - createdAt이 없으므로 deletedAt을 사용하여 정렬
+ */
+export type TrashItem = TrashItemResp & {
+ /** 정렬을 위한 createdAt (deletedAt으로 설정) */
+ createdAt: string;
+};
+
+/**
+ * 정렬 키 타입
+ * API의 sort 파라미터에 맞춤 (CREATED_AT, NAME)
+ */
+export type ApiSortKey = 'CREATED_AT' | 'NAME';
+
+/**
+ * UI 정렬 키 타입 (TYPE은 클라이언트 전용)
+ */
+export type UISortKey = 'TYPE' | 'TITLE' | 'DATE';
+
+/**
+ * 정렬 방향 타입
+ */
+export type SortOrder = 'ASC' | 'DESC';
+
+/**
+ * 필터 타입
+ */
+export type FilterType = 'ALL' | 'FOLDER' | 'SCRAP';
+
+/**
+ * 검색 파라미터 타입
+ */
+export type SearchScrapParams = {
+ folderId?: number;
+ query?: string;
+ filter?: FilterType;
+ sort?: ApiSortKey;
+ order?: SortOrder;
+ page?: number;
+ size?: number;
+};
+
+/**
+ * UI 헬퍼: timestamp를 Date로 변환
+ */
+export const parseTimestamp = (dateString: string): number => {
+ return new Date(dateString).getTime();
+};
diff --git a/apps/native/src/features/student/scrap/utils/validation.ts b/apps/native/src/features/student/scrap/utils/validation.ts
new file mode 100644
index 00000000..31360a14
--- /dev/null
+++ b/apps/native/src/features/student/scrap/utils/validation.ts
@@ -0,0 +1,26 @@
+import { SelectedItem } from './reducer';
+import { showToast } from '../components/Notification/Toast';
+
+/**
+ * 선택된 아이템 중 폴더가 포함되어 있는지 확인하고, 포함되어 있으면 에러 토스트를 표시합니다.
+ * 스크랩만 이동 가능한 경우에 사용합니다.
+ *
+ * @param selectedItems - 선택된 아이템 목록
+ * @returns 폴더가 포함되어 있으면 true, 아니면 false
+ *
+ * @example
+ * if (validateOnlyScrapCanMove(reducerState.selectedItems)) {
+ * return; // 폴더가 포함되어 있으면 중단
+ * }
+ * // 스크랩만 이동하는 로직 계속...
+ */
+export const validateOnlyScrapCanMove = (selectedItems: SelectedItem[]): boolean => {
+ const selectedFolders = selectedItems.filter((selected) => selected.type === 'FOLDER');
+
+ if (selectedFolders.length > 0) {
+ showToast('error', '스크랩만 이동이 가능합니다.');
+ return true; // 검증 실패 (폴더가 포함됨)
+ }
+
+ return false; // 검증 성공 (스크랩만 있음)
+};
diff --git a/apps/native/src/navigation/student/StudentNavigator.tsx b/apps/native/src/navigation/student/StudentNavigator.tsx
index e5bcae3f..27f2025c 100644
--- a/apps/native/src/navigation/student/StudentNavigator.tsx
+++ b/apps/native/src/navigation/student/StudentNavigator.tsx
@@ -2,10 +2,18 @@ import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import NotificationScreen from '@features/student/home/screens/notifications/NotificationsScreen';
import NotificationDetailScreen from '@features/student/home/screens/notifications/NotificationDetailScreen';
-import { ProblemScreen, PointingScreen, AnalysisScreen, AllPointingsScreen } from '@features/student/problem';
+import {
+ ProblemScreen,
+ PointingScreen,
+ AnalysisScreen,
+ AllPointingsScreen,
+} from '@features/student/problem';
import StudentTabs from './StudentTabs';
import { StudentRootStackParamList } from './types';
import NotificationHeader from './components/NotificationHeader';
+import { DeletedScrapScreen, ScrapScreen, SearchScrapScreen } from '@/features/student/scrap';
+import FolderScrapScreen from '@/features/student/scrap/screens/FolderScrapScreen';
+import ScrapDetailScreen from '@/features/student/scrap/screens/ScrapDetailScreen';
const StudentRootStack = createNativeStackNavigator();
@@ -33,6 +41,11 @@ const StudentNavigator = () => {
+
+
+
+
+
);
};
diff --git a/apps/native/src/navigation/student/types.ts b/apps/native/src/navigation/student/types.ts
index fcd3ad76..1a0e3fa2 100644
--- a/apps/native/src/navigation/student/types.ts
+++ b/apps/native/src/navigation/student/types.ts
@@ -22,4 +22,13 @@ export type StudentRootStackParamList = {
publishAt?: string;
problemSetTitle?: string;
};
+ Scrap: undefined;
+ ScrapContent: {
+ id: number;
+ };
+ SearchScrap: undefined;
+ DeletedScrap: undefined;
+ ScrapContentDetail: {
+ id: number;
+ };
};
diff --git a/apps/native/src/stores/index.ts b/apps/native/src/stores/index.ts
index 8d099b8d..44626b6d 100644
--- a/apps/native/src/stores/index.ts
+++ b/apps/native/src/stores/index.ts
@@ -1,2 +1,4 @@
export * from './authStore';
export * from './problemSessionStore';
+export * from './searchHistoryStore';
+export * from './scrapNoteStore';
diff --git a/apps/native/src/stores/recentScrapStore.ts b/apps/native/src/stores/recentScrapStore.ts
new file mode 100644
index 00000000..29b5af71
--- /dev/null
+++ b/apps/native/src/stores/recentScrapStore.ts
@@ -0,0 +1,42 @@
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+interface RecentScrapStore {
+ /** 최근 본 스크랩 ID 목록 (최신순) */
+ scrapIds: number[];
+ /** 스크랩 추가 */
+ addScrap: (scrapId: number) => void;
+ /** 스크랩 제거 */
+ removeScrap: (scrapId: number) => void;
+ /** 전체 초기화 */
+ clear: () => void;
+}
+
+export const useRecentScrapStore = create()(
+ persist(
+ (set) => ({
+ scrapIds: [],
+
+ addScrap: (scrapId) =>
+ set((state) => {
+ // 중복 제거 후 맨 앞에 추가
+ const filtered = state.scrapIds.filter((id) => id !== scrapId);
+ return {
+ scrapIds: [scrapId, ...filtered].slice(0, 30),
+ };
+ }),
+
+ removeScrap: (scrapId) =>
+ set((state) => ({
+ scrapIds: state.scrapIds.filter((id) => id !== scrapId),
+ })),
+
+ clear: () => set({ scrapIds: [] }),
+ }),
+ {
+ name: 'recent-scrap-store',
+ storage: createJSONStorage(() => AsyncStorage),
+ }
+ )
+);
diff --git a/apps/native/src/stores/scrapNoteStore.ts b/apps/native/src/stores/scrapNoteStore.ts
new file mode 100644
index 00000000..9aa975ca
--- /dev/null
+++ b/apps/native/src/stores/scrapNoteStore.ts
@@ -0,0 +1,68 @@
+import { create } from 'zustand';
+
+export type Note = {
+ id: number;
+ title: string;
+};
+
+type NoteStore = {
+ openNotes: Note[];
+ activeNoteId: number | null;
+
+ openNote: (note: Note) => void;
+ closeNote: (noteId: number) => void;
+ setActiveNote: (noteId: number) => void;
+ reorderNotes: (fromIndex: number, toIndex: number) => void;
+};
+
+export const useNoteStore = create((set, get) => ({
+ openNotes: [],
+ activeNoteId: null,
+
+ openNote: (note) => {
+ const { openNotes } = get();
+ const exists = openNotes.find((n) => n.id === note.id);
+
+ // 이미 열려 있으면 추가 X
+ if (!exists) {
+ set({
+ openNotes: [...openNotes, note],
+ activeNoteId: note.id,
+ });
+ } else {
+ set({ activeNoteId: note.id });
+ }
+ },
+
+ closeNote: (noteId) => {
+ const { openNotes, activeNoteId } = get();
+ const filtered = openNotes.filter((n) => n.id !== noteId);
+
+ set({
+ openNotes: filtered,
+ activeNoteId:
+ noteId === activeNoteId ? (filtered[filtered.length - 1]?.id ?? null) : activeNoteId,
+ });
+ },
+
+ setActiveNote: (noteId) => {
+ set({ activeNoteId: noteId });
+ },
+
+ reorderNotes: (fromIndex, toIndex) => {
+ const { openNotes } = get();
+ if (
+ fromIndex === toIndex ||
+ fromIndex < 0 ||
+ toIndex < 0 ||
+ fromIndex >= openNotes.length ||
+ toIndex >= openNotes.length
+ ) {
+ return;
+ }
+ const newNotes = [...openNotes];
+ const [moved] = newNotes.splice(fromIndex, 1);
+ newNotes.splice(toIndex, 0, moved);
+ set({ openNotes: newNotes });
+ },
+}));
diff --git a/apps/native/src/stores/searchHistoryStore.ts b/apps/native/src/stores/searchHistoryStore.ts
new file mode 100644
index 00000000..1f0d553d
--- /dev/null
+++ b/apps/native/src/stores/searchHistoryStore.ts
@@ -0,0 +1,97 @@
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import type { FilterType, ApiSortKey, SortOrder } from '@/features/student/scrap/utils/types';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+interface SearchHistoryStore {
+ keywords: string[];
+ addKeyword: (keyword: string) => void;
+ removeKeyword: (keyword: string) => void;
+ clear: () => void;
+}
+
+export const useSearchHistoryStore = create()(
+ persist(
+ (set) => ({
+ keywords: [],
+
+ addKeyword: (keyword) =>
+ set((state) => {
+ const trimmed = keyword.trim();
+ if (!trimmed) return state;
+
+ const filtered = state.keywords.filter((k) => k !== trimmed);
+ return {
+ keywords: [trimmed, ...filtered].slice(0, 10),
+ };
+ }),
+
+ removeKeyword: (keyword) =>
+ set((state) => ({
+ keywords: state.keywords.filter((k) => k !== keyword),
+ })),
+
+ clear: () => set({ keywords: [] }),
+ }),
+ {
+ name: 'search-history-store',
+ storage: createJSONStorage(() => AsyncStorage),
+ }
+ )
+);
+
+interface ScrapUIStore {
+ /** 현재 선택된 폴더 ID */
+ currentFolderId: number | null;
+ /** 선택 모드 활성화 여부 */
+ isSelectionMode: boolean;
+ /** 현재 필터 (ALL/FOLDER/SCRAP) */
+ currentFilter: FilterType;
+ /** 현재 정렬 키 (CREATED_AT/NAME) */
+ currentSort: ApiSortKey;
+ /** 현재 정렬 방향 (ASC/DESC) */
+ currentOrder: SortOrder;
+
+ /** 현재 폴더 ID 설정 */
+ setCurrentFolderId: (id: number | null) => void;
+ /** 선택 모드 설정 */
+ setSelectionMode: (enabled: boolean) => void;
+ /** 필터 설정 */
+ setFilter: (filter: FilterType) => void;
+ /** 정렬 설정 */
+ setSort: (sort: ApiSortKey, order: SortOrder) => void;
+ /** 상태 초기화 */
+ reset: () => void;
+}
+
+const initialScrapUIState = {
+ currentFolderId: null,
+ isSelectionMode: false,
+ currentFilter: 'ALL' as FilterType,
+ currentSort: 'CREATED_AT' as ApiSortKey,
+ currentOrder: 'DESC' as SortOrder,
+};
+export const useScrapUIStore = create()(
+ persist(
+ (set) => ({
+ ...initialScrapUIState,
+
+ setCurrentFolderId: (id) => set({ currentFolderId: id }),
+ setSelectionMode: (enabled) => set({ isSelectionMode: enabled }),
+ setFilter: (filter) => set({ currentFilter: filter }),
+ setSort: (sort, order) => set({ currentSort: sort, currentOrder: order }),
+
+ reset: () => set(initialScrapUIState),
+ }),
+ {
+ name: 'scrap-ui-store',
+ storage: createJSONStorage(() => AsyncStorage),
+
+ partialize: (state) => ({
+ currentFilter: state.currentFilter,
+ currentSort: state.currentSort,
+ currentOrder: state.currentOrder,
+ }),
+ }
+ )
+);
diff --git a/apps/native/src/types/api/schema.d.ts b/apps/native/src/types/api/schema.d.ts
index 2d827526..9ad270eb 100644
--- a/apps/native/src/types/api/schema.d.ts
+++ b/apps/native/src/types/api/schema.d.ts
@@ -40,6 +40,179 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/student/scrap/{scrapId}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 스크랩 전체 수정 */
+ put: operations['updateScrap'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/{scrapId}/thumbnail': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 스크랩 썸네일 수정 (thumbnail만) */
+ put: operations['updateScrapThumbnail'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/{scrapId}/textBox': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 스크랩 메모 수정 (textBox만) */
+ put: operations['updateScrapText'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/{scrapId}/name': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 스크랩 이름 수정 (name만) */
+ put: operations['updateScrapName'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/{scrapId}/handwriting': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 필기 데이터 조회 */
+ get: operations['getHandwriting'];
+ /** 필기 데이터 저장/수정 */
+ put: operations['updateHandwriting'];
+ post?: never;
+ /** 필기 데이터 삭제 */
+ delete: operations['deleteHandwriting'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/trash/restore': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 휴지통 복원 (폴더/스크랩 혼합 배치) */
+ put: operations['restoreTrash'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/move': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 스크랩 이동 (배치) */
+ put: operations['moveScraps'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/folder/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 폴더 상세 조회 */
+ get: operations['getFolderDetail'];
+ /** 폴더 수정 */
+ put: operations['updateFolder'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/folder/{id}/thumbnail': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 폴더 썸네일 수정 */
+ put: operations['updateFolderThumbnail'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/folder/{id}/name': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 폴더 이름 수정 */
+ put: operations['updateFolderName'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/student/qna/{qnaId}': {
parameters: {
query?: never;
@@ -146,6 +319,24 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/admin/qna/chat/{chatId}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** 채팅메시지 수정 */
+ put: operations['updateChat_2'];
+ post?: never;
+ /** 채팅메시지 삭제 */
+ delete: operations['deleteChat_2'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/admin/problem/{id}': {
parameters: {
query?: never;
@@ -327,6 +518,40 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/teacher/me/push/token': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** 푸시 토큰 등록/갱신 */
+ post: operations['updatePushToken'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/teacher/me/push/allow/toggle': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** 푸시 알림 허용 토글 */
+ post: operations['toggleAllowPush'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/teacher/auth/refresh': {
parameters: {
query?: never;
@@ -395,25 +620,25 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/student/qna': {
+ '/api/student/scrap': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Q&A 목록 조회 */
- get: operations['gets'];
+ get?: never;
put?: never;
- /** Q&A 생성 */
- post: operations['create_1'];
- delete?: never;
+ /** 일반 스크랩 생성(쓸지, 안쓸지 모르겠음) */
+ post: operations['createScrap'];
+ /** 스크랩 삭제 (폴더/스크랩 혼합 배치) */
+ delete: operations['deleteScraps'];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/qna/exist': {
+ '/api/student/scrap/toggle/from-reading-tip': {
parameters: {
query?: never;
header?: never;
@@ -422,15 +647,15 @@ export interface paths {
};
get?: never;
put?: never;
- /** Q&A 존재 여부 확인, 사용자가 동일한 항목에 대해 QnA 작성 여부 확인 */
- post: operations['checkExists'];
+ /** 리딩팁 콘텐츠 스크랩 토글 */
+ post: operations['toggleScrapFromReadingTip'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/qna/chat': {
+ '/api/student/scrap/toggle/from-problem': {
parameters: {
query?: never;
header?: never;
@@ -439,15 +664,15 @@ export interface paths {
};
get?: never;
put?: never;
- /** 채팅메시지 생성 */
- post: operations['addChat_1'];
+ /** 문제 기반 스크랩 토글 (있으면 삭제, 없으면 생성) */
+ post: operations['toggleScrapFromProblem'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/auth/register/social': {
+ '/api/student/scrap/toggle/from-pointing': {
parameters: {
query?: never;
header?: never;
@@ -456,15 +681,15 @@ export interface paths {
};
get?: never;
put?: never;
- /** 소셜 로그인 이후, 정보 등록 */
- post: operations['registerSocial'];
+ /** 포인팅 기반 스크랩 토글 (있으면 삭제, 없으면 생성) */
+ post: operations['toggleScrapFromPointing'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/auth/refresh': {
+ '/api/student/scrap/toggle/from-one-step-more': {
parameters: {
query?: never;
header?: never;
@@ -473,15 +698,15 @@ export interface paths {
};
get?: never;
put?: never;
- /** 토큰 갱신 */
- post: operations['refresh_1'];
+ /** 원스텝모어 콘텐츠 스크랩 토글 */
+ post: operations['toggleScrapFromOneStepMore'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/auth/login/social': {
+ '/api/student/scrap/from-problem': {
parameters: {
query?: never;
header?: never;
@@ -490,15 +715,16 @@ export interface paths {
};
get?: never;
put?: never;
- /** 소셜 로그인 URL 요청 [네이버만 완료] */
- post: operations['getSocialLoginUrl'];
- delete?: never;
+ /** 문제에서 스크랩 (problem + 모든 pointing) */
+ post: operations['createScrapFromProblem'];
+ /** 문제에서 스크랩 취소 (휴지통 처리) */
+ delete: operations['unscrapFromProblem'];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/common/upload-file': {
+ '/api/student/scrap/from-pointing': {
parameters: {
query?: never;
header?: never;
@@ -507,15 +733,16 @@ export interface paths {
};
get?: never;
put?: never;
- /** 파일 업로드, (응답의 uploadUrl로 AWS S3 업로드 해야합니다) */
- post: operations['getPreSignedUrl'];
- delete?: never;
+ /** 포인팅에서 스크랩 (problem + 해당 pointing만) */
+ post: operations['createScrapFromPointing'];
+ /** 포인팅에서 스크랩 취소 (다른 포인팅이 없으면 휴지통 처리) */
+ delete: operations['unscrapFromPointing'];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/user': {
+ '/api/student/scrap/from-image': {
parameters: {
query?: never;
header?: never;
@@ -524,69 +751,69 @@ export interface paths {
};
get?: never;
put?: never;
- /** 관리자 계정 생성 */
- post: operations['create_2'];
+ /** 이미지 기반 스크랩 (문제 연결 없이) */
+ post: operations['createScrapFromImage'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/teacher': {
+ '/api/student/scrap/folder': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 검색 */
- get: operations['search'];
+ /** 폴더 목록 조회 */
+ get: operations['getFolders'];
put?: never;
- /** 생성 */
- post: operations['create_3'];
- delete?: never;
+ /** 폴더 생성 */
+ post: operations['createFolder'];
+ /** 폴더 삭제 */
+ delete: operations['deleteFolders'];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/publish': {
+ '/api/student/qna': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 검색 */
- get: operations['search_1'];
+ /** Q&A 목록 조회 */
+ get: operations['gets'];
put?: never;
- /** 생성 */
- post: operations['create_4'];
+ /** Q&A 생성 */
+ post: operations['create_1'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/problem': {
+ '/api/student/qna/exist': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 검색 */
- get: operations['search_2'];
+ get?: never;
put?: never;
- /** 단일 생성 */
- post: operations['createProblem'];
+ /** Q&A 존재 여부 확인, 사용자가 동일한 항목에 대해 QnA 작성 여부 확인 */
+ post: operations['checkExists'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/problem/with-child': {
+ '/api/student/qna/chat': {
parameters: {
query?: never;
header?: never;
@@ -595,51 +822,49 @@ export interface paths {
};
get?: never;
put?: never;
- /** 생성(새끼문제, 일반문제 한번에 생성) */
- post: operations['createProblemWithChild'];
+ /** 채팅메시지 생성 */
+ post: operations['addChat_1'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/problem-set': {
+ '/api/student/notification/read/{notificationId}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 검색 */
- get: operations['search_3'];
+ get?: never;
put?: never;
- /** 생성 */
- post: operations['create_5'];
+ /** 알림 읽음 처리 */
+ post: operations['readNotification'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/practice-test': {
+ '/api/student/notification/read-all': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 검색 */
- get: operations['search_4'];
+ get?: never;
put?: never;
- /** 생성 */
- post: operations['create_6'];
+ /** 전체 알림 읽음 처리 */
+ post: operations['readAllNotifications'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/ocr': {
+ '/api/student/me/push/token': {
parameters: {
query?: never;
header?: never;
@@ -648,86 +873,83 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations['redirect'];
+ /** 푸시 토큰 등록/갱신 */
+ post: operations['updatePushToken_1'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/notice': {
+ '/api/student/me/push/allow/toggle': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 별 공지사항 전체 조회 */
- get: operations['getsAll_1'];
+ get?: never;
put?: never;
- /** 생성 */
- post: operations['create_7'];
+ /** 푸시 알림 허용 토글 */
+ post: operations['toggleAllowPush_1'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/diagnosis': {
+ '/api/student/me/password': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 진단 가져오기 */
- get: operations['gets_1'];
+ get?: never;
put?: never;
- /** 생성 */
- post: operations['create_8'];
+ /** 비밀번호 변경 */
+ post: operations['changePassword'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/concept': {
+ '/api/student/auth/signup/local': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 개념 태그 검색 */
- get: operations['search_5'];
+ get?: never;
put?: never;
- /** 개념태그 생성 */
- post: operations['create_9'];
+ /** [학생] 이메일 회원가입 */
+ post: operations['signup'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/concept/category': {
+ '/api/student/auth/register': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 대분류 검색 */
- get: operations['searchCategory'];
+ get?: never;
put?: never;
- /** 대분류 생성 */
- post: operations['createCategory'];
+ /** 초기 정보 등록 */
+ post: operations['register'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/auth/refresh': {
+ '/api/student/auth/refresh': {
parameters: {
query?: never;
header?: never;
@@ -737,14 +959,14 @@ export interface paths {
get?: never;
put?: never;
/** 토큰 갱신 */
- post: operations['refresh_2'];
+ post: operations['refresh_1'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/auth/login/local': {
+ '/api/student/auth/login/social': {
parameters: {
query?: never;
header?: never;
@@ -753,378 +975,391 @@ export interface paths {
};
get?: never;
put?: never;
- /** 이메일 로그인 */
- post: operations['login_1'];
+ /** 소셜 로그인 URL 요청 [네이버만 완료] */
+ post: operations['getSocialLoginUrl'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/your-redirect-url': {
+ '/api/common/upload-file': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 소셜 로그인 콜백 example */
- get: operations['oauthRedirectExample'];
+ get?: never;
put?: never;
- post?: never;
+ /** 파일 업로드, (응답의 uploadUrl로 AWS S3 업로드 해야합니다) */
+ post: operations['getPreSignedUrl'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/json-convert/content': {
+ '/api/auth/phone/verify': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations['content'];
+ get?: never;
put?: never;
- post?: never;
+ /** 휴대폰 인증 코드 검증 */
+ post: operations['verify'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/json-convert/childProblem-to-Problem': {
+ '/api/auth/phone/send': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations['problem'];
+ get?: never;
put?: never;
- post?: never;
+ /** 휴대폰 인증 코드 발송 */
+ post: operations['send'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/study/publish/weekly': {
+ '/api/auth/phone/resend': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 주간 발행(숙제) 조회 */
- get: operations['search_6'];
+ get?: never;
put?: never;
- post?: never;
+ /** 휴대폰 인증 코드 재발송 */
+ post: operations['resend'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/study/publish/monthly': {
+ '/api/admin/user': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 월간 발행(숙제) 조회 */
- get: operations['searchMonthly'];
+ get?: never;
put?: never;
- post?: never;
+ /** 관리자 계정 생성 */
+ post: operations['create_2'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/study/publish/detail/{id}': {
+ '/api/admin/teacher': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 발행(숙제) 상세 조회 */
- get: operations['getPublishById'];
+ /** 검색 */
+ get: operations['search'];
put?: never;
- post?: never;
+ /** 생성 */
+ post: operations['create_3'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/study/progress/weekly': {
+ '/api/admin/school/batch': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 주간 학습 진행 상황 조회 */
- get: operations['getWeeklyProgress'];
+ get?: never;
put?: never;
- post?: never;
+ /** 학교 정보 업로드 batch */
+ post: operations['batch'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/study/problem/{publishId}/{problemId}': {
+ '/api/admin/qna/chat': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 문제 상세 조회 */
- get: operations['getProblemById'];
+ get?: never;
put?: never;
- post?: never;
+ /** 채팅메시지 생성 */
+ post: operations['addChat_2'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/study/child-problem/{publishId}/{problemId}': {
+ '/api/admin/publish': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 새끼 문제 상세 조회 */
- get: operations['getChildProblemById'];
+ /** 검색 */
+ get: operations['search_1'];
put?: never;
- post?: never;
+ /** 생성 */
+ post: operations['create_4'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/students': {
+ '/api/admin/problem': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 내 학생 전체 조회 */
- get: operations['getMyStudents'];
+ /** 검색 */
+ get: operations['search_2'];
put?: never;
- post?: never;
+ /** 단일 생성 */
+ post: operations['createProblem'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/qna': {
+ '/api/admin/problem/with-child': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Q&A 목록 조회 */
- get: operations['gets_2'];
+ get?: never;
put?: never;
- post?: never;
+ /** 생성(새끼문제, 일반문제 한번에 생성) */
+ post: operations['createProblemWithChild'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/qna/{qnaId}': {
+ '/api/admin/problem-set': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Q&A 상세 조회 */
- get: operations['getById_2'];
+ /** 검색 */
+ get: operations['search_3'];
put?: never;
- post?: never;
+ /** 생성 */
+ post: operations['create_5'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/notice/available': {
+ '/api/admin/practice-test': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 별 유효 공지사항(현재 학생이 볼 수 있는 공지사항) 조회 */
- get: operations['getsAvailable'];
+ /** 검색 */
+ get: operations['search_4'];
put?: never;
- post?: never;
+ /** 생성 */
+ post: operations['create_6'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/teacher/me': {
+ '/api/admin/ocr': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 내 정보 조회 */
- get: operations['getTeacherMe'];
+ get?: never;
put?: never;
- post?: never;
+ post: operations['redirect'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/study/publish/weekly': {
+ '/api/admin/notification/send': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 주간 발행(숙제) 조회 */
- get: operations['search_7'];
+ get?: never;
put?: never;
- post?: never;
+ /**
+ * 알림 발송
+ * @description isAll=true이면 전체 학생에게 발송, false이면 studentIds에 지정된 학생들에게만 발송
+ */
+ post: operations['sendNotification'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/study/publish/monthly': {
+ '/api/admin/notice': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 월간 발행(숙제) 조회 */
- get: operations['searchMonthly_1'];
+ /** 학생 별 공지사항 전체 조회 */
+ get: operations['getsAll_1'];
put?: never;
- post?: never;
+ /** 생성 */
+ post: operations['create_7'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/study/publish/detail/{id}': {
+ '/api/admin/diagnosis': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 발행(숙제) 상세 조회 */
- get: operations['getPublishById_1'];
+ /** 학생 진단 가져오기 */
+ get: operations['gets_1'];
put?: never;
- post?: never;
+ /** 생성 */
+ post: operations['create_8'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/study/progress/weekly': {
+ '/api/admin/concept': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 주간 학습 진행 상황 조회 */
- get: operations['getWeeklyProgress_1'];
+ /** 개념 태그 검색 */
+ get: operations['search_5'];
put?: never;
- post?: never;
+ /** 개념태그 생성 */
+ post: operations['create_9'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/study/problem/{publishId}/{problemId}': {
+ '/api/admin/concept/category': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 문제 상세 조회 */
- get: operations['getProblemById_1'];
+ /** 대분류 검색 */
+ get: operations['searchCategory'];
put?: never;
- post?: never;
+ /** 대분류 생성 */
+ post: operations['createCategory'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/notice': {
+ '/api/admin/auth/refresh': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 공지사항 목록 조회 */
- get: operations['getsAvailable_1'];
+ get?: never;
put?: never;
- post?: never;
+ /** 토큰 갱신 */
+ post: operations['refresh_2'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/notice/count': {
+ '/api/admin/auth/login/local': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 읽지 않은 공지사항 개수 조회 */
- get: operations['countAvailable'];
+ get?: never;
put?: never;
- post?: never;
+ /** 이메일 로그인 */
+ post: operations['login_1'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/student/diagnosis': {
+ '/your-redirect-url': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 내 진단 리스트 가져오기 */
- get: operations['gets_3'];
+ /** 소셜 로그인 콜백 example */
+ get: operations['oauthRedirectExample'];
put?: never;
post?: never;
delete?: never;
@@ -1133,15 +1368,14 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/student/diagnosis/last': {
+ '/json-convert/content': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 내 최신 진단 가져오기 */
- get: operations['getById_3'];
+ get: operations['content'];
put?: never;
post?: never;
delete?: never;
@@ -1150,15 +1384,14 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/student/diagnosis/detail/{id}': {
+ '/json-convert/childProblem-to-Problem': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 진단 상세보기 */
- get: operations['getById_4'];
+ get: operations['problem'];
put?: never;
post?: never;
delete?: never;
@@ -1167,15 +1400,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/common/auth/refresh': {
+ '/api/teacher/study/publish/weekly': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 토큰 갱신 */
- get: operations['refresh_3'];
+ /** 학생 주간 발행(숙제) 조회 */
+ get: operations['search_6'];
put?: never;
post?: never;
delete?: never;
@@ -1184,15 +1417,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/admin/student': {
+ '/api/teacher/study/publish/monthly': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 검색 */
- get: operations['search_8'];
+ /** 학생 월간 발행(숙제) 조회 */
+ get: operations['searchMonthly'];
put?: never;
post?: never;
delete?: never;
@@ -1201,33 +1434,32 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/admin/publish/{id}': {
+ '/api/teacher/study/publish/detail/{id}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 상세 조회 */
- get: operations['getById_5'];
+ /** 발행(숙제) 상세 조회 */
+ get: operations['getPublishById'];
put?: never;
post?: never;
- /** 삭제 */
- delete: operations['delete_7'];
+ delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/problem/custom-id/generate': {
+ '/api/teacher/study/progress/weekly': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** unique customId 받기 */
- get: operations['getCustomId'];
+ /** 학생 주간 학습 진행 상황 조회 */
+ get: operations['getWeeklyProgress'];
put?: never;
post?: never;
delete?: never;
@@ -1236,15 +1468,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/admin/notice/available': {
+ '/api/teacher/study/problem/{publishId}/{problemId}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** 학생 별 유효 공지사항(현재 학생이 볼 수 있는 공지사항) 조회 */
- get: operations['getsAvailable_2'];
+ /** 문제 상세 조회 */
+ get: operations['getProblemById'];
put?: never;
post?: never;
delete?: never;
@@ -1253,860 +1485,3720 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/student/auth/quit': {
+ '/api/teacher/study/child-problem/{publishId}/{problemId}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /** 새끼 문제 상세 조회 */
+ get: operations['getChildProblemById'];
put?: never;
post?: never;
- /** 회원 탈퇴 */
- delete: operations['quit'];
+ delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/common/upload-file/{id}': {
+ '/api/teacher/students': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /** 내 학생 전체 조회 */
+ get: operations['getMyStudents'];
put?: never;
post?: never;
- /** 삭제 */
- delete: operations['delete_8'];
+ delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/teacher/{teacherId}': {
+ '/api/teacher/qna': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
- put?: never;
+ /** Q&A 목록 조회 */
+ get: operations['gets_2'];
+ put?: never;
post?: never;
- /** 선생님 삭제 */
- delete: operations['delete_9'];
+ delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/admin/problem-set/{id}/{problemId}': {
+ '/api/teacher/qna/{qnaId}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /** Q&A 상세 조회 */
+ get: operations['getById_2'];
put?: never;
post?: never;
- /** 문제 세트에서 문제 삭제 */
- delete: operations['deleteItem'];
+ delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
-}
-export type webhooks = Record;
-export interface components {
- schemas: {
- ChatUpdateRequest: {
- content: string;
- images?: number[];
- };
- ChatResp: {
- /** Format: int64 */
- id: number;
- isMine: boolean;
- content: string;
- images: components['schemas']['UploadFileResp'][];
- };
- QnAMetaResp: {
- /** Format: int64 */
- id: number;
- title: string;
- /** @enum {string} */
- type:
- | 'PROBLEM_CONTENT'
- | 'PROBLEM_POINTING_QUESTION'
- | 'PROBLEM_POINTING_COMMENT'
- | 'PROBLEM_MAIN_ANALYSIS'
- | 'PROBLEM_MAIN_HAND_ANALYSIS'
- | 'PROBLEM_READING_TIP_CONTENT'
- | 'PROBLEM_ONE_STEP_MORE'
- | 'CHILD_PROBLEM_CONTENT'
- | 'CHILD_PROBLEM_POINTING_QUESTION'
- | 'CHILD_PROBLEM_POINTING_COMMENT';
- /** Format: date */
- publishDate: string;
- /** Format: int64 */
- publishId?: number;
- /** Format: int32 */
- unreadCount?: number;
- studentName?: string;
+ '/api/teacher/qna/search': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- QnAResp: {
- /** Format: int64 */
- id: number;
- title: string;
- /** @enum {string} */
- type:
- | 'PROBLEM_CONTENT'
- | 'PROBLEM_POINTING_QUESTION'
- | 'PROBLEM_POINTING_COMMENT'
- | 'PROBLEM_MAIN_ANALYSIS'
- | 'PROBLEM_MAIN_HAND_ANALYSIS'
- | 'PROBLEM_READING_TIP_CONTENT'
- | 'PROBLEM_ONE_STEP_MORE'
- | 'CHILD_PROBLEM_CONTENT'
- | 'CHILD_PROBLEM_POINTING_QUESTION'
- | 'CHILD_PROBLEM_POINTING_COMMENT';
- /** Format: date */
- publishDate: string;
- /** Format: int64 */
- publishId?: number;
- /** Format: int32 */
- unreadCount?: number;
- studentName?: string;
- contentTitle: string;
- content: string;
- question: string;
- images: components['schemas']['UploadFileResp'][];
- /** Format: int64 */
- problemId?: number;
- chats: components['schemas']['ChatResp'][];
+ /** Q&A 검색 (제목/채팅 분리) */
+ get: operations['search_7'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/teacher/notice/available': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- UploadFileResp: {
- /** Format: int64 */
- id: number;
- fileName: string;
- url: string;
+ /** 학생 별 유효 공지사항(현재 학생이 볼 수 있는 공지사항) 조회 */
+ get: operations['getsAvailable'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/teacher/me': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- NoticeUpdateRequest: {
- /** Format: date */
- startAt: string;
- /** Format: date */
- endAt: string;
- content: string;
+ /** 내 정보 조회 */
+ get: operations['getTeacherMe'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/study/publish/weekly': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- NoticeResp: {
- /** Format: int64 */
- id: number;
- student: components['schemas']['StudentResp'];
- /** Format: date */
- startAt: string;
- /** Format: date */
- endAt: string;
- isRead: boolean;
- content: string;
+ /** 주간 발행(숙제) 조회 */
+ get: operations['search_8'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/study/publish/monthly': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- StudentResp: {
- /** Format: int64 */
- id: number;
- name: string;
- /** Format: int32 */
- grade: number;
- isFirstLogin: boolean;
+ /** 월간 발행(숙제) 조회 */
+ get: operations['searchMonthly_1'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/study/publish/detail/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- QnAUpdateRequest: {
- question: string;
- images?: number[];
+ /** 발행(숙제) 상세 조회 */
+ get: operations['getPublishById_1'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/study/progress/weekly': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- StudentUpdateRequest: {
- name: string;
- /** Format: int32 */
- grade: number;
+ /** 학생 주간 학습 진행 상황 조회 */
+ get: operations['getWeeklyProgress_1'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/study/problem/{publishId}/{problemId}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- TeacherUpdateRequest: {
- name: string;
- email: string;
- newPassword?: string;
+ /** 문제 상세 조회 */
+ get: operations['getProblemById_1'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- TeacherResp: {
- /** Format: int64 */
- id: number;
- name: string;
- email: string;
- students: components['schemas']['StudentResp'][];
+ /** 스크랩 상세 조회 */
+ get: operations['getScrap'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/trash': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 휴지통 목록 조회 */
+ get: operations['getTrash'];
+ put?: never;
+ post?: never;
+ /** 휴지통 영구 삭제 (폴더/스크랩 혼합 배치) */
+ delete: operations['permanentDelete'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/search': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 검색 (폴더 + 스크랩 각각 반환) */
+ get: operations['searchScraps'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/folder/{folderId}/scraps': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 폴더 내 스크랩 목록 조회 */
+ get: operations['getScrapsByFolder'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/school': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 검색 (유사도 상위순 7개 반환) */
+ get: operations['search_9'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/qna/{qnaId}/images': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Q&A 전체 이미지 조회 (질문 + 채팅) */
+ get: operations['getImages'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/qna/search': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Q&A 검색 (제목/채팅 분리) */
+ get: operations['search_10'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/qna/images': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 내가 참여한 모든 Q&A 이미지 조회 (최신순) */
+ get: operations['getAllImages'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/qna/admin-chat': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 출제진 채팅방 조회/생성 */
+ get: operations['getOrCreateAdminChatroom'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/notification': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * 알림 목록 조회
+ * @description dayLimit 파라미터로 특정 기간 내 알림만 조회 가능
+ */
+ get: operations['getNotifications'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/notification/count': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 읽지 않은 알림 개수 조회 */
+ get: operations['countUnread'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/notice': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 공지사항 목록 조회 */
+ get: operations['getsAvailable_1'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/notice/count': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 읽지 않은 공지사항 개수 조회 */
+ get: operations['countAvailable'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/diagnosis': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 내 진단 리스트 가져오기 */
+ get: operations['gets_3'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/diagnosis/last': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 내 최신 진단 가져오기 */
+ get: operations['getById_3'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/diagnosis/detail/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 학생 진단 상세보기 */
+ get: operations['getById_4'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/auth/email/exists': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** [학생] 이메일 중복확인 */
+ get: operations['existsByEmail'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/qna/{qnaId}/subscribe': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Q&A 채팅 SSE 구독
+ * @description 클라이언트에서 SSE로 채팅/읽음 이벤트를 구독합니다.
+ *
+ * - 요청 예: GET /api/qna/{qnaId}/subscribe?token={accessToken}
+ * - 응답 Content-Type: text/event-stream
+ * - 이벤트 이름:
+ * - chat: 채팅 생성/수정/삭제 (QnAChatEvent 스키마 참조)
+ * - read_status: 읽음 상태 변경 (QnAReadStatusEvent 스키마 참조)
+ * - heartbeat: 초기 연결 확인
+ */
+ get: operations['subscribe'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/exception/throw': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations['throwException'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/common/auth/refresh': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 토큰 갱신 */
+ get: operations['refresh_3'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/student': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 검색 */
+ get: operations['search_11'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/qna': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 출제진 채팅방 목록 조회 */
+ get: operations['gets_4'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/qna/{qnaId}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 출제진 채팅방 상세 조회 */
+ get: operations['getById_5'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/publish/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 상세 조회 */
+ get: operations['getById_6'];
+ put?: never;
+ post?: never;
+ /** 삭제 */
+ delete: operations['delete_7'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/problem/custom-id/generate': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** unique customId 받기 */
+ get: operations['getCustomId'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/notification': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 특정 학생의 알림 목록 조회 */
+ get: operations['getNotificationsByStudent'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/notice/available': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** 학생 별 유효 공지사항(현재 학생이 볼 수 있는 공지사항) 조회 */
+ get: operations['getsAvailable_2'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/auth/issue-admin-token': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations['issueTemporaryToken'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/scrap/trash/all': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /** 휴지통 비우기 */
+ delete: operations['emptyTrash'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/student/auth/quit': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /** 회원 탈퇴 */
+ delete: operations['quit'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/common/upload-file/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /** 삭제 */
+ delete: operations['delete_8'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/teacher/{teacherId}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /** 선생님 삭제 */
+ delete: operations['delete_9'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/problem-set/{id}/{problemId}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /** 문제 세트에서 문제 삭제 */
+ delete: operations['deleteItem'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ ChatUpdateRequest: {
+ content: string;
+ images?: number[];
+ };
+ ChatResp: {
+ /** Format: int64 */
+ id: number;
+ isMine: boolean;
+ content: string;
+ images: components['schemas']['UploadFileResp'][];
+ /**
+ * Format: int64
+ * @description 답장 대상 채팅 ID
+ */
+ replyToId?: number;
+ /** @description 답장 대상 채팅 내용 미리보기 */
+ replyToContent?: string;
+ };
+ QnAMetaResp: {
+ /** Format: int64 */
+ id: number;
+ title: string;
+ /** @enum {string} */
+ type:
+ | 'PROBLEM_CONTENT'
+ | 'PROBLEM_POINTING_QUESTION'
+ | 'PROBLEM_POINTING_COMMENT'
+ | 'PROBLEM_MAIN_ANALYSIS'
+ | 'PROBLEM_MAIN_HAND_ANALYSIS'
+ | 'PROBLEM_READING_TIP_CONTENT'
+ | 'PROBLEM_ONE_STEP_MORE'
+ | 'CHILD_PROBLEM_CONTENT'
+ | 'CHILD_PROBLEM_POINTING_QUESTION'
+ | 'CHILD_PROBLEM_POINTING_COMMENT'
+ | 'ADMIN_CHAT';
+ /** Format: date */
+ publishDate: string;
+ /** Format: int64 */
+ publishId?: number;
+ /** Format: int32 */
+ unreadCount?: number;
+ studentName?: string;
+ /**
+ * Format: date-time
+ * @description 최신 메시지 시간
+ */
+ latestMessageTime?: string;
+ /** @description 최신 메시지 내용 미리보기 */
+ latestMessageContent?: string;
+ };
+ QnAResp: {
+ /** Format: int64 */
+ id: number;
+ title: string;
+ /** @enum {string} */
+ type:
+ | 'PROBLEM_CONTENT'
+ | 'PROBLEM_POINTING_QUESTION'
+ | 'PROBLEM_POINTING_COMMENT'
+ | 'PROBLEM_MAIN_ANALYSIS'
+ | 'PROBLEM_MAIN_HAND_ANALYSIS'
+ | 'PROBLEM_READING_TIP_CONTENT'
+ | 'PROBLEM_ONE_STEP_MORE'
+ | 'CHILD_PROBLEM_CONTENT'
+ | 'CHILD_PROBLEM_POINTING_QUESTION'
+ | 'CHILD_PROBLEM_POINTING_COMMENT'
+ | 'ADMIN_CHAT';
+ /** Format: date */
+ publishDate: string;
+ /** Format: int64 */
+ publishId?: number;
+ /** Format: int32 */
+ unreadCount?: number;
+ studentName?: string;
+ /**
+ * Format: date-time
+ * @description 최신 메시지 시간
+ */
+ latestMessageTime?: string;
+ /** @description 최신 메시지 내용 미리보기 */
+ latestMessageContent?: string;
+ contentTitle: string;
+ content: string;
+ question: string;
+ images: components['schemas']['UploadFileResp'][];
+ /** Format: int64 */
+ problemId?: number;
+ chats: components['schemas']['ChatResp'][];
+ };
+ UploadFileResp: {
+ /** Format: int64 */
+ id: number;
+ fileName: string;
+ url: string;
+ /** @enum {string} */
+ fileType: 'IMAGE' | 'DOCUMENT' | 'OTHER';
+ };
+ NoticeUpdateRequest: {
+ title: string;
+ /** Format: date */
+ startAt: string;
+ /** Format: date */
+ endAt: string;
+ content: string;
+ };
+ NoticeResp: {
+ /** Format: int64 */
+ id: number;
+ student: components['schemas']['StudentResp'];
+ title: string;
+ /** Format: date */
+ startAt: string;
+ /** Format: date */
+ endAt: string;
+ isRead: boolean;
+ content: string;
+ };
+ SchoolResp: {
+ /** Format: int64 */
+ id: number;
+ name?: string;
+ sido?: string;
+ };
+ StudentResp: {
+ /** Format: int64 */
+ id: number;
+ isGteFourteen?: boolean;
+ isAgreeServiceUsage?: boolean;
+ isAgreePersonalInformation?: boolean;
+ isAgreeReceiveMarketing?: boolean;
+ /** Format: date-time */
+ agreeAt?: string;
+ email: string;
+ name: string;
+ /** Format: date */
+ birth?: string;
+ /** @enum {string} */
+ gender?: 'MALE' | 'FEMALE';
+ phoneNumber?: string;
+ /** @enum {string} */
+ mobileCarrier?: 'KT' | 'SKT' | 'LG' | 'KT_MVNO' | 'SKT_MVNO' | 'LG_MVNO';
+ /** Format: date-time */
+ phoneNumberVerifiedAt?: string;
+ /** @enum {string} */
+ grade: 'ONE' | 'TWO' | 'THREE' | 'N_TIME';
+ /** @enum {string} */
+ selectSubject?: 'MIJUKBUN' | 'HWAKTONG' | 'KEEHA';
+ school?: components['schemas']['SchoolResp'];
+ /** Format: int32 */
+ level?: number;
+ nickname?: string;
+ isAllowPush?: boolean;
+ /** @enum {string} */
+ provider?: 'KAKAO' | 'GOOGLE' | 'APPLE';
+ isFirstLogin: boolean;
+ /** Format: int64 */
+ teacherId?: number;
+ teacherName?: string;
+ };
+ ScrapUpdateRequest: {
+ /**
+ * Format: int64
+ * @description 폴더 ID (null이면 루트로 이동)
+ * @example 1
+ */
+ folderId?: number;
+ /**
+ * Format: int64
+ * @description 썸네일 이미지 ID
+ * @example 456
+ */
+ thumbnailImageId?: number;
+ /**
+ * @description 텍스트 메모
+ * @example 수정된 메모 내용
+ */
+ textBox?: string;
+ };
+ ConceptCategoryResp: {
+ /** Format: int64 */
+ id: number;
+ name: string;
+ };
+ ConceptResp: {
+ /** Format: int64 */
+ id: number;
+ name: string;
+ category: components['schemas']['ConceptCategoryResp'];
+ };
+ /** @description 포인팅 목록 */
+ PointingResp: {
+ /** Format: int64 */
+ id: number;
+ /** Format: int32 */
+ no: number;
+ questionContent: string;
+ commentContent: string;
+ concepts: components['schemas']['ConceptResp'][];
+ };
+ PracticeTestResp: {
+ /** Format: int64 */
+ id: number;
+ /** Format: int32 */
+ year: number;
+ /** Format: int32 */
+ month: number;
+ /** Format: int32 */
+ grade: number;
+ name: string;
+ displayName: string;
+ };
+ /** @description 문제 정보 */
+ ProblemExtendResp: {
+ /** Format: int64 */
+ id: number;
+ /** @enum {string} */
+ problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
+ /** Format: int64 */
+ parentProblem?: number;
+ parentProblemTitle?: string;
+ customId: string;
+ /** @enum {string} */
+ createType: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
+ practiceTest: components['schemas']['PracticeTestResp'];
+ /** Format: int32 */
+ practiceTestNo: number;
+ problemContent: string;
+ title: string;
+ /** @enum {string} */
+ answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
+ /** Format: int32 */
+ answer: number;
+ /** Format: int32 */
+ difficulty: number;
+ /** Format: int32 */
+ recommendedTimeSec: number;
+ memo: string;
+ concepts: components['schemas']['ConceptResp'][];
+ mainAnalysisImage: components['schemas']['UploadFileResp'];
+ mainHandAnalysisImage: components['schemas']['UploadFileResp'];
+ readingTipContent: string;
+ oneStepMoreContent: string;
+ };
+ ProblemMetaResp: {
+ /** Format: int64 */
+ id: number;
+ /** @enum {string} */
+ problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
+ /** Format: int64 */
+ parentProblem?: number;
+ parentProblemTitle?: string;
+ customId: string;
+ /** @enum {string} */
+ createType: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
+ practiceTest: components['schemas']['PracticeTestResp'];
+ /** Format: int32 */
+ practiceTestNo: number;
+ problemContent: string;
+ title: string;
+ /** @enum {string} */
+ answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
+ /** Format: int32 */
+ answer: number;
+ /** Format: int32 */
+ difficulty: number;
+ /** Format: int32 */
+ recommendedTimeSec: number;
+ memo: string;
+ concepts: components['schemas']['ConceptResp'][];
+ };
+ /** @description 스크랩 상세 정보 */
+ ScrapDetailResp: {
+ /**
+ * Format: int64
+ * @description 스크랩 ID
+ */
+ id: number;
+ folder?: components['schemas']['ScrapFolderResp'];
+ problem?: components['schemas']['ProblemExtendResp'];
+ /** @description 썸네일 URL */
+ thumbnailUrl?: string;
+ /** @description 스크랩 이름 */
+ name?: string;
+ /** @description 텍스트 메모 */
+ textBox?: string;
+ /** @description 포인팅 목록 */
+ pointings: components['schemas']['PointingResp'][];
+ /** @description 리딩팁 스크랩 여부 */
+ isReadingTipScrapped: boolean;
+ /** @description 원스텝모어 스크랩 여부 */
+ isOneStepMoreScrapped: boolean;
+ /** @description 필기 데이터 존재 여부 */
+ hasHandwriting: boolean;
+ /**
+ * Format: date-time
+ * @description 생성일시
+ */
+ createdAt: string;
+ /**
+ * Format: date-time
+ * @description 수정일시
+ */
+ updatedAt: string;
+ };
+ /** @description 스크랩 폴더 응답 */
+ ScrapFolderResp: {
+ /**
+ * Format: int64
+ * @description 폴더 ID
+ */
+ id: number;
+ /** @description 폴더 이름 */
+ name: string;
+ /**
+ * Format: int64
+ * @description 폴더 내 스크랩 개수
+ */
+ scrapCount: number;
+ /** @description 썸네일 이미지 URL */
+ thumbnailUrl?: string;
+ /** @description 최근 스크랩 2개의 썸네일 URL (최신순) */
+ top2ScrapThumbnail?: string[];
+ /**
+ * Format: date-time
+ * @description 생성일시
+ */
+ createdAt: string;
+ /**
+ * Format: date-time
+ * @description 수정일시
+ */
+ updatedAt: string;
+ };
+ ScrapThumbnailUpdateRequest: {
+ /**
+ * Format: int64
+ * @description 썸네일 이미지 ID (null이면 썸네일 삭제)
+ */
+ thumbnailImageId?: number;
+ };
+ ScrapTextBoxUpdateRequest: {
+ /**
+ * @description 텍스트 메모
+ * @example 메모 수정
+ */
+ textBox?: string;
+ };
+ ScrapNameUpdateRequest: {
+ /**
+ * @description 스크랩 이름
+ * @example 미적분 정리
+ */
+ name?: string;
+ };
+ ScrapHandwritingUpdateRequest: {
+ /** @description 필기 데이터 (Base64 인코딩) */
+ data: string;
+ };
+ /** @description 필기 데이터 응답 */
+ ScrapHandwritingResp: {
+ /**
+ * Format: int64
+ * @description 스크랩 ID
+ */
+ scrapId: number;
+ /** @description 필기 데이터 (Base64 인코딩) */
+ data: string;
+ /**
+ * Format: date-time
+ * @description 수정일시
+ */
+ updatedAt: string;
+ };
+ /** @description 복원할 항목 목록 (폴더/스크랩 혼합) */
+ Item: {
+ /**
+ * Format: int64
+ * @description 항목 ID
+ * @example 1
+ */
+ id: number;
+ /**
+ * @description 항목 타입 (FOLDER/SCRAP)
+ * @example FOLDER
+ * @enum {string}
+ */
+ type: 'FOLDER' | 'SCRAP';
+ };
+ ScrapBatchRestoreRequest: {
+ /** @description 복원할 항목 목록 (폴더/스크랩 혼합) */
+ items: components['schemas']['Item'][];
+ };
+ ScrapBatchMoveRequest: {
+ /** @description 이동할 스크랩 ID 목록 */
+ scrapIds: number[];
+ /**
+ * Format: int64
+ * @description 대상 폴더 ID (null이면 루트로 이동)
+ * @example 2
+ */
+ targetFolderId?: number;
+ };
+ ListRespScrapDetailResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['ScrapDetailResp'][];
+ };
+ ScrapFolderUpdateRequest: {
+ /**
+ * @description 폴더 이름
+ * @example 기하 오답노트
+ */
+ name: string;
+ /**
+ * Format: int64
+ * @description 썸네일 이미지 ID
+ */
+ thumbnailImageId?: number;
+ };
+ ScrapFolderThumbnailUpdateRequest: {
+ /**
+ * Format: int64
+ * @description 썸네일 이미지 ID (null이면 썸네일 삭제)
+ */
+ thumbnailImageId?: number;
+ };
+ ScrapFolderNameUpdateRequest: {
+ /**
+ * @description 폴더 이름
+ * @example 기하 오답노트
+ */
+ name: string;
+ };
+ QnAUpdateRequest: {
+ question: string;
+ images?: number[];
+ };
+ StudentUpdateRequest: {
+ isGteFourteen?: boolean;
+ isAgreeServiceUsage?: boolean;
+ isAgreePersonalInformation?: boolean;
+ isAgreeReceiveMarketing?: boolean;
+ email?: string;
+ name?: string;
+ /** Format: date */
+ birth?: string;
+ /** @enum {string} */
+ gender?: 'MALE' | 'FEMALE';
+ phoneNumber?: string;
+ /** @enum {string} */
+ mobileCarrier?: 'KT' | 'SKT' | 'LG' | 'KT_MVNO' | 'SKT_MVNO' | 'LG_MVNO';
+ /** Format: date-time */
+ phoneNumberVerifiedAt?: string;
+ /** @enum {string} */
+ grade?: 'ONE' | 'TWO' | 'THREE' | 'N_TIME';
+ /** @enum {string} */
+ selectSubject?: 'MIJUKBUN' | 'HWAKTONG' | 'KEEHA';
+ /** Format: int64 */
+ schoolId?: number;
+ /** Format: int32 */
+ level?: number;
+ nickname?: string;
+ };
+ TeacherUpdateRequest: {
+ name: string;
+ email: string;
+ newPassword?: string;
+ };
+ TeacherResp: {
+ /** Format: int64 */
+ id: number;
+ name: string;
+ email: string;
+ students: components['schemas']['StudentResp'][];
+ isAllowPush?: boolean;
+ };
+ TeacherStudentAssignReq: {
+ students: number[];
+ };
+ PointingUpdateRequest: {
+ /** Format: int64 */
+ id?: number;
+ /** Format: int32 */
+ no?: number;
+ questionContent?: string;
+ commentContent?: string;
+ concepts?: number[];
+ };
+ ProblemUpdateRequest: {
+ /** @enum {string} */
+ createType?: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
+ /** Format: int64 */
+ practiceTestId?: number;
+ /** Format: int32 */
+ practiceTestNo?: number;
+ /** Format: int32 */
+ no?: number;
+ title: string;
+ concepts?: number[];
+ /** @enum {string} */
+ answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
+ /** Format: int32 */
+ answer: number;
+ /** Format: int32 */
+ difficulty: number;
+ /** Format: int32 */
+ recommendedTimeSec: number;
+ memo?: string;
+ problemContent: string;
+ pointings?: components['schemas']['PointingUpdateRequest'][];
+ /** Format: int64 */
+ mainAnalysisImageId?: number;
+ /** Format: int64 */
+ mainHandAnalysisImageId?: number;
+ readingTipContent?: string;
+ oneStepMoreContent?: string;
+ };
+ ProblemInfoResp: {
+ /** Format: int64 */
+ id: number;
+ /** @enum {string} */
+ problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
+ /** Format: int64 */
+ parentProblem?: number;
+ parentProblemTitle?: string;
+ customId: string;
+ /** @enum {string} */
+ createType: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
+ practiceTest: components['schemas']['PracticeTestResp'];
+ /** Format: int32 */
+ practiceTestNo: number;
+ problemContent: string;
+ title: string;
+ /** @enum {string} */
+ answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
+ /** Format: int32 */
+ answer: number;
+ /** Format: int32 */
+ difficulty: number;
+ /** Format: int32 */
+ recommendedTimeSec: number;
+ memo: string;
+ concepts: components['schemas']['ConceptResp'][];
+ mainAnalysisImage: components['schemas']['UploadFileResp'];
+ mainHandAnalysisImage: components['schemas']['UploadFileResp'];
+ readingTipContent: string;
+ oneStepMoreContent: string;
+ pointings: components['schemas']['PointingResp'][];
+ childProblems: components['schemas']['ProblemInfoResp'][];
+ };
+ ProblemSetItemRequest: {
+ /** Format: int32 */
+ no: number;
+ /** Format: int64 */
+ problemId: number;
+ };
+ ProblemSetUpdateRequest: {
+ title: string;
+ /** @enum {string} */
+ status: 'CONFIRMED' | 'DOING';
+ problems?: components['schemas']['ProblemSetItemRequest'][];
+ };
+ ProblemSetItemResp: {
+ /** Format: int64 */
+ id: number;
+ /** Format: int32 */
+ no: number;
+ problem: components['schemas']['ProblemMetaResp'];
+ };
+ ProblemSetResp: {
+ /** Format: int64 */
+ id: number;
+ title: string;
+ /** @enum {string} */
+ status: 'CONFIRMED' | 'DOING';
+ firstProblem: components['schemas']['ProblemMetaResp'];
+ problems: components['schemas']['ProblemSetItemResp'][];
+ };
+ Request: {
+ /** Format: int32 */
+ year: number;
+ /** Format: int32 */
+ month: number;
+ /** Format: int32 */
+ grade: number;
+ name: string;
+ };
+ DiagnosisUpdateReq: {
+ content?: string;
+ };
+ DiagnosisResp: {
+ /** Format: int64 */
+ id: number;
+ /** Format: int64 */
+ studentId?: number;
+ /** Format: date-time */
+ createdAt?: string;
+ content?: string;
+ };
+ ConceptUpdateRequest: {
+ name: string;
+ /** Format: int64 */
+ categoryId: number;
+ };
+ ConceptCategoryUpdateRequest: {
+ name: string;
+ };
+ ChatCreateRequest: {
+ /** Format: int64 */
+ qnaId: number;
+ content: string;
+ images?: number[];
+ /**
+ * Format: int64
+ * @description 답장 대상 채팅 ID
+ */
+ replyToId?: number;
+ };
+ NoticeCreateRequest: {
+ title: string;
+ /** Format: date */
+ startAt: string;
+ /** Format: date */
+ endAt: string;
+ content: string;
+ /** Format: int64 */
+ studentId: number;
+ };
+ 'TeacherPushDTO.UpdateTokenRequest': {
+ fcmToken: string;
+ };
+ RefreshReq: {
+ refreshToken: string;
+ };
+ JwtResp: {
+ accessToken: string;
+ refreshToken?: string;
+ };
+ TeacherTokenResp: {
+ /** Format: int64 */
+ id: number;
+ name: string;
+ email: string;
+ students: components['schemas']['StudentResp'][];
+ isAllowPush?: boolean;
+ token?: components['schemas']['JwtResp'];
+ };
+ TeacherLoginReq: {
+ email: string;
+ password: string;
+ };
+ PointingFeedbackRequest: {
+ /** Format: int64 */
+ pointingId: number;
+ isUnderstood: boolean;
+ };
+ SubmissionRequest: {
+ /** Format: int64 */
+ publishId: number;
+ /** Format: int64 */
+ problemId?: number;
+ /** Format: int32 */
+ submitAnswer?: number;
+ };
+ SubmissionResp: {
+ /** @enum {string} */
+ progress?: 'CORRECT' | 'INCORRECT' | 'SEMI_CORRECT' | 'NONE';
+ /** Format: int32 */
+ submitAnswer: number;
+ isCorrect: boolean;
+ isDone: boolean;
+ };
+ ScrapCreateRequest: {
+ /**
+ * Format: int64
+ * @description 폴더 ID (null이면 루트에 생성)
+ * @example 1
+ */
+ folderId?: number;
+ /**
+ * Format: int64
+ * @description 문제 ID (null 가능 - 이미지만 스크랩 시)
+ * @example 123
+ */
+ problemId?: number;
+ /**
+ * Format: int64
+ * @description 썸네일 이미지 ID
+ * @example 456
+ */
+ thumbnailImageId?: number;
+ /**
+ * @description 텍스트 메모
+ * @example 이 문제는 미적분 개념이 핵심
+ */
+ textBox?: string;
+ /**
+ * @description 포인팅 ID 목록
+ * @example [
+ * 1,
+ * 2,
+ * 3
+ * ]
+ */
+ pointingIds?: number[];
+ };
+ ScrapFromReadingTipCreateRequest: {
+ /**
+ * Format: int64
+ * @description 문제 ID
+ * @example 123
+ */
+ problemId: number;
+ /**
+ * Format: int64
+ * @description 폴더 ID (null이면 루트에 생성)
+ * @example 1
+ */
+ folderId?: number;
+ };
+ /** @description 스크랩 토글 응답 */
+ ScrapToggleResp: {
+ /** @description 토글 후 스크랩 상태 (true: 스크랩됨, false: 스크랩 취소됨) */
+ isScraped: boolean;
+ scrap?: components['schemas']['ScrapDetailResp'];
+ };
+ ScrapFromProblemCreateRequest: {
+ /**
+ * Format: int64
+ * @description 문제 ID
+ * @example 123
+ */
+ problemId: number;
+ /**
+ * Format: int64
+ * @description 폴더 ID (null이면 루트에 생성)
+ * @example 1
+ */
+ folderId?: number;
+ };
+ ScrapFromPointingCreateRequest: {
+ /**
+ * Format: int64
+ * @description 포인팅 ID
+ * @example 456
+ */
+ pointingId: number;
+ /**
+ * Format: int64
+ * @description 폴더 ID (null이면 루트에 생성)
+ * @example 1
+ */
+ folderId?: number;
+ };
+ ScrapFromOneStepMoreCreateRequest: {
+ /**
+ * Format: int64
+ * @description 문제 ID
+ * @example 123
+ */
+ problemId: number;
+ /**
+ * Format: int64
+ * @description 폴더 ID (null이면 루트에 생성)
+ * @example 1
+ */
+ folderId?: number;
+ };
+ ScrapFromImageCreateRequest: {
+ /**
+ * Format: int64
+ * @description 이미지 ID
+ * @example 789
+ */
+ imageId: number;
+ /**
+ * Format: int64
+ * @description 폴더 ID (null이면 루트에 생성)
+ * @example 1
+ */
+ folderId?: number;
+ /**
+ * @description 텍스트 메모
+ * @example 외부 문제 스크랩
+ */
+ textBox?: string;
+ };
+ ScrapFolderCreateRequest: {
+ /**
+ * @description 폴더 이름
+ * @example 수학 오답노트
+ */
+ name: string;
+ /**
+ * Format: int64
+ * @description 썸네일 이미지 ID
+ */
+ thumbnailImageId?: number;
+ };
+ /** @description problemId, childProblemId, pointingId 중 하나만 입력 가능 */
+ QnACreateRequest: {
+ /** Format: int64 */
+ publishId: number;
+ /** @enum {string} */
+ type:
+ | 'PROBLEM_CONTENT'
+ | 'PROBLEM_POINTING_QUESTION'
+ | 'PROBLEM_POINTING_COMMENT'
+ | 'PROBLEM_MAIN_ANALYSIS'
+ | 'PROBLEM_MAIN_HAND_ANALYSIS'
+ | 'PROBLEM_READING_TIP_CONTENT'
+ | 'PROBLEM_ONE_STEP_MORE'
+ | 'CHILD_PROBLEM_CONTENT'
+ | 'CHILD_PROBLEM_POINTING_QUESTION'
+ | 'CHILD_PROBLEM_POINTING_COMMENT'
+ | 'ADMIN_CHAT';
+ /** Format: int64 */
+ problemId?: number;
+ /** Format: int64 */
+ pointingId?: number;
+ question: string;
+ images?: number[];
+ };
+ QnACheckRequest: {
+ /** Format: int64 */
+ publishId: number;
+ /** @enum {string} */
+ type:
+ | 'PROBLEM_CONTENT'
+ | 'PROBLEM_POINTING_QUESTION'
+ | 'PROBLEM_POINTING_COMMENT'
+ | 'PROBLEM_MAIN_ANALYSIS'
+ | 'PROBLEM_MAIN_HAND_ANALYSIS'
+ | 'PROBLEM_READING_TIP_CONTENT'
+ | 'PROBLEM_ONE_STEP_MORE'
+ | 'CHILD_PROBLEM_CONTENT'
+ | 'CHILD_PROBLEM_POINTING_QUESTION'
+ | 'CHILD_PROBLEM_POINTING_COMMENT'
+ | 'ADMIN_CHAT';
+ /**
+ * Format: int64
+ * @description 메인문제ID(메인 문제에 대한 질문일 경우)
+ */
+ problemId?: number;
+ /**
+ * Format: int64
+ * @description 포인팅ID(포인팅에 대한 질문일 경우)
+ */
+ pointingId?: number;
+ };
+ QnACheckResp: {
+ /** Format: int64 */
+ id: number;
+ isExist: boolean;
+ };
+ NotificationResp: {
+ /** Format: int64 */
+ id: number;
+ /** Format: int64 */
+ studentId: number;
+ studentName?: string;
+ /** @enum {string} */
+ type: 'ASSIGNMENT' | 'SYSTEM' | 'QNA' | 'MARKETING';
+ title: string;
+ /** Format: int64 */
+ payload?: number;
+ url?: string;
+ isRead: boolean;
+ /** Format: date-time */
+ createdAt: string;
+ };
+ 'StudentPushDTO.UpdateTokenRequest': {
+ fcmToken: string;
+ };
+ 'StudentPasswordDTO.UpdatePasswordRequest': {
+ newPassword: string;
+ };
+ StudentSignupReq: {
+ email: string;
+ password: string;
+ };
+ StudentTokenResp: {
+ /** Format: int64 */
+ id: number;
+ isGteFourteen?: boolean;
+ isAgreeServiceUsage?: boolean;
+ isAgreePersonalInformation?: boolean;
+ isAgreeReceiveMarketing?: boolean;
+ /** Format: date-time */
+ agreeAt?: string;
+ email: string;
+ name: string;
+ /** Format: date */
+ birth?: string;
+ /** @enum {string} */
+ gender?: 'MALE' | 'FEMALE';
+ phoneNumber?: string;
+ /** @enum {string} */
+ mobileCarrier?: 'KT' | 'SKT' | 'LG' | 'KT_MVNO' | 'SKT_MVNO' | 'LG_MVNO';
+ /** Format: date-time */
+ phoneNumberVerifiedAt?: string;
+ /** @enum {string} */
+ grade: 'ONE' | 'TWO' | 'THREE' | 'N_TIME';
+ /** @enum {string} */
+ selectSubject?: 'MIJUKBUN' | 'HWAKTONG' | 'KEEHA';
+ school?: components['schemas']['SchoolResp'];
+ /** Format: int32 */
+ level?: number;
+ nickname?: string;
+ isAllowPush?: boolean;
+ /** @enum {string} */
+ provider?: 'KAKAO' | 'GOOGLE' | 'APPLE';
+ isFirstLogin: boolean;
+ /** Format: int64 */
+ teacherId?: number;
+ teacherName?: string;
+ token: components['schemas']['JwtResp'];
+ };
+ 'StudentInitialRegisterDTO.Req': {
+ isGteFourteen: boolean;
+ isAgreeServiceUsage: boolean;
+ isAgreePersonalInformation: boolean;
+ isAgreeReceiveMarketing?: boolean;
+ email?: string;
+ name: string;
+ /** Format: date */
+ birth?: string;
+ /** @enum {string} */
+ gender?: 'MALE' | 'FEMALE';
+ phoneNumber?: string;
+ /** @enum {string} */
+ mobileCarrier?: 'KT' | 'SKT' | 'LG' | 'KT_MVNO' | 'SKT_MVNO' | 'LG_MVNO';
+ /** Format: date-time */
+ phoneNumberVerifiedAt?: string;
+ /** @enum {string} */
+ grade: 'ONE' | 'TWO' | 'THREE' | 'N_TIME';
+ /** @enum {string} */
+ selectSubject?: 'MIJUKBUN' | 'HWAKTONG' | 'KEEHA';
+ /** Format: int64 */
+ schoolId?: number;
+ /** Format: int32 */
+ level?: number;
+ nickname?: string;
+ };
+ SocialLoginReq: {
+ /** @enum {string} */
+ provider: 'KAKAO' | 'GOOGLE' | 'APPLE';
+ redirectUri: string;
+ };
+ SocialLoginUrlResp: {
+ /** @enum {string} */
+ provider: 'KAKAO' | 'GOOGLE' | 'APPLE';
+ loginUrl: string;
+ };
+ PreSignedReq: {
+ fileName: string;
+ /**
+ * @description 파일 유형 (지정하지 않으면 서버가 확장자로 판단)
+ * @enum {string}
+ */
+ fileType?: 'IMAGE' | 'DOCUMENT' | 'OTHER';
+ };
+ PreSignedResp: {
+ file: components['schemas']['UploadFileResp'];
+ contentDisposition: string;
+ uploadUrl: string;
+ };
+ /** @description 휴대폰 인증 코드 검증 요청 */
+ PhoneAuthVerifyRequest: {
+ /** @description 휴대폰 번호 */
+ phone: string;
+ /** @description 인증 용도 (예: signup, reset-password 등) */
+ purpose?: string;
+ /** @description 인증 코드 */
+ code: string;
+ };
+ SimpleSuccessResp: {
+ success?: boolean;
+ message?: string;
+ };
+ /** @description 휴대폰 인증 코드 발송 요청 */
+ PhoneAuthSendRequest: {
+ /** @description 휴대폰 번호 */
+ phone: string;
+ /** @description 인증 용도 (예: signup, reset-password 등) */
+ purpose?: string;
+ };
+ /** @description 휴대폰 인증 코드 재발송 요청 */
+ PhoneAuthResendRequest: {
+ /** @description 휴대폰 번호 */
+ phone: string;
+ /** @description 인증 용도 (예: signup, reset-password 등) */
+ purpose?: string;
+ };
+ AdminCreateRequest: {
+ email: string;
+ password: string;
+ };
+ TeacherCreateRequest: {
+ name: string;
+ email: string;
+ password: string;
+ };
+ SchoolSaveRespDTO: {
+ /** Format: int32 */
+ count?: number;
+ };
+ PublishCreateRequest: {
+ /** Format: int64 */
+ problemSetId: number;
+ /** Format: int64 */
+ studentId: number;
+ /** Format: date */
+ publishAt: string;
+ };
+ PointingWithFeedbackResp: {
+ /** Format: int64 */
+ id: number;
+ /** Format: int32 */
+ no: number;
+ questionContent: string;
+ commentContent: string;
+ concepts: components['schemas']['ConceptResp'][];
+ isUnderstood?: boolean;
+ isScrapped?: boolean;
+ };
+ ProblemRef: {
+ /**
+ * Format: int64
+ * @description 부모문제인 경우 -> 첫 새끼문제, 새끼문제일 경우 -> 다음 새끼문제
+ */
+ prevChildId?: number;
+ /**
+ * Format: int64
+ * @description 새끼문제일 경우에만 다음 새끼문제
+ */
+ nextChildId?: number;
+ /**
+ * Format: int64
+ * @description 새끼문제일 경우에만 부모문제Id
+ */
+ parentId?: number;
+ };
+ ProblemScrapInfo: {
+ isProblemScrapped?: boolean;
+ isReadingTipScrapped?: boolean;
+ isOneStepMoreScrapped?: boolean;
+ scrappedPointingIds?: number[];
+ };
+ ProblemWithStudyInfoResp: {
+ /** Format: int32 */
+ no?: number;
+ /** Format: int64 */
+ id: number;
+ /** @enum {string} */
+ problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
+ /** Format: int64 */
+ parentProblem?: number;
+ parentProblemTitle?: string;
+ customId: string;
+ /** @enum {string} */
+ createType: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
+ practiceTest: components['schemas']['PracticeTestResp'];
+ /** Format: int32 */
+ practiceTestNo: number;
+ problemContent: string;
+ title: string;
+ /** @enum {string} */
+ answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
+ /** Format: int32 */
+ answer: number;
+ /** Format: int32 */
+ difficulty: number;
+ /** Format: int32 */
+ recommendedTimeSec: number;
+ memo: string;
+ concepts: components['schemas']['ConceptResp'][];
+ mainAnalysisImage: components['schemas']['UploadFileResp'];
+ mainHandAnalysisImage: components['schemas']['UploadFileResp'];
+ readingTipContent: string;
+ oneStepMoreContent: string;
+ studentDisplayParentName?: string;
+ studentDisplayName?: string;
+ pointings: components['schemas']['PointingWithFeedbackResp'][];
+ /** @enum {string} */
+ progress?: 'CORRECT' | 'INCORRECT' | 'SEMI_CORRECT' | 'NONE';
+ /** Format: int32 */
+ submitAnswer: number;
+ isCorrect: boolean;
+ isDone: boolean;
+ scrapInfo?: components['schemas']['ProblemScrapInfo'];
+ childProblems: components['schemas']['ProblemWithStudyInfoResp'][];
+ ref?: components['schemas']['ProblemRef'];
+ };
+ PublishProblemGroupResp: {
+ /** Format: int32 */
+ no: number;
+ /** Format: int64 */
+ problemId: number;
+ /** @enum {string} */
+ progress: 'DONE' | 'DOING' | 'NONE';
+ problem: components['schemas']['ProblemWithStudyInfoResp'];
+ childProblems: components['schemas']['ProblemWithStudyInfoResp'][];
+ };
+ PublishResp: {
+ /** Format: int64 */
+ id: number;
+ /** Format: date */
+ publishAt: string;
+ /** @enum {string} */
+ progress: 'DONE' | 'DOING' | 'NONE';
+ problemSet: components['schemas']['ProblemSetResp'];
+ data: components['schemas']['PublishProblemGroupResp'][];
+ };
+ PointingCreateRequest: {
+ /** Format: int32 */
+ no?: number;
+ questionContent?: string;
+ commentContent?: string;
+ concepts?: number[];
+ };
+ ProblemCreateRequest: {
+ /** Format: int64 */
+ parentProblemId?: number;
+ /** @enum {string} */
+ createType?: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
+ /** Format: int64 */
+ practiceTestId?: number;
+ /** Format: int32 */
+ practiceTestNo?: number;
+ /** Format: int32 */
+ no?: number;
+ /** @enum {string} */
+ problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
+ title: string;
+ concepts?: number[];
+ /** @enum {string} */
+ answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
+ /** Format: int32 */
+ answer: number;
+ /** Format: int32 */
+ difficulty: number;
+ /** Format: int32 */
+ recommendedTimeSec: number;
+ memo?: string;
+ problemContent: string;
+ pointings?: components['schemas']['PointingCreateRequest'][];
+ /** Format: int64 */
+ mainAnalysisImageId?: number;
+ /** Format: int64 */
+ mainHandAnalysisImageId?: number;
+ readingTipContent?: string;
+ oneStepMoreContent?: string;
+ };
+ ProblemEntireCreateRequest: {
+ childProblems?: components['schemas']['ProblemCreateRequest'][];
+ };
+ ProblemSetCreateRequest: {
+ title: string;
+ problems?: components['schemas']['ProblemSetItemRequest'][];
+ };
+ PracticeTestCreateRequest: {
+ /** Format: int32 */
+ year: number;
+ /** Format: int32 */
+ month: number;
+ /** Format: int32 */
+ grade: number;
+ name: string;
+ };
+ NotificationSendRequest: {
+ /**
+ * @description 알림 종류
+ * @enum {string}
+ */
+ type: 'ASSIGNMENT' | 'SYSTEM' | 'QNA' | 'MARKETING';
+ /** @description 알림 제목 */
+ title: string;
+ /**
+ * Format: int64
+ * @description 페이로드 (관련 리소스 ID)
+ */
+ payload?: number;
+ /** @description 앱 딥링크 URL */
+ url?: string;
+ /** @description 전체 학생 발송 여부 */
+ isAll: boolean;
+ /** @description 발송 대상 학생 ID 리스트 (isAll=false일 때 필수) */
+ studentIds?: number[];
+ };
+ NotificationSendResp: {
+ /**
+ * Format: int32
+ * @description 발송 대상 학생 수
+ */
+ targetCount: number;
+ /**
+ * Format: int32
+ * @description 알림 저장 성공 수
+ */
+ savedCount: number;
+ /**
+ * Format: int32
+ * @description FCM 발송 시도 수
+ */
+ fcmAttemptCount: number;
+ /** @description FCM 지원 여부 */
+ fcmSupported: boolean;
+ };
+ DiagnosisCreateReq: {
+ /** Format: int64 */
+ studentId?: number;
+ content?: string;
+ };
+ ConceptCreateRequest: {
+ name: string;
+ /** Format: int64 */
+ categoryId: number;
+ };
+ ConceptCategoryCreateRequest: {
+ name: string;
+ };
+ AdminTokenResp: {
+ /** Format: int64 */
+ id: number;
+ email: string;
+ token: components['schemas']['JwtResp'];
+ };
+ AdminLoginReq: {
+ email: string;
+ password: string;
+ };
+ ListRespPublishResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['PublishResp'][];
+ };
+ PublishStudentProgressResp: {
+ /** Format: double */
+ progress: number;
+ };
+ ListRespStudentResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['StudentResp'][];
+ };
+ PageRespNotListQnAGroupByWeekResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['QnAGroupByWeekResp'];
+ };
+ QnAGroupByWeekResp: {
+ groups?: components['schemas']['QnAGroupItem'][];
+ };
+ QnAGroupItem: {
+ /** Format: int32 */
+ order?: number;
+ weekName?: string;
+ data?: components['schemas']['QnAMetaResp'][];
+ };
+ ChatSearchResultResp: {
+ /** Format: int64 */
+ qnaId?: number;
+ qnaTitle?: string;
+ /** @enum {string} */
+ qnaType?:
+ | 'PROBLEM_CONTENT'
+ | 'PROBLEM_POINTING_QUESTION'
+ | 'PROBLEM_POINTING_COMMENT'
+ | 'PROBLEM_MAIN_ANALYSIS'
+ | 'PROBLEM_MAIN_HAND_ANALYSIS'
+ | 'PROBLEM_READING_TIP_CONTENT'
+ | 'PROBLEM_ONE_STEP_MORE'
+ | 'CHILD_PROBLEM_CONTENT'
+ | 'CHILD_PROBLEM_POINTING_QUESTION'
+ | 'CHILD_PROBLEM_POINTING_COMMENT'
+ | 'ADMIN_CHAT';
+ /** Format: date */
+ publishDate?: string;
+ /** Format: int64 */
+ matchedChatId?: number;
+ matchedChatPreview?: string;
+ };
+ PageRespNotListListChatSearchResultResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['ChatSearchResultResp'][];
+ };
+ QnASearchResp: {
+ qnaResults?: components['schemas']['PageRespNotListQnAGroupByWeekResp'];
+ chatResults?: components['schemas']['PageRespNotListListChatSearchResultResp'];
+ };
+ ListRespNoticeResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['NoticeResp'][];
+ };
+ ListRespTrashItemResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['TrashItemResp'][];
+ };
+ /** @description 휴지통 항목 */
+ TrashItemResp: {
+ /**
+ * Format: int64
+ * @description 항목 ID
+ */
+ id: number;
+ /**
+ * @description 항목 유형 (FOLDER/SCRAP)
+ * @enum {string}
+ */
+ type: 'FOLDER' | 'SCRAP';
+ /** @description 항목 이름 (폴더명 또는 문제 제목) */
+ name: string;
+ /**
+ * Format: int32
+ * @description 포함된 스크랩 수 (폴더인 경우)
+ */
+ itemCount?: number;
+ /** @description 썸네일 URL (스크랩: 스크랩 썸네일, 폴더: 폴더 썸네일) */
+ thumbnailUrl?: string;
+ /** @description 최근 스크랩 2개의 썸네일 URL (폴더인 경우) */
+ top2ScrapThumbnail?: string[];
+ /**
+ * Format: date-time
+ * @description 삭제일시
+ */
+ deletedAt: string;
+ /**
+ * Format: int32
+ * @description 영구삭제까지 남은 일수
+ */
+ daysUntilPermanentDelete: number;
+ };
+ /** @description 스크랩/폴더 목록 아이템 */
+ ScrapListItemResp: {
+ /**
+ * @description 유형 (FOLDER/SCRAP)
+ * @enum {string}
+ */
+ type: 'FOLDER' | 'SCRAP';
+ /**
+ * Format: int64
+ * @description 아이템 ID
+ */
+ id: number;
+ /** @description 표시 이름 (폴더명 또는 문제 제목) */
+ name: string;
+ /**
+ * Format: int64
+ * @description 폴더 ID (스크랩일 때)
+ */
+ folderId?: number;
+ /** @description 썸네일 URL (스크랩일 때) */
+ thumbnailUrl?: string;
+ /**
+ * Format: date-time
+ * @description 생성일시
+ */
+ createdAt: string;
+ /**
+ * Format: date-time
+ * @description 수정일시
+ */
+ updatedAt: string;
+ };
+ /** @description 스크랩 검색 응답 */
+ ScrapSearchResp: {
+ /** @description 검색된 폴더 목록 */
+ folders: components['schemas']['ScrapFolderResp'][];
+ /** @description 검색된 스크랩 목록 */
+ scraps: components['schemas']['ScrapListItemResp'][];
+ };
+ ListRespScrapFolderResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['ScrapFolderResp'][];
+ };
+ ListRespScrapListItemResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['ScrapListItemResp'][];
+ };
+ ListRespSchoolResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['SchoolResp'][];
+ };
+ ListRespUploadFileResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['UploadFileResp'][];
+ };
+ ListRespNotificationResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['NotificationResp'][];
+ };
+ NotificationUnreadCountResp: {
+ /**
+ * Format: int32
+ * @description 전체 알림 개수
+ */
+ totalCount: number;
+ /**
+ * Format: int32
+ * @description 읽지 않은 알림 개수
+ */
+ unreadCount: number;
+ latestNotification?: components['schemas']['NotificationResp'];
+ };
+ NoticeUnreadCountResp: {
+ /** Format: int64 */
+ totalCount?: number;
+ /** Format: int64 */
+ unreadCount?: number;
+ latestNotice?: components['schemas']['NoticeResp'];
+ };
+ ListRespDiagnosisResp: {
+ /** Format: int32 */
+ total: number;
+ data: components['schemas']['DiagnosisResp'][];
+ };
+ BooleanResp: {
+ value: boolean;
+ };
+ /** @description Q&A 채팅 이벤트 (SSE event name: chat) */
+ QnAChatEvent: {
+ /**
+ * @description Q&A 채팅 이벤트 타입
+ * @example CREATED
+ * @enum {string}
+ */
+ type?: 'CREATED' | 'UPDATED' | 'DELETED';
+ /**
+ * Format: int64
+ * @description Q&A ID
+ * @example 1
+ */
+ qnaId?: number;
+ /**
+ * Format: int64
+ * @description 채팅 메시지 ID
+ * @example 123
+ */
+ chatId?: number;
+ /**
+ * Format: int64
+ * @description 발신자 ID
+ * @example 1
+ */
+ senderId?: number;
+ /**
+ * @description 발신자 타입
+ * @example STUDENT
+ * @enum {string}
+ */
+ senderType?: 'ADMIN' | 'STUDENT' | 'TEACHER';
+ /**
+ * @description 메시지 내용
+ * @example 안녕하세요
+ */
+ content?: string;
+ /**
+ * Format: int64
+ * @description 답장 대상 메시지 ID (답장인 경우)
+ * @example 122
+ */
+ replyToId?: number | null;
+ /**
+ * Format: date-time
+ * @description 이벤트 발생 시간
+ */
+ timestamp?: string;
+ };
+ /** @description Q&A 읽음 상태 이벤트 (SSE event name: read_status) */
+ QnAReadStatusEvent: {
+ /**
+ * Format: int64
+ * @description Q&A ID
+ * @example 1
+ */
+ qnaId?: number;
+ /**
+ * Format: int64
+ * @description 읽은 사용자 ID
+ * @example 1
+ */
+ userId?: number;
+ /**
+ * @description 사용자 타입
+ * @example TEACHER
+ * @enum {string}
+ */
+ userType?: 'ADMIN' | 'STUDENT' | 'TEACHER';
+ /**
+ * Format: date-time
+ * @description 읽은 시간
+ */
+ readAt?: string;
+ };
+ PageRespTeacherResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['TeacherResp'][];
+ };
+ PageRespStudentResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['StudentResp'][];
+ };
+ PageRespProblemMetaResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['ProblemMetaResp'][];
+ };
+ ProblemCustomIdResp: {
+ customId?: string;
+ };
+ PageRespProblemSetResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['ProblemSetResp'][];
+ };
+ PageRespPracticeTestResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['PracticeTestResp'][];
+ };
+ PageRespConceptResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['ConceptResp'][];
+ };
+ PageRespConceptCategoryResp: {
+ /** Format: int32 */
+ page: number;
+ /** Format: int32 */
+ size: number;
+ /** Format: int32 */
+ lastPage: number;
+ data: components['schemas']['ConceptCategoryResp'][];
+ };
+ ScrapBatchDeleteRequest: {
+ /** @description 삭제할 항목 목록 (폴더/스크랩 혼합) */
+ items: components['schemas']['Item'][];
+ };
+ ScrapBatchPermanentDeleteRequest: {
+ /** @description 영구 삭제할 항목 목록 (폴더/스크랩 혼합) */
+ items: components['schemas']['Item'][];
+ };
+ UnscrapFromProblemRequest: {
+ /**
+ * Format: int64
+ * @description 문제 ID
+ * @example 123
+ */
+ problemId: number;
+ };
+ UnscrapFromPointingRequest: {
+ /**
+ * Format: int64
+ * @description 포인팅 ID
+ * @example 456
+ */
+ pointingId: number;
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export interface operations {
+ updateChat: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ chatId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ChatUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
+ };
+ };
+ deleteChat: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ chatId: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
+ };
+ };
+ update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['NoticeUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['NoticeResp'];
+ };
+ };
+ };
+ };
+ delete: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ updateScrap: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ scrapId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapDetailResp'];
+ };
+ };
+ };
+ };
+ updateScrapThumbnail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ scrapId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapThumbnailUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapDetailResp'];
+ };
+ };
+ };
+ };
+ updateScrapText: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ scrapId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapTextBoxUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapDetailResp'];
+ };
+ };
+ };
+ };
+ updateScrapName: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ scrapId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapNameUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapDetailResp'];
+ };
+ };
+ };
+ };
+ getHandwriting: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ scrapId: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapHandwritingResp'];
+ };
+ };
+ };
+ };
+ updateHandwriting: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ scrapId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapHandwritingUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapHandwritingResp'];
+ };
+ };
+ };
+ };
+ deleteHandwriting: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ scrapId: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ restoreTrash: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapBatchRestoreRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ moveScraps: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapBatchMoveRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ListRespScrapDetailResp'];
+ };
+ };
+ };
+ };
+ getFolderDetail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapFolderResp'];
+ };
+ };
+ };
+ };
+ updateFolder: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFolderUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapFolderResp'];
+ };
+ };
+ };
+ };
+ updateFolderThumbnail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFolderThumbnailUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapFolderResp'];
+ };
+ };
+ };
+ };
+ updateFolderName: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFolderNameUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapFolderResp'];
+ };
+ };
+ };
+ };
+ getById: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ qnaId: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
+ };
+ };
+ update_1: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ qnaId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['QnAUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
+ };
+ };
+ delete_1: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ qnaId: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ updateChat_1: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ chatId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ChatUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
+ };
+ };
+ deleteChat_1: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ chatId: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
+ };
+ };
+ readNotice: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ noticeId: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['NoticeResp'];
+ };
+ };
+ };
+ };
+ me: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['StudentResp'];
+ };
+ };
+ };
+ };
+ update_2: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['StudentUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['StudentResp'];
+ };
+ };
+ };
+ };
+ update_3: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['TeacherUpdateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['TeacherResp'];
+ };
+ };
+ };
+ };
+ assignStudentsToTeacher: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ teacherId: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['TeacherStudentAssignReq'];
+ };
};
- TeacherStudentAssignReq: {
- students: number[];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['TeacherResp'];
+ };
+ };
};
- 'ChildProblemUpdateDTO.Request': {
- /** Format: int64 */
- id?: number;
- /** Format: int32 */
- no?: number;
- problemContent?: string;
- /** @enum {string} */
- answerType?: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
- /** Format: int32 */
- answer?: number;
- concepts?: number[];
- pointings?: components['schemas']['PointingUpdateRequest'][];
+ };
+ updateChat_2: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ chatId: number;
+ };
+ cookie?: never;
};
- PointingUpdateRequest: {
- /** Format: int64 */
- id?: number;
- /** Format: int32 */
- no?: number;
- questionContent?: string;
- commentContent?: string;
- concepts?: number[];
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ChatUpdateRequest'];
+ };
};
- ProblemUpdateRequest: {
- /** @enum {string} */
- createType?: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
- /** Format: int64 */
- practiceTestId?: number;
- /** Format: int32 */
- practiceTestNo?: number;
- /** Format: int32 */
- no?: number;
- title: string;
- concepts?: number[];
- /** @enum {string} */
- answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
- /** Format: int32 */
- answer: number;
- /** Format: int32 */
- difficulty: number;
- /** Format: int32 */
- recommendedTimeSec: number;
- memo?: string;
- problemContent: string;
- pointings?: components['schemas']['PointingUpdateRequest'][];
- /** Format: int64 */
- mainAnalysisImageId?: number;
- /** Format: int64 */
- mainHandAnalysisImageId?: number;
- readingTipContent?: string;
- oneStepMoreContent?: string;
- childProblems?: components['schemas']['ChildProblemUpdateDTO.Request'][];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
};
- ConceptCategoryResp: {
- /** Format: int64 */
- id: number;
- name: string;
+ };
+ deleteChat_2: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ chatId: number;
+ };
+ cookie?: never;
};
- ConceptResp: {
- /** Format: int64 */
- id: number;
- name: string;
- category: components['schemas']['ConceptCategoryResp'];
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
};
- PointingResp: {
- /** Format: int64 */
- id: number;
- /** Format: int32 */
- no: number;
- questionContent: string;
- commentContent: string;
- concepts: components['schemas']['ConceptResp'][];
+ };
+ getProblem: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- PracticeTestResp: {
- /** Format: int64 */
- id: number;
- /** Format: int32 */
- year: number;
- /** Format: int32 */
- month: number;
- /** Format: int32 */
- grade: number;
- name: string;
- displayName: string;
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ProblemInfoResp'];
+ };
+ };
};
- ProblemInfoResp: {
- /** Format: int64 */
- id: number;
- /** @enum {string} */
- problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
- /** Format: int64 */
- parentProblem?: number;
- parentProblemTitle?: string;
- customId: string;
- /** @enum {string} */
- createType: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
- practiceTest: components['schemas']['PracticeTestResp'];
- /** Format: int32 */
- practiceTestNo: number;
- problemContent: string;
- title: string;
- /** @enum {string} */
- answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
- /** Format: int32 */
- answer: number;
- /** Format: int32 */
- difficulty: number;
- /** Format: int32 */
- recommendedTimeSec: number;
- memo: string;
- concepts: components['schemas']['ConceptResp'][];
- mainAnalysisImage: components['schemas']['UploadFileResp'];
- mainHandAnalysisImage: components['schemas']['UploadFileResp'];
- readingTipContent: string;
- oneStepMoreContent: string;
- pointings: components['schemas']['PointingResp'][];
- childProblems: components['schemas']['ProblemInfoResp'][];
+ };
+ updateProblem: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- ProblemMetaResp: {
- /** Format: int64 */
- id: number;
- /** @enum {string} */
- problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
- /** Format: int64 */
- parentProblem?: number;
- parentProblemTitle?: string;
- customId: string;
- /** @enum {string} */
- createType: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
- practiceTest: components['schemas']['PracticeTestResp'];
- /** Format: int32 */
- practiceTestNo: number;
- problemContent: string;
- title: string;
- /** @enum {string} */
- answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
- /** Format: int32 */
- answer: number;
- /** Format: int32 */
- difficulty: number;
- /** Format: int32 */
- recommendedTimeSec: number;
- memo: string;
- concepts: components['schemas']['ConceptResp'][];
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ProblemUpdateRequest'];
+ };
};
- ProblemSetItemRequest: {
- /** Format: int32 */
- no: number;
- /** Format: int64 */
- problemId: number;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ProblemInfoResp'];
+ };
+ };
};
- ProblemSetUpdateRequest: {
- title: string;
- /** @enum {string} */
- status: 'CONFIRMED' | 'DOING';
- problems?: components['schemas']['ProblemSetItemRequest'][];
+ };
+ deleteProblem: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- ProblemSetItemResp: {
- /** Format: int64 */
- id: number;
- /** Format: int32 */
- no: number;
- problem: components['schemas']['ProblemMetaResp'];
+ };
+ getProblemSet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- ProblemSetResp: {
- /** Format: int64 */
- id: number;
- title: string;
- /** @enum {string} */
- status: 'CONFIRMED' | 'DOING';
- firstProblem: components['schemas']['ProblemMetaResp'];
- problems: components['schemas']['ProblemSetItemResp'][];
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ProblemSetResp'];
+ };
+ };
};
- Request: {
- /** Format: int32 */
- year: number;
- /** Format: int32 */
- month: number;
- /** Format: int32 */
- grade: number;
- name: string;
+ };
+ update_4: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- DiagnosisUpdateReq: {
- content?: string;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ProblemSetUpdateRequest'];
+ };
};
- DiagnosisResp: {
- /** Format: int64 */
- id: number;
- /** Format: int64 */
- studentId?: number;
- /** Format: date-time */
- createdAt?: string;
- content?: string;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ProblemSetResp'];
+ };
+ };
};
- ConceptUpdateRequest: {
- name: string;
- /** Format: int64 */
- categoryId: number;
+ };
+ delete_2: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- ConceptCategoryUpdateRequest: {
- name: string;
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- ChatCreateRequest: {
- /** Format: int64 */
- qnaId: number;
- content: string;
- images?: number[];
+ };
+ toggleStatus: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- NoticeCreateRequest: {
- /** Format: date */
- startAt: string;
- /** Format: date */
- endAt: string;
- content: string;
- /** Format: int64 */
- studentId: number;
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ProblemSetResp'];
+ };
+ };
};
- RefreshReq: {
- refreshToken: string;
+ };
+ update_5: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- JwtResp: {
- accessToken: string;
- refreshToken?: string;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['Request'];
+ };
};
- TeacherTokenResp: {
- /** Format: int64 */
- id: number;
- name: string;
- email: string;
- students: components['schemas']['StudentResp'][];
- token?: components['schemas']['JwtResp'];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['PracticeTestResp'];
+ };
+ };
};
- TeacherLoginReq: {
- email: string;
- password: string;
+ };
+ delete_3: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- PointingFeedbackRequest: {
- /** Format: int64 */
- pointingId: number;
- isUnderstood: boolean;
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- SubmissionRequest: {
- /** Format: int64 */
- publishId: number;
- /** Format: int64 */
- problemId?: number;
- /** Format: int32 */
- submitAnswer?: number;
+ };
+ update_6: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- SubmissionResp: {
- /** @enum {string} */
- progress?: 'CORRECT' | 'INCORRECT' | 'SEMI_CORRECT' | 'NONE';
- /** Format: int32 */
- submitAnswer: number;
- isCorrect: boolean;
- isDone: boolean;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['NoticeUpdateRequest'];
+ };
};
- /** @description problemId, childProblemId, pointingId 중 하나만 입력 가능 */
- QnACreateRequest: {
- /** Format: int64 */
- publishId: number;
- /** @enum {string} */
- type:
- | 'PROBLEM_CONTENT'
- | 'PROBLEM_POINTING_QUESTION'
- | 'PROBLEM_POINTING_COMMENT'
- | 'PROBLEM_MAIN_ANALYSIS'
- | 'PROBLEM_MAIN_HAND_ANALYSIS'
- | 'PROBLEM_READING_TIP_CONTENT'
- | 'PROBLEM_ONE_STEP_MORE'
- | 'CHILD_PROBLEM_CONTENT'
- | 'CHILD_PROBLEM_POINTING_QUESTION'
- | 'CHILD_PROBLEM_POINTING_COMMENT';
- /** Format: int64 */
- problemId?: number;
- /** Format: int64 */
- pointingId?: number;
- question: string;
- images?: number[];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['NoticeResp'];
+ };
+ };
+ };
+ };
+ delete_4: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- QnACheckRequest: {
- /** Format: int64 */
- publishId: number;
- /** @enum {string} */
- type:
- | 'PROBLEM_CONTENT'
- | 'PROBLEM_POINTING_QUESTION'
- | 'PROBLEM_POINTING_COMMENT'
- | 'PROBLEM_MAIN_ANALYSIS'
- | 'PROBLEM_MAIN_HAND_ANALYSIS'
- | 'PROBLEM_READING_TIP_CONTENT'
- | 'PROBLEM_ONE_STEP_MORE'
- | 'CHILD_PROBLEM_CONTENT'
- | 'CHILD_PROBLEM_POINTING_QUESTION'
- | 'CHILD_PROBLEM_POINTING_COMMENT';
- /**
- * Format: int64
- * @description 메인문제ID(메인 문제에 대한 질문일 경우)
- */
- problemId?: number;
- /**
- * Format: int64
- * @description 포인팅ID(포인팅에 대한 질문일 경우)
- */
- pointingId?: number;
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- QnACheckResp: {
- /** Format: int64 */
- id: number;
- isExist: boolean;
+ };
+ getById_1: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- StudentTokenResp: {
- /** Format: int64 */
- id: number;
- name: string;
- /** Format: int32 */
- grade: number;
- isFirstLogin: boolean;
- token: components['schemas']['JwtResp'];
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['DiagnosisResp'];
+ };
+ };
};
- SocialLoginReq: {
- /** @enum {string} */
- provider: 'KAKAO' | 'GOOGLE';
- redirectUri: string;
+ };
+ update_7: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- SocialLoginUrlResp: {
- /** @enum {string} */
- provider: 'KAKAO' | 'GOOGLE';
- loginUrl: string;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['DiagnosisUpdateReq'];
+ };
};
- PreSignedReq: {
- fileName: string;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['DiagnosisResp'];
+ };
+ };
};
- PreSignedResp: {
- file: components['schemas']['UploadFileResp'];
- contentDisposition: string;
- uploadUrl: string;
+ };
+ delete_5: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
};
- AdminCreateRequest: {
- email: string;
- password: string;
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- TeacherCreateRequest: {
- name: string;
- email: string;
- password: string;
+ };
+ update_8: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ conceptId: number;
+ };
+ cookie?: never;
};
- PublishCreateRequest: {
- /** Format: int64 */
- problemSetId: number;
- /** Format: int64 */
- studentId: number;
- /** Format: date */
- publishAt: string;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ConceptUpdateRequest'];
+ };
};
- PointingWithFeedbackResp: {
- /** Format: int64 */
- id: number;
- /** Format: int32 */
- no: number;
- questionContent: string;
- commentContent: string;
- concepts: components['schemas']['ConceptResp'][];
- isUnderstood?: boolean;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ConceptResp'];
+ };
+ };
};
- ProblemRef: {
- /**
- * Format: int64
- * @description 부모문제인 경우 -> 첫 새끼문제, 새끼문제일 경우 -> 다음 새끼문제
- */
- prevChildId?: number;
- /**
- * Format: int64
- * @description 새끼문제일 경우에만 다음 새끼문제
- */
- nextChildId?: number;
- /**
- * Format: int64
- * @description 새끼문제일 경우에만 부모문제Id
- */
- parentId?: number;
+ };
+ delete_6: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ conceptId: number;
+ };
+ cookie?: never;
};
- ProblemWithStudyInfoResp: {
- /** Format: int32 */
- no?: number;
- /** Format: int64 */
- id: number;
- /** @enum {string} */
- problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
- /** Format: int64 */
- parentProblem?: number;
- parentProblemTitle?: string;
- customId: string;
- /** @enum {string} */
- createType: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
- practiceTest: components['schemas']['PracticeTestResp'];
- /** Format: int32 */
- practiceTestNo: number;
- problemContent: string;
- title: string;
- /** @enum {string} */
- answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
- /** Format: int32 */
- answer: number;
- /** Format: int32 */
- difficulty: number;
- /** Format: int32 */
- recommendedTimeSec: number;
- memo: string;
- concepts: components['schemas']['ConceptResp'][];
- mainAnalysisImage: components['schemas']['UploadFileResp'];
- mainHandAnalysisImage: components['schemas']['UploadFileResp'];
- readingTipContent: string;
- oneStepMoreContent: string;
- studentDisplayParentName?: string;
- studentDisplayName?: string;
- pointings: components['schemas']['PointingWithFeedbackResp'][];
- /** @enum {string} */
- progress?: 'CORRECT' | 'INCORRECT' | 'SEMI_CORRECT' | 'NONE';
- /** Format: int32 */
- submitAnswer: number;
- isCorrect: boolean;
- isDone: boolean;
- childProblems: components['schemas']['ProblemWithStudyInfoResp'][];
- ref?: components['schemas']['ProblemRef'];
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ updateCategory: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ categoryId: number;
+ };
+ cookie?: never;
};
- PublishProblemGroupResp: {
- /** Format: int32 */
- no: number;
- /** Format: int64 */
- problemId: number;
- /** @enum {string} */
- progress: 'DONE' | 'DOING' | 'NONE';
- problem: components['schemas']['ProblemWithStudyInfoResp'];
- childProblems: components['schemas']['ProblemWithStudyInfoResp'][];
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ConceptCategoryUpdateRequest'];
+ };
};
- PublishResp: {
- /** Format: int64 */
- id: number;
- /** Format: date */
- publishAt: string;
- /** @enum {string} */
- progress: 'DONE' | 'DOING' | 'NONE';
- problemSet: components['schemas']['ProblemSetResp'];
- data: components['schemas']['PublishProblemGroupResp'][];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ConceptCategoryResp'];
+ };
+ };
};
- PointingCreateRequest: {
- /** Format: int32 */
- no?: number;
- questionContent?: string;
- commentContent?: string;
- concepts?: number[];
+ };
+ deleteCategory: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ categoryId: number;
+ };
+ cookie?: never;
};
- ProblemCreateRequest: {
- /** Format: int64 */
- parentProblemId?: number;
- /** @enum {string} */
- createType?: 'GICHUL_PROBLEM' | 'VARIANT_PROBLEM' | 'CREATION_PROBLEM';
- /** Format: int64 */
- practiceTestId?: number;
- /** Format: int32 */
- practiceTestNo?: number;
- /** Format: int32 */
- no?: number;
- /** @enum {string} */
- problemType: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
- title: string;
- concepts?: number[];
- /** @enum {string} */
- answerType: 'MULTIPLE_CHOICE' | 'SHORT_ANSWER';
- /** Format: int32 */
- answer: number;
- /** Format: int32 */
- difficulty: number;
- /** Format: int32 */
- recommendedTimeSec: number;
- memo?: string;
- problemContent: string;
- pointings?: components['schemas']['PointingCreateRequest'][];
- /** Format: int64 */
- mainAnalysisImageId?: number;
- /** Format: int64 */
- mainHandAnalysisImageId?: number;
- readingTipContent?: string;
- oneStepMoreContent?: string;
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- ProblemEntireCreateRequest: {
- childProblems?: components['schemas']['ProblemCreateRequest'][];
+ };
+ addChat: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- ProblemSetCreateRequest: {
- title: string;
- problems?: components['schemas']['ProblemSetItemRequest'][];
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ChatCreateRequest'];
+ };
};
- PracticeTestCreateRequest: {
- /** Format: int32 */
- year: number;
- /** Format: int32 */
- month: number;
- /** Format: int32 */
- grade: number;
- name: string;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnAResp'];
+ };
+ };
};
- DiagnosisCreateReq: {
- /** Format: int64 */
- studentId?: number;
- content?: string;
+ };
+ getsAll: {
+ parameters: {
+ query: {
+ studentId: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- ConceptCreateRequest: {
- name: string;
- /** Format: int64 */
- categoryId: number;
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ListRespNoticeResp'];
+ };
+ };
};
- ConceptCategoryCreateRequest: {
- name: string;
+ };
+ create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- AdminTokenResp: {
- /** Format: int64 */
- id: number;
- email: string;
- token: components['schemas']['JwtResp'];
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['NoticeCreateRequest'];
+ };
};
- AdminLoginReq: {
- email: string;
- password: string;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['NoticeResp'];
+ };
+ };
};
- ListRespPublishResp: {
- /** Format: int32 */
- total: number;
- data: components['schemas']['PublishResp'][];
+ };
+ updatePushToken: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- PublishStudentProgressResp: {
- /** Format: double */
- progress: number;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['TeacherPushDTO.UpdateTokenRequest'];
+ };
};
- ListRespStudentResp: {
- /** Format: int32 */
- total: number;
- data: components['schemas']['StudentResp'][];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['TeacherResp'];
+ };
+ };
};
- PageRespNotListQnAGroupByWeekResp: {
- /** Format: int32 */
- page: number;
- /** Format: int32 */
- size: number;
- /** Format: int32 */
- lastPage: number;
- data: components['schemas']['QnAGroupByWeekResp'];
+ };
+ toggleAllowPush: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- QnAGroupByWeekResp: {
- groups?: components['schemas']['QnAGroupItem'][];
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['TeacherResp'];
+ };
+ };
};
- QnAGroupItem: {
- /** Format: int32 */
- order?: number;
- weekName?: string;
- data?: components['schemas']['QnAMetaResp'][];
+ };
+ refresh: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- ListRespNoticeResp: {
- /** Format: int32 */
- total: number;
- data: components['schemas']['NoticeResp'][];
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['RefreshReq'];
+ };
};
- NoticeUnreadCountResp: {
- /** Format: int64 */
- totalCount?: number;
- /** Format: int64 */
- unreadCount?: number;
- latestNotice?: components['schemas']['NoticeResp'];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['TeacherTokenResp'];
+ };
+ };
};
- ListRespDiagnosisResp: {
- /** Format: int32 */
- total: number;
- data: components['schemas']['DiagnosisResp'][];
+ };
+ login: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- PageRespTeacherResp: {
- /** Format: int32 */
- page: number;
- /** Format: int32 */
- size: number;
- /** Format: int32 */
- lastPage: number;
- data: components['schemas']['TeacherResp'][];
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['TeacherLoginReq'];
+ };
};
- PageRespStudentResp: {
- /** Format: int32 */
- page: number;
- /** Format: int32 */
- size: number;
- /** Format: int32 */
- lastPage: number;
- data: components['schemas']['StudentResp'][];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['TeacherTokenResp'];
+ };
+ };
};
- PageRespProblemMetaResp: {
- /** Format: int32 */
- page: number;
- /** Format: int32 */
- size: number;
- /** Format: int32 */
- lastPage: number;
- data: components['schemas']['ProblemMetaResp'][];
+ };
+ feedback: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- ProblemCustomIdResp: {
- customId?: string;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['PointingFeedbackRequest'];
+ };
};
- PageRespProblemSetResp: {
- /** Format: int32 */
- page: number;
- /** Format: int32 */
- size: number;
- /** Format: int32 */
- lastPage: number;
- data: components['schemas']['ProblemSetResp'][];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- PageRespPracticeTestResp: {
- /** Format: int32 */
- page: number;
- /** Format: int32 */
- size: number;
- /** Format: int32 */
- lastPage: number;
- data: components['schemas']['PracticeTestResp'][];
+ };
+ submit: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- PageRespConceptResp: {
- /** Format: int32 */
- page: number;
- /** Format: int32 */
- size: number;
- /** Format: int32 */
- lastPage: number;
- data: components['schemas']['ConceptResp'][];
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['SubmissionRequest'];
+ };
};
- PageRespConceptCategoryResp: {
- /** Format: int32 */
- page: number;
- /** Format: int32 */
- size: number;
- /** Format: int32 */
- lastPage: number;
- data: components['schemas']['ConceptCategoryResp'][];
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['SubmissionResp'];
+ };
+ };
};
};
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-}
-export type $defs = Record;
-export interface operations {
- updateChat: {
+ createScrap: {
parameters: {
query?: never;
header?: never;
- path: {
- chatId: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['ChatUpdateRequest'];
+ 'application/json': components['schemas']['ScrapCreateRequest'];
};
};
responses: {
@@ -2116,21 +5208,45 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnAResp'];
+ '*/*': components['schemas']['ScrapDetailResp'];
};
};
};
};
- deleteChat: {
+ deleteScraps: {
parameters: {
query?: never;
header?: never;
- path: {
- chatId: number;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapBatchDeleteRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
};
+ };
+ };
+ toggleScrapFromReadingTip: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFromReadingTipCreateRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -2138,23 +5254,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnAResp'];
+ '*/*': components['schemas']['ScrapToggleResp'];
};
};
};
};
- update: {
+ toggleScrapFromProblem: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['NoticeUpdateRequest'];
+ 'application/json': components['schemas']['ScrapFromProblemCreateRequest'];
};
};
responses: {
@@ -2164,41 +5278,47 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['NoticeResp'];
+ '*/*': components['schemas']['ScrapToggleResp'];
};
};
};
};
- delete: {
+ toggleScrapFromPointing: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFromPointingCreateRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['ScrapToggleResp'];
+ };
};
};
};
- getById: {
+ toggleScrapFromOneStepMore: {
parameters: {
query?: never;
header?: never;
- path: {
- qnaId: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFromOneStepMoreCreateRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -2206,23 +5326,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnAResp'];
+ '*/*': components['schemas']['ScrapToggleResp'];
};
};
};
};
- update_1: {
+ createScrapFromProblem: {
parameters: {
query?: never;
header?: never;
- path: {
- qnaId: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['QnAUpdateRequest'];
+ 'application/json': components['schemas']['ScrapFromProblemCreateRequest'];
};
};
responses: {
@@ -2232,21 +5350,23 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnAResp'];
+ '*/*': components['schemas']['ScrapDetailResp'];
};
};
};
};
- delete_1: {
+ unscrapFromProblem: {
parameters: {
query?: never;
header?: never;
- path: {
- qnaId: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['UnscrapFromProblemRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -2257,18 +5377,40 @@ export interface operations {
};
};
};
- updateChat_1: {
+ createScrapFromPointing: {
parameters: {
query?: never;
header?: never;
- path: {
- chatId: number;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFromPointingCreateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['ScrapDetailResp'];
+ };
};
+ };
+ };
+ unscrapFromPointing: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['ChatUpdateRequest'];
+ 'application/json': components['schemas']['UnscrapFromPointingRequest'];
};
};
responses: {
@@ -2277,22 +5419,22 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['QnAResp'];
- };
+ content?: never;
};
};
};
- deleteChat_1: {
+ createScrapFromImage: {
parameters: {
query?: never;
header?: never;
- path: {
- chatId: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFromImageCreateRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -2300,18 +5442,16 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnAResp'];
+ '*/*': components['schemas']['ScrapDetailResp'];
};
};
};
};
- readNotice: {
+ getFolders: {
parameters: {
query?: never;
header?: never;
- path: {
- noticeId: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -2322,19 +5462,23 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['NoticeResp'];
+ '*/*': components['schemas']['ListRespScrapFolderResp'];
};
};
};
};
- me: {
+ createFolder: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapFolderCreateRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -2342,12 +5486,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['StudentResp'];
+ '*/*': components['schemas']['ScrapFolderResp'];
};
};
};
};
- update_2: {
+ deleteFolders: {
parameters: {
query?: never;
header?: never;
@@ -2356,7 +5500,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['StudentUpdateRequest'];
+ 'application/json': number[];
};
};
responses: {
@@ -2365,26 +5509,20 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['StudentResp'];
- };
+ content?: never;
};
};
};
- update_3: {
+ gets: {
parameters: {
- query?: never;
- header?: never;
- path: {
- id: number;
+ query?: {
+ query?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['TeacherUpdateRequest'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -2392,23 +5530,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['TeacherResp'];
+ '*/*': components['schemas']['PageRespNotListQnAGroupByWeekResp'];
};
};
};
};
- assignStudentsToTeacher: {
+ create_1: {
parameters: {
query?: never;
header?: never;
- path: {
- teacherId: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['TeacherStudentAssignReq'];
+ 'application/json': components['schemas']['QnACreateRequest'];
};
};
responses: {
@@ -2418,21 +5554,23 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['TeacherResp'];
+ '*/*': components['schemas']['QnAResp'];
};
};
};
};
- getProblem: {
+ checkExists: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['QnACheckRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -2440,23 +5578,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemInfoResp'];
+ '*/*': components['schemas']['QnACheckResp'];
};
};
};
};
- updateProblem: {
+ addChat_1: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['ProblemUpdateRequest'];
+ 'application/json': components['schemas']['ChatCreateRequest'];
};
};
responses: {
@@ -2466,17 +5602,17 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemInfoResp'];
+ '*/*': components['schemas']['QnAResp'];
};
};
};
};
- deleteProblem: {
+ readNotification: {
parameters: {
query?: never;
header?: never;
path: {
- id: number;
+ notificationId: number;
};
cookie?: never;
};
@@ -2487,17 +5623,17 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['NotificationResp'];
+ };
};
};
};
- getProblemSet: {
+ readAllNotifications: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -2507,24 +5643,20 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['ProblemSetResp'];
- };
+ content?: never;
};
};
};
- update_4: {
+ updatePushToken_1: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['ProblemSetUpdateRequest'];
+ 'application/json': components['schemas']['StudentPushDTO.UpdateTokenRequest'];
};
};
responses: {
@@ -2534,18 +5666,16 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemSetResp'];
+ '*/*': components['schemas']['StudentResp'];
};
};
};
};
- delete_2: {
+ toggleAllowPush_1: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -2555,20 +5685,24 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['StudentResp'];
+ };
};
};
};
- toggleStatus: {
+ changePassword: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['StudentPasswordDTO.UpdatePasswordRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -2576,23 +5710,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemSetResp'];
+ '*/*': components['schemas']['StudentResp'];
};
};
};
};
- update_5: {
+ signup: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['Request'];
+ 'application/json': components['schemas']['StudentSignupReq'];
};
};
responses: {
@@ -2602,43 +5734,45 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PracticeTestResp'];
+ '*/*': components['schemas']['StudentTokenResp'];
};
};
};
};
- delete_3: {
+ register: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['StudentInitialRegisterDTO.Req'];
+ };
+ };
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['StudentResp'];
+ };
};
};
};
- update_6: {
+ refresh_1: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['NoticeUpdateRequest'];
+ 'application/json': components['schemas']['RefreshReq'];
};
};
responses: {
@@ -2648,41 +5782,47 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['NoticeResp'];
+ '*/*': components['schemas']['StudentTokenResp'];
};
};
};
};
- delete_4: {
+ getSocialLoginUrl: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['SocialLoginReq'];
+ };
+ };
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['SocialLoginUrlResp'];
+ };
};
};
};
- getById_1: {
+ getPreSignedUrl: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['PreSignedReq'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -2690,23 +5830,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['DiagnosisResp'];
+ '*/*': components['schemas']['PreSignedResp'];
};
};
};
};
- update_7: {
+ verify: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['DiagnosisUpdateReq'];
+ 'application/json': components['schemas']['PhoneAuthVerifyRequest'];
};
};
responses: {
@@ -2716,43 +5854,45 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['DiagnosisResp'];
+ '*/*': components['schemas']['SimpleSuccessResp'];
};
};
};
};
- delete_5: {
+ send: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['PhoneAuthSendRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['SimpleSuccessResp'];
+ };
};
};
};
- update_8: {
+ resend: {
parameters: {
query?: never;
header?: never;
- path: {
- conceptId: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['ConceptUpdateRequest'];
+ 'application/json': components['schemas']['PhoneAuthResendRequest'];
};
};
responses: {
@@ -2762,18 +5902,42 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ConceptResp'];
+ '*/*': components['schemas']['SimpleSuccessResp'];
};
};
};
};
- delete_6: {
+ create_2: {
parameters: {
query?: never;
header?: never;
- path: {
- conceptId: number;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['AdminCreateRequest'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ search: {
+ parameters: {
+ query?: {
+ query?: string;
+ page?: number;
+ size?: number;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -2783,22 +5947,22 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['PageRespTeacherResp'];
+ };
};
};
};
- updateCategory: {
+ create_3: {
parameters: {
query?: never;
header?: never;
- path: {
- categoryId: number;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- 'application/json': components['schemas']['ConceptCategoryUpdateRequest'];
+ 'application/json': components['schemas']['TeacherCreateRequest'];
};
};
responses: {
@@ -2808,32 +5972,39 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ConceptCategoryResp'];
+ '*/*': components['schemas']['TeacherResp'];
};
};
};
};
- deleteCategory: {
+ batch: {
parameters: {
query?: never;
header?: never;
- path: {
- categoryId: number;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody?: {
+ content: {
+ 'multipart/form-data': {
+ /** Format: binary */
+ file: string;
+ };
+ };
+ };
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['SchoolSaveRespDTO'];
+ };
};
};
};
- addChat: {
+ addChat_2: {
parameters: {
query?: never;
header?: never;
@@ -2857,10 +6028,12 @@ export interface operations {
};
};
};
- getsAll: {
+ search_1: {
parameters: {
- query: {
- studentId: number;
+ query?: {
+ year?: number;
+ month?: number;
+ studentId?: number;
};
header?: never;
path?: never;
@@ -2874,12 +6047,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespNoticeResp'];
+ '*/*': components['schemas']['ListRespPublishResp'];
};
};
};
};
- create: {
+ create_4: {
parameters: {
query?: never;
header?: never;
@@ -2888,7 +6061,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['NoticeCreateRequest'];
+ 'application/json': components['schemas']['PublishCreateRequest'];
};
};
responses: {
@@ -2898,23 +6071,26 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['NoticeResp'];
+ '*/*': components['schemas']['PublishResp'];
};
};
};
};
- refresh: {
+ search_2: {
parameters: {
- query?: never;
+ query?: {
+ customId?: string;
+ title?: string;
+ concepts?: number[];
+ problemType?: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
+ page?: number;
+ size?: number;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['RefreshReq'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -2922,12 +6098,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['TeacherTokenResp'];
+ '*/*': components['schemas']['PageRespProblemMetaResp'];
};
};
};
};
- login: {
+ createProblem: {
parameters: {
query?: never;
header?: never;
@@ -2936,7 +6112,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['TeacherLoginReq'];
+ 'application/json': components['schemas']['ProblemCreateRequest'];
};
};
responses: {
@@ -2946,12 +6122,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['TeacherTokenResp'];
+ '*/*': components['schemas']['ProblemInfoResp'];
};
};
};
};
- feedback: {
+ createProblemWithChild: {
parameters: {
query?: never;
header?: never;
@@ -2960,7 +6136,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['PointingFeedbackRequest'];
+ 'application/json': components['schemas']['ProblemEntireCreateRequest'];
};
};
responses: {
@@ -2969,11 +6145,38 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['ProblemInfoResp'];
+ };
};
};
};
- submit: {
+ search_3: {
+ parameters: {
+ query?: {
+ setTitle?: string;
+ problemTitle?: string;
+ page?: number;
+ size?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['PageRespProblemSetResp'];
+ };
+ };
+ };
+ };
+ create_5: {
parameters: {
query?: never;
header?: never;
@@ -2982,7 +6185,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['SubmissionRequest'];
+ 'application/json': components['schemas']['ProblemSetCreateRequest'];
};
};
responses: {
@@ -2992,15 +6195,20 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['SubmissionResp'];
+ '*/*': components['schemas']['ProblemSetResp'];
};
};
};
};
- gets: {
+ search_4: {
parameters: {
query?: {
query?: string;
+ year?: number;
+ month?: number;
+ grade?: number;
+ page?: number;
+ size?: number;
};
header?: never;
path?: never;
@@ -3014,12 +6222,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PageRespNotListQnAGroupByWeekResp'];
+ '*/*': components['schemas']['PageRespPracticeTestResp'];
};
};
};
};
- create_1: {
+ create_6: {
parameters: {
query?: never;
header?: never;
@@ -3028,7 +6236,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['QnACreateRequest'];
+ 'application/json': components['schemas']['PracticeTestCreateRequest'];
};
};
responses: {
@@ -3038,12 +6246,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnAResp'];
+ '*/*': components['schemas']['PracticeTestResp'];
};
};
};
};
- checkExists: {
+ redirect: {
parameters: {
query?: never;
header?: never;
@@ -3052,7 +6260,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['QnACheckRequest'];
+ 'application/json': string;
};
};
responses: {
@@ -3062,12 +6270,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnACheckResp'];
+ '*/*': string;
};
};
};
};
- addChat_1: {
+ sendNotification: {
parameters: {
query?: never;
header?: never;
@@ -3076,7 +6284,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['ChatCreateRequest'];
+ 'application/json': components['schemas']['NotificationSendRequest'];
};
};
responses: {
@@ -3086,23 +6294,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnAResp'];
+ '*/*': components['schemas']['NotificationSendResp'];
};
};
};
};
- registerSocial: {
+ getsAll_1: {
parameters: {
- query?: never;
+ query: {
+ studentId: number;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['StudentUpdateRequest'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3110,12 +6316,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['StudentResp'];
+ '*/*': components['schemas']['ListRespNoticeResp'];
};
};
};
};
- refresh_1: {
+ create_7: {
parameters: {
query?: never;
header?: never;
@@ -3124,7 +6330,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['RefreshReq'];
+ 'application/json': components['schemas']['NoticeCreateRequest'];
};
};
responses: {
@@ -3134,23 +6340,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['StudentTokenResp'];
+ '*/*': components['schemas']['NoticeResp'];
};
};
};
};
- getSocialLoginUrl: {
+ gets_1: {
parameters: {
- query?: never;
+ query: {
+ studentId: number;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['SocialLoginReq'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3158,12 +6362,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['SocialLoginUrlResp'];
+ '*/*': components['schemas']['ListRespDiagnosisResp'];
};
};
};
};
- getPreSignedUrl: {
+ create_8: {
parameters: {
query?: never;
header?: never;
@@ -3172,7 +6376,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['PreSignedReq'];
+ 'application/json': components['schemas']['DiagnosisCreateReq'];
};
};
responses: {
@@ -3182,12 +6386,36 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PreSignedResp'];
+ '*/*': components['schemas']['DiagnosisResp'];
};
};
};
};
- create_2: {
+ search_5: {
+ parameters: {
+ query?: {
+ query?: string;
+ page?: number;
+ size?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['PageRespConceptResp'];
+ };
+ };
+ };
+ };
+ create_9: {
parameters: {
query?: never;
header?: never;
@@ -3196,7 +6424,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['AdminCreateRequest'];
+ 'application/json': components['schemas']['ConceptCreateRequest'];
};
};
responses: {
@@ -3205,11 +6433,13 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['ConceptResp'];
+ };
};
};
};
- search: {
+ searchCategory: {
parameters: {
query?: {
query?: string;
@@ -3228,12 +6458,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PageRespTeacherResp'];
+ '*/*': components['schemas']['PageRespConceptCategoryResp'];
};
};
};
};
- create_3: {
+ createCategory: {
parameters: {
query?: never;
header?: never;
@@ -3242,7 +6472,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['TeacherCreateRequest'];
+ 'application/json': components['schemas']['ConceptCategoryCreateRequest'];
};
};
responses: {
@@ -3252,23 +6482,23 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['TeacherResp'];
+ '*/*': components['schemas']['ConceptCategoryResp'];
};
};
};
};
- search_1: {
+ refresh_2: {
parameters: {
- query?: {
- year?: number;
- month?: number;
- studentId?: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['RefreshReq'];
+ };
+ };
responses: {
/** @description OK */
200: {
@@ -3276,12 +6506,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespPublishResp'];
+ '*/*': components['schemas']['AdminTokenResp'];
};
};
};
};
- create_4: {
+ login_1: {
parameters: {
query?: never;
header?: never;
@@ -3290,31 +6520,53 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['PublishCreateRequest'];
+ 'application/json': components['schemas']['AdminLoginReq'];
+ };
+ };
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['AdminTokenResp'];
+ };
+ };
+ };
+ };
+ oauthRedirectExample: {
+ parameters: {
+ query: {
+ /** @description 성공 여부 */
+ isSuccess: boolean;
+ /** @description 첫 로그인 여부 */
+ isFirstLogin: boolean;
+ /** @description 응답 메시지 */
+ message: string;
+ /** @description accessToken */
+ accessToken: string;
+ /** @description refreshToken */
+ refreshToken: string;
};
+ header?: never;
+ path?: never;
+ cookie?: never;
};
+ requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['PublishResp'];
- };
+ content?: never;
};
};
};
- search_2: {
+ content: {
parameters: {
- query?: {
- customId?: string;
- title?: string;
- concepts?: number[];
- problemType?: 'MAIN_PROBLEM' | 'CHILD_PROBLEM';
- page?: number;
- size?: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
@@ -3326,48 +6578,38 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['PageRespProblemMetaResp'];
- };
+ content?: never;
};
};
};
- createProblem: {
+ problem: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['ProblemCreateRequest'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['ProblemInfoResp'];
- };
+ content?: never;
};
};
};
- createProblemWithChild: {
+ search_6: {
parameters: {
- query?: never;
+ query: {
+ studentId: number;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['ProblemEntireCreateRequest'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3375,18 +6617,17 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemInfoResp'];
+ '*/*': components['schemas']['ListRespPublishResp'];
};
};
};
};
- search_3: {
+ searchMonthly: {
parameters: {
- query?: {
- setTitle?: string;
- problemTitle?: string;
- page?: number;
- size?: number;
+ query: {
+ year: number;
+ month: number;
+ studentId: number;
};
header?: never;
path?: never;
@@ -3400,23 +6641,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PageRespProblemSetResp'];
+ '*/*': components['schemas']['ListRespPublishResp'];
};
};
};
};
- create_5: {
+ getPublishById: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- 'application/json': components['schemas']['ProblemSetCreateRequest'];
+ path: {
+ id: number;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3424,20 +6663,15 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemSetResp'];
+ '*/*': components['schemas']['PublishResp'];
};
};
};
};
- search_4: {
+ getWeeklyProgress: {
parameters: {
- query?: {
- query?: string;
- year?: number;
- month?: number;
- grade?: number;
- page?: number;
- size?: number;
+ query: {
+ studentId: number;
};
header?: never;
path?: never;
@@ -3451,23 +6685,24 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PageRespPracticeTestResp'];
+ '*/*': components['schemas']['PublishStudentProgressResp'];
};
};
};
};
- create_6: {
+ getProblemById: {
parameters: {
- query?: never;
+ query: {
+ studentId: number;
+ };
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- 'application/json': components['schemas']['PracticeTestCreateRequest'];
+ path: {
+ publishId: number;
+ problemId: number;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3475,23 +6710,24 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PracticeTestResp'];
+ '*/*': components['schemas']['ProblemWithStudyInfoResp'];
};
};
};
};
- redirect: {
+ getChildProblemById: {
parameters: {
- query?: never;
+ query: {
+ studentId: number;
+ };
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- 'application/json': string;
+ path: {
+ publishId: number;
+ problemId: number;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3499,16 +6735,14 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': string;
+ '*/*': components['schemas']['ProblemWithStudyInfoResp'];
};
};
};
};
- getsAll_1: {
+ getMyStudents: {
parameters: {
- query: {
- studentId: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
@@ -3521,23 +6755,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespNoticeResp'];
+ '*/*': components['schemas']['ListRespStudentResp'];
};
};
};
};
- create_7: {
+ gets_2: {
parameters: {
- query?: never;
+ query?: {
+ query?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['NoticeCreateRequest'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3545,18 +6777,18 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['NoticeResp'];
+ '*/*': components['schemas']['PageRespNotListQnAGroupByWeekResp'];
};
};
};
};
- gets_1: {
+ getById_2: {
parameters: {
- query: {
- studentId: number;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ qnaId: number;
+ };
cookie?: never;
};
requestBody?: never;
@@ -3567,23 +6799,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespDiagnosisResp'];
+ '*/*': components['schemas']['QnAResp'];
};
};
};
};
- create_8: {
+ search_7: {
parameters: {
- query?: never;
+ query?: {
+ query?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['DiagnosisCreateReq'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3591,17 +6821,15 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['DiagnosisResp'];
+ '*/*': components['schemas']['QnASearchResp'];
};
};
};
};
- search_5: {
+ getsAvailable: {
parameters: {
- query?: {
- query?: string;
- page?: number;
- size?: number;
+ query: {
+ studentId: number;
};
header?: never;
path?: never;
@@ -3615,23 +6843,19 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PageRespConceptResp'];
+ '*/*': components['schemas']['ListRespNoticeResp'];
};
};
};
};
- create_9: {
+ getTeacherMe: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['ConceptCreateRequest'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3639,18 +6863,14 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ConceptResp'];
+ '*/*': components['schemas']['TeacherResp'];
};
};
};
};
- searchCategory: {
+ search_8: {
parameters: {
- query?: {
- query?: string;
- page?: number;
- size?: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
@@ -3663,23 +6883,22 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PageRespConceptCategoryResp'];
+ '*/*': components['schemas']['ListRespPublishResp'];
};
};
};
};
- createCategory: {
+ searchMonthly_1: {
parameters: {
- query?: never;
+ query: {
+ year: number;
+ month: number;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['ConceptCategoryCreateRequest'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3687,23 +6906,21 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ConceptCategoryResp'];
+ '*/*': components['schemas']['ListRespPublishResp'];
};
};
};
};
- refresh_2: {
+ getPublishById_1: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- 'application/json': components['schemas']['RefreshReq'];
+ path: {
+ id: number;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3711,23 +6928,19 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['AdminTokenResp'];
+ '*/*': components['schemas']['PublishResp'];
};
};
};
};
- login_1: {
+ getWeeklyProgress_1: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': components['schemas']['AdminLoginReq'];
- };
- };
+ requestBody?: never;
responses: {
/** @description OK */
200: {
@@ -3735,27 +6948,19 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['AdminTokenResp'];
+ '*/*': components['schemas']['PublishStudentProgressResp'];
};
};
};
};
- oauthRedirectExample: {
+ getProblemById_1: {
parameters: {
- query: {
- /** @description 성공 여부 */
- isSuccess: boolean;
- /** @description 첫 로그인 여부 */
- isFirstLogin: boolean;
- /** @description 응답 메시지 */
- message: string;
- /** @description accessToken */
- accessToken: string;
- /** @description refreshToken */
- refreshToken: string;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ publishId: number;
+ problemId: number;
+ };
cookie?: never;
};
requestBody?: never;
@@ -3765,15 +6970,19 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['ProblemWithStudyInfoResp'];
+ };
};
};
};
- content: {
+ getScrap: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ id: number;
+ };
cookie?: never;
};
requestBody?: never;
@@ -3783,13 +6992,26 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['ScrapDetailResp'];
+ };
};
};
};
- problem: {
+ getTrash: {
parameters: {
- query?: never;
+ query?: {
+ /**
+ * @description 정렬 필드 (CREATED_AT/NAME/TYPE)
+ * @example CREATED_AT
+ */
+ sort?: 'CREATED_AT' | 'NAME' | 'TYPE' | 'SIMILARITY';
+ /**
+ * @description 정렬 방향 (ASC/DESC)
+ * @example DESC
+ */
+ order?: 'ASC' | 'DESC';
+ };
header?: never;
path?: never;
cookie?: never;
@@ -3801,38 +7023,51 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['ListRespTrashItemResp'];
+ };
};
};
};
- search_6: {
+ permanentDelete: {
parameters: {
- query: {
- studentId: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ScrapBatchPermanentDeleteRequest'];
+ };
+ };
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['ListRespPublishResp'];
- };
+ content?: never;
};
};
};
- searchMonthly: {
+ searchScraps: {
parameters: {
- query: {
- year: number;
- month: number;
- studentId: number;
+ query?: {
+ /** @description 폴더 ID (null이면 루트 스크랩) */
+ folderId?: number;
+ /** @description 검색어 (폴더명, 문제 제목) */
+ query?: string;
+ /**
+ * @description 정렬 필드 (CREATED_AT/NAME/TYPE/SIMILARITY)
+ * @example CREATED_AT
+ */
+ sort?: 'CREATED_AT' | 'NAME' | 'TYPE' | 'SIMILARITY';
+ /**
+ * @description 정렬 방향 (ASC/DESC)
+ * @example DESC
+ */
+ order?: 'ASC' | 'DESC';
};
header?: never;
path?: never;
@@ -3846,17 +7081,17 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespPublishResp'];
+ '*/*': components['schemas']['ScrapSearchResp'];
};
};
};
};
- getPublishById: {
+ getScrapsByFolder: {
parameters: {
query?: never;
header?: never;
path: {
- id: number;
+ folderId: number;
};
cookie?: never;
};
@@ -3868,15 +7103,15 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PublishResp'];
+ '*/*': components['schemas']['ListRespScrapListItemResp'];
};
};
};
};
- getWeeklyProgress: {
+ search_9: {
parameters: {
- query: {
- studentId: number;
+ query?: {
+ query?: string;
};
header?: never;
path?: never;
@@ -3890,20 +7125,17 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PublishStudentProgressResp'];
+ '*/*': components['schemas']['ListRespSchoolResp'];
};
};
};
};
- getProblemById: {
+ getImages: {
parameters: {
- query: {
- studentId: number;
- };
+ query?: never;
header?: never;
path: {
- publishId: number;
- problemId: number;
+ qnaId: number;
};
cookie?: never;
};
@@ -3915,21 +7147,38 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemWithStudyInfoResp'];
+ '*/*': components['schemas']['ListRespUploadFileResp'];
};
};
};
};
- getChildProblemById: {
+ search_10: {
parameters: {
- query: {
- studentId: number;
+ query?: {
+ query?: string;
};
header?: never;
- path: {
- publishId: number;
- problemId: number;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ '*/*': components['schemas']['QnASearchResp'];
+ };
};
+ };
+ };
+ getAllImages: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -3940,12 +7189,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemWithStudyInfoResp'];
+ '*/*': components['schemas']['ListRespUploadFileResp'];
};
};
};
};
- getMyStudents: {
+ getOrCreateAdminChatroom: {
parameters: {
query?: never;
header?: never;
@@ -3960,15 +7209,16 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespStudentResp'];
+ '*/*': components['schemas']['QnAResp'];
};
};
};
};
- gets_2: {
+ getNotifications: {
parameters: {
query?: {
- query?: string;
+ /** @description 조회 기간 (일 단위, 예: 30이면 30일 전까지) */
+ dayLimit?: number;
};
header?: never;
path?: never;
@@ -3982,18 +7232,16 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PageRespNotListQnAGroupByWeekResp'];
+ '*/*': components['schemas']['ListRespNotificationResp'];
};
};
};
};
- getById_2: {
+ countUnread: {
parameters: {
query?: never;
header?: never;
- path: {
- qnaId: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -4004,16 +7252,14 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['QnAResp'];
+ '*/*': components['schemas']['NotificationUnreadCountResp'];
};
};
};
};
- getsAvailable: {
+ getsAvailable_1: {
parameters: {
- query: {
- studentId: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
@@ -4031,7 +7277,7 @@ export interface operations {
};
};
};
- getTeacherMe: {
+ countAvailable: {
parameters: {
query?: never;
header?: never;
@@ -4046,12 +7292,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['TeacherResp'];
+ '*/*': components['schemas']['NoticeUnreadCountResp'];
};
};
};
};
- search_7: {
+ gets_3: {
parameters: {
query?: never;
header?: never;
@@ -4066,17 +7312,14 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespPublishResp'];
+ '*/*': components['schemas']['ListRespDiagnosisResp'];
};
};
};
};
- searchMonthly_1: {
+ getById_3: {
parameters: {
- query: {
- year: number;
- month: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
@@ -4089,12 +7332,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespPublishResp'];
+ '*/*': components['schemas']['DiagnosisResp'];
};
};
};
};
- getPublishById_1: {
+ getById_4: {
parameters: {
query?: never;
header?: never;
@@ -4111,14 +7354,16 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PublishResp'];
+ '*/*': components['schemas']['DiagnosisResp'];
};
};
};
};
- getWeeklyProgress_1: {
+ existsByEmail: {
parameters: {
- query?: never;
+ query: {
+ email: string;
+ };
header?: never;
path?: never;
cookie?: never;
@@ -4131,37 +7376,48 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PublishStudentProgressResp'];
+ '*/*': components['schemas']['BooleanResp'];
};
};
};
};
- getProblemById_1: {
+ subscribe: {
parameters: {
- query?: never;
+ query: {
+ token: string;
+ };
header?: never;
path: {
- publishId: number;
- problemId: number;
+ qnaId: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description OK */
+ /** @description SSE 스트림 연결 성공. 드롭다운에서 이벤트 타입별 스키마를 확인하세요. */
200: {
headers: {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemWithStudyInfoResp'];
+ 'chat (application/json)': components['schemas']['QnAChatEvent'];
+ 'read_status (application/json)': components['schemas']['QnAReadStatusEvent'];
};
};
+ /** @description 토큰 검증 실패 */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
};
- getsAvailable_1: {
+ throwException: {
parameters: {
- query?: never;
+ query?: {
+ message?: string;
+ };
header?: never;
path?: never;
cookie?: never;
@@ -4173,13 +7429,11 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['ListRespNoticeResp'];
- };
+ content?: never;
};
};
};
- countAvailable: {
+ refresh_3: {
parameters: {
query?: never;
header?: never;
@@ -4194,14 +7448,18 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['NoticeUnreadCountResp'];
+ '*/*': components['schemas']['JwtResp'];
};
};
};
};
- gets_3: {
+ search_11: {
parameters: {
- query?: never;
+ query?: {
+ query?: string;
+ page?: number;
+ size?: number;
+ };
header?: never;
path?: never;
cookie?: never;
@@ -4214,14 +7472,16 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespDiagnosisResp'];
+ '*/*': components['schemas']['PageRespStudentResp'];
};
};
};
};
- getById_3: {
+ gets_4: {
parameters: {
- query?: never;
+ query?: {
+ query?: string;
+ };
header?: never;
path?: never;
cookie?: never;
@@ -4234,17 +7494,17 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['DiagnosisResp'];
+ '*/*': components['schemas']['PageRespNotListQnAGroupByWeekResp'];
};
};
};
};
- getById_4: {
+ getById_5: {
parameters: {
query?: never;
header?: never;
path: {
- id: number;
+ qnaId: number;
};
cookie?: never;
};
@@ -4256,16 +7516,18 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['DiagnosisResp'];
+ '*/*': components['schemas']['QnAResp'];
};
};
};
};
- refresh_3: {
+ getById_6: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ id: number;
+ };
cookie?: never;
};
requestBody?: never;
@@ -4276,20 +7538,18 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['JwtResp'];
+ '*/*': components['schemas']['PublishResp'];
};
};
};
};
- search_8: {
+ delete_7: {
parameters: {
- query?: {
- query?: string;
- page?: number;
- size?: number;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ id: number;
+ };
cookie?: never;
};
requestBody?: never;
@@ -4299,19 +7559,15 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content: {
- '*/*': components['schemas']['PageRespStudentResp'];
- };
+ content?: never;
};
};
};
- getById_5: {
+ getCustomId: {
parameters: {
query?: never;
header?: never;
- path: {
- id: number;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -4322,18 +7578,19 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['PublishResp'];
+ '*/*': components['schemas']['ProblemCustomIdResp'];
};
};
};
};
- delete_7: {
+ getNotificationsByStudent: {
parameters: {
- query?: never;
- header?: never;
- path: {
- id: number;
+ query: {
+ /** @description 학생 ID */
+ studentId: number;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -4343,13 +7600,17 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ '*/*': components['schemas']['ListRespNotificationResp'];
+ };
};
};
};
- getCustomId: {
+ getsAvailable_2: {
parameters: {
- query?: never;
+ query: {
+ studentId: number;
+ };
header?: never;
path?: never;
cookie?: never;
@@ -4362,15 +7623,16 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ProblemCustomIdResp'];
+ '*/*': components['schemas']['ListRespNoticeResp'];
};
};
};
};
- getsAvailable_2: {
+ issueTemporaryToken: {
parameters: {
query: {
- studentId: number;
+ id: number;
+ type: 'ADMIN' | 'STUDENT' | 'TEACHER';
};
header?: never;
path?: never;
@@ -4384,8 +7646,26 @@ export interface operations {
[name: string]: unknown;
};
content: {
- '*/*': components['schemas']['ListRespNoticeResp'];
+ '*/*': components['schemas']['JwtResp'];
+ };
+ };
+ };
+ };
+ emptyTrash: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
};
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5651772..8a19956f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -186,6 +186,9 @@ importers:
'@gorhom/bottom-sheet':
specifier: ^5.2.7
version: 5.2.7(@types/react@19.1.8)(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.0)(react-native-worklets@0.5.1(@babel/core@7.28.0)(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ '@react-native-async-storage/async-storage':
+ specifier: ^2.2.0
+ version: 2.2.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))
'@react-native-community/datetimepicker':
specifier: ^8.5.1
version: 8.5.1(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
@@ -204,6 +207,9 @@ importers:
'@react-navigation/stack':
specifier: ^7.1.1
version: 7.6.7(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ '@shopify/react-native-skia':
+ specifier: 2.2.12
+ version: 2.2.12(react-native-reanimated@4.1.5(@babel/core@7.28.0)(react-native-worklets@0.5.1(@babel/core@7.28.0)(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
'@tanstack/react-query':
specifier: ^5.66.0
version: 5.81.5(react@19.1.0)
@@ -216,6 +222,9 @@ importers:
expo-asset:
specifier: ^12.0.10
version: 12.0.10(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ expo-blur:
+ specifier: ^15.0.8
+ version: 15.0.8(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
expo-constants:
specifier: ~18.0.10
version: 18.0.10(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))
@@ -231,6 +240,9 @@ importers:
expo-image:
specifier: ~3.0.10
version: 3.0.10(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ expo-image-picker:
+ specifier: ^17.0.10
+ version: 17.0.10(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))
expo-linking:
specifier: ~8.0.9
version: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
@@ -282,9 +294,18 @@ importers:
react-native-css-interop:
specifier: ^0.2.1
version: 0.2.1(react-native-reanimated@4.1.5(@babel/core@7.28.0)(react-native-worklets@0.5.1(@babel/core@7.28.0)(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-svg@15.15.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(tsx@4.20.3)(yaml@2.8.1))
+ react-native-element-dropdown:
+ specifier: ^2.12.4
+ version: 2.12.4(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
react-native-gesture-handler:
specifier: ~2.28.0
version: 2.28.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ react-native-image-picker:
+ specifier: ^8.2.1
+ version: 8.2.1(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ react-native-popover-view:
+ specifier: ^6.1.0
+ version: 6.1.0
react-native-reanimated:
specifier: ~4.1.5
version: 4.1.5(@babel/core@7.28.0)(react-native-worklets@0.5.1(@babel/core@7.28.0)(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
@@ -297,6 +318,12 @@ importers:
react-native-svg:
specifier: ^15.15.0
version: 15.15.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ react-native-toast-message:
+ specifier: ^2.3.3
+ version: 2.3.3(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ react-native-tooltips:
+ specifier: ^1.0.3
+ version: 1.0.3
react-native-web:
specifier: ~0.21.0
version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2447,6 +2474,11 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+ '@react-native-async-storage/async-storage@2.2.0':
+ resolution: {integrity: sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==}
+ peerDependencies:
+ react-native: ^0.0.0-0 || >=0.65 <1.0
+
'@react-native-community/datetimepicker@8.5.1':
resolution: {integrity: sha512-TuwM1ORbxCjOp1GOtONj0QnpDpVfq0F4UlfKZYPxL/vmriaLHt2Kgvw63Bv0Bpep4eOkslVVSS1IRfRI6d392g==}
peerDependencies:
@@ -2508,6 +2540,9 @@ packages:
resolution: {integrity: sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==}
engines: {node: '>= 20.19.4'}
+ '@react-native/normalize-color@2.1.0':
+ resolution: {integrity: sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA==}
+
'@react-native/normalize-colors@0.74.89':
resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==}
@@ -2713,6 +2748,19 @@ packages:
'@rushstack/eslint-patch@1.12.0':
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
+ '@shopify/react-native-skia@2.2.12':
+ resolution: {integrity: sha512-P5wZSMPTp00hM0do+awNFtb5aPh5hSpodMGwy7NaxK90AV+SmUu7wZe6NGevzQIwgFa89Epn6xK3j4jKWdQi+A==}
+ hasBin: true
+ peerDependencies:
+ react: '>=19.0'
+ react-native: '>=0.78'
+ react-native-reanimated: '>=3.19.1'
+ peerDependenciesMeta:
+ react-native:
+ optional: true
+ react-native-reanimated:
+ optional: true
+
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@@ -3626,6 +3674,9 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
+ '@webgpu/types@0.1.21':
+ resolution: {integrity: sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==}
+
'@xmldom/xmldom@0.8.11':
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
engines: {node: '>=10.0.0'}
@@ -4005,6 +4056,9 @@ packages:
caniuse-lite@1.0.30001726:
resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==}
+ canvaskit-wasm@0.40.0:
+ resolution: {integrity: sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==}
+
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@@ -4341,6 +4395,9 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
+ deprecated-react-native-prop-types@2.3.0:
+ resolution: {integrity: sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==}
+
destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -4724,6 +4781,13 @@ packages:
react: '*'
react-native: '*'
+ expo-blur@15.0.8:
+ resolution: {integrity: sha512-rWyE1NBRZEu9WD+X+5l7gyPRszw7n12cW3IRNAb5i6KFzaBp8cxqT5oeaphJapqURvcqhkOZn2k5EtBSbsuU7w==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
expo-constants@18.0.10:
resolution: {integrity: sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==}
peerDependencies:
@@ -4748,6 +4812,16 @@ packages:
peerDependencies:
expo: '*'
+ expo-image-loader@6.0.0:
+ resolution: {integrity: sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==}
+ peerDependencies:
+ expo: '*'
+
+ expo-image-picker@17.0.10:
+ resolution: {integrity: sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==}
+ peerDependencies:
+ expo: '*'
+
expo-image@3.0.10:
resolution: {integrity: sha512-i4qNCEf9Ur7vDqdfDdFfWnNCAF2efDTdahuDy9iELPS2nzMKBLeeGA2KxYEPuRylGCS96Rwm+SOZJu6INc2ADQ==}
peerDependencies:
@@ -5385,6 +5459,10 @@ packages:
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
engines: {node: '>=8'}
+ is-plain-obj@2.1.0:
+ resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
+ engines: {node: '>=8'}
+
is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
@@ -5878,6 +5956,10 @@ packages:
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+ merge-options@3.0.4:
+ resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==}
+ engines: {node: '>=10'}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -6819,18 +6901,34 @@ packages:
react-native-svg:
optional: true
+ react-native-element-dropdown@2.12.4:
+ resolution: {integrity: sha512-abZc5SVji9FIt7fjojRYrbuvp03CoeZJrgvezQoDoSOrpiTqkX69ix5m+j06W2AVncA0VWvbT+vCMam8SoVadw==}
+ engines: {node: '>= 16.0.0'}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
react-native-gesture-handler@2.28.0:
resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}
peerDependencies:
react: '*'
react-native: '*'
+ react-native-image-picker@8.2.1:
+ resolution: {integrity: sha512-FBeGYJGFDjMdGCcyubDJgBAPCQ4L1D3hwLXyUU91jY9ahOZMTbluceVvRmrEKqnDPFJ0gF1NVhJ0nr1nROFLdg==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
react-native-is-edge-to-edge@1.2.1:
resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==}
peerDependencies:
react: '*'
react-native: '*'
+ react-native-popover-view@6.1.0:
+ resolution: {integrity: sha512-j1CB+yPwTKlBvIJBNb1AwiHyF/r+W5+AJIbHk79GRa+0z6PVtW4C7NWJWPqUVkCMOcJtewl6Pr6f2dc/87cVyQ==}
+
react-native-reanimated@4.1.5:
resolution: {integrity: sha512-UA6VUbxwhRjEw2gSNrvhkusUq3upfD3Cv+AnB07V+kC8kpvwRVI+ivwY95ePbWNFkFpP+Y2Sdw1WHpHWEV+P2Q==}
peerDependencies:
@@ -6857,6 +6955,15 @@ packages:
react: '*'
react-native: '*'
+ react-native-toast-message@2.3.3:
+ resolution: {integrity: sha512-4IIUHwUPvKHu4gjD0Vj2aGQzqPATiblL1ey8tOqsxOWRPGGu52iIbL8M/mCz4uyqecvPdIcMY38AfwRuUADfQQ==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-tooltips@1.0.3:
+ resolution: {integrity: sha512-f2XEL23FlPMpIq11t6EIYGf1sl1FoWCwQRFIvQB9+6v5Jyod1O61yoKy2lgg91Egz531Szmqac0wTXuxVC9vjw==}
+
react-native-web@0.21.2:
resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==}
peerDependencies:
@@ -6887,6 +6994,12 @@ packages:
'@types/react':
optional: true
+ react-reconciler@0.31.0:
+ resolution: {integrity: sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==}
+ engines: {node: '>=0.10.0'}
+ peerDependencies:
+ react: ^19.0.0
+
react-refresh@0.14.2:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
@@ -7128,6 +7241,9 @@ packages:
scheduler@0.19.1:
resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==}
+ scheduler@0.25.0:
+ resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
+
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
@@ -10280,6 +10396,11 @@ snapshots:
'@radix-ui/rect@1.1.1': {}
+ '@react-native-async-storage/async-storage@2.2.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))':
+ dependencies:
+ merge-options: 3.0.4
+ react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
+
'@react-native-community/datetimepicker@8.5.1(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)':
dependencies:
invariant: 2.2.4
@@ -10396,6 +10517,8 @@ snapshots:
'@react-native/js-polyfills@0.81.5': {}
+ '@react-native/normalize-color@2.1.0': {}
+
'@react-native/normalize-colors@0.74.89': {}
'@react-native/normalize-colors@0.81.5': {}
@@ -10587,6 +10710,15 @@ snapshots:
'@rushstack/eslint-patch@1.12.0': {}
+ '@shopify/react-native-skia@2.2.12(react-native-reanimated@4.1.5(@babel/core@7.28.0)(react-native-worklets@0.5.1(@babel/core@7.28.0)(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ canvaskit-wasm: 0.40.0
+ react: 19.1.0
+ react-reconciler: 0.31.0(react@19.1.0)
+ optionalDependencies:
+ react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
+ react-native-reanimated: 4.1.5(@babel/core@7.28.0)(react-native-worklets@0.5.1(@babel/core@7.28.0)(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+
'@sinclair/typebox@0.27.8': {}
'@sinonjs/commons@3.0.1':
@@ -11870,6 +12002,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@webgpu/types@0.1.21': {}
+
'@xmldom/xmldom@0.8.11': {}
abort-controller@3.0.0:
@@ -12328,6 +12462,10 @@ snapshots:
caniuse-lite@1.0.30001726: {}
+ canvaskit-wasm@0.40.0:
+ dependencies:
+ '@webgpu/types': 0.1.21
+
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
@@ -12699,6 +12837,12 @@ snapshots:
depd@2.0.0: {}
+ deprecated-react-native-prop-types@2.3.0:
+ dependencies:
+ '@react-native/normalize-color': 2.1.0
+ invariant: 2.2.4
+ prop-types: 15.8.1
+
destroy@1.2.0: {}
detect-libc@1.0.3: {}
@@ -13397,6 +13541,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ expo-blur@15.0.8(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0):
+ dependencies:
+ expo: 54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
+
expo-constants@18.0.10(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)):
dependencies:
'@expo/config': 12.0.10
@@ -13422,6 +13572,15 @@ snapshots:
dependencies:
expo: 54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ expo-image-loader@6.0.0(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)):
+ dependencies:
+ expo: 54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+
+ expo-image-picker@17.0.10(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)):
+ dependencies:
+ expo: 54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
+ expo-image-loader: 6.0.0(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))
+
expo-image@3.0.10(expo@54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0):
dependencies:
expo: 54.0.25(@babel/core@7.28.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0)
@@ -14137,6 +14296,8 @@ snapshots:
is-path-inside@3.0.3: {}
+ is-plain-obj@2.1.0: {}
+
is-regex@1.2.1:
dependencies:
call-bound: 1.0.4
@@ -14599,6 +14760,10 @@ snapshots:
memoize-one@6.0.0: {}
+ merge-options@3.0.4:
+ dependencies:
+ is-plain-obj: 2.1.0
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -15748,6 +15913,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ react-native-element-dropdown@2.12.4(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0):
+ dependencies:
+ lodash: 4.17.21
+ react: 19.1.0
+ react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
+
react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0):
dependencies:
'@egjs/hammerjs': 2.0.17
@@ -15756,11 +15927,21 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
+ react-native-image-picker@8.2.1(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
+
react-native-is-edge-to-edge@1.2.1(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
+ react-native-popover-view@6.1.0:
+ dependencies:
+ deprecated-react-native-prop-types: 2.3.0
+ prop-types: 15.8.1
+
react-native-reanimated@4.1.5(@babel/core@7.28.0)(react-native-worklets@0.5.1(@babel/core@7.28.0)(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0):
dependencies:
'@babel/core': 7.28.0
@@ -15791,6 +15972,13 @@ snapshots:
react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
warn-once: 0.1.1
+ react-native-toast-message@2.3.3(react-native@0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0))(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-native: 0.81.5(@babel/core@7.28.0)(@types/react@19.1.8)(react@19.1.0)
+
+ react-native-tooltips@1.0.3: {}
+
react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@babel/runtime': 7.28.4
@@ -15879,6 +16067,11 @@ snapshots:
- supports-color
- utf-8-validate
+ react-reconciler@0.31.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ scheduler: 0.25.0
+
react-refresh@0.14.2: {}
react-refresh@0.17.0: {}
@@ -16151,6 +16344,8 @@ snapshots:
loose-envify: 1.4.0
object-assign: 4.1.1
+ scheduler@0.25.0: {}
+
scheduler@0.26.0: {}
semver@6.3.1: {}