diff --git a/src/app/(private)/(main)/attendance/page.tsx b/src/app/(private)/(main)/attendance/page.tsx new file mode 100644 index 0000000..0d07340 --- /dev/null +++ b/src/app/(private)/(main)/attendance/page.tsx @@ -0,0 +1,25 @@ +import { AttendanceContent } from '@/components/attendance'; +import type { AttendanceData } from '@/types/attendance'; + +// TODO: API 연동 시 실제 데이터로 교체 +function createMockAttendance(): AttendanceData { + const now = new Date(); + const start = now; + const end = new Date(now.getTime() + 10 * 60 * 1000); // 10분 후 + + return { + attendanceRate: 80, + title: '1주차 정기모임', + status: 'ATTEND', + code: '123456', + start: start.toISOString(), + end: end.toISOString(), + location: '공학관 401호', + }; +} + +export default function AttendancePage() { + // TODO: API 연동 시 실제 사용자 이름으로 교체 + const displayName = '사용자'; + return ; +} diff --git a/src/app/(public)/attendance/page.tsx b/src/app/(public)/attendance/page.tsx deleted file mode 100644 index b1d6531..0000000 --- a/src/app/(public)/attendance/page.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import Image from 'next/image'; -import { AttendanceProgressBar } from '@/components/attendance/AttendanceProgressBar'; -import { - Card, - Button, - Dialog, - DialogContent, - DialogHeader, - DialogBody, - DialogFooter, - DialogTrigger, - DialogClose, -} from '@/components/ui'; -import DummyImage from '@/assets/icons/dummy.svg'; - -export default function AttendancePage() { - const [currentPage, setCurrentPage] = useState(1); - const [qrModalOpen, setQrModalOpen] = useState(false); - - return ( -
- - - setQrModalOpen(true)} - onSecondaryClick={() => console.log('출석코드 확인')} - /> - - - - - -
-

- 운영진이 공유한 6자리 출석 코드를 입력하세요 -

-

- QR 스캔이 어려운 경우 코드를 직접 입력할 수 있어요 -

-
-
-
- {[1, 2, 3, 4, 5, 6].map((num) => ( -
- {num === 1 ? '2' : ''} -
- ))} -
-

출석 가능 시간 10:00

-
-
- - - - - -
-
- - - - {/* 모달 예시 버튼들 */} -
-

모달 예시

-
- {/* 모달 2: 간단한 모달 (title만) */} - - - - - - - -

출석이 완료되었습니다!

-
- - - - - -
-
- - {/* 모달 3: 페이지네이션 모달 */} - - - - - - - -
-
-

2024년 7월 1일 - 출석 완료

-
-
-

2024년 7월 8일 - 출석 완료

-
-
-

2024년 7월 15일 - 결석

-
-
-
- - - 1 / 3 - -
- } - > - - - - - - - - {/* 모달 4: 커스텀 헤더 (children 사용) */} - - - - - - -
-
- -
-

출석 성공

-
-
- -

출석이 성공적으로 처리되었습니다.

-
- - - - - -
-
- - {/* 모달 5: 온보딩 모달 (이미지 참조) */} - - - - - - - - {/* 이미지 영역 */} -
- 웹사이트 미리보기 -
- {/* 설정하러 가기 링크 */} -

설정하러 가기

-
- - - {currentPage} / 4 - -
- } - > - - - - - - -
- - - ); -} diff --git a/src/assets/icons/complete.svg b/src/assets/icons/complete.svg new file mode 100644 index 0000000..a05c5c0 --- /dev/null +++ b/src/assets/icons/complete.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/home.svg b/src/assets/icons/home.svg new file mode 100644 index 0000000..b1b07d6 --- /dev/null +++ b/src/assets/icons/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 3d54ac3..b2297a5 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -28,3 +28,6 @@ export { default as FolderPlusIcon } from './folder_plus.svg'; export { default as SendIcon } from './send.svg'; export { default as DownloadIcon } from './download.svg'; export { default as FolderIcon } from './folder.svg'; +export { default as HomeIcon } from './home.svg'; +export { default as MoreHorizIcon } from './more-horiz.svg'; +export { default as CompleteIcon } from './complete.svg'; diff --git a/src/assets/icons/more-horiz.svg b/src/assets/icons/more-horiz.svg new file mode 100644 index 0000000..cabcd64 --- /dev/null +++ b/src/assets/icons/more-horiz.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/send.svg b/src/assets/icons/send.svg index e23abc5..5a00d74 100644 --- a/src/assets/icons/send.svg +++ b/src/assets/icons/send.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/components/attendance/AttendanceCodeModal.tsx b/src/components/attendance/AttendanceCodeModal.tsx new file mode 100644 index 0000000..64c3808 --- /dev/null +++ b/src/components/attendance/AttendanceCodeModal.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { useState } from 'react'; + +import { CheckRoundIcon } from '@/assets/icons'; +import { + Button, + Dialog, + DialogContent, + DialogHeader, + DialogBody, + DialogFooter, + Icon, +} from '@/components/ui'; +import { InputOTP } from '@/components/attendance/InputOTP'; +import { useRemainingTime } from '@/hooks'; +import { formatModalDescription } from '@/lib/formatTime'; + +interface AttendanceCodeModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm?: (code: string) => void; + title: string; + start: string; + endTime: string; + location: string; +} + +function AttendanceCodeModal({ + open, + onOpenChange, + onConfirm, + title, + start, + endTime, + location, +}: AttendanceCodeModalProps) { + const [code, setCode] = useState(''); + const { minutes, seconds, isExpired } = useRemainingTime(endTime); + const isComplete = code.length === 6; + const description = formatModalDescription(start, location); + + function handleOpenChange(nextOpen: boolean) { + if (!nextOpen) setCode(''); + onOpenChange(nextOpen); + } + + return ( + + + } + overline="QR 출석하기" + title={title} + description={description} + showClose + onClose={() => handleOpenChange(false)} + /> + + +
+
+

+ 운영진이 공유한 6자리 출석 코드를 입력하세요 +

+

+ QR 스캔이 어려운 경우 코드를 직접 입력할 수 있어요 +

+
+
+ + + + {!isExpired ? ( +

+ 출석 가능 시간{' '} + + {minutes}:{seconds} + +

+ ) : ( +

+ 출석 가능 시간이 만료되었습니다 +

+ )} + + + + + + + + ); +} + +export { AttendanceCodeModal, type AttendanceCodeModalProps }; diff --git a/src/components/attendance/AttendanceCompleteModal.tsx b/src/components/attendance/AttendanceCompleteModal.tsx new file mode 100644 index 0000000..a9ffdc2 --- /dev/null +++ b/src/components/attendance/AttendanceCompleteModal.tsx @@ -0,0 +1,52 @@ +'use client'; + +import Image from 'next/image'; + +import { CompleteIcon } from '@/assets/icons'; +import { + Button, + Dialog, + DialogContent, + DialogHeader, + DialogBody, + DialogFooter, +} from '@/components/ui'; + +interface AttendanceCompleteModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +function AttendanceCompleteModal({ open, onOpenChange }: AttendanceCompleteModalProps) { + return ( + + + onOpenChange(false)} /> + + + 출석 완료 +
+

이미 출석을 완료했네요!

+

오늘도 즐거운 활동을 이어가세요.

+
+
+ + + + +
+
+ ); +} + +export { AttendanceCompleteModal, type AttendanceCompleteModalProps }; diff --git a/src/components/attendance/AttendanceContent.tsx b/src/components/attendance/AttendanceContent.tsx new file mode 100644 index 0000000..222cb98 --- /dev/null +++ b/src/components/attendance/AttendanceContent.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useState } from 'react'; + +import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbPage, Card } from '@/components/ui'; +import { AttendanceStatus } from '@/components/attendance/AttendanceStatus'; +import { AttendanceTodayCard } from '@/components/attendance/AttendanceTodayCard'; +import { formatAttendanceDescription } from '@/lib/formatTime'; +import type { AttendanceData } from '@/types/attendance'; + +interface AttendanceContentProps { + name: string; + attendance: AttendanceData; + isAdmin?: boolean; +} + +function AttendanceContent({ name, attendance, isAdmin = false }: AttendanceContentProps) { + const [isChecked, setIsChecked] = useState(false); + const { attendanceRate, title, start, end, location } = attendance; + const description = formatAttendanceDescription(start, end, location); + + function handleAttendanceComplete(_code: string) { + // TODO: API 연결 시 출석 코드 검증 로직 추가 + setIsChecked(true); + } + + return ( +
+ + + + 출석 + + + + + + +
+ + + +
+
+ ); +} + +export { AttendanceContent, type AttendanceContentProps }; diff --git a/src/components/attendance/AttendanceProgressBar.tsx b/src/components/attendance/AttendanceProgressBar.tsx index 7113d08..89e0b81 100644 --- a/src/components/attendance/AttendanceProgressBar.tsx +++ b/src/components/attendance/AttendanceProgressBar.tsx @@ -5,6 +5,8 @@ interface AttendanceProgressBarProps extends React.HTMLAttributes { + name: string; + attendanceRate: number; +} + +function AttendanceStatus({ name, attendanceRate, className, ...props }: AttendanceStatusProps) { + return ( +
+

+ {name}님의 +
+ 출석률은 {attendanceRate}% +

+ +
+ ); +} + +export { AttendanceStatus, type AttendanceStatusProps }; diff --git a/src/components/attendance/AttendanceTodayCard.tsx b/src/components/attendance/AttendanceTodayCard.tsx new file mode 100644 index 0000000..e7dbfac --- /dev/null +++ b/src/components/attendance/AttendanceTodayCard.tsx @@ -0,0 +1,86 @@ +'use client'; + +import Image from 'next/image'; +import { useState } from 'react'; + +import { CompleteIcon } from '@/assets/icons'; +import { Card } from '@/components/ui'; +import { AttendanceCodeModal } from '@/components/attendance/AttendanceCodeModal'; +import { AttendanceCompleteModal } from '@/components/attendance/AttendanceCompleteModal'; + +interface AttendanceTodayCardProps { + overline: string; + title: string; + description: string; + start: string; + endTime: string; + location: string; + isAdmin?: boolean; + isChecked?: boolean; + onAttendanceComplete?: (code: string) => void; +} + +function AttendanceCompleteBanner() { + return ( +
+ 출석 완료 +
+

출석이 완료되었어요!

+

오늘도 즐거운 활동을 이어가세요.

+
+
+ ); +} + +function AttendanceTodayCard({ + overline, + title, + description, + start, + endTime, + location, + isAdmin = false, + isChecked = false, + onAttendanceComplete, +}: AttendanceTodayCardProps) { + const [codeModalOpen, setCodeModalOpen] = useState(false); + const [completeModalOpen, setCompleteModalOpen] = useState(false); + + function handleCodeConfirm(code: string) { + onAttendanceComplete?.(code); + setCompleteModalOpen(true); + } + + return ( + <> + setCompleteModalOpen(true) : () => setCodeModalOpen(true)} + primaryButtonText={isChecked ? '출석 완료' : '출석하기'} + // TODO: 관리자 출석코드 확인 기능 별도 브랜치에서 구현 예정 + onSecondaryClick={isAdmin ? () => {} : undefined} + secondaryButtonText="출석코드 확인" + > + {isChecked && } + + + + + + + ); +} + +export { AttendanceTodayCard, type AttendanceTodayCardProps }; diff --git a/src/components/attendance/InputOTP.tsx b/src/components/attendance/InputOTP.tsx new file mode 100644 index 0000000..edcd224 --- /dev/null +++ b/src/components/attendance/InputOTP.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useRef } from 'react'; + +import { cn } from '@/lib/cn'; + +/** 숫자가 아닌 문자 제거용 정규식 (모듈 레벨 호이스팅) */ +const NON_DIGIT = /\D/g; + +interface InputOTPProps { + /** 입력 칸 개수 (기본값: 6) */ + length?: number; + /** 현재 입력된 코드 문자열 */ + value: string; + /** 코드 변경 시 호출되는 콜백 */ + onChange: (value: string) => void; + className?: string; + ref?: React.Ref; +} + +/** + * OTP(일회용 코드) 입력 컴포넌트 + * + * - 각 칸에 숫자 1자리씩 입력 + * - 자동 포커스 이동 (입력 시 다음 칸, Backspace 시 이전 칸) + * - 화살표 키로 칸 이동 + * - 붙여넣기 지원 + */ +function InputOTP({ length = 6, value, onChange, className, ref }: InputOTPProps) { + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + // 단일 패스로 digits 배열 생성 (split + concat + slice 대체) + const digits = Array.from({ length }, (_, i) => value[i] ?? ''); + + function focusAt(index: number) { + inputRefs.current[index]?.focus(); + } + + /** 특정 인덱스의 값을 교체한 새 코드를 onChange로 전달 */ + function updateDigitAt(index: number, digit: string) { + const next = digits.with(index, digit); + onChange(next.join('')); + } + + /** 숫자만 허용하고, 입력 후 다음 칸으로 포커스 이동 */ + function handleInputChange(index: number, inputValue: string) { + const digit = inputValue.replace(NON_DIGIT, '').slice(-1); + if (!digit) return; + + updateDigitAt(index, digit); + + if (index < length - 1) { + focusAt(index + 1); + } + } + + /** Backspace: 현재 칸 삭제 또는 이전 칸으로 이동, 화살표 키: 칸 이동 */ + function handleKeyDown(index: number, e: React.KeyboardEvent) { + if (e.key === 'Backspace') { + e.preventDefault(); + if (digits[index]) { + updateDigitAt(index, ''); + } else if (index > 0) { + updateDigitAt(index - 1, ''); + focusAt(index - 1); + } + return; + } + + if (e.key === 'ArrowLeft' && index > 0) { + focusAt(index - 1); + return; + } + + if (e.key === 'ArrowRight' && index < length - 1) { + focusAt(index + 1); + } + } + + /** 클립보드에서 숫자만 추출하여 한 번에 입력 */ + function handlePaste(e: React.ClipboardEvent) { + e.preventDefault(); + const pasted = e.clipboardData.getData('text').replace(NON_DIGIT, '').slice(0, length); + if (!pasted) return; + + onChange(pasted); + focusAt(Math.min(pasted.length, length - 1)); + } + + return ( +
+ {digits.map((digit, index) => ( +
+ { + inputRefs.current[index] = el; + }} + type="text" + inputMode="numeric" + pattern="[0-9]" + maxLength={1} + value={digit} + aria-label={`${index + 1}번째 자리`} + onChange={(e) => handleInputChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={handlePaste} + onFocus={(e) => e.target.select()} + className="bg-container-neutral text-text-strong typo-body1 focus:border-brand-secondary flex w-full items-center self-stretch rounded-lg border-1 border-transparent px-200 py-300 text-center outline-none" + autoComplete="one-time-code" + /> +
+ ))} +
+ ); +} + +export { InputOTP, type InputOTPProps }; diff --git a/src/components/attendance/index.ts b/src/components/attendance/index.ts new file mode 100644 index 0000000..e2ec980 --- /dev/null +++ b/src/components/attendance/index.ts @@ -0,0 +1,19 @@ +export { AttendanceContent } from './AttendanceContent'; + +export { AttendanceProgressBar } from './AttendanceProgressBar'; +export type { AttendanceProgressBarProps } from './AttendanceProgressBar'; + +export { AttendanceTodayCard } from './AttendanceTodayCard'; +export type { AttendanceTodayCardProps } from './AttendanceTodayCard'; + +export { AttendanceCodeModal } from './AttendanceCodeModal'; +export type { AttendanceCodeModalProps } from './AttendanceCodeModal'; + +export { AttendanceCompleteModal } from './AttendanceCompleteModal'; +export type { AttendanceCompleteModalProps } from './AttendanceCompleteModal'; + +export { InputOTP } from './InputOTP'; +export type { InputOTPProps } from './InputOTP'; + +export { AttendanceStatus } from './AttendanceStatus'; +export type { AttendanceStatusProps } from './AttendanceStatus'; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 6251e67..ff7a2ae 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -4,15 +4,8 @@ import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { usePathname } from 'next/navigation'; -import { Button } from '../ui'; -import { - MenuIcon, - EditIcon, - CheckRoundIcon, - ExitToAppIcon, - AvatarIcon, - LogoIcon, -} from '@/assets/icons'; +import { Button, Icon } from '../ui'; +import { MenuIcon, EditIcon, SendIcon, ExitToAppIcon, AvatarIcon, LogoIcon } from '@/assets/icons'; interface HeaderProps { isMain?: boolean; @@ -82,7 +75,7 @@ export default function Header({ isMain = true }: HeaderProps) { 작성 취소 @@ -106,7 +99,7 @@ export default function Header({ isMain = true }: HeaderProps) { className="typo-button1 text-text-strong gap-100" > exit - 관리자 서비스 + 관리자 - + {onPrimaryClick && ( + + )} + {onSecondaryClick && ( + + )} ); diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 3968f22..a0e3f4b 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -3,12 +3,12 @@ import * as React from 'react'; import type { ReactNode } from 'react'; import { Dialog as DialogPrimitive } from 'radix-ui'; -import type { StaticImageData } from 'next/image'; +import { DeleteIcon } from '@/assets/icons'; import { cn } from '@/lib/cn'; import { Button } from '@/components/ui/Button'; import { Divider } from '@/components/ui/Divider'; -import deleteIcon from '@/assets/icons/delete.svg'; +import { Icon } from '@/components/ui/Icon'; function Dialog({ ...props }: React.ComponentProps) { return ; @@ -34,7 +34,7 @@ function DialogOverlay({ - - Close + )} @@ -88,6 +77,7 @@ function DialogContent({ } interface DialogHeaderProps extends React.HTMLAttributes { + icon?: ReactNode; overline?: string; title?: string; description?: string; @@ -97,6 +87,7 @@ interface DialogHeaderProps extends React.HTMLAttributes { } function DialogHeader({ + icon, overline, title, description, @@ -106,6 +97,14 @@ function DialogHeader({ className, ...props }: DialogHeaderProps) { + const closeButton = showClose && ( + + + + ); + // If children provided, use custom content mode if (children) { return ( @@ -116,20 +115,7 @@ function DialogHeader({ > Dialog
{children}
- {showClose && onClose && ( - - )} + {closeButton} ); } @@ -137,34 +123,24 @@ function DialogHeader({ return (
- {overline &&

{overline}

} - {title ? ( - -

{title}

-
- ) : ( - Dialog - )} - {description &&

{description}

} + {icon &&
{icon}
} +
+ {overline &&

{overline}

} + {title ? ( + +

{title}

+
+ ) : ( + Dialog + )} + {description &&

{description}

} +
- {showClose && onClose && ( - - )} + {closeButton}
); } @@ -201,10 +177,10 @@ function DialogFooter({ return (
{showDivider && } -
+
{children} {description && ( -

{description}

+

{description}

)} {showCloseButton && ( diff --git a/src/hooks/index.ts b/src/hooks/index.ts index eb0c3f6..848b41d 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -5,4 +5,5 @@ export { useDragScroll } from './useDragScroll'; export { useGenerationConfirm } from './useGenerationConfirm'; export { useFileAttach } from './useFileAttach'; export { useScrollIntoView } from './useScrollIntoView'; +export { useRemainingTime } from './useRemainingTime'; export { useScrollOnGrow } from './useScrollOnGrow'; diff --git a/src/hooks/useRemainingTime.ts b/src/hooks/useRemainingTime.ts new file mode 100644 index 0000000..6fbfae7 --- /dev/null +++ b/src/hooks/useRemainingTime.ts @@ -0,0 +1,38 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +/** + * 종료 시각까지 남은 시간을 1초 간격으로 카운트다운하는 훅 + * + * @param endTime - ISO 8601 형식의 종료 시각 문자열 + * @returns minutes - 남은 분 (2자리, 예: "05") + * @returns seconds - 남은 초 (2자리, 예: "09") + * @returns isExpired - 시간 만료 여부 + */ +function getRemainingSeconds(endTime: string) { + const end = new Date(endTime).getTime(); + if (Number.isNaN(end)) return 0; + return Math.max(0, Math.floor((end - Date.now()) / 1000)); +} + +function useRemainingTime(endTime: string) { + const [remaining, setRemaining] = useState(() => getRemainingSeconds(endTime)); + + useEffect(() => { + const interval = setInterval(() => { + const seconds = getRemainingSeconds(endTime); + setRemaining(seconds); + if (seconds <= 0) clearInterval(interval); + }, 1000); + + return () => clearInterval(interval); + }, [endTime]); + + const minutes = String(Math.floor(remaining / 60)).padStart(2, '0'); + const seconds = String(remaining % 60).padStart(2, '0'); + + return { minutes, seconds, isExpired: remaining <= 0 }; +} + +export { useRemainingTime }; diff --git a/src/lib/formatTime.ts b/src/lib/formatTime.ts new file mode 100644 index 0000000..98d51f9 --- /dev/null +++ b/src/lib/formatTime.ts @@ -0,0 +1,35 @@ +/** + * Date 객체를 "7:00 PM" 형식의 문자열로 변환 + */ +function formatTime(date: Date) { + return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); +} + +/** + * "2026년 3월 20일" 형식의 날짜 문자열로 변환 + */ +function formatKoreanDate(date: Date) { + return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`; +} + +/** + * 출석 카드용 설명 문자열 생성 + * "날짜 : 2026년 3월 20일 (7:00 PM~9:00 PM)\n장소 : 동아리방" + */ +function formatAttendanceDescription(start: string, end: string, location: string) { + const startDate = new Date(start); + const endDate = new Date(end); + + return `날짜 : ${formatKoreanDate(startDate)} (${formatTime(startDate)}~${formatTime(endDate)})\n장소 : ${location}`; +} + +/** + * 출석 모달용 설명 문자열 생성 + * "2026년 3월 20일 · 동아리방" + */ +function formatModalDescription(start: string, location: string) { + const date = new Date(start); + return `${formatKoreanDate(date)} · ${location}`; +} + +export { formatTime, formatKoreanDate, formatAttendanceDescription, formatModalDescription }; diff --git a/src/types/attendance.ts b/src/types/attendance.ts new file mode 100644 index 0000000..7ae5534 --- /dev/null +++ b/src/types/attendance.ts @@ -0,0 +1,17 @@ +import type { ApiResponse } from '@/types/common'; + +type AttendanceStatus = 'ATTEND' | 'ABSENT' | 'PENDING'; + +interface AttendanceData { + attendanceRate: number; + title: string; + status: AttendanceStatus; + code: string; + start: string; + end: string; + location: string; +} + +type AttendanceResponse = ApiResponse; + +export type { AttendanceStatus, AttendanceData, AttendanceResponse }; diff --git a/src/types/common.ts b/src/types/common.ts index 092ba4f..c67b5b8 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,3 +1,9 @@ +export interface ApiResponse { + code: number; + message: string; + data: T; +} + export type MutationCallbacks = { onSuccess?: () => void; onError?: (error: TError) => void; diff --git a/src/types/index.ts b/src/types/index.ts index e4cf5fe..c1116f4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,2 @@ // types index file -export type { MutationCallbacks } from './common'; +export type { ApiResponse, MutationCallbacks } from './common';