diff --git a/package.json b/package.json index f0b04e74..27634b49 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^5.28.9", "@toast-ui/react-editor": "^3.2.3", - "@types/react-dropzone": "^5.1.0", "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -58,6 +57,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@toast-ui/editor": "^3.2.2", "@types/node": "^20.12.2", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", @@ -78,7 +78,9 @@ "vite": "^5.2.0" }, "resolutions": { - "rollup": "4.24.0" + "rollup": "4.24.0", + "react": "^18.3.0", + "@types/react": "^18.3.0" }, - "packageManager": "yarn@4.5.1" + "packageManager": "yarn@4.5.3" } diff --git a/src/components/Board/BoardSelector.tsx b/src/components/Board/BoardSelector.tsx index 6e4237a7..ea7f124d 100644 --- a/src/components/Board/BoardSelector.tsx +++ b/src/components/Board/BoardSelector.tsx @@ -28,3 +28,12 @@ export function BoardSelector({ ); } + +BoardSelector.Skeleton = () => { + return ( +
+ + +
+ ); +}; diff --git a/src/components/Category/index.tsx b/src/components/Category/index.tsx index cc8f0d58..c4a9a34b 100644 --- a/src/components/Category/index.tsx +++ b/src/components/Category/index.tsx @@ -1,4 +1,5 @@ import { cn } from '@/libs/utils'; +import { Skeleton } from '@/components/ui/skeleton.tsx'; interface CategoryProps extends React.ButtonHTMLAttributes { isActive?: boolean; @@ -22,3 +23,13 @@ export function Category({ isActive = false, children, className = '', ...props ); } + +Category.Skeleton = () => { + return ( + + ); +}; diff --git a/src/components/PostContent/PostContent.tsx b/src/components/PostContent/PostContent.tsx new file mode 100644 index 00000000..f0f4031b --- /dev/null +++ b/src/components/PostContent/PostContent.tsx @@ -0,0 +1,58 @@ +import { RefAttributes } from 'react'; +import { cn } from '@/libs/utils.ts'; +import dayjs from 'dayjs'; +import { Skeleton } from '@/components/ui/skeleton.tsx'; +import { Link, LinkProps } from 'react-router-dom'; + +interface PostContentProp extends LinkProps, RefAttributes { + category: { + name: C; + className: string; + }; + date: Date; + title: string; + author?: string; + className?: string; +} + +/** + * # 일반 게시판 목록 게시글 항목 + * + * 일반 형태의 게시판 목록에서 사용할 수 있는 게시글 항목 컴포넌트입니다. + * 일반적으로 `BodyLayout` 아래에 리스트 형태 아이템으로 표시할 수 있습니다. + * C는 literal union이길 권장합니다만, 상황에 따라 다양한 타입을 넣을 수 있습니다. + */ +export function PostContent({ category, title, author, date, ...props }: PostContentProp) { + const formattedDate = dayjs(date).format('YYYY/MM/DD'); + + return ( + +
[{category.name}]
+ {/* 잘못된 tailwind.config.js: `min-`, `max-` prefix로 range가 지원되는데 왜 이렇게 breakpoint를 짰을까요?? + * Reference: https://tailwindcss.com/docs/responsive-design#targeting-mobile-screens + */} +
+
{title}
+
+ {author} + {formattedDate} +
+
+ + ); +} + +PostContent.Skeleton = () => { + return ( +
+ +
+ +
+ + +
+
+
+ ); +}; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 66af50f1..c9b93d63 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -19,7 +19,7 @@ const buttonVariants = cva( Register: 'bg-[#2F4BF7] hover:bg-[#2F4BF7]/90 w-[105px] md:w-full sm:w-full xs:w-full h-10 text-white text-lg xs:text-xs text-center font-semibold rounded-[7px]', List_Edit: - 'pl-2 flex gap-2 pr-6 w-32 h-10 bg-white border border-gray-400 text-lg text-center font-semibold text-gray-700 hover:border-primary hover:bg-white hover:text-primary', + 'flex gap-2 w-32 h-10 bg-white border border-gray-400 text-lg text-center font-semibold text-gray-700 hover:border-primary hover:bg-white hover:text-primary', Write: 'pl-5 flex gap-1 pr-6 w-32 h-10 bg-white border border-gray-400 text-lg text-center font-semibold text-gray-700 hover:border-primary hover:bg-white hover:text-primary', }, diff --git a/src/containers/common/Header/const/pathData.tsx b/src/containers/common/Header/const/pathData.tsx index e7568e4c..796ccba7 100644 --- a/src/containers/common/Header/const/pathData.tsx +++ b/src/containers/common/Header/const/pathData.tsx @@ -34,8 +34,8 @@ export const menuItems = { ], 소통: [ { name: '학생청원게시판', path: '/petition-notice' }, - // { name: '건의게시판', path: '/sug-notice' }, - // { name: '인권신고게시판', path: '/human-notice' }, + { name: '건의게시판', path: '/sug-notice' }, + { name: '인권신고게시판', path: '/human-rights' }, ], }; diff --git a/src/hooks/useContentEditor.ts b/src/hooks/useContentEditor.ts new file mode 100644 index 00000000..19d3224d --- /dev/null +++ b/src/hooks/useContentEditor.ts @@ -0,0 +1,136 @@ +import { RefObject, useRef, useState } from 'react'; +import { EditorType, HookMap, PreviewStyle } from '@toast-ui/editor'; +import { Editor } from '@toast-ui/react-editor'; +import { clientAuth } from '@/apis/client.ts'; +import { PostFileResponse, UploadFilesResponse } from '@/pages/human-rights/hooks/mutations/useUploadFiles.ts'; +import { ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { FileResponse } from '@/types/apis/get'; + +interface UseContentEditorReturn { + register: { + hooks: HookMap; + ref: RefObject; + previewStyle: PreviewStyle; + initialEditType: EditorType; + hideModeSwitch: boolean; + language: string; + autofocus: boolean; + }; + isImageProcessing: boolean; + processImages: (uploadedFiles?: FileResponse[]) => Promise<{ + existedImages: FileResponse[]; + newImages: PostFileResponse[]; + content: string; + }>; +} + +async function postBoardImages(boardCode: string, files: FormData) { + return await clientAuth>({ + url: `/board/${boardCode}/files`, + method: 'post', + data: files, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +} + +/** + * useContentEditor 훅 + * + * `ContentEditor`의 동작을 정의하는 훅입니다. `Editor` 컴포넌트에 `register` 값을 분해 할당하면 됩니다. + * 첨부 이미지의 처리는 아래의 단계를 따릅니다. + * 1. 이미지 첨부 시 이미지의 `Blob`을 `objectUrl`로 변경하여 표시 + * 2. `processImages` 호출 시 로컬의 이미지를 업로드 시도 + * 3. 업로드된 이미지의 URL으로 기존 objectURL을 대체 + * 4. 업로드된 이미지 정보와 대체된 컨텐츠 문자열 반환 + * + * @param boardCode - 게시판 코드 + * @param ref - `Editor` 컴포넌트의 ref + * @returns + * `register` - `Editor` 컴포넌트에 분해 할당하여 에디터를 훅에 등록합니다. + * `isImageProcessing` - 이미지가 업로드 중이라면 `true`를 반환합니다. + * `processImages` - 이미지를 업로드하고 처리하는 함수입니다. 이미 업로드된 이미지의 `FileResponse` 배열을 인자로 받아, 최종적으로 에디터에 존재하는 파일 목록을 반환합니다. + * 반환 값을 그대로 `postFileList`에 넣을 수 있습니다(첨부파일이 있다면 합쳐야 합니다). + * + * @example + * ```tsx + * const editorRef = useRef(null); + * const { register, processImages, isImageProcessing } = useContentEditor('청원게시판', editorRef); + * return (
+ * + *

{isImageProcessing && '이미지 업로드 중...'}

+ *
); + * ``` + */ +// TODO: Define boardCode as enum +export function useContentEditor(boardCode: string, ref: RefObject): UseContentEditorReturn { + const imageObjectUrlsRef = useRef<[File | Blob, string][]>([]); + const [isImageProcessing, setIsImageProcessing] = useState(false); + + function addImageBlobHook(file: File | Blob, callback: (url: string, text?: string) => void) { + if (file) { + const objectUrl = URL.createObjectURL(file); + imageObjectUrlsRef.current.push([file, objectUrl]); + if ('name' in file) { + callback(objectUrl, file.name); + } else { + callback(objectUrl, 'image'); + } + } + return false; + } + + async function processImages(existedImages: FileResponse[] = []) { + setIsImageProcessing(true); + try { + const markdownContent = ref.current!.getInstance().getMarkdown(); + const imageUploadPromises = imageObjectUrlsRef.current.map(([image, objectUrl]) => { + async function uploadImage() { + const formData = new FormData(); + formData.append('images', image); + const imageResponse = await postBoardImages(boardCode, formData); + return { + objectUrl, + success: imageResponse.data.isSuccess, + ...(imageResponse.data.isSuccess ? { files: imageResponse.data.data.postFiles } : {}), + }; + } + + return uploadImage(); + }); + const uploadedImages = await Promise.all(imageUploadPromises); + const processedContent = uploadedImages.reduce((content, { objectUrl, success, files }) => { + if (success && files) { + URL.revokeObjectURL(objectUrl); + return content.replace(objectUrl, files[0].url); + } + return content; + }, markdownContent); + imageObjectUrlsRef.current = []; + setIsImageProcessing(false); + return { + existedImages: existedImages.filter(({ fileUrl }) => processedContent.includes(fileUrl)), + newImages: uploadedImages.filter(({ files }) => files).flatMap(({ files }) => files as PostFileResponse[]), + content: processedContent, + }; + } catch (error) { + setIsImageProcessing(false); + throw error; + } + } + + return { + register: { + hooks: { addImageBlobHook }, + ref: ref, + previewStyle: 'vertical', + initialEditType: 'wysiwyg', + hideModeSwitch: true, + language: 'ko-KR', + autofocus: false, + }, + isImageProcessing, + processImages, + }; +} diff --git a/src/pages/audit/page.tsx b/src/pages/audit/page.tsx index 67d59b69..4378f941 100644 --- a/src/pages/audit/page.tsx +++ b/src/pages/audit/page.tsx @@ -10,14 +10,13 @@ import { useCategory } from './hooks/useCategory'; export function AuditPage() { const boardCode = '감사기구게시판'; + const navigate = useNavigate(); const { category } = useCategory(); const { data, totalPages, currentPage, handlePageChange, subcategories, isLoading } = useAuditBoard( boardCode, category ); - const navigate = useNavigate(); - return ( <> + + {fileName} + + ); +} diff --git a/src/pages/human-rights/[id]/components/ContentViewer.tsx b/src/pages/human-rights/[id]/components/ContentViewer.tsx new file mode 100644 index 00000000..e2338ddf --- /dev/null +++ b/src/pages/human-rights/[id]/components/ContentViewer.tsx @@ -0,0 +1,11 @@ +import { Viewer } from '@toast-ui/react-editor'; +import { useRef } from 'react'; + +interface ContentViewerProps { + content?: string; +} + +export function ContentViewer({ content }: ContentViewerProps) { + const viewerRef = useRef(null); + return ; +} diff --git a/src/pages/human-rights/[id]/components/DropdownButton.tsx b/src/pages/human-rights/[id]/components/DropdownButton.tsx new file mode 100644 index 00000000..c97be906 --- /dev/null +++ b/src/pages/human-rights/[id]/components/DropdownButton.tsx @@ -0,0 +1,71 @@ +import { ButtonHTMLAttributes, forwardRef, RefObject, useEffect, useRef, useState } from 'react'; +import { cn } from '@/libs/utils.ts'; + +export interface DropdownButtonItem { + id: string; + text: string; +} + +interface DropdownButtonProps extends ButtonHTMLAttributes { + items: DropdownButtonItem[]; + onItemClick: (id: string) => void; +} + +function useOutsideClick(ref: RefObject, onOutsideClick: () => void) { + useEffect(() => { + const handler = (evt: MouseEvent) => { + if (ref.current && !ref.current.contains(evt.target as Node)) { + onOutsideClick(); + } + }; + document.addEventListener('mousedown', handler); + return () => { + document.removeEventListener('mousedown', handler); + }; + }, [onOutsideClick, ref]); +} + +const DropdownMenu = forwardRef>( + ({ items, onItemClick, className }, ref) => { + return ( +
    + {items.map((item) => ( +
  • onItemClick(item.id)} + > + {item.text} +
  • + ))} +
+ ); + } +); + +export function DropdownButton({ items, onItemClick, children, className, ...props }: DropdownButtonProps) { + const ref = useRef(null); + const [opened, setOpened] = useState(false); + + useOutsideClick(ref, () => opened && setOpened(false)); + return ( +
+ +
+ { + setOpened(false); + onItemClick(id); + }} + /> +
+
+ ); +} diff --git a/src/pages/human-rights/[id]/components/Frontmatter.tsx b/src/pages/human-rights/[id]/components/Frontmatter.tsx new file mode 100644 index 00000000..d2c55d2d --- /dev/null +++ b/src/pages/human-rights/[id]/components/Frontmatter.tsx @@ -0,0 +1,22 @@ +import { Fragment } from 'react'; + +interface FrontmatterProps { + title: string; + pairs: [string, string][]; +} + +export function Frontmatter({ title, pairs }: FrontmatterProps) { + return ( +
+

{title}

+
+ {pairs.map(([term, detail]) => ( + +
{term}
+
{detail}
+
+ ))} +
+
+ ); +} diff --git a/src/pages/human-rights/[id]/components/PostBody.tsx b/src/pages/human-rights/[id]/components/PostBody.tsx new file mode 100644 index 00000000..b4982cd9 --- /dev/null +++ b/src/pages/human-rights/[id]/components/PostBody.tsx @@ -0,0 +1,24 @@ +import { FileResponse } from '@/types/apis/get'; +import { Attachment } from '@/pages/human-rights/[id]/components/Attachment.tsx'; +import { ContentViewer } from '@/pages/human-rights/[id]/components/ContentViewer.tsx'; + +interface PostBodyProps { + content: string; + files: FileResponse[]; +} + +export function PostBody({ content, files }: PostBodyProps) { + const attachments = files.filter((file) => file.fileType === 'files'); + return ( + <> +
+ +
+
+ {attachments.map((attachment) => ( + + ))} +
+ + ); +} diff --git a/src/pages/human-rights/[id]/components/PostComment.tsx b/src/pages/human-rights/[id]/components/PostComment.tsx new file mode 100644 index 00000000..1e375053 --- /dev/null +++ b/src/pages/human-rights/[id]/components/PostComment.tsx @@ -0,0 +1,111 @@ +import { ReactNode, useState } from 'react'; +import { cn } from '@/libs/utils.ts'; +import { DotsThree, User } from '@phosphor-icons/react'; +import dayjs from 'dayjs'; +import { DropdownButton } from '@/pages/human-rights/[id]/components/DropdownButton.tsx'; +import { PostCommentEditor } from '@/pages/human-rights/[id]/components/PostCommentEditor.tsx'; +import { Skeleton } from '@/components/ui/skeleton.tsx'; + +interface PostCommentProps { + children?: ReactNode; + author: string; + commentType?: 'GENERAL' | 'OFFICIAL'; + createdAt: Date; + lastEditedAt?: Date; + editable?: boolean; + deletable?: boolean; + deleted?: boolean; + onEdit?: (value: string) => void; + onDelete?: () => void; +} + +export function PostComment({ + author, + createdAt, + commentType = 'GENERAL', + lastEditedAt, + editable, + deletable, + deleted, + children, + onEdit, + onDelete, +}: PostCommentProps) { + const [editing, setEditing] = useState(false); + const formattedDate = dayjs(lastEditedAt ? lastEditedAt : createdAt).format('YYYY-MM-DD HH:mm'); + + function onItemClick(id: string) { + if (id === 'edit') { + setEditing(true); + } else if (id === 'delete' && onDelete) { + onDelete(); + } + } + + return ( +
+
+
+ + {deleted ? '삭제된 댓글' : author} +
+ {(editable || deletable) && !deleted && !editing && ( + + {/* TODO: Breakpoint 수정 후 변경 */} + + + + )} +
+ {editing ? ( + setEditing(false)} + /> + ) : ( +
{deleted ? '삭제된 댓글입니다.' : children}
+ )} + +
+ ); +} + +PostComment.Skeleton = () => { + return ( +
+
+
+ + +
+
+ + +
+ ); +}; diff --git a/src/pages/human-rights/[id]/components/PostCommentEditor.tsx b/src/pages/human-rights/[id]/components/PostCommentEditor.tsx new file mode 100644 index 00000000..30aecf95 --- /dev/null +++ b/src/pages/human-rights/[id]/components/PostCommentEditor.tsx @@ -0,0 +1,86 @@ +import { ChangeEvent, RefObject, useEffect, useRef, useState } from 'react'; +import { cn } from '@/libs/utils.ts'; +import { Button } from '@/components/ui/button.tsx'; +import { Loader2 } from 'lucide-react'; + +interface PostCommentEditorProps { + value?: string; + placeholder?: string; + className?: string; + maxLength?: number; + editing?: boolean; + uploading?: boolean; + onSubmit?: (value: string) => void; + onCancel?: () => void; +} + +export function PostCommentEditor({ + value, + placeholder, + className, + maxLength, + editing, + uploading, + onSubmit, + onCancel, +}: PostCommentEditorProps) { + const [innerValue, setInnerValue] = useState(''); + const ref: RefObject = useRef(null); + useEffect(() => { + setInnerValue(value ?? ''); + }, [value]); + useEffect(() => { + if (ref.current) { + ref.current.style.height = 'auto'; + ref.current.style.height = `${ref.current?.scrollHeight}px`; + } + }, [innerValue]); + + function handleChange(e: ChangeEvent) { + setInnerValue(e.target.value); + } + + function handleClick() { + onSubmit?.(innerValue); + setInnerValue(''); + } + + return ( +
+ +
+ {maxLength && ( +

+ {innerValue.length}/{maxLength} +

+ )} + {editing && ( + + )} + +
+
+ ); +} diff --git a/src/pages/human-rights/[id]/components/PostFooter.tsx b/src/pages/human-rights/[id]/components/PostFooter.tsx new file mode 100644 index 00000000..396fd1ff --- /dev/null +++ b/src/pages/human-rights/[id]/components/PostFooter.tsx @@ -0,0 +1,47 @@ +import { DeleteButton } from '@/components/Buttons/BoardActionButtons.tsx'; +import { buttonVariants } from '@/components/ui/button.tsx'; +import { List, Pencil } from '@phosphor-icons/react'; +import { Skeleton } from '@/components/ui/skeleton.tsx'; +import { ArticleFooter } from '@/pages/human-rights/containers/ArticleFooter.tsx'; +import { cn } from '@/libs/utils.ts'; + +interface PostFooterProps { + boardUrl: string; + deletable?: boolean; + editable?: boolean; + editUrl?: string; + className?: string; + onDelete?: () => void; +} + +export function PostFooter({ boardUrl, deletable, editable, editUrl, onDelete, className }: PostFooterProps) { + return ( + +
+ {deletable && } + {editable && ( + + +

편집

+
+ )} + + +

목록

+
+
+
+ ); +} + +PostFooter.Skeleton = () => { + return ( +
+
+ + + +
+
+ ); +}; diff --git a/src/pages/human-rights/[id]/components/PostHeader.tsx b/src/pages/human-rights/[id]/components/PostHeader.tsx new file mode 100644 index 00000000..8d4fd0fe --- /dev/null +++ b/src/pages/human-rights/[id]/components/PostHeader.tsx @@ -0,0 +1,45 @@ +import dayjs from 'dayjs'; +import Breadcrumb from '@/components/Breadcrumb'; +import { User } from '@phosphor-icons/react'; +import { Skeleton } from '@/components/ui/skeleton.tsx'; +import { ArticleHeader } from '@/pages/human-rights/containers/ArticleHeader.tsx'; + +interface PostHeaderProps { + title: string; + authorName: string; + createdAt: Date; + breadcrumbItems: [string, string | null][]; + className?: string; +} + +export function PostHeader({ title, authorName: author, createdAt, breadcrumbItems, className }: PostHeaderProps) { + const breadcrumbMap = new Map(breadcrumbItems); + const formattedDate = dayjs(createdAt).format('YYYY/MM/DD HH:mm'); + + return ( + + +

{title}

+ {/* TODO: 이후 디자인 변경시 text 색상 tailwind 토큰으로 변경 */} +
+
+ + {author} +
+ +
+
+ ); +} + +PostHeader.Skeleton = () => { + return ( +
+
+ + + +
+
+ ); +}; diff --git a/src/pages/human-rights/[id]/page.tsx b/src/pages/human-rights/[id]/page.tsx new file mode 100644 index 00000000..7550e2b8 --- /dev/null +++ b/src/pages/human-rights/[id]/page.tsx @@ -0,0 +1,254 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { PostHeader } from '@/pages/human-rights/[id]/components/PostHeader.tsx'; +import { Frontmatter } from '@/pages/human-rights/[id]/components/Frontmatter.tsx'; +import { PostBody } from '@/pages/human-rights/[id]/components/PostBody.tsx'; +import { PostFooter } from '@/pages/human-rights/[id]/components/PostFooter.tsx'; +import { PostCommentEditor } from '@/pages/human-rights/[id]/components/PostCommentEditor.tsx'; +import { PostComment } from '@/pages/human-rights/[id]/components/PostComment.tsx'; +import { + HumanRightsComment, + HumanRightsCommentResponse, + HumanRightsPerson, + HumanRightsReporter, +} from '@/pages/human-rights/schema.ts'; +import { Container } from '@/pages/human-rights/containers/Container.tsx'; +import { + useDeleteHumanRightsPost, + useGetHumanRightsComments, + useGetHumanRightsPost, +} from '@/pages/human-rights/queries.ts'; +import { useCreateComment } from '@/pages/human-rights/hooks/mutations/useCreateComment.ts'; +import { usePatchComment } from '@/pages/human-rights/hooks/mutations/usePatchComment.ts'; +import { useDeleteComment } from '@/pages/human-rights/hooks/mutations/useDeleteComment.ts'; +import { useQueryClient } from '@tanstack/react-query'; +import { useState } from 'react'; + +const BOARD_CODE = '인권신고게시판' as const; + +const breadcrumbItems: [string, string | null][] = [ + ['소통', null], + [BOARD_CODE, '/human-rights'], +]; + +function personToFrontmatter(person: HumanRightsPerson): [string, string][] { + return [ + ['성명', person.name], + ['학번', person.studentId], + ['학과/부', person.major], + ]; +} + +function reporterToFrontmatter(reporter: HumanRightsReporter): [string, string][] { + return [...personToFrontmatter(reporter), ['연락처', reporter.phoneNumber]]; +} + +function ReporterFrontmatter({ reporter }: { reporter: HumanRightsReporter }) { + return ; +} + +function PersonFrontmatter({ title, person }: { title: string; person: HumanRightsPerson }) { + return ; +} + +function PageSkeleton() { + return ( +
+ +
+ + +
+ ); +} + +export function HumanRightsDetailPage() { + /* Router Props */ + const { id } = useParams<{ id: string }>(); + const postId = parseInt(id ?? ''); + const navigate = useNavigate(); + + /* Load data by query */ + const queryClient = useQueryClient(); + const { data: post, isLoading, error, isError } = useGetHumanRightsPost({ postId, queryOptions: { retry: true } }); + const { + data: comments, + isLoading: isCommentsLoading, + error: commentsError, + isError: isCommentsError, + } = useGetHumanRightsComments({ postId, type: '최신순', queryOptions: { retry: true } }); + + /* Set up Mutations */ + const { mutate: deletePost, isPending: isDeletePostPending } = useDeleteHumanRightsPost(); + const { mutate: submitComment, isPending: isSubmitCommentPending } = useCreateComment({ + postId, + }); + const { mutate: patchComment, isPending: isPatchCommentPending } = usePatchComment(); + const { mutate: deleteComment, isPending: isDeleteCommentPending } = useDeleteComment(); + + /* State to check updating comments */ + const [lastUpdatedComment, setLastUpdatedComment] = useState(null); + + if (isLoading || isDeletePostPending || isCommentsLoading) { + return ; + } + + if (isNaN(postId) || !post || isError || !comments || isCommentsError) { + console.log(error, commentsError); + // TODO: 오류 발생 시 세부정보 제공 + return ( +
+

오류가 발생하였습니다. 관리자에게 문의하십시오.

+
+ ); + } + + /* Data Preparation */ + const reporter = post.rightsDetailList.find((person) => person.personType === 'REPORTER') as + | HumanRightsReporter + | undefined; + const victims = post.rightsDetailList.filter((person) => person.personType === 'VICTIM'); + const attackers = post.rightsDetailList.filter((person) => person.personType === 'ATTACKER'); + + const fullComments: HumanRightsComment[] = [...post.officialCommentList.reverse(), ...comments.postComments]; + const totalComments = comments.total + post.officialCommentList.length; + const commentAcl = comments.allowedAuthorities; + + const editable = post.isAuthor || post.allowedAuthorities.includes('EDIT'); + const deletable = post.isAuthor || post.allowedAuthorities.includes('DELETE'); + const commentable = post.isAuthor || commentAcl.includes('COMMENT'); + const commentDeletable = commentAcl.includes('DELETE_COMMENT'); + + /* Mutation handler */ + function handleDeletePost() { + if (post) { + deletePost( + { postId: `${postId}`, fileUrls: post.postFileList.map((file) => file.fileUrl) }, + { + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['searchPosts', BOARD_CODE] }); + navigate('/human-rights'); + }, + } + ); + } + } + + function handleSubmitComment(content: string) { + submitComment( + { content }, + { + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['getComments', postId], + }); + await queryClient.invalidateQueries({ + queryKey: ['getPost', BOARD_CODE, postId], + }); + }, + } + ); + } + + function handleDeleteComment(commentId: number) { + setLastUpdatedComment(commentId); + deleteComment( + { commentId }, + { + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['getComments', postId] }); + await queryClient.invalidateQueries({ + queryKey: ['getPost', BOARD_CODE, postId], + }); + }, + } + ); + } + + function handlePatchComment(commentId: number, content: string) { + setLastUpdatedComment(commentId); + patchComment( + { commentId, content }, + { + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['getComments', postId] }); + await queryClient.invalidateQueries({ + queryKey: ['getPost', BOARD_CODE, postId], + }); + }, + } + ); + } + + return ( + <> +
+ +
+ + {reporter && } + {victims.map((victim, idx) => ( + + ))} + {attackers.map((attacker, idx) => ( + + ))} + + + +
+
+ + {/* TODO: 댓글 로딩 중 스켈레톤 */} + {!isCommentsLoading && ( +
+

+ 댓글 {totalComments} +

+ {commentable && ( + + )} + {[...fullComments].map((comment) => { + if ((lastUpdatedComment ?? -1) === comment.id && (isPatchCommentPending || isDeleteCommentPending)) { + return ; + } + return ( + handleDeleteComment(comment.id)} + onEdit={(content) => handlePatchComment(comment.id, content)} + > + {comment.content} + + ); + })} +
+ )} +
+ + ); +} diff --git a/src/pages/human-rights/containers/ArticleFooter.tsx b/src/pages/human-rights/containers/ArticleFooter.tsx new file mode 100644 index 00000000..12b4d5a2 --- /dev/null +++ b/src/pages/human-rights/containers/ArticleFooter.tsx @@ -0,0 +1,17 @@ +import { cn } from '@/libs/utils.ts'; +import { ReactNode } from 'react'; + +interface ArticleFooterProps { + children?: ReactNode; + className?: string; +} + +export function ArticleFooter({ children, className }: ArticleFooterProps) { + return ( +
+
{children}
+
+ ); +} diff --git a/src/pages/human-rights/containers/ArticleHeader.tsx b/src/pages/human-rights/containers/ArticleHeader.tsx new file mode 100644 index 00000000..f9b3e383 --- /dev/null +++ b/src/pages/human-rights/containers/ArticleHeader.tsx @@ -0,0 +1,17 @@ +import { cn } from '@/libs/utils.ts'; +import { ReactNode } from 'react'; + +interface ArticleHeaderProps { + children?: ReactNode; + className?: string; +} + +export function ArticleHeader({ children, className }: ArticleHeaderProps) { + return ( +
+
{children}
+
+ ); +} diff --git a/src/pages/human-rights/containers/Container.tsx b/src/pages/human-rights/containers/Container.tsx new file mode 100644 index 00000000..2d231976 --- /dev/null +++ b/src/pages/human-rights/containers/Container.tsx @@ -0,0 +1,28 @@ +import { ReactNode } from 'react'; +import { cn } from '@/libs/utils.ts'; +import { Skeleton } from '@/components/ui/skeleton.tsx'; + +interface ContainerProps { + className?: string; + children: ReactNode; +} + +export function Container({ className, children }: ContainerProps) { + return ( +
+
{children}
+
+ ); +} + +Container.Skeleton = () => { + return ( +
+
+ +
+
+ ); +}; diff --git a/src/pages/human-rights/edit/components/FileInput.tsx b/src/pages/human-rights/edit/components/FileInput.tsx new file mode 100644 index 00000000..52e8ad57 --- /dev/null +++ b/src/pages/human-rights/edit/components/FileInput.tsx @@ -0,0 +1,158 @@ +import { cn } from '@/libs/utils.ts'; +import { FileText, Plus, Trash } from '@phosphor-icons/react'; +import { + ChangeEvent, + DragEvent, + forwardRef, + InputHTMLAttributes, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import { humanFileSize } from '@/pages/human-rights/edit/utils.ts'; + +export type PostFile = UploadedPostFile | LocalPostFile; + +export interface UploadedPostFile { + name: string; + isUploaded: true; + id: number; +} + +export interface LocalPostFile { + name: string; + isUploaded: false; + file: File; +} + +interface FileItemProps extends Omit, 'type' | 'multiple' | 'value'> { + sizeLimit?: number; + file?: PostFile; +} + +export const FileInput = forwardRef(function ( + { file, className, onChange, sizeLimit, ...props }: FileItemProps, + ref +) { + const [innerFile, setInnerFile] = useState(null); + const innerRef = useRef(null); + const [isDragging, setDragging] = useState(false); + const [error, setError] = useState(null); + useImperativeHandle(ref, () => innerRef.current!, []); + + useEffect(() => { + if (innerRef.current) { + const dataTransfer = new DataTransfer(); + if (file) { + if (!file.isUploaded) dataTransfer.items.add(file.file); + } + innerRef.current.files = dataTransfer.files; + } + setInnerFile(file ?? null); + }, [file]); + + function clearFile() { + if (innerRef.current) { + innerRef.current.files = new DataTransfer().files; + innerRef.current.dispatchEvent(new Event('change', { bubbles: true })); + } + } + + function fileDropHandler(evt: DragEvent): void { + evt.preventDefault(); + setDragging(false); + if (innerRef.current && evt.dataTransfer.files.length > 0) { + innerRef.current.files = evt.dataTransfer.files; + innerRef.current.dispatchEvent(new Event('change', { bubbles: true })); + } + } + + function dragOverHandler(evt: DragEvent): void { + evt.preventDefault(); + setDragging(true); + } + + function dragExitHandler(evt: DragEvent): void { + evt.preventDefault(); + setDragging(false); + } + + function fileChangeHandler(evt: ChangeEvent): void { + evt.preventDefault(); + const fileSize = evt.currentTarget.files?.item(0)?.size ?? -1; + if (fileSize >= 0 && sizeLimit !== undefined && fileSize > sizeLimit) { + clearFile(); + setError(`파일이 ${humanFileSize(sizeLimit)}를 초과합니다. (파일 크기: ${humanFileSize(fileSize)})`); + return; + } else { + setError(null); + } + // Give priority to user's onChange -- this allows intercept files input before component process file itself. + // 컴포넌트 사용자의 onChange를 우선 실행하여 파일 입력을 처리하기 전에 사용자가 가로챌 수 있도록 합니다. + if (onChange) { + onChange(evt); + } + const file = evt.currentTarget.files?.item(0); + setInnerFile( + file + ? { + name: file.name, + isUploaded: false, + file, + } + : null + ); + } + + return ( +
+
innerRef.current?.showPicker()} + > + + + {isDragging ? ( + innerFile ? ( + '파일을 끌어놓아 변경하기' + ) : ( + '파일을 끌어놓아 추가하기' + ) + ) : error ? ( + <> + 업로드 실패 + {error} + + ) : ( + (innerFile?.name ?? '파일을 선택해주세요') + )} + +
+ + +
+ ); +}); diff --git a/src/pages/human-rights/edit/components/FileInputs.tsx b/src/pages/human-rights/edit/components/FileInputs.tsx new file mode 100644 index 00000000..3071c36c --- /dev/null +++ b/src/pages/human-rights/edit/components/FileInputs.tsx @@ -0,0 +1,63 @@ +import { ChangeEvent, useEffect, useState } from 'react'; +import { FileInput, PostFile } from '@/pages/human-rights/edit/components/FileInput.tsx'; +import { cn } from '@/libs/utils.ts'; + +interface FileInputsProps { + className?: string; + sizeLimit?: number; + files?: PostFile[]; + onChange?: (files: PostFile[]) => void; +} + +export function FileInputs({ className, files, onChange, sizeLimit }: FileInputsProps) { + const [innerFiles, setInnerFiles] = useState([]); + + useEffect(() => { + // Do not trigger onChange if files are controlled from outside. + // 외부에서 files 값이 변경될 경우 re-render는 수행하지만 onChange 이벤트를 발생시키지 않습니다. + if (files) setInnerFiles(files); + else setInnerFiles([]); + }, [files]); + + function onNewFile(evt: ChangeEvent) { + const file = evt.currentTarget.files?.item(0); + if (file) { + const postFile: PostFile = { + name: file.name, + isUploaded: false, + file: file, + }; + const newFiles = [...innerFiles, postFile]; + setInnerFiles(newFiles); + if (onChange) onChange(newFiles); + // Remove the existing file in the new file input to keep input fresh. + // 새 파일을 입력받는 input 의 파일을 제거하여 파일이 추가된 상태로 보이지 않도록 합니다. + evt.currentTarget.files = new DataTransfer().files; + } + } + + function onFileChange(idx: number, evt: ChangeEvent) { + const file = evt.currentTarget.files?.item(0); + const newFiles = innerFiles; + if (file) { + newFiles[idx] = { + name: file.name, + isUploaded: false, + file: file, + }; + } else { + newFiles.splice(idx, 1); + } + if (onChange) onChange(newFiles); + setInnerFiles([...newFiles]); + } + + return ( +
+ {innerFiles.map((file, idx) => ( + onFileChange(idx, evt)} sizeLimit={sizeLimit} /> + ))} + +
+ ); +} diff --git a/src/pages/human-rights/edit/components/FrontmatterEditor.tsx b/src/pages/human-rights/edit/components/FrontmatterEditor.tsx new file mode 100644 index 00000000..247b73be --- /dev/null +++ b/src/pages/human-rights/edit/components/FrontmatterEditor.tsx @@ -0,0 +1,66 @@ +import { Fragment } from 'react'; +import { FrontmatterInput } from '@/pages/human-rights/edit/components/FrontmatterInput.tsx'; +import { cn } from '@/libs/utils.ts'; +import { FieldErrors, FieldValues, get, Path, UseFormRegister } from 'react-hook-form'; + +interface FrontmatterEditProps { + id: string; + items: { + id: string; + term: string; + required?: boolean; + value?: string; + disabled?: boolean; + errorMessage?: string; + registerPath?: Path; + }[]; + errors: FieldErrors; + register?: UseFormRegister; +} + +export function FrontmatterEditor>({ + id, + items, + errors, + register, +}: FrontmatterEditProps) { + return ( +
+ {items.map(({ id: itemId, term, required, value, disabled, errorMessage, registerPath }) => { + const error = get(errors, registerPath)?.type; + return ( + + + {/* Input 컴포넌트를 사용하려 했으나 Input 컴포넌트의 기본 스타일의 크기가 규격과 맞지 않네요... */} +
+ +

+ {errorMessage ?? error} +

+
+
+ ); + })} +
+ ); +} diff --git a/src/pages/human-rights/edit/components/FrontmatterInput.tsx b/src/pages/human-rights/edit/components/FrontmatterInput.tsx new file mode 100644 index 00000000..db2e5960 --- /dev/null +++ b/src/pages/human-rights/edit/components/FrontmatterInput.tsx @@ -0,0 +1,17 @@ +import { forwardRef, InputHTMLAttributes } from 'react'; +import { cn } from '@/libs/utils.ts'; + +// TODO: 기존 Input 디자인과 상이하여 새로운 컴포넌트인 `FrontmatterInput` 생성, 추후 디자인팀과 논의하여 다른 페이지의 폼과 동일한 형태가 되도록 리팩토링 필요 +const FrontmatterInput = forwardRef>( + ({ className, ...props }, ref) => { + return ( + + ); + } +); + +export { FrontmatterInput }; diff --git a/src/pages/human-rights/edit/form.ts b/src/pages/human-rights/edit/form.ts new file mode 100644 index 00000000..3f955f2c --- /dev/null +++ b/src/pages/human-rights/edit/form.ts @@ -0,0 +1,55 @@ +import { DefaultValues, useFieldArray, UseFieldArrayReturn, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { HumanRightsPostEditForm, HumanRightsPostEditFormSchema } from '@/pages/human-rights/schema.ts'; + +/** + * PrefixKeys 유틸리티 + * Object의 각 key에 camelCase를 유지하며 접두사를 붙입니다. + * + * 예시: + * ```typescript + * const original: OriginalObject = { test: 'hello', world: 'vanquisher' }; + * const prefixed: PrefixKeys = prefixKeys(original, 'hello'); + * console.log(prefixed) // { helloTest: 'hello', helloWorld: 'vanquisher' } + * ``` + */ +type PrefixKeys = { + [K in keyof T as K extends string ? `${P}${Capitalize}` : never]: T[K]; +}; + +function prefixKeys(obj: T, prefix: P): PrefixKeys { + function capitalize(string: S): Capitalize { + return (string[0].toUpperCase() + string.slice(1)) as Capitalize; + } + + return Object.fromEntries(Object.entries(obj).map(([k, v]) => [`${prefix}${capitalize(k)}`, v])) as PrefixKeys; +} + +export function useHumanRightsForm(defaultValues?: DefaultValues) { + const form = useForm({ + resolver: zodResolver(HumanRightsPostEditFormSchema), + mode: 'onBlur', + defaultValues, + }); + const victimFieldArray: PrefixKeys, 'victim'> = prefixKeys( + useFieldArray({ + control: form.control, + name: 'rightsDetailList.victims', + rules: { minLength: 1 }, + }), + 'victim' + ); + const attackerFieldArray: PrefixKeys, 'attacker'> = prefixKeys( + useFieldArray({ + control: form.control, + name: 'rightsDetailList.attackers', + rules: { minLength: 1 }, + }), + 'attacker' + ); + return { + ...form, + ...victimFieldArray, + ...attackerFieldArray, + }; +} diff --git a/src/pages/human-rights/edit/page.tsx b/src/pages/human-rights/edit/page.tsx new file mode 100644 index 00000000..a6991149 --- /dev/null +++ b/src/pages/human-rights/edit/page.tsx @@ -0,0 +1,556 @@ +import { Input } from '@/components/ui/input.tsx'; +import { Editor } from '@toast-ui/react-editor'; +import { FrontmatterEditor } from '@/pages/human-rights/edit/components/FrontmatterEditor.tsx'; +import { cn } from '@/libs/utils.ts'; +import { ArticleHeader } from '@/pages/human-rights/containers/ArticleHeader.tsx'; +import { Container } from '@/pages/human-rights/containers/Container.tsx'; +import { ArticleFooter } from '@/pages/human-rights/containers/ArticleFooter.tsx'; +import { MinusCircle, Plus } from '@phosphor-icons/react'; +import { useHumanRightsForm } from '@/pages/human-rights/edit/form.ts'; +import { FileInputs } from '@/pages/human-rights/edit/components/FileInputs.tsx'; +import { useEffect, useRef, useState } from 'react'; +import { useContentEditor } from '@/hooks/useContentEditor.ts'; +import { + HumanRightsPerson, + HumanRightsPost, + HumanRightsPostEditForm, + HumanRightsPostEditRequest, + HumanRightsPostEditRequestSchema, + HumanRightsReporter, +} from '@/pages/human-rights/schema.ts'; +import { + useCreateHumanRightsPost, + useGetHumanRightsPost, + usePatchHumanRightsPost, + useUploadHumanRightsFiles, +} from '@/pages/human-rights/queries.ts'; +import { useNavigate, useParams } from 'react-router-dom'; +import { PostHeader } from '@/pages/human-rights/[id]/components/PostHeader.tsx'; +import { PostFooter } from '@/pages/human-rights/[id]/components/PostFooter.tsx'; +import { Button } from '@/components/ui/button.tsx'; +import { Loader2 } from 'lucide-react'; +import { LocalPostFile, PostFile, UploadedPostFile } from '@/pages/human-rights/edit/components/FileInput.tsx'; +import { FileResponse } from '@/types/apis/get'; +import { useGetUserInfo } from '@/pages/human-rights/hooks/query/useGetUserInfo.ts'; +import { useQueryClient } from '@tanstack/react-query'; + +const BOARD_CODE = '인권신고게시판'; + +const DISCLAIMER = `학생인권위원회는 인권침해구제와 관련하여 아래와 같이 개인정보를 수집·이용하고자 합니다. + +✔수집 및 이용 대상 +학생인권위원회 + +✔개인정보 수집 및 이용에 대한 목적 +학내 인권침해 사례 조사 및 해결을 위한 답변 수집 + +✔개인정보 수집 항목 +이름, 학과, 학번, 전화번호 + +✔개인정보 보관 일자 +2024년 5월 23일 ~ 2024년 12월 31일 + +` as const; + +function PageSkeleton() { + return ( +
+ +
+ + +
+ ); +} + +function postTransformer({ + postId, + category, + title, + postFileList, + rightsDetailList, + content, +}: HumanRightsPost): HumanRightsPostEditForm { + type Victim = HumanRightsPerson & { personType: 'VICTIM' }; + type Attacker = HumanRightsPerson & { personType: 'ATTACKER' }; + const victims = rightsDetailList.filter((person): person is Victim => person.personType === 'VICTIM'); + if (victims.length === 0) + victims.push({ + name: '', + studentId: '', + major: '', + personType: 'VICTIM', + }); + const attackers = rightsDetailList.filter((person): person is Attacker => person.personType === 'ATTACKER'); + if (attackers.length === 0) + attackers.push({ + name: '', + studentId: '', + major: '', + personType: 'ATTACKER', + }); + return { + postId, + title, + category, + postFileList: postFileList.map((file) => file.postFileId), + isNotice: false, + rightsDetailList: { + reporter: (rightsDetailList.find( + (person): person is Victim => person.personType === 'REPORTER' + ) as HumanRightsReporter) ?? { + name: '', + studentId: '', + major: '', + phoneNumber: '', + personType: 'REPORTER', + }, + victims: victims as [Victim, ...Victim[]], + attackers: attackers as [Attacker, ...Attacker[]], + }, + content, + }; +} + +export function HumanRightsEditPage() { + /* Router Props */ + const { id } = useParams<{ id?: string }>(); + const postId = id ? parseInt(id ?? '') || null : null; + const navigate = useNavigate(); + + /* Load data by query */ + const queryClient = useQueryClient(); + const { + data: userInfo, + isLoading: isUserInfoLoading, + error: userInfoError, + isError: isUserInfoError, + } = useGetUserInfo(); + const { + data: post, + isLoading, + error, + isError, + } = useGetHumanRightsPost({ + postId: postId ?? 0, + queryOptions: { enabled: postId !== null }, + }); + const [isPostLoaded, setIsPostLoaded] = useState(false); + + /* Register form hooks */ + const { + register, + reset, + handleSubmit, + victimFields, + victimAppend, + victimRemove, + attackerFields, + attackerAppend, + attackerRemove, + setValue, + trigger, + formState: { errors }, + } = useHumanRightsForm({ + category: '접수대기', + isNotice: false, + postFileList: [], + rightsDetailList: { + reporter: { + personType: 'REPORTER', + }, + victims: [ + { + name: '', + personType: 'VICTIM', + }, + ], + attackers: [ + { + name: '', + personType: 'ATTACKER', + }, + ], + }, + }); + + // 에디터 기능 훅 + const editorRef = useRef(null); + const { register: registerEditor, processImages, isImageProcessing } = useContentEditor('인권신고게시판', editorRef); + const [files, setFiles] = useState([]); + const [disclaimerAgreed, setDisclaimerAgreed] = useState(false); + + /* Mutation hooks */ + const { + mutate: createPost, + error: createError, + isError: isCreateError, + isPending: isCreatePending, + } = useCreateHumanRightsPost(); + const { + mutate: patchPost, + error: patchError, + isError: isPatchError, + isPending: isPatchPending, + } = usePatchHumanRightsPost(); + const { + mutateAsync: uploadFiles, + error: fileUploadError, + isError: isFileUploadError, + isPending: isFileUploadPending, + } = useUploadHumanRightsFiles(); + + // 기존 데이터 입력 + useEffect(() => { + if (post && editorRef.current && !isPostLoaded) { + setIsPostLoaded(false); + reset(postTransformer(post)); + editorRef.current!.getInstance().setMarkdown(post.content); + const uploadedFiles = post.postFileList + .filter(({ fileType }) => fileType === 'files') + .map( + ({ postFileId, fileName }): UploadedPostFile => ({ + name: fileName, + isUploaded: true, + id: postFileId, + }) + ); + setFiles(uploadedFiles); + setIsPostLoaded(true); + } + if (!postId) { + setIsPostLoaded(true); + } + }, [post, postId, reset, isPostLoaded]); + + // 사용자 정보 입력 + useEffect(() => { + if (userInfo && isPostLoaded) { + setValue('rightsDetailList.reporter.name', userInfo.name); + setValue('rightsDetailList.reporter.studentId', userInfo.studentId); + setValue('rightsDetailList.reporter.major', userInfo.major); + } + }, [userInfo, isPostLoaded, setValue]); + + // 디버그: 폼 검증 결과 + // useEffect(() => { + // console.log(errors); + // }, [errors]); + + function handleContentChange() { + if (editorRef.current && isPostLoaded) { + setValue('content', editorRef.current.getInstance().getMarkdown()); + } + } + + function handleContentBlur() { + (async () => await trigger('content'))(); + } + + function handleFilesChange(newFiles: PostFile[]) { + setFiles(newFiles); + } + + async function submitForm(formData: HumanRightsPostEditForm) { + const postFileList: number[] = files + .filter((file): file is UploadedPostFile => file.isUploaded) + .map(({ id }) => id); + if (files) { + const localFiles = files.filter((file): file is LocalPostFile => !file.isUploaded).map(({ file }) => file); + const uploaded = await uploadFiles({ files: localFiles }); + uploaded.postFiles.forEach(({ id }) => postFileList.push(id)); + } + const uploadedImages: FileResponse[] = post?.postFileList?.filter(({ fileType }) => fileType === 'images') ?? []; + const { existedImages, newImages, content } = await processImages(uploadedImages); + existedImages.forEach(({ postFileId }) => postFileList.push(postFileId)); + newImages.forEach(({ id }) => postFileList.push(id)); + formData.postFileList = postFileList; + formData.content = content; + const data: HumanRightsPostEditRequest = HumanRightsPostEditRequestSchema.parse(formData); + if (postId) { + patchPost( + { id: postId, post: data }, + { + onSuccess: (data) => { + queryClient + .invalidateQueries({ queryKey: ['searchPosts', BOARD_CODE] }) + .then(() => queryClient.invalidateQueries({ queryKey: ['getPost', BOARD_CODE, postId] })) + .then(() => navigate(`/human-rights/${data}`)); + }, + } + ); + } else { + createPost( + { post: data }, + { + onSuccess: (data) => { + queryClient + .invalidateQueries({ queryKey: ['searchPosts', BOARD_CODE] }) + .then(() => navigate(`/human-rights/${data.post_id}`)); + }, + } + ); + } + } + + if (isLoading || isCreatePending || isPatchPending || isUserInfoLoading) { + return ; + } + + if ((postId && !post) || isError || isCreateError || isPatchError || isFileUploadError || isUserInfoError) { + if (isError) console.log(error); + if (isCreateError) console.log(createError); + if (isPatchError) console.log(patchError); + if (isFileUploadError) console.log(fileUploadError); + if (isUserInfoError) console.log(userInfoError); + // TODO: 오류 발생 시 세부정보 제공 + return ( +
+

오류가 발생하였습니다. 관리자에게 문의하십시오.

+
+ ); + } + + const titleError = errors?.title; + + return ( +
+ {/* TODO: EditLayout에 `className` property 추가 필요, divider 추가 필요 */} + +

인권신고게시판

+
+
+ +
+

+ 신고자 정보 입력 +

+ +
+
+

피침해자 정보 입력

+

빈칸을 모두 채우지 않아도 되며,알고 있는 정보를 기입하여 주세요.

+
    + {victimFields.map((field, index) => ( +
  1. +
    + {index > 0 && ( + + )} +

    [피침해자{index + 1}]

    +
    + +
  2. + ))} +
+ +
+
+

침해자(신고 대상자) 정보 입력

+

빈칸을 모두 채우지 않아도 되며,알고 있는 정보를 기입하여 주세요.

+
    + {attackerFields.map((field, index) => ( +
  1. +
    + {index > 0 && ( + + )} +

    [침해자{index + 1}]

    +
    + +
  2. + ))} +
+ +
+
+

+ 피해 사실 기술 +

+
+ +

+ 이 값은 필수입니다. +

+
+ +
+
+

증거 및 자료 첨부

+ {/* Limit file size to 5MB */} + +
+
+

개인정보 수집 및 이용에 관한 동의

+
{DISCLAIMER}
+
+

+ 위와 같이 개인정보를 수집·이용하는 데 동의하십니까? +

+
+ + setDisclaimerAgreed(evt.target.checked)} + /> +
+
+

+ 동의 후 등록하실 수 있습니다. +

+
+
+ + + +
+ ); +} diff --git a/src/pages/human-rights/edit/utils.ts b/src/pages/human-rights/edit/utils.ts new file mode 100644 index 00000000..80fa462d --- /dev/null +++ b/src/pages/human-rights/edit/utils.ts @@ -0,0 +1,30 @@ +/** + * Format bytes as human-readable text. + * + * @param bytes Number of bytes. + * @param si True to use metric (SI) units, aka powers of 1000. False to use + * binary (IEC), aka powers of 1024. + * @param dp Number of decimal places to display. + * + * @return Formatted string. + */ +export function humanFileSize(bytes: number, si: boolean = false, dp: number = 1) { + const thresh = si ? 1000 : 1024; + + if (Math.abs(bytes) < thresh) { + return bytes + ' B'; + } + + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + const r = 10 ** dp; + + do { + bytes /= thresh; + ++u; + } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); + + return bytes.toFixed(dp) + ' ' + units[u]; +} diff --git a/src/pages/human-rights/hooks/mutations/useCreateComment.ts b/src/pages/human-rights/hooks/mutations/useCreateComment.ts new file mode 100644 index 00000000..714e82a6 --- /dev/null +++ b/src/pages/human-rights/hooks/mutations/useCreateComment.ts @@ -0,0 +1,26 @@ +import { UseMutationOptions } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { ApiError, ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { useStuMutation } from '@/pages/human-rights/hooks/useStuMutation.ts'; +import { clientAuth } from '@/apis/client.ts'; + +export interface UseCreateCommentOptions { + postId: number; + mutationOptions?: Omit, 'mutationFn'>; +} + +interface CreateCommentVariables { + content: string; +} + +export function useCreateComment({ postId, mutationOptions }: UseCreateCommentOptions) { + return useStuMutation(async ({ content }) => { + return ( + await clientAuth>({ + method: 'post', + url: `/board/posts/${postId}/comments`, + data: { content }, + }) + ).data; + }, mutationOptions); +} diff --git a/src/pages/human-rights/hooks/mutations/useCreatePost.ts b/src/pages/human-rights/hooks/mutations/useCreatePost.ts new file mode 100644 index 00000000..37d06fa7 --- /dev/null +++ b/src/pages/human-rights/hooks/mutations/useCreatePost.ts @@ -0,0 +1,34 @@ +import { UseMutationOptions } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { ApiError, ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { useStuMutation } from '@/pages/human-rights/hooks/useStuMutation.ts'; +import { clientAuth } from '@/apis/client.ts'; + +export interface UseCreatePostOptions { + boardCode: string; + mutationOptions?: Omit< + UseMutationOptions>, + 'mutationFn' + >; +} + +interface CreatePostVariables { + post: T; +} + +interface CreatePostResponse { + post_id: number; + boardCode: string; +} + +export function useCreatePost({ boardCode, mutationOptions }: UseCreatePostOptions) { + return useStuMutation(async ({ post }) => { + return ( + await clientAuth>({ + method: 'post', + url: `/board/${boardCode}/posts`, + data: post, + }) + ).data; + }, mutationOptions); +} diff --git a/src/pages/human-rights/hooks/mutations/useDeleteComment.ts b/src/pages/human-rights/hooks/mutations/useDeleteComment.ts new file mode 100644 index 00000000..eea8d257 --- /dev/null +++ b/src/pages/human-rights/hooks/mutations/useDeleteComment.ts @@ -0,0 +1,24 @@ +import { UseMutationOptions } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { ApiError, ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { useStuMutation } from '@/pages/human-rights/hooks/useStuMutation.ts'; +import { clientAuth } from '@/apis/client.ts'; + +export interface UseDeleteCommentOptions { + mutationOptions?: Omit, 'mutationFn'>; +} + +interface DeleteCommentVariables { + commentId: number; +} + +export function useDeleteComment({ mutationOptions }: UseDeleteCommentOptions = {}) { + return useStuMutation(async ({ commentId }) => { + return ( + await clientAuth>({ + method: 'delete', + url: `/board/posts/comments/${commentId}`, + }) + ).data; + }, mutationOptions); +} diff --git a/src/pages/human-rights/hooks/mutations/useDeletePost.ts b/src/pages/human-rights/hooks/mutations/useDeletePost.ts new file mode 100644 index 00000000..7d4b55d6 --- /dev/null +++ b/src/pages/human-rights/hooks/mutations/useDeletePost.ts @@ -0,0 +1,27 @@ +import { UseMutationOptions } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { ApiError, ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { useStuMutation } from '@/pages/human-rights/hooks/useStuMutation.ts'; +import { clientAuth } from '@/apis/client.ts'; + +export interface UseDeletePostOptions { + boardCode: string; + mutationOptions?: Omit, 'mutationFn'>; +} + +interface DeletePostVariables { + postId: string; + fileUrls: string[]; +} + +export function useDeletePost({ boardCode, mutationOptions }: UseDeletePostOptions) { + return useStuMutation(async ({ postId, fileUrls }) => { + return ( + await clientAuth>({ + method: 'delete', + url: `/board/${boardCode}/posts/${postId}`, + data: { fileUrls }, + }) + ).data; + }, mutationOptions); +} diff --git a/src/pages/human-rights/hooks/mutations/usePatchComment.ts b/src/pages/human-rights/hooks/mutations/usePatchComment.ts new file mode 100644 index 00000000..f8e47f28 --- /dev/null +++ b/src/pages/human-rights/hooks/mutations/usePatchComment.ts @@ -0,0 +1,26 @@ +import { UseMutationOptions } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { ApiError, ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { useStuMutation } from '@/pages/human-rights/hooks/useStuMutation.ts'; +import { clientAuth } from '@/apis/client.ts'; + +export interface UsePatchCommentOptions { + mutationOptions?: Omit, 'mutationFn'>; +} + +interface PatchCommentVariables { + commentId: number; + content: string; +} + +export function usePatchComment({ mutationOptions }: UsePatchCommentOptions = {}) { + return useStuMutation(async ({ commentId, content }) => { + return ( + await clientAuth>({ + method: 'patch', + url: `/board/posts/comments/${commentId}`, + data: { content }, + }) + ).data; + }, mutationOptions); +} diff --git a/src/pages/human-rights/hooks/mutations/usePatchPost.ts b/src/pages/human-rights/hooks/mutations/usePatchPost.ts new file mode 100644 index 00000000..fe0dffb3 --- /dev/null +++ b/src/pages/human-rights/hooks/mutations/usePatchPost.ts @@ -0,0 +1,30 @@ +import { UseMutationOptions } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { ApiError, ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { useStuMutation } from '@/pages/human-rights/hooks/useStuMutation.ts'; +import { clientAuth } from '@/apis/client.ts'; + +export interface UsePatchPostOptions { + boardCode: string; + mutationOptions?: Omit< + UseMutationOptions>, + 'mutationFn' + >; +} + +interface PatchPostVariables { + id: number; + post: T; +} + +export function usePatchPost({ boardCode, mutationOptions }: UsePatchPostOptions) { + return useStuMutation(async ({ id, post }) => { + return ( + await clientAuth>({ + method: 'patch', + url: `/board/${boardCode}/posts/${id}`, + data: post, + }) + ).data; + }, mutationOptions); +} diff --git a/src/pages/human-rights/hooks/mutations/useUploadFiles.ts b/src/pages/human-rights/hooks/mutations/useUploadFiles.ts new file mode 100644 index 00000000..c81757b2 --- /dev/null +++ b/src/pages/human-rights/hooks/mutations/useUploadFiles.ts @@ -0,0 +1,51 @@ +import { UseMutationOptions } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { ApiError, ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { useStuMutation } from '@/pages/human-rights/hooks/useStuMutation.ts'; +import { clientAuth } from '@/apis/client.ts'; + +export interface UseUploadFilesOptions { + boardCode: string; + mutationOptions?: Omit< + UseMutationOptions, + 'mutationFn' + >; +} + +interface UploadFilesVariables { + files?: File[]; + images?: File[]; +} + +export interface UploadFilesResponse { + thumbnailUrl: string | null; + postFiles: PostFileResponse[]; +} + +export interface PostFileResponse { + id: number; + url: string; + originalFileName: string; +} + +function appendFormData(formData: FormData, name: string, files: File[]) { + files.forEach((file) => formData.append(name, file)); +} + +export function useUploadFiles({ boardCode, mutationOptions }: UseUploadFilesOptions) { + return useStuMutation(async ({ files, images }) => { + const formData = new FormData(); + files && appendFormData(formData, 'files', files); + images && appendFormData(formData, 'images', images); + return ( + await clientAuth>({ + method: 'post', + url: `/board/${boardCode}/files`, + data: formData, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + ).data; + }, mutationOptions); +} diff --git a/src/pages/human-rights/hooks/query/useGetComments.ts b/src/pages/human-rights/hooks/query/useGetComments.ts new file mode 100644 index 00000000..feb93827 --- /dev/null +++ b/src/pages/human-rights/hooks/query/useGetComments.ts @@ -0,0 +1,61 @@ +import { ApiError, useStuQuery } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { AxiosError, AxiosRequestConfig } from 'axios'; +import { UndefinedInitialDataOptions } from '@tanstack/react-query'; +import z, { ZodError, ZodSchema, ZodTypeDef } from 'zod'; +import { PostAcl } from '@/pages/human-rights/schema.ts'; + +/** + * 게시글 댓글 목록 데이터입니다. + * @typeParam P - 댓글의 타입입니다. + */ +export interface GetCommentsResponse

{ + postComments: P[]; + allowedAuthorities: PostAcl[]; + total: number; +} + +export interface GetCommentsOptions { + postId: number; + type: '인기순' | '최신순'; + zodSchema?: ZodSchema; + queryOptions?: Omit< + UndefinedInitialDataOptions< + GetCommentsResponse, + AxiosError | ApiError | ZodError, + GetCommentsResponse + >, + 'queryKey' | 'queryFn' | 'select' + >; +} + +export function useGetComments({ + postId, + type, + zodSchema, + queryOptions, +}: GetCommentsOptions) { + const queryKey = ['getComments', postId, type]; + const config: AxiosRequestConfig = { + url: `/board/posts/${postId}/comments`, + method: 'get', + params: { + type, + }, + }; + return useStuQuery, GetCommentsResponse, AxiosError | ApiError | ZodError>( + queryKey, + config, + { + select: ({ postComments, ...data }) => { + if (!zodSchema) return { postComments, ...data } as GetCommentsResponse as GetCommentsResponse; + const schemaArray = z.array(zodSchema); + const list = schemaArray.parse(postComments); + return { + postComments: list, + ...data, + }; + }, + ...queryOptions, + } + ); +} diff --git a/src/pages/human-rights/hooks/query/useGetPost.ts b/src/pages/human-rights/hooks/query/useGetPost.ts new file mode 100644 index 00000000..36bbb43b --- /dev/null +++ b/src/pages/human-rights/hooks/query/useGetPost.ts @@ -0,0 +1,42 @@ +import { ApiError, useStuQuery } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { AxiosError, AxiosRequestConfig } from 'axios'; +import { UndefinedInitialDataOptions } from '@tanstack/react-query'; +import { ZodError, ZodSchema, ZodTypeDef } from 'zod'; + +/** + * 게시판의 단건 조회 데이터입니다. + * @typeParam P - 게시물의 타입입니다. + */ +export interface GetPostResponse

{ + postDetailResDto: P; +} + +export interface GetPostOptions { + boardCode: string; + postId: number; + zodSchema?: ZodSchema; + queryOptions?: Omit< + UndefinedInitialDataOptions, AxiosError | ApiError | ZodError, TData>, + 'queryKey' | 'queryFn' | 'select' + >; +} + +export function useGetPost({ + boardCode, + postId, + zodSchema, + queryOptions, +}: GetPostOptions) { + const queryKey = ['getPost', boardCode, postId]; + const config: AxiosRequestConfig = { + url: `/board/${boardCode}/posts/${postId}`, + method: 'get', + }; + return useStuQuery, TData, AxiosError | ApiError | ZodError>(queryKey, config, { + select: ({ postDetailResDto }) => { + if (!zodSchema) return postDetailResDto as unknown as TData; + return zodSchema.parse(postDetailResDto); + }, + ...queryOptions, + }); +} diff --git a/src/pages/human-rights/hooks/query/useGetUserInfo.ts b/src/pages/human-rights/hooks/query/useGetUserInfo.ts new file mode 100644 index 00000000..bb516534 --- /dev/null +++ b/src/pages/human-rights/hooks/query/useGetUserInfo.ts @@ -0,0 +1,38 @@ +import { ApiError, useStuQuery } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { AxiosError, AxiosRequestConfig } from 'axios'; +import { UndefinedInitialDataOptions } from '@tanstack/react-query'; +import z, { ZodError } from 'zod'; + +/** + * 사용자 정보 조회 데이터입니다. + */ +export type GetUserInfoResponse = z.infer; + +export const GetUserInfoResponseSchema = z.object({ + name: z.string(), + studentId: z.string(), + major: z.string(), + isCouncil: z.boolean(), +}); + +export interface GetUserInfoOptions { + queryOptions?: Omit< + UndefinedInitialDataOptions, + 'queryKey' | 'queryFn' | 'select' + >; +} + +export function useGetUserInfo({ queryOptions }: GetUserInfoOptions = {}) { + const accessToken = localStorage.getItem('accessToken'); + const queryKey = ['getUserInfo', accessToken]; + const config: AxiosRequestConfig = { + url: `/users/user-info`, + method: 'get', + }; + return useStuQuery(queryKey, config, { + select: (res) => { + return GetUserInfoResponseSchema.parse(res); + }, + ...queryOptions, + }); +} diff --git a/src/pages/human-rights/hooks/query/useSearchPosts.ts b/src/pages/human-rights/hooks/query/useSearchPosts.ts new file mode 100644 index 00000000..a05abc1d --- /dev/null +++ b/src/pages/human-rights/hooks/query/useSearchPosts.ts @@ -0,0 +1,72 @@ +import { ApiError, useStuQuery } from '@/pages/human-rights/hooks/useStuQuery.ts'; +import { AxiosError, AxiosRequestConfig } from 'axios'; +import { UndefinedInitialDataOptions } from '@tanstack/react-query'; +import { PostAcl } from '@/pages/human-rights/schema.ts'; +import z, { ZodError, ZodSchema, ZodTypeDef } from 'zod'; +import { PageInfo } from '@/types/apis/get'; + +/** + * 게시글 목록 데이터입니다. + * `pageInfo`는 현재 페이지 정보, `allowedAuthorities`와 `deniedAuthorities`는 각각 부여된 권한, 거부된 권한을 표현합니다. + * @typeParam P - 반환된 게시물의 타입입니다. + */ +export interface PostsResponse

{ + postListResDto: P[]; + pageInfo: PageInfo; + allowedAuthorities: PostAcl[]; + deniedAuthorities: PostAcl[]; +} + +export interface SearchPostsOptions { + boardCode: string; + q?: string; + page?: number; + take?: number; + category?: string; + groupCode?: string; + memberCode?: string; + zodSchema?: ZodSchema; + queryOptions?: Omit< + UndefinedInitialDataOptions, AxiosError | ApiError | ZodError, PostsResponse>, + 'queryKey' | 'queryFn' | 'select' + >; +} + +export function useSearchPosts({ + boardCode, + q, + page, + take, + category, + groupCode, + memberCode, + zodSchema, + queryOptions, +}: SearchPostsOptions) { + const accessToken = localStorage.getItem('accessToken'); + const queryKey = ['searchPosts', boardCode, accessToken, category, q, take, page]; + const config: AxiosRequestConfig = { + url: `/board/${boardCode}/posts/search`, + method: 'get', + params: { + page, + take: take ?? 15, + category, + q: q ?? '', + groupCode, + memberCode, + }, + }; + return useStuQuery, PostsResponse, AxiosError | ApiError | ZodError>(queryKey, config, { + select: ({ postListResDto, ...data }) => { + if (!zodSchema) return { postListResDto, ...data } as PostsResponse as PostsResponse; + const schemaArray = z.array(zodSchema); + const list = schemaArray.parse(postListResDto); + return { + postListResDto: list, + ...data, + }; + }, + ...queryOptions, + }); +} diff --git a/src/pages/human-rights/hooks/useStuMutation.ts b/src/pages/human-rights/hooks/useStuMutation.ts new file mode 100644 index 00000000..10539305 --- /dev/null +++ b/src/pages/human-rights/hooks/useStuMutation.ts @@ -0,0 +1,28 @@ +import { AxiosError } from 'axios'; +import { useMutation, UseMutationOptions } from '@tanstack/react-query'; +import { ApiError, ApiResponse } from '@/pages/human-rights/hooks/useStuQuery.ts'; + +/** + * 총학생회 홈페이지에서 반환하는 공통된 API 형식을 처리하는 tanstack query 훅입니다. + * 기본적인 API 응답을 처리하고, 내부 data를 반환합니다. + * 요청 중 오류가 발생할 경우 AxiosError가, API 자체 오류가 발생할 경우 ApiError가 throw됩니다. + * @param mutationFn - `TVariables`를 입력받고, `Promise>`를 반환하는 함수입니다. + * @param mutationOptions - 추가로 tanstack query mutation을 설정할 수 있습니다. `mutationFn` 값은 미리 지정되어 있어 덮어씌울 수 없습니다. + * @typeParam TData - 이 mutation이 최종적으로 변환할 값의 타입입니다. + * @typeParam TVariables - Mutation function에 넘겨줄 variables입니다. + * @typeParam TError - Api가 발생시킬 수 있는 오류의 타입입니다. 기본값은 `AxiosError | ApiError`입니다. + * @typeParam TContext - Mutation 시 보존할 Context 정보의 타입입니다. 보통 `onMutate` 함수에서 사용합니다. + */ +export function useStuMutation( + mutationFn: (variables: TVariables) => Promise>, + mutationOptions?: Omit, 'mutationFn'> +) { + return useMutation({ + mutationFn: async (variables: TVariables) => { + const response = await mutationFn(variables); + if (!response.isSuccess) throw response as ApiError; + return response.data; + }, + ...mutationOptions, + }); +} diff --git a/src/pages/human-rights/hooks/useStuQuery.ts b/src/pages/human-rights/hooks/useStuQuery.ts new file mode 100644 index 00000000..983c79f5 --- /dev/null +++ b/src/pages/human-rights/hooks/useStuQuery.ts @@ -0,0 +1,53 @@ +import { AxiosError, AxiosRequestConfig } from 'axios'; +import { QueryKey, UndefinedInitialDataOptions, useQuery } from '@tanstack/react-query'; +import { clientAuth } from '@/apis/client.ts'; + +// TODO: Move ApiResponse and ApiError to global scope +/** + * API의 기본 반환 응답입니다. + * @typeParam T - 요청이 성공하였을 때 반환할 데이터 + */ +export interface ApiResponse { + code: string; + message: string; + data: T; + isSuccess: boolean; +} + +/** + * API 오류 시 응답입니다. + */ +export interface ApiError { + code: string; + message: string; + data: null; + isSuccess: false; +} + +/** + * 총학생회 홈페이지에서 반환하는 공통된 API 형식을 처리하는 tanstack query 훅입니다. + * 기본적인 API 응답을 처리하고, 내부 data를 반환합니다. + * 요청 중 오류가 발생할 경우 AxiosError가, API 자체 오류가 발생할 경우 ApiError가 throw됩니다. + * @param queryKey - 해당 쿼리에 지정할 queryKey입니다. + * @param requestConfig - Axios에 보낼 요청의 정보입니다. + * @param queryOptions - 추가로 tanstack query를 설정할 수 있습니다. `queryKey`와 `queryFn` 값은 미리 지정되어 있어 덮어씌울 수 없습니다. + * @typeParam TRaw - Api가 반환하는 내부 값의 타입입니다. `ApiResponse`의 `T`와 동일합니다. + * @typeParam TError - Api가 발생시킬 수 있는 오류의 타입입니다. 기본값은 `AxiosError | ApiError`입니다. + * @typeParam TData - `queryOptions.select` 함수를 정의하여 변환한 값의 타입입니다. 기본값은 `TRaw`와 동일합니다. + * @typeParam TQueryKey - `queryKey`의 타입을 바꾸고 싶다면 사용합니다. 기본 타입은 기본 제공되는 `QueryKey`입니다. + */ +export function useStuQuery( + queryKey: TQueryKey, + requestConfig: AxiosRequestConfig, + queryOptions?: Omit, 'queryKey' | 'queryFn'> +) { + return useQuery({ + queryKey, + queryFn: async () => { + const response = (await clientAuth>(requestConfig)).data; + if (!response.isSuccess) throw response as ApiError; + return response.data; + }, + ...queryOptions, + }); +} diff --git a/src/pages/human-rights/page.tsx b/src/pages/human-rights/page.tsx new file mode 100644 index 00000000..bc2a0177 --- /dev/null +++ b/src/pages/human-rights/page.tsx @@ -0,0 +1,149 @@ +import { HeadLayout } from '@/template/HeadLayout.tsx'; +import { BodyLayout } from '@/template/BodyLayout.tsx'; +import { BoardSelector } from '@/components/Board/BoardSelector.tsx'; +import { PostContent } from '@/components/PostContent/PostContent.tsx'; +import { HumanRightsCategory } from '@/pages/human-rights/schema.ts'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useRecoilState } from 'recoil'; +import { SearchState } from '@/recoil/atoms/atom.ts'; +import { useSearchHumanRightsPosts } from '@/pages/human-rights/queries.ts'; +import { useEffect } from 'react'; + +type SelectorCategory = T extends T ? '전체' | T : never; + +const categoryColors: { [category: string]: string } = { + 접수대기: 'text-gray-500', + 접수완료: 'text-primary', +} as const; + +const ensureCategory = (str: string | null): SelectorCategory => { + if (str === '접수대기' || str === '접수완료') return str; + return '전체'; +}; + +const subtitle = ( +

+ 해당 게시판을 통해 신고해주신 내용은 학생인권위원회에서 접수 및 처리됩니다. +

+); + +function PageSkeleton() { + return ( + <> + + + + {Array.from(Array(10).keys()).map((_, i) => ( + + ))} + + + ); +} + +export function HumanRightsPage() { + /* Navigation function for write operation */ + const navigate = useNavigate(); + /* Obtain query parameters */ + const [searchParams, setSearchParams] = useSearchParams(); + const page = parseInt(searchParams.get('page') ?? '1') || 1; + const category = ensureCategory(searchParams.get('category')); + + /* Search state management */ + // TODO: Use search parameters instead of recoil state + const [q] = useRecoilState(SearchState); + + /* Load data from Query */ + const { data, isLoading, isError, error } = useSearchHumanRightsPosts({ + q, + page: page - 1, + category: category === '전체' ? undefined : category, + }); + + // check wrong page params + useEffect(() => { + if (data && (page < 1 || page > data.pageInfo.totalPages)) { + setSearchParams((prev) => { + prev.delete('page'); + return prev; + }); + } + }, [data, page, setSearchParams]); + + if (isLoading) { + return ; + } + + if (!data || isError) { + // TODO: 오류 발생 시 세부정보 제공 + console.log(error); + return ( +
+

오류가 발생하였습니다. 관리자에게 문의하십시오.

+
+ ); + } + + /* Data preparation */ + const { pageNum: currentPage, totalPages } = data.pageInfo; + const posts = data.postListResDto; + + /* Handle user inputs */ + function selectCategory(category: SelectorCategory) { + setSearchParams((prev) => { + if (category === '전체') { + prev.delete('category'); + } else { + prev.set('category', category); + } + prev.delete('page'); + return prev; + }); + window.scrollTo(0, 0); + } + + function navigatePage(page: number) { + setSearchParams((prev) => { + prev.set('page', `${page}`); + return prev; + }); + window.scrollTo(0, 0); + } + + function navigateToWrite() { + navigate('/human-rights/edit'); + } + + return ( + <> + + + + {posts.map((post) => ( + + key={post.postId} + to={`/human-rights/${post.postId}`} + category={{ name: post.category, className: categoryColors[post.category] }} + date={post.date} + title={post.title} + author={post.reportName} + /> + ))} + {posts.length === 0 && ( +
등록된 게시글이 없습니다.
+ )} +
+ + ); +} diff --git a/src/pages/human-rights/queries.ts b/src/pages/human-rights/queries.ts new file mode 100644 index 00000000..8519492d --- /dev/null +++ b/src/pages/human-rights/queries.ts @@ -0,0 +1,87 @@ +import { SearchPostsOptions, useSearchPosts } from '@/pages/human-rights/hooks/query/useSearchPosts.ts'; +import { + HumanRightsComment, + HumanRightsCommentResponse, + HumanRightsCommentSchema, + HumanRightsPost, + HumanRightsPostEditRequest, + HumanRightsPostResponse, + HumanRightsPostSchema, + HumanRightsPostSummary, + HumanRightsPostSummaryResponse, + HumanRightsPostSummarySchema, +} from '@/pages/human-rights/schema.ts'; +import { GetPostOptions, useGetPost } from '@/pages/human-rights/hooks/query/useGetPost.ts'; +import { GetCommentsOptions, useGetComments } from '@/pages/human-rights/hooks/query/useGetComments.ts'; +import { useCreatePost, UseCreatePostOptions } from '@/pages/human-rights/hooks/mutations/useCreatePost.ts'; +import { useDeletePost, UseDeletePostOptions } from '@/pages/human-rights/hooks/mutations/useDeletePost.ts'; +import { usePatchPost, UsePatchPostOptions } from '@/pages/human-rights/hooks/mutations/usePatchPost.ts'; +import { useUploadFiles, UseUploadFilesOptions } from '@/pages/human-rights/hooks/mutations/useUploadFiles.ts'; + +const BOARD_CODE = '인권신고게시판' as const; + +export function useSearchHumanRightsPosts({ + q, + page, + take, + category, + queryOptions, +}: Omit, 'boardCode' | 'zodSchema'>) { + const zodSchema = HumanRightsPostSummarySchema; + return useSearchPosts({ + boardCode: BOARD_CODE, + q, + page, + take, + category, + zodSchema, + queryOptions, + }); +} + +export function useGetHumanRightsPost({ + postId, + queryOptions, +}: Omit, 'boardCode' | 'zodSchema'>) { + const zodSchema = HumanRightsPostSchema; + return useGetPost({ + boardCode: BOARD_CODE, + postId, + zodSchema, + queryOptions, + }); +} + +export function useGetHumanRightsComments({ + postId, + type, + queryOptions, +}: Omit, 'zodSchema'>) { + const zodSchema = HumanRightsCommentSchema; + return useGetComments({ + postId, + type, + zodSchema, + queryOptions, + }); +} + +export function useCreateHumanRightsPost({ + mutationOptions, +}: Omit, 'boardCode'> = {}) { + return useCreatePost({ boardCode: BOARD_CODE, mutationOptions }); +} + +export function usePatchHumanRightsPost({ + mutationOptions, +}: Omit, 'boardCode'> = {}) { + return usePatchPost({ boardCode: BOARD_CODE, mutationOptions }); +} + +export function useDeleteHumanRightsPost({ mutationOptions }: Omit = {}) { + return useDeletePost({ boardCode: BOARD_CODE, mutationOptions }); +} + +export function useUploadHumanRightsFiles({ mutationOptions }: Omit = {}) { + return useUploadFiles({ boardCode: BOARD_CODE, mutationOptions }); +} diff --git a/src/pages/human-rights/schema.ts b/src/pages/human-rights/schema.ts new file mode 100644 index 00000000..efe84d20 --- /dev/null +++ b/src/pages/human-rights/schema.ts @@ -0,0 +1,165 @@ +import z from 'zod'; + +/** + * 인권조회게시판 카테고리입니다. + */ +export type HumanRightsCategory = z.infer; + +/** + * 인권조회게시판 대상자 분류입니다. + */ +export type HumanRightsPersonType = z.infer; + +/** + * 인권조회게시판 대상자의 기본 타입입니다. `personType`이 `REPORTER` 일 경우 HumanRightsReporter 타입을 사용하세요. + */ +export type HumanRightsPerson = z.infer; + +/** + * 인권조회게시판 제보자 타입입니다. + */ +export type HumanRightsReporter = z.infer; + +// Type inconsistency between Post-related types; cannot set a relation between types +/** + * 인권조회게시판 목록에서 사용하는 각 게시물 정보의 원본 데이터입니다. + */ +export type HumanRightsPostSummaryResponse = z.input; +/** + * 인권조회게시판 목록에서 사용하는 각 게시물 정보입니다. + */ +export type HumanRightsPostSummary = z.output; + +/** + * 인권신고게시판 조회에서 사용하는 세부 정보가 포함된 게시물 원본 데이터입니다. + */ +export type HumanRightsPostResponse = z.input; + +/** + * 인권신고게시판 조회에서 사용하는 세부 정보가 포함된 게시물 정보입니다. + */ +export type HumanRightsPost = z.output; + +/** + * 인권신고게시판 작성/수정 폼 작성 시 사용하는 게시물 정보입니다. + */ +export type HumanRightsPostEditForm = z.infer; + +/** + * 인권신고게시판 작성/수정 요청 시 사용하는 게시물 정보입니다. + */ +export type HumanRightsPostEditRequest = z.output; + +/** + * 인권신고게시판 댓글 원본 데이터입니다. + */ +export type HumanRightsCommentResponse = z.input; +/** + * 인권신고게시판 댓글 정보입니다. + */ +export type HumanRightsComment = z.output; + +// TODO: Move PostAcl Schema to global scope +/** + * 권한 정보입니다. + */ +export type PostAcl = z.infer; + +export const FileResponseSchema = z.object({ + postFileId: z.number(), + fileName: z.string().min(1), + fileUrl: z.string(), + fileType: z.enum(['files', 'images']), +}); + +export const PostAclSchema = z.enum([ + 'ALL_READ', + 'READ', + 'WRITE', + 'EDIT', + 'DELETE', + 'COMMENT', + 'DELETE_COMMENT', + 'REACTION', +]); + +export const HumanRightsCategorySchema = z.enum(['접수대기', '접수완료'] as const); + +export const HumanRightsPersonTypeSchema = z.enum(['REPORTER', 'VICTIM', 'ATTACKER'] as const); + +export const HumanRightsPersonSchema = z.object({ + name: z.string().min(1), + studentId: z.string(), + major: z.string(), + phoneNumber: z.string().nullable().optional(), + personType: HumanRightsPersonTypeSchema, +}); + +export const HumanRightsReporterSchema = HumanRightsPersonSchema.extend({ + phoneNumber: z.string().min(1), + personType: z.literal('REPORTER').default('REPORTER'), +}); + +export const HumanRightsCommentSchema = z.object({ + id: z.number(), + authorName: z.string(), + studentId: z.string().optional(), + createdAt: z.string().transform((str) => new Date(str)), + commentType: z.enum(['GENERAL', 'OFFICIAL']), + lastEditedAt: z.coerce.date().nullable(), + isDeleted: z.boolean().optional().default(false), + isAuthor: z.boolean(), + content: z.string().min(1), +}); + +export const HumanRightsPostSchema = z.object({ + postId: z.number(), + category: HumanRightsCategorySchema.nullable().transform((str) => str ?? '접수대기'), + authorName: z.string(), + title: z.string(), + createdAt: z.string().transform((str) => new Date(str)), + lastEditedAt: z + .string() + .transform((str) => new Date(str)) + .nullable(), + isAuthor: z.boolean(), + allowedAuthorities: z.array(PostAclSchema), + postFileList: z.array(FileResponseSchema), + officialCommentList: z.array(HumanRightsCommentSchema), + rightsDetailList: z.array(HumanRightsPersonSchema.or(HumanRightsReporterSchema)), + content: z.string(), +}); + +export const HumanRightsPostSummarySchema = z.object({ + postId: z.number(), + title: z.string().min(1), + date: z.string().transform((str) => new Date(str)), + category: HumanRightsCategorySchema.nullable().transform((str) => str ?? '접수대기'), + reportName: z.string().min(1), +}); + +export const HumanRightsPostEditFormSchema = z.object({ + postId: z.number().optional(), + title: z.string().min(1), + category: HumanRightsCategorySchema, + isNotice: z.literal(false), + postFileList: z.array(z.number()), + rightsDetailList: z.object({ + reporter: HumanRightsReporterSchema, + victims: z.array(HumanRightsPersonSchema.extend({ personType: z.literal('VICTIM').default('VICTIM') })).nonempty(), + attackers: z + .array(HumanRightsPersonSchema.extend({ personType: z.literal('ATTACKER').default('ATTACKER') })) + .nonempty(), + }), + content: z.string().min(1), +}); + +export const HumanRightsPostEditRequestSchema = HumanRightsPostEditFormSchema.omit({ postId: true }).extend({ + rightsDetailList: z + .object({ + reporter: HumanRightsReporterSchema, + victims: z.array(HumanRightsPersonSchema.extend({ personType: z.literal('VICTIM') })).nonempty(), + attackers: z.array(HumanRightsPersonSchema.extend({ personType: z.literal('ATTACKER') })).nonempty(), + }) + .transform((obj) => [obj.reporter, ...obj.victims, ...obj.attackers]), +}); diff --git a/src/pages/index.ts b/src/pages/index.ts index 1855bd86..f010a677 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -7,6 +7,10 @@ import { IntroEditPage } from './intro/IntroEdit/page'; import { AuditPage } from './audit/page'; import { AuditDetailPage } from './audit/auditDetail/page'; import { AuditEditPage } from './audit/auditEdit/page'; +import { HumanRightsPage } from './human-rights/page'; +import { HumanRightsDetailPage } from './human-rights/[id]/page'; +import { HumanRightsEditPage } from './human-rights/edit/page'; +import { SuggestPage } from './sug-notice/page'; import { PartnershipPage } from './partnership/page'; import { PetitionNoticePage } from './petition-notice/page'; import { PetitionNoticeEditPage } from './petition-notice/edit/page'; @@ -38,6 +42,10 @@ export { AuditPage, AuditDetailPage, AuditEditPage, + HumanRightsPage, + HumanRightsDetailPage, + HumanRightsEditPage, + SuggestPage, PartnershipPage, PetitionNoticePage, PetitionNoticeEditPage, diff --git a/src/pages/kakao/containers/KakaoRedirect.tsx b/src/pages/kakao/containers/KakaoRedirect.tsx index 4222ec54..adb1dc01 100644 --- a/src/pages/kakao/containers/KakaoRedirect.tsx +++ b/src/pages/kakao/containers/KakaoRedirect.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { kakaoAuthCodeApi } from '@/apis/kakaoLoginApi'; import { useSetRecoilState } from 'recoil'; import { LoginState } from '@/recoil/atoms/atom'; +import { baseUrl } from '@/pages/kakao/containers/const/data'; const KakaoRedirect = () => { const setLoginState = useSetRecoilState(LoginState); @@ -33,7 +34,7 @@ const KakaoRedirect = () => { } else { // 최초 회원가입 or 타 사이트 로그인이 아니라면 accessToken 로컬에 저장 localStorage.setItem('accessToken', accessToken); - window.location.href = 'https://stu.ssu.ac.kr/'; // 이미 가입된 유저는 메인 화면으로 이동 + window.location.href = baseUrl; // 이미 가입된 유저는 메인 화면으로 이동 setLoginState(true); } } diff --git a/src/pages/kakao/containers/RegisterButtonSection.tsx b/src/pages/kakao/containers/RegisterButtonSection.tsx index 5f329d19..03b9bd6d 100644 --- a/src/pages/kakao/containers/RegisterButtonSection.tsx +++ b/src/pages/kakao/containers/RegisterButtonSection.tsx @@ -1,9 +1,9 @@ import { Link } from 'react-router-dom'; import { KakaoButton } from '@/components/Logo/KakaoButton'; +import { baseUrl } from '@/pages/kakao/containers/const/data'; const rest_api_key = import.meta.env.VITE_REST_API_KEY; const redirect_uri = import.meta.env.VITE_REDIRECT_URI; -const baseUrl = `${window.location.protocol}//${window.location.host}/`; const TAG = ['ussum_001', 'ussum_002', 'ussum_003']; const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${rest_api_key}&redirect_uri=${baseUrl}${redirect_uri}&service_terms=${TAG}`; diff --git a/src/pages/kakao/containers/const/data.ts b/src/pages/kakao/containers/const/data.ts new file mode 100644 index 00000000..6a432751 --- /dev/null +++ b/src/pages/kakao/containers/const/data.ts @@ -0,0 +1 @@ +export const baseUrl = `${window.location.protocol}//${window.location.host}/`; diff --git a/src/pages/petition-notice/edit/containers/PetitionNoticeEditorSection.tsx b/src/pages/petition-notice/edit/containers/PetitionNoticeEditorSection.tsx index c6105cd1..16ec5757 100644 --- a/src/pages/petition-notice/edit/containers/PetitionNoticeEditorSection.tsx +++ b/src/pages/petition-notice/edit/containers/PetitionNoticeEditorSection.tsx @@ -13,12 +13,8 @@ import history from '@/hooks/useHistory'; import { usePostBoardPosts } from '@/hooks/api/post/usePostBoardPosts'; import { GUIDE_LINE } from '../components/GuideLine'; import { useDelBoardFiles } from '@/hooks/api/del/useDelBoardFiles'; +import { HookMap } from '@toast-ui/editor'; -type HookMap = { - addImageBlobHook?: (blob: File, callback: HookCallback) => void; -}; - -type HookCallback = (url: string, text?: string) => void; export function PetitionNoticeEditorSection() { const titleRef = useRef(null); @@ -168,7 +164,7 @@ export function PetitionNoticeEditorSection() { }, [initialContent]); const hooks: HookMap = { - addImageBlobHook: async (blob: File, callback: HookCallback) => { + addImageBlobHook: async (blob: File | Blob, callback: (url: string, text?: string) => void) => { if (blob !== null) { const file = new FormData(); file.append('images', blob); diff --git a/src/pages/router.tsx b/src/pages/router.tsx index 70cc726a..a7404445 100644 --- a/src/pages/router.tsx +++ b/src/pages/router.tsx @@ -36,6 +36,12 @@ export function MainRouter() { } /> } /> } /> + {/* 4-1. 인권신고게시판*/} + } /> + } /> + } /> + {/* 4-2. 건의 게시판 */} + } /> {/* 개인정보이용약관 */} } /> diff --git a/src/pages/sug-notice/page.tsx b/src/pages/sug-notice/page.tsx new file mode 100644 index 00000000..d0a47bb2 --- /dev/null +++ b/src/pages/sug-notice/page.tsx @@ -0,0 +1,120 @@ +import { HeadLayout } from '@/template/HeadLayout'; +import { BodyLayout } from '@/template/BodyLayout'; +import { BoardSelector } from '@/components/Board/BoardSelector'; +// import { PostContent } from '@/components/PostContent/PostContent'; +import { SuggestCategory } from './schema'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +// import { useRecoilState } from 'recoil'; +// import { SearchState } from '@/recoil/atoms/atom.ts'; +import { useEffect } from 'react'; + +type SelectorCategory = T extends T ? '전체' | T : never; + +// const categoryColors: { [category: string]: string } = { +// 답변대기: 'text-gray-500', +// 답변완료: 'text-primary', +// } as const; + +const ensureCategory = (str: string | null): SelectorCategory => { + if (str === '답변대기' || str === '답변완료') return str; + return '전체'; +}; + +const subtitle: JSX.Element =

학생자치기구에게 건의 및 문의할 수 있습니다.

; + +// function PageSkeleton() { +// return ( +//
+// +// +// +// {Array.from(Array(10).keys()).map((_, i) => ( +// +// ))} +// +//
+// ); +// } + +const data = { + postListResDto: [], + allowedAuthorities: [], + deniedAuthorities: ['WRITE', 'ALL_READ'], + pageInfo: { + pageNum: 0, + pageSize: 15, + totalElements: 0, + totalPages: 0, + }, +}; + +export function SuggestPage() { + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const page = parseInt(searchParams.get('page') ?? '1') || 1; + const category = ensureCategory(searchParams.get('category')); + + // 검색 데이터 불러오는 API 추가 예정 + + // const [q] = useRecoilState(SearchState); + + useEffect(() => { + if (data && (page < 1 || page > data.pageInfo.totalPages)) { + setSearchParams((prev) => { + prev.delete('page'); + return prev; + }); + } + }, [data, page, setSearchParams]); + + const { pageNum: currentPage, totalPages } = data.pageInfo; + const posts = data.postListResDto; + + function selectCategory(category: SelectorCategory) { + setSearchParams((prev) => { + if (category === '전체') { + prev.delete('category'); + } else { + prev.set('category', category); + } + prev.delete('page'); + return prev; + }); + window.scrollTo(0, 0); + } + + function navigatePage(page: number) { + setSearchParams((prev) => { + prev.set('page', `${page}`); + return prev; + }); + window.scrollTo(0, 0); + } + + function navigateToWrite() { + navigate('/suggest/edit'); + } + + return ( +
+ + + + {posts.length === 0 && ( +
등록된 게시글이 없습니다.
+ )} +
+
+ ); +} diff --git a/src/pages/sug-notice/schema.ts b/src/pages/sug-notice/schema.ts new file mode 100644 index 00000000..4b1abed6 --- /dev/null +++ b/src/pages/sug-notice/schema.ts @@ -0,0 +1,5 @@ +import z from 'zod'; + +export type SuggestCategory = z.infer; + +export const SuggestCategorySchema = z.enum(['답변대기', '답변완료'] as const); diff --git a/src/template/BodyLayout.tsx b/src/template/BodyLayout.tsx index 2fbc92bf..1578446a 100644 --- a/src/template/BodyLayout.tsx +++ b/src/template/BodyLayout.tsx @@ -3,6 +3,7 @@ import { WriteButton } from '@/components/Buttons/BoardActionButtons'; import { Search } from '@/components/Search/Search'; import { BodyLayoutProps } from '@/types/layout'; import { cn } from '@/libs/utils'; +import { ReactNode } from 'react'; export function BodyLayout({ title, @@ -16,7 +17,7 @@ export function BodyLayout({ className = '', }: BodyLayoutProps) { return ( -
+
{title}
{selector}
@@ -41,3 +42,16 @@ export function BodyLayout({
); } + +BodyLayout.Skeleton = ({ children }: { children: ReactNode }) => { + return ( +
+
+
{children}
+
+
+
+
+
+ ); +}; diff --git a/tailwind.config.js b/tailwind.config.cjs similarity index 100% rename from tailwind.config.js rename to tailwind.config.cjs diff --git a/tsconfig.json b/tsconfig.json index cde772ca..88536c56 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,13 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, @@ -13,18 +16,28 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ], + "@toast-ui/editor": [ + "./node_modules/@toast-ui/editor/types" + ] } }, - "include": ["src", "react-slick.d.ts"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": [ + "src", + "react-slick.d.ts" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] } diff --git a/vercel.json b/vercel.json index 3a48e56b..5ae59501 100644 --- a/vercel.json +++ b/vercel.json @@ -1,3 +1,6 @@ { - "rewrites": [{ "source": "/(.*)", "destination": "/" }] + "rewrites": [ + { "source": "/api/:path*", "destination": "/api/:path*" }, + { "source": "/:path*", "destination": "/" } + ] } diff --git a/yarn.lock b/yarn.lock index 089c01e7..4d4bd387 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,25 +22,25 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/code-frame@npm:7.26.0" +"@babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" dependencies: "@babel/helper-validator-identifier": "npm:^7.25.9" js-tokens: "npm:^4.0.0" picocolors: "npm:^1.0.0" - checksum: 10c0/46f7e367714be736b52ea3c01b24f47e2102e210fb83021d1c8237d8fc511b9538909e16e2fcdbb5cb6173e0794e28624309a59014e52fcfb7bde908f5284388 + checksum: 10c0/7d79621a6849183c415486af99b1a20b84737e8c11cd55b6544f688c51ce1fd710e6d869c3dd21232023da272a79b91efb3e83b5bc2dc65c1187c5fcd1b72ea8 languageName: node linkType: hard "@babel/compat-data@npm:^7.25.9": - version: 7.26.0 - resolution: "@babel/compat-data@npm:7.26.0" - checksum: 10c0/6325c9151a3c9b0a3a807e854a26255ef66d989bff331475a935af9bb18f160e0fffe6aed550e4e96b63f91efcd874bfbaab2a1f4a2f8d25645d712a0de590fb + version: 7.26.3 + resolution: "@babel/compat-data@npm:7.26.3" + checksum: 10c0/d63e71845c34dfad8d7ff8c15b562e620dbf60e68e3abfa35681d24d612594e8e5ec9790d831a287ecd79ce00f48e7ffddc85c5ce94af7242d45917b9c1a5f90 languageName: node linkType: hard -"@babel/core@npm:^7.25.2": +"@babel/core@npm:^7.26.0": version: 7.26.0 resolution: "@babel/core@npm:7.26.0" dependencies: @@ -63,16 +63,16 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.25.9, @babel/generator@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/generator@npm:7.26.0" +"@babel/generator@npm:^7.26.0, @babel/generator@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/generator@npm:7.26.3" dependencies: - "@babel/parser": "npm:^7.26.0" - "@babel/types": "npm:^7.26.0" + "@babel/parser": "npm:^7.26.3" + "@babel/types": "npm:^7.26.3" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^3.0.2" - checksum: 10c0/b6bb9185f19a97eaf58e04a6d39a13237076678e7ed16b6321dea914535d4bf6a8d7727c9dcb65539845aa0096b326eb67be4bab764bd74bcfd848e2eda68609 + checksum: 10c0/54f260558e3e4ec8942da3cde607c35349bb983c3a7c5121243f96893fba3e8cd62e1f1773b2051f936f8c8a10987b758d5c7d76dbf2784e95bb63ab4843fa00 languageName: node linkType: hard @@ -150,18 +150,18 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.0": - version: 7.26.1 - resolution: "@babel/parser@npm:7.26.1" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.0, @babel/parser@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/parser@npm:7.26.3" dependencies: - "@babel/types": "npm:^7.26.0" + "@babel/types": "npm:^7.26.3" bin: parser: ./bin/babel-parser.js - checksum: 10c0/dc7d4e6b7eb667fa0784e7e2c3f6f92ca12ad72242f6d4311995310dae55093f02acdb595b69b0dbbf04cb61ad87156ac03186ff32eacfa35149c655bc22c14b + checksum: 10c0/48f736374e61cfd10ddbf7b80678514ae1f16d0e88bc793d2b505d73d9b987ea786fc8c2f7ee8f8b8c467df062030eb07fd0eb2168f0f541ca1f542775852cad languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-self@npm:^7.24.7": +"@babel/plugin-transform-react-jsx-self@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-transform-react-jsx-self@npm:7.25.9" dependencies: @@ -172,7 +172,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-source@npm:^7.24.7": +"@babel/plugin-transform-react-jsx-source@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-transform-react-jsx-source@npm:7.25.9" dependencies: @@ -204,27 +204,27 @@ __metadata: linkType: hard "@babel/traverse@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/traverse@npm:7.25.9" + version: 7.26.4 + resolution: "@babel/traverse@npm:7.26.4" dependencies: - "@babel/code-frame": "npm:^7.25.9" - "@babel/generator": "npm:^7.25.9" - "@babel/parser": "npm:^7.25.9" + "@babel/code-frame": "npm:^7.26.2" + "@babel/generator": "npm:^7.26.3" + "@babel/parser": "npm:^7.26.3" "@babel/template": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" + "@babel/types": "npm:^7.26.3" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10c0/e90be586a714da4adb80e6cb6a3c5cfcaa9b28148abdafb065e34cc109676fc3db22cf98cd2b2fff66ffb9b50c0ef882cab0f466b6844be0f6c637b82719bba1 + checksum: 10c0/cf25d0eda9505daa0f0832ad786b9e28c9d967e823aaf7fbe425250ab198c656085495aa6bed678b27929e095c84eea9fd778b851a31803da94c9bc4bf4eaef7 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/types@npm:7.26.0" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/types@npm:7.26.3" dependencies: "@babel/helper-string-parser": "npm:^7.25.9" "@babel/helper-validator-identifier": "npm:^7.25.9" - checksum: 10c0/b694f41ad1597127e16024d766c33a641508aad037abd08d0d1f73af753e1119fa03b4a107d04b5f92cc19c095a594660547ae9bead1db2299212d644b0a5cb8 + checksum: 10c0/966c5242c5e55c8704bf7a7418e7be2703a0afa4d19a8480999d5a4ef13d095dd60686615fe5983cb7593b4b06ba3a7de8d6ca501c1d78bdd233a10d90be787b languageName: node linkType: hard @@ -401,9 +401,9 @@ __metadata: linkType: hard "@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.6.1": - version: 4.11.2 - resolution: "@eslint-community/regexpp@npm:4.11.2" - checksum: 10c0/c6ab16307c64bc72ea05b9c1740734dfe4a3eea8f7cc395266eb7f04a0ab8f84fe58d41888e906c18bc56262b685eb3074443a0375fb8c44fb4ff20fdb11e250 + version: 4.12.1 + resolution: "@eslint-community/regexpp@npm:4.12.1" + checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6 languageName: node linkType: hard @@ -441,12 +441,12 @@ __metadata: linkType: hard "@floating-ui/dom@npm:^1.0.0": - version: 1.6.11 - resolution: "@floating-ui/dom@npm:1.6.11" + version: 1.6.12 + resolution: "@floating-ui/dom@npm:1.6.12" dependencies: "@floating-ui/core": "npm:^1.6.0" "@floating-ui/utils": "npm:^0.2.8" - checksum: 10c0/02ef34a75a515543c772880338eea7b66724997bd5ec7cd58d26b50325709d46d480a306b84e7d5509d734434411a4bcf23af5680c2e461e6e6a8bf45d751df8 + checksum: 10c0/c67b39862175b175c6ac299ea970f17a22c7482cfdf3b1bc79313407bf0880188b022b878953fa69d3ce166ff2bd9ae57c86043e5dd800c262b470d877591b7d languageName: node linkType: hard @@ -470,11 +470,11 @@ __metadata: linkType: hard "@hookform/resolvers@npm:^3.9.0": - version: 3.9.0 - resolution: "@hookform/resolvers@npm:3.9.0" + version: 3.9.1 + resolution: "@hookform/resolvers@npm:3.9.1" peerDependencies: react-hook-form: ^7.0.0 - checksum: 10c0/0e0e55f63abbd212cf14abbd39afad1f9b6105d6b25ce827fc651b624ed2be467ebe9b186026e0f032062db59ce2370b14e9583b436ae2d057738bdd6f04356c + checksum: 10c0/8a4056db3860b12ee30921ba352996104d6ae75ac45996d4c8b6df429e07ee73f5b87c82a22a15403789213f6f52f5fead1c2637b26ef624068b68d213362cd1 languageName: node linkType: hard @@ -517,6 +517,15 @@ __metadata: languageName: node linkType: hard +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" @@ -586,25 +595,25 @@ __metadata: languageName: node linkType: hard -"@npmcli/agent@npm:^2.0.0": - version: 2.2.2 - resolution: "@npmcli/agent@npm:2.2.2" +"@npmcli/agent@npm:^3.0.0": + version: 3.0.0 + resolution: "@npmcli/agent@npm:3.0.0" dependencies: agent-base: "npm:^7.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.1" lru-cache: "npm:^10.0.1" socks-proxy-agent: "npm:^8.0.3" - checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae + checksum: 10c0/efe37b982f30740ee77696a80c196912c274ecd2cb243bc6ae7053a50c733ce0f6c09fda085145f33ecf453be19654acca74b69e81eaad4c90f00ccffe2f9271 languageName: node linkType: hard -"@npmcli/fs@npm:^3.1.0": - version: 3.1.1 - resolution: "@npmcli/fs@npm:3.1.1" +"@npmcli/fs@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/fs@npm:4.0.0" dependencies: semver: "npm:^7.3.5" - checksum: 10c0/c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 + checksum: 10c0/c90935d5ce670c87b6b14fab04a965a3b8137e585f8b2a6257263bd7f97756dd736cb165bb470e5156a9e718ecd99413dccc54b1138c1a46d6ec7cf325982fe5 languageName: node linkType: hard @@ -1297,8 +1306,8 @@ __metadata: linkType: hard "@radix-ui/react-tooltip@npm:^1.0.7": - version: 1.1.3 - resolution: "@radix-ui/react-tooltip@npm:1.1.3" + version: 1.1.4 + resolution: "@radix-ui/react-tooltip@npm:1.1.4" dependencies: "@radix-ui/primitive": "npm:1.1.0" "@radix-ui/react-compose-refs": "npm:1.1.0" @@ -1322,7 +1331,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10c0/388f2b74277dc8ac39ef6218e61f5cbebdc9ff5d03a8759bbd9d234561f43fab2771c4537c2e0faaaa19976d5b4cf7eb08112a493dc119e8abc45cbe7a416c97 + checksum: 10c0/721cfb0bf34e74af5a58d89a73e087522517b9502fb7ae9d1dc99137d4952f2bfd1696001e17aa83dfb533c39b4333030149562ebfe62d31238a1a2995bc6bd6 languageName: node linkType: hard @@ -1451,10 +1460,10 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.20.0": - version: 1.20.0 - resolution: "@remix-run/router@npm:1.20.0" - checksum: 10c0/2e017dea530717a6e93a16d478714c4c9165313a1c48e39172ec609bc20324ca6362e8ee2243602df6343644c9268d82a3f50f154d3bb8a17dddde6c37be6e83 +"@remix-run/router@npm:1.21.0": + version: 1.21.0 + resolution: "@remix-run/router@npm:1.21.0" + checksum: 10c0/570792211c083a1c7146613b79cbb8e0d1e14f34e974052e060e7f9dcad38c800d80fe0a18bf42811bc278ab12c0e8fd62cfce649e905046c4e55bd5a09eafdc languageName: node linkType: hard @@ -1570,21 +1579,21 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:5.59.16": - version: 5.59.16 - resolution: "@tanstack/query-core@npm:5.59.16" - checksum: 10c0/487a1ac0df5e02ca4ea5bf3b9ee0010ac7fe0856a36fa7bd10947598d3f0ba356c91aa21b123729b26f2116f05b8b69cd8fb17681c24cdd586de17b6fe021521 +"@tanstack/query-core@npm:5.62.2": + version: 5.62.2 + resolution: "@tanstack/query-core@npm:5.62.2" + checksum: 10c0/5d271dbc43d780f1210140b4f4e82af4a773fb57c8d649897a1203e1478c99efed3249523f4073cb7c351a8ce7c0bab5f019b8955927c2f67d3a17dac6ad3947 languageName: node linkType: hard "@tanstack/react-query@npm:^5.28.9": - version: 5.59.16 - resolution: "@tanstack/react-query@npm:5.59.16" + version: 5.62.2 + resolution: "@tanstack/react-query@npm:5.62.2" dependencies: - "@tanstack/query-core": "npm:5.59.16" + "@tanstack/query-core": "npm:5.62.2" peerDependencies: react: ^18 || ^19 - checksum: 10c0/82acf170d2d169ad18081e4dc52bdcec3b92bd1b134bb704ab6949937a59fd4d7d4ddb1172dd7fdbd1667a2d2e0ffa118e68bde5f53972649b8e55bd0e87244c + checksum: 10c0/3032d40af948781bbd2317766cc327326ae348bd3119d2e56ebb307f95da985b7063fa9a27efd172b5656734986ffd5b12232839ba631d867a540f2ec80f6746 languageName: node linkType: hard @@ -1664,36 +1673,27 @@ __metadata: linkType: hard "@types/node@npm:^20.12.2": - version: 20.17.1 - resolution: "@types/node@npm:20.17.1" + version: 20.17.9 + resolution: "@types/node@npm:20.17.9" dependencies: undici-types: "npm:~6.19.2" - checksum: 10c0/214cf1fffff9c80ae0d49d7dd1f04254215d49711276fff44ff6f61e36dc8d53520509a88add6955fe029b2259c87eaf284b43bc1236d4f4f06bd80c46f0e2b8 + checksum: 10c0/1c37c3618407d56b76301578edabcb4c6a7ef093d0811c50fc4df8df68fc546797a294cafac0e50789f4e0e485cd1d6871964d8e6222fd420658bdae89c1fb4a languageName: node linkType: hard "@types/prop-types@npm:*": - version: 15.7.13 - resolution: "@types/prop-types@npm:15.7.13" - checksum: 10c0/1b20fc67281902c6743379960247bc161f3f0406ffc0df8e7058745a85ea1538612109db0406290512947f9632fe9e10e7337bf0ce6338a91d6c948df16a7c61 + version: 15.7.14 + resolution: "@types/prop-types@npm:15.7.14" + checksum: 10c0/1ec775160bfab90b67a782d735952158c7e702ca4502968aa82565bd8e452c2de8601c8dfe349733073c31179116cf7340710160d3836aa8a1ef76d1532893b1 languageName: node linkType: hard "@types/react-dom@npm:^18.2.22": - version: 18.3.1 - resolution: "@types/react-dom@npm:18.3.1" - dependencies: - "@types/react": "npm:*" - checksum: 10c0/8b416551c60bb6bd8ec10e198c957910cfb271bc3922463040b0d57cf4739cdcd24b13224f8d68f10318926e1ec3cd69af0af79f0291b599a992f8c80d47f1eb - languageName: node - linkType: hard - -"@types/react-dropzone@npm:^5.1.0": - version: 5.1.0 - resolution: "@types/react-dropzone@npm:5.1.0" + version: 18.3.2 + resolution: "@types/react-dom@npm:18.3.2" dependencies: - react-dropzone: "npm:*" - checksum: 10c0/3f54291dc6d3ef49042827a315042a305173f78603eefd7dfbd04b2ee311bdba801a7e23f1bbf3f5823e4435300d1dca5d5658e7098a28717dab0eea78d7b19d + "@types/react": "npm:^18" + checksum: 10c0/22510231af67044a9542633b5b52ec16a8d71fa1da177f82428b8120d36619fd874c3b975b2eda6895baa53667f9fe8cba3acea1232a0244dffe8b11f6b32284 languageName: node linkType: hard @@ -1706,13 +1706,13 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.2.66": - version: 18.3.12 - resolution: "@types/react@npm:18.3.12" +"@types/react@npm:^18.3.0": + version: 18.3.14 + resolution: "@types/react@npm:18.3.14" dependencies: "@types/prop-types": "npm:*" csstype: "npm:^3.0.2" - checksum: 10c0/8bae8d9a41619804561574792e29112b413044eb0d53746dde2b9720c1f9a59f71c895bbd7987cd8ce9500b00786e53bc032dced38cddf42910458e145675290 + checksum: 10c0/d925fbfcf084238b93d1a0b5406d4cf9aeb37c4a1191559aa4ee107c2e55cc15327989140f03eddda4d471f5b935d4673fd74a86f451860edea18eae48ca44f8 languageName: node linkType: hard @@ -1842,17 +1842,17 @@ __metadata: linkType: hard "@vitejs/plugin-react@npm:^4.2.1": - version: 4.3.3 - resolution: "@vitejs/plugin-react@npm:4.3.3" + version: 4.3.4 + resolution: "@vitejs/plugin-react@npm:4.3.4" dependencies: - "@babel/core": "npm:^7.25.2" - "@babel/plugin-transform-react-jsx-self": "npm:^7.24.7" - "@babel/plugin-transform-react-jsx-source": "npm:^7.24.7" + "@babel/core": "npm:^7.26.0" + "@babel/plugin-transform-react-jsx-self": "npm:^7.25.9" + "@babel/plugin-transform-react-jsx-source": "npm:^7.25.9" "@types/babel__core": "npm:^7.20.5" react-refresh: "npm:^0.14.2" peerDependencies: - vite: ^4.2.0 || ^5.0.0 - checksum: 10c0/b370c25fb47bb96f0cc51f3aadbbcfce54e40f95a4de67cf932e5ec526f139648da703725c6ea2c71a1b525eb3dd6e3e8ff877da143627cd2575de5ec4f00aa3 + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + checksum: 10c0/38a47a1dbafae0b97142943d83ee3674cb3331153a60b1a3fd29d230c12c9dfe63b7c345b231a3450168ed8a9375a9a1a253c3d85e9efdc19478c0d56b98496c languageName: node linkType: hard @@ -1873,11 +1873,11 @@ __metadata: linkType: hard "acorn@npm:^8.9.0": - version: 8.13.0 - resolution: "acorn@npm:8.13.0" + version: 8.14.0 + resolution: "acorn@npm:8.14.0" bin: acorn: bin/acorn - checksum: 10c0/f35dd53d68177c90699f4c37d0bb205b8abe036d955d0eb011ddb7f14a81e6fd0f18893731c457c1b5bd96754683f4c3d80d9a5585ddecaa53cdf84e0b3d68f7 + checksum: 10c0/6d4ee461a7734b2f48836ee0fbb752903606e576cc100eb49340295129ca0b452f3ba91ddd4424a1d4406a98adfb2ebb6bd0ff4c49d7a0930c10e462719bbfd7 languageName: node linkType: hard @@ -1890,16 +1890,6 @@ __metadata: languageName: node linkType: hard -"aggregate-error@npm:^3.0.0": - version: 3.1.0 - resolution: "aggregate-error@npm:3.1.0" - dependencies: - clean-stack: "npm:^2.0.0" - indent-string: "npm:^4.0.0" - checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 - languageName: node - linkType: hard - "ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -2003,10 +1993,10 @@ __metadata: languageName: node linkType: hard -"attr-accept@npm:^2.2.2": - version: 2.2.4 - resolution: "attr-accept@npm:2.2.4" - checksum: 10c0/602d88b40cb039f1159b86e389ca4f908c13dba513753f7c511e69499ba6216c153519f31a484bac9c9efa633f8f6a4ec25b4f777bd55198f8cb2514cef04618 +"attr-accept@npm:^2.2.4": + version: 2.2.5 + resolution: "attr-accept@npm:2.2.5" + checksum: 10c0/9b4cb82213925cab2d568f71b3f1c7a7778f9192829aac39a281e5418cd00c04a88f873eb89f187e0bf786fa34f8d52936f178e62cbefb9254d57ecd88ada99b languageName: node linkType: hard @@ -2039,13 +2029,13 @@ __metadata: linkType: hard "axios@npm:^1.6.8": - version: 1.7.7 - resolution: "axios@npm:1.7.7" + version: 1.7.9 + resolution: "axios@npm:1.7.9" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/4499efc89e86b0b49ffddc018798de05fab26e3bf57913818266be73279a6418c3ce8f9e934c7d2d707ab8c095e837fc6c90608fb7715b94d357720b5f568af7 + checksum: 10c0/b7a41e24b59fee5f0f26c1fc844b45b17442832eb3a0fb42dd4f1430eb4abc571fe168e67913e8a1d91c993232bd1d1ab03e20e4d1fee8c6147649b576fc1b0b languageName: node linkType: hard @@ -2105,11 +2095,11 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^18.0.0": - version: 18.0.4 - resolution: "cacache@npm:18.0.4" +"cacache@npm:^19.0.1": + version: 19.0.1 + resolution: "cacache@npm:19.0.1" dependencies: - "@npmcli/fs": "npm:^3.1.0" + "@npmcli/fs": "npm:^4.0.0" fs-minipass: "npm:^3.0.0" glob: "npm:^10.2.2" lru-cache: "npm:^10.0.1" @@ -2117,11 +2107,11 @@ __metadata: minipass-collect: "npm:^2.0.1" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^4.0.0" - ssri: "npm:^10.0.0" - tar: "npm:^6.1.11" - unique-filename: "npm:^3.0.0" - checksum: 10c0/6c055bafed9de4f3dcc64ac3dc7dd24e863210902b7c470eb9ce55a806309b3efff78033e3d8b4f7dcc5d467f2db43c6a2857aaaf26f0094b8a351d44c42179f + p-map: "npm:^7.0.2" + ssri: "npm:^12.0.0" + tar: "npm:^7.4.3" + unique-filename: "npm:^4.0.0" + checksum: 10c0/01f2134e1bd7d3ab68be851df96c8d63b492b1853b67f2eecb2c37bb682d37cb70bb858a16f2f0554d3c0071be6dfe21456a1ff6fa4b7eed996570d6a25ffe9c languageName: node linkType: hard @@ -2140,9 +2130,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001669": - version: 1.0.30001672 - resolution: "caniuse-lite@npm:1.0.30001672" - checksum: 10c0/0ba63451bbb972987146ffadba8724fcf67ff89168f330e6bdcb0c3b73ef5e2ddbcd75089b59bd3f87ada61670ec7a1180f239169203132b7d4efd241d6e5d91 + version: 1.0.30001687 + resolution: "caniuse-lite@npm:1.0.30001687" + checksum: 10c0/9ca0f6d33dccaf4692339d0fda50e03e4dd7eb7f25faabd1cb33e2099d9a76b0bc30c37be3315e91c1d990da1b5cc864eee2077494f4d0ba94d68b48fe2ea7f1 languageName: node linkType: hard @@ -2156,7 +2146,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.5.3": +"chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -2175,19 +2165,19 @@ __metadata: languageName: node linkType: hard -"chownr@npm:^2.0.0": - version: 2.0.0 - resolution: "chownr@npm:2.0.0" - checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 languageName: node linkType: hard "class-variance-authority@npm:^0.7.0": - version: 0.7.0 - resolution: "class-variance-authority@npm:0.7.0" + version: 0.7.1 + resolution: "class-variance-authority@npm:0.7.1" dependencies: - clsx: "npm:2.0.0" - checksum: 10c0/e11c57edf4bf50ef1c97bae41d68885afbaaedba26c48b7cc5dfb033390fed7012147e9532168d8c4f3497fce4dff15e20e6e60b8c9c9a4b0fe26b0e804513db + clsx: "npm:^2.1.1" + checksum: 10c0/0f438cea22131808b99272de0fa933c2532d5659773bfec0c583de7b3f038378996d3350683426b8e9c74a6286699382106d71fbec52f0dd5fbb191792cccb5b languageName: node linkType: hard @@ -2198,21 +2188,7 @@ __metadata: languageName: node linkType: hard -"clean-stack@npm:^2.0.0": - version: 2.2.0 - resolution: "clean-stack@npm:2.2.0" - checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 - languageName: node - linkType: hard - -"clsx@npm:2.0.0": - version: 2.0.0 - resolution: "clsx@npm:2.0.0" - checksum: 10c0/c09f43b3144a0b7826b6b11b6a111b2c7440831004eecc02d333533c5e58ef0aa5f2dce071d3b25fbb8c8ea97b45df96c74bcc1d51c8c2027eb981931107b0cd - languageName: node - linkType: hard - -"clsx@npm:^2.1.0": +"clsx@npm:^2.1.0, clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839 @@ -2280,13 +2256,13 @@ __metadata: linkType: hard "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" dependencies: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" which: "npm:^2.0.1" - checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 languageName: node linkType: hard @@ -2321,14 +2297,14 @@ __metadata: linkType: hard "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": - version: 4.3.7 - resolution: "debug@npm:4.3.7" + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b + checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de languageName: node linkType: hard @@ -2393,9 +2369,9 @@ __metadata: linkType: hard "dotenv@npm:^16.0.1": - version: 16.4.5 - resolution: "dotenv@npm:16.4.5" - checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f + version: 16.4.7 + resolution: "dotenv@npm:16.4.7" + checksum: 10c0/be9f597e36a8daf834452daa1f4cc30e5375a5968f98f46d89b16b983c567398a330580c88395069a77473943c06b877d1ca25b4afafcdd6d4adb549e8293462 languageName: node linkType: hard @@ -2407,9 +2383,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.5.41": - version: 1.5.47 - resolution: "electron-to-chromium@npm:1.5.47" - checksum: 10c0/5f8c4a9f0698695960f7bef5242d52b1043020ce50b51fb534409a768847f9bdc9672cb4a6a560eeb8f8b47a04327ae9b31b2cee376cb637b3eb04a4daeaa3b8 + version: 1.5.71 + resolution: "electron-to-chromium@npm:1.5.71" + checksum: 10c0/f6fdeec0e1d68634cf92c267bdce3e50af947ce2c8fb1034df3e738c536b3033e311ad0fb9a6c4c35f678f10a299e4f78fdfcedbaa78d8992fedc443a7363d6d languageName: node linkType: hard @@ -2421,30 +2397,30 @@ __metadata: linkType: hard "embla-carousel-react@npm:^8.0.0": - version: 8.3.0 - resolution: "embla-carousel-react@npm:8.3.0" + version: 8.5.1 + resolution: "embla-carousel-react@npm:8.5.1" dependencies: - embla-carousel: "npm:8.3.0" - embla-carousel-reactive-utils: "npm:8.3.0" + embla-carousel: "npm:8.5.1" + embla-carousel-reactive-utils: "npm:8.5.1" peerDependencies: - react: ^16.8.0 || ^17.0.1 || ^18.0.0 - checksum: 10c0/ee3492dfbc900391c8137cca2f55c403307e3dd121a77ea3bbacca33afb82715647eaae5a6d6cc25590284892e45c75e70ab5bb033ebc20016a1972333104dc6 + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + checksum: 10c0/4194e648979d29da52dbc5e922fdcdd7ae44dc8adfe54b54ad4d4c694dddbdc00e7faa133cca50da3ed4f3b6261e8ec78b3584ee5becb68397a2234892183bbe languageName: node linkType: hard -"embla-carousel-reactive-utils@npm:8.3.0": - version: 8.3.0 - resolution: "embla-carousel-reactive-utils@npm:8.3.0" +"embla-carousel-reactive-utils@npm:8.5.1": + version: 8.5.1 + resolution: "embla-carousel-reactive-utils@npm:8.5.1" peerDependencies: - embla-carousel: 8.3.0 - checksum: 10c0/64769d7b5d0169f7cda810d909b446ccd47b28afbcb205196c0e1799c51ebbefaa71465cabd6bc5ab92bad78da99fdf2808dc8e1a3f3f18a7e428ae2e0136a0e + embla-carousel: 8.5.1 + checksum: 10c0/f9f99f05014eb36902fb5a852c26e38afe3827a357bdcb8f744afe2af7786548369cbd8948022b635c85324f9b195c31af5f16934354690073e400829af7359d languageName: node linkType: hard -"embla-carousel@npm:8.3.0": - version: 8.3.0 - resolution: "embla-carousel@npm:8.3.0" - checksum: 10c0/0240156d6a736603d82ddfe93b03ce296e385e9c18ed2cca9465634c8adb7560bfc2fbef6368a4da2a54926c4ba6de1012ebb33d2fc92c052030ea2288e4cc92 +"embla-carousel@npm:8.5.1": + version: 8.5.1 + resolution: "embla-carousel@npm:8.5.1" + checksum: 10c0/457f3ae6f13a55504a61b4cb57529fbc33102a921cbb2f234d6b53709ded03b8cf46fb7a1c95d51d9f37a2348013c6a4479705367b0b75bfb83862f52dd23d7f languageName: node linkType: hard @@ -2645,11 +2621,11 @@ __metadata: linkType: hard "eslint-plugin-react-refresh@npm:^0.4.6": - version: 0.4.14 - resolution: "eslint-plugin-react-refresh@npm:0.4.14" + version: 0.4.16 + resolution: "eslint-plugin-react-refresh@npm:0.4.16" peerDependencies: - eslint: ">=7" - checksum: 10c0/427108008ffcc2e0be36897398e61a2fae54c5bf092af0171bc4cf1927080d40619bb07be02ecd7c515372210228cf849023997cfa0252d37115f9b0c0debcd2 + eslint: ">=8.40" + checksum: 10c0/0628d54b6cc6773a89252e2a7c82c7905a00dc8dc99a6ae2885a64f3b45bd3012a40cf9791ee24aa5dcf75665d8c8be4699845bbbf205cd0ef652702701a7865 languageName: node linkType: hard @@ -2782,7 +2758,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0": +"fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -2827,12 +2803,12 @@ __metadata: languageName: node linkType: hard -"file-selector@npm:^0.6.0": - version: 0.6.0 - resolution: "file-selector@npm:0.6.0" +"file-selector@npm:^2.1.0": + version: 2.1.2 + resolution: "file-selector@npm:2.1.2" dependencies: - tslib: "npm:^2.4.0" - checksum: 10c0/477ca1b56274db9fee1a8a623c4bfef580389726a5fef843af8c1f2f17f70ec2d1e41b29115777c92e120a15f1cca734c6ef36bb48bfa2ee027c68da16cd0d28 + tslib: "npm:^2.7.0" + checksum: 10c0/fe827e0e95410aacfcc3eabc38c29cc36055257f03c1c06b631a2b5af9730c142ad2c52f5d64724d02231709617bda984701f52bd1f4b7aca50fb6585a27c1d2 languageName: node linkType: hard @@ -2906,9 +2882,9 @@ __metadata: linkType: hard "flatted@npm:^3.2.9": - version: 3.3.1 - resolution: "flatted@npm:3.3.1" - checksum: 10c0/324166b125ee07d4ca9bcf3a5f98d915d5db4f39d711fba640a3178b959919aae1f7cfd8aabcfef5826ed8aa8a2aa14cc85b2d7d18ff638ddf4ae3df39573eaf + version: 3.3.2 + resolution: "flatted@npm:3.3.2" + checksum: 10c0/24cc735e74d593b6c767fe04f2ef369abe15b62f6906158079b9874bdb3ee5ae7110bb75042e70cd3f99d409d766f357caf78d5ecee9780206f5fdc5edbad334 languageName: node linkType: hard @@ -2961,15 +2937,6 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0": - version: 2.1.0 - resolution: "fs-minipass@npm:2.1.0" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 - languageName: node - linkType: hard - "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -3062,7 +3029,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -3188,11 +3155,11 @@ __metadata: "@radix-ui/react-tabs": "npm:^1.0.4" "@radix-ui/react-tooltip": "npm:^1.0.7" "@tanstack/react-query": "npm:^5.28.9" + "@toast-ui/editor": "npm:^3.2.2" "@toast-ui/react-editor": "npm:^3.2.3" "@types/node": "npm:^20.12.2" "@types/react": "npm:^18.2.66" "@types/react-dom": "npm:^18.2.22" - "@types/react-dropzone": "npm:^5.1.0" "@types/react-slick": "npm:^0.23.13" "@typescript-eslint/eslint-plugin": "npm:^7.2.0" "@typescript-eslint/parser": "npm:^7.2.0" @@ -3298,13 +3265,6 @@ __metadata: languageName: node linkType: hard -"indent-string@npm:^4.0.0": - version: 4.0.0 - resolution: "indent-string@npm:4.0.0" - checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f - languageName: node - linkType: hard - "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -3382,13 +3342,6 @@ __metadata: languageName: node linkType: hard -"is-lambda@npm:^1.0.1": - version: 1.0.1 - resolution: "is-lambda@npm:1.0.1" - checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d - languageName: node - linkType: hard - "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -3430,7 +3383,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.21.0": +"jiti@npm:^1.21.6": version: 1.21.6 resolution: "jiti@npm:1.21.6" bin: @@ -3544,17 +3497,10 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^2.1.0": - version: 2.1.0 - resolution: "lilconfig@npm:2.1.0" - checksum: 10c0/64645641aa8d274c99338e130554abd6a0190533c0d9eb2ce7ebfaf2e05c7d9961f3ffe2bfa39efd3b60c521ba3dd24fa236fe2775fc38501bf82bf49d4678b8 - languageName: node - linkType: hard - -"lilconfig@npm:^3.0.0": - version: 3.1.2 - resolution: "lilconfig@npm:3.1.2" - checksum: 10c0/f059630b1a9bddaeba83059db00c672b64dc14074e9f232adce32b38ca1b5686ab737eb665c5ba3c32f147f0002b4bee7311ad0386a9b98547b5623e87071fbe +"lilconfig@npm:^3.0.0, lilconfig@npm:^3.1.3": + version: 3.1.3 + resolution: "lilconfig@npm:3.1.3" + checksum: 10c0/f5604e7240c5c275743561442fbc5abf2a84ad94da0f5adc71d25e31fa8483048de3dcedcb7a44112a942fed305fd75841cdf6c9681c7f640c63f1049e9a5dcc languageName: node linkType: hard @@ -3642,23 +3588,22 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^13.0.0": - version: 13.0.1 - resolution: "make-fetch-happen@npm:13.0.1" +"make-fetch-happen@npm:^14.0.3": + version: 14.0.3 + resolution: "make-fetch-happen@npm:14.0.3" dependencies: - "@npmcli/agent": "npm:^2.0.0" - cacache: "npm:^18.0.0" + "@npmcli/agent": "npm:^3.0.0" + cacache: "npm:^19.0.1" http-cache-semantics: "npm:^4.1.1" - is-lambda: "npm:^1.0.1" minipass: "npm:^7.0.2" - minipass-fetch: "npm:^3.0.0" + minipass-fetch: "npm:^4.0.0" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^0.6.3" - proc-log: "npm:^4.2.0" + negotiator: "npm:^1.0.0" + proc-log: "npm:^5.0.0" promise-retry: "npm:^2.0.1" - ssri: "npm:^10.0.0" - checksum: 10c0/df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e + ssri: "npm:^12.0.0" + checksum: 10c0/c40efb5e5296e7feb8e37155bde8eb70bc57d731b1f7d90e35a092fde403d7697c56fb49334d92d330d6f1ca29a98142036d6480a12681133a0a1453164cb2f0 languageName: node linkType: hard @@ -3669,7 +3614,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": +"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -3722,18 +3667,18 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^3.0.0": - version: 3.0.5 - resolution: "minipass-fetch@npm:3.0.5" +"minipass-fetch@npm:^4.0.0": + version: 4.0.0 + resolution: "minipass-fetch@npm:4.0.0" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" minipass-sized: "npm:^1.0.3" - minizlib: "npm:^2.1.2" + minizlib: "npm:^3.0.1" dependenciesMeta: encoding: optional: true - checksum: 10c0/9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b + checksum: 10c0/7fa30ce7c373fb6f94c086b374fff1589fd7e78451855d2d06c2e2d9df936d131e73e952163063016592ed3081444bd8d1ea608533313b0149156ce23311da4b languageName: node linkType: hard @@ -3773,36 +3718,29 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0": - version: 5.0.0 - resolution: "minipass@npm:5.0.0" - checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 - languageName: node - linkType: hard - -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 languageName: node linkType: hard -"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": - version: 2.1.2 - resolution: "minizlib@npm:2.1.2" +"minizlib@npm:^3.0.1": + version: 3.0.1 + resolution: "minizlib@npm:3.0.1" dependencies: - minipass: "npm:^3.0.0" - yallist: "npm:^4.0.0" - checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + minipass: "npm:^7.0.4" + rimraf: "npm:^5.0.5" + checksum: 10c0/82f8bf70da8af656909a8ee299d7ed3b3372636749d29e105f97f20e88971be31f5ed7642f2e898f00283b68b701cc01307401cdc209b0efc5dd3818220e5093 languageName: node linkType: hard -"mkdirp@npm:^1.0.3": - version: 1.0.4 - resolution: "mkdirp@npm:1.0.4" +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" bin: - mkdirp: bin/cmd.js - checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + mkdirp: dist/cjs/src/bin.js + checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d languageName: node linkType: hard @@ -3825,11 +3763,11 @@ __metadata: linkType: hard "nanoid@npm:^3.3.7": - version: 3.3.7 - resolution: "nanoid@npm:3.3.7" + version: 3.3.8 + resolution: "nanoid@npm:3.3.8" bin: nanoid: bin/nanoid.cjs - checksum: 10c0/e3fb661aa083454f40500473bb69eedb85dc160e763150b9a2c567c7e9ff560ce028a9f833123b618a6ea742e311138b591910e795614a629029e86e180660f3 + checksum: 10c0/4b1bb29f6cfebf3be3bc4ad1f1296fb0a10a3043a79f34fbffe75d1621b4318319211cd420549459018ea3592f0d2f159247a6f874911d6d26eaaadda2478120 languageName: node linkType: hard @@ -3840,10 +3778,10 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^0.6.3": - version: 0.6.4 - resolution: "negotiator@npm:0.6.4" - checksum: 10c0/3e677139c7fb7628a6f36335bf11a885a62c21d5390204590a1a214a5631fcbe5ea74ef6a610b60afe84b4d975cbe0566a23f20ee17c77c73e74b80032108dea +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b languageName: node linkType: hard @@ -3858,22 +3796,22 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 10.2.0 - resolution: "node-gyp@npm:10.2.0" + version: 11.0.0 + resolution: "node-gyp@npm:11.0.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" glob: "npm:^10.3.10" graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^13.0.0" - nopt: "npm:^7.0.0" - proc-log: "npm:^4.1.0" + make-fetch-happen: "npm:^14.0.3" + nopt: "npm:^8.0.0" + proc-log: "npm:^5.0.0" semver: "npm:^7.3.5" - tar: "npm:^6.2.1" - which: "npm:^4.0.0" + tar: "npm:^7.4.3" + which: "npm:^5.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10c0/00630d67dbd09a45aee0a5d55c05e3916ca9e6d427ee4f7bc392d2d3dc5fad7449b21fc098dd38260a53d9dcc9c879b36704a1994235d4707e7271af7e9a835b + checksum: 10c0/a3b885bbee2d271f1def32ba2e30ffcf4562a3db33af06b8b365e053153e2dd2051b9945783c3c8e852d26a0f20f65b251c7e83361623383a99635c0280ee573 languageName: node linkType: hard @@ -3884,14 +3822,14 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^7.0.0": - version: 7.2.1 - resolution: "nopt@npm:7.2.1" +"nopt@npm:^8.0.0": + version: 8.0.0 + resolution: "nopt@npm:8.0.0" dependencies: abbrev: "npm:^2.0.0" bin: nopt: bin/nopt.js - checksum: 10c0/a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 + checksum: 10c0/19cb986f79abaca2d0f0b560021da7b32ee6fcc3de48f3eaeb0c324d36755c17754f886a754c091f01f740c17caf7d6aea8237b7fbaf39f476ae5e30a249f18f languageName: node linkType: hard @@ -3989,12 +3927,10 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^4.0.0": - version: 4.0.0 - resolution: "p-map@npm:4.0.0" - dependencies: - aggregate-error: "npm:^3.0.0" - checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 +"p-map@npm:^7.0.2": + version: 7.0.3 + resolution: "p-map@npm:7.0.3" + checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c languageName: node linkType: hard @@ -4066,7 +4002,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0": +"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -4127,7 +4063,7 @@ __metadata: languageName: node linkType: hard -"postcss-load-config@npm:^4.0.1": +"postcss-load-config@npm:^4.0.2": version: 4.0.2 resolution: "postcss-load-config@npm:4.0.2" dependencies: @@ -4145,7 +4081,7 @@ __metadata: languageName: node linkType: hard -"postcss-nested@npm:^6.0.1": +"postcss-nested@npm:^6.2.0": version: 6.2.0 resolution: "postcss-nested@npm:6.2.0" dependencies: @@ -4156,7 +4092,7 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.1.1": +"postcss-selector-parser@npm:^6.1.1, postcss-selector-parser@npm:^6.1.2": version: 6.1.2 resolution: "postcss-selector-parser@npm:6.1.2" dependencies: @@ -4173,14 +4109,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.23, postcss@npm:^8.4.38, postcss@npm:^8.4.43": - version: 8.4.47 - resolution: "postcss@npm:8.4.47" +"postcss@npm:^8.4.38, postcss@npm:^8.4.43, postcss@npm:^8.4.47": + version: 8.4.49 + resolution: "postcss@npm:8.4.49" dependencies: nanoid: "npm:^3.3.7" - picocolors: "npm:^1.1.0" + picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/929f68b5081b7202709456532cee2a145c1843d391508c5a09de2517e8c4791638f71dd63b1898dba6712f8839d7a6da046c72a5e44c162e908f5911f57b5f44 + checksum: 10c0/f1b3f17aaf36d136f59ec373459f18129908235e65dbdc3aee5eef8eba0756106f52de5ec4682e29a2eab53eb25170e7e871b3e4b52a8f1de3d344a514306be3 languageName: node linkType: hard @@ -4256,18 +4192,18 @@ __metadata: linkType: hard "prettier@npm:^3.3.3": - version: 3.3.3 - resolution: "prettier@npm:3.3.3" + version: 3.4.2 + resolution: "prettier@npm:3.4.2" bin: prettier: bin/prettier.cjs - checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26 + checksum: 10c0/99e076a26ed0aba4ebc043880d0f08bbb8c59a4c6641cdee6cdadf2205bdd87aa1d7823f50c3aea41e015e99878d37c58d7b5f0e663bba0ef047f94e36b96446 languageName: node linkType: hard -"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": - version: 4.2.0 - resolution: "proc-log@npm:4.2.0" - checksum: 10c0/17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 +"proc-log@npm:^5.0.0": + version: 5.0.0 + resolution: "proc-log@npm:5.0.0" + checksum: 10c0/bbe5edb944b0ad63387a1d5b1911ae93e05ce8d0f60de1035b218cdcceedfe39dbd2c697853355b70f1a090f8f58fe90da487c85216bf9671f9499d1a897e9e3 languageName: node linkType: hard @@ -4336,11 +4272,11 @@ __metadata: linkType: hard "prosemirror-model@npm:^1.0.0, prosemirror-model@npm:^1.14.1, prosemirror-model@npm:^1.20.0, prosemirror-model@npm:^1.21.0": - version: 1.23.0 - resolution: "prosemirror-model@npm:1.23.0" + version: 1.24.0 + resolution: "prosemirror-model@npm:1.24.0" dependencies: orderedmap: "npm:^2.0.0" - checksum: 10c0/394f8921e723fb5860381cd0b2ff6988025005a6472a886a748cc1ad72055fd194801c1f12f8fbbd54f47f075c95fd23b68ad0811628e649f06f6005fb5790d6 + checksum: 10c0/2751faacd509dc64682152c6938fe05ca6aa0384ece13308eff2b9ff182142f36d73fe967d924fee67a5721107fe6ea1c6a97b39d2ee17d0c7e18949030380b6 languageName: node linkType: hard @@ -4365,13 +4301,13 @@ __metadata: linkType: hard "prosemirror-view@npm:^1.18.7, prosemirror-view@npm:^1.27.0, prosemirror-view@npm:^1.31.0": - version: 1.34.3 - resolution: "prosemirror-view@npm:1.34.3" + version: 1.37.0 + resolution: "prosemirror-view@npm:1.37.0" dependencies: prosemirror-model: "npm:^1.20.0" prosemirror-state: "npm:^1.0.0" prosemirror-transform: "npm:^1.1.0" - checksum: 10c0/f08a9eeb6988e2180ec19989bb11c11db02c37952d1ab8b3ae8c7c1ebf9ce143725855bfa8cee59e4819aab3c9045ef5721cc90e69353f8fd81b7bb3a1c4ea03 + checksum: 10c0/4c98e2740622c79418ea5eabaf2ef6e69b162de28e4a3cb5b6d3796952e899fffc504ea2cc5fec00268dc65a33ceb67662b0fba8e33b3e9421492277c52f79eb languageName: node linkType: hard @@ -4408,25 +4344,25 @@ __metadata: languageName: node linkType: hard -"react-dropzone@npm:*, react-dropzone@npm:^14.2.3": - version: 14.2.10 - resolution: "react-dropzone@npm:14.2.10" +"react-dropzone@npm:^14.2.3": + version: 14.3.5 + resolution: "react-dropzone@npm:14.3.5" dependencies: - attr-accept: "npm:^2.2.2" - file-selector: "npm:^0.6.0" + attr-accept: "npm:^2.2.4" + file-selector: "npm:^2.1.0" prop-types: "npm:^15.8.1" peerDependencies: react: ">= 16.8 || 18.0.0" - checksum: 10c0/07f24b77cae951f8bdbcaeabfca1eacf8699ed79e5924c534d81c0da23bf98a83dae19d5e04419f2010b527233830f69159caabf0cdaedb1aaaaa4fe7efa99f0 + checksum: 10c0/e3e5dddd3bead7c6410bd3fccc3a87e93086ceac47526a2d35421ef7e11a9e59f47c8af8da5c4600a58ef238a5af87c751a71b6391d5c6f77f1f2857946c07cc languageName: node linkType: hard "react-hook-form@npm:^7.51.2": - version: 7.53.1 - resolution: "react-hook-form@npm:7.53.1" + version: 7.53.2 + resolution: "react-hook-form@npm:7.53.2" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10c0/dd2466359a633f873755b366d367d51ab17100566b687fb3b098f704232bc6ab1c79d29f879151e492880ca5eeac35e9425fbe5a309e2a55f7a4b5baf7826e8d + checksum: 10c0/18336d8e8798a70dcd0af703a0becca2d5dbf82a7b7a3ca334ae0e1f26410490bc3ef2ea51adcf790bb1e7006ed7a763fd00d664e398f71225b23529a7ccf0bf languageName: node linkType: hard @@ -4480,26 +4416,26 @@ __metadata: linkType: hard "react-router-dom@npm:^6.22.3": - version: 6.27.0 - resolution: "react-router-dom@npm:6.27.0" + version: 6.28.0 + resolution: "react-router-dom@npm:6.28.0" dependencies: - "@remix-run/router": "npm:1.20.0" - react-router: "npm:6.27.0" + "@remix-run/router": "npm:1.21.0" + react-router: "npm:6.28.0" peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: 10c0/7db48ffd0b387af0eed060ceaf42075d074e63fbd30f4cf60993526b3610883a9ff82615965001165ed69d2bf2f1bce05c594a21c8d0d845e7b9bf203201116e + checksum: 10c0/e2930cf83e8c843a932b008c7ce11059fd83390502a433f0e41f192e3cb80081a621d069eeda7af3cf4bf74d7f8029f0141cdce741bca3f0af82d4bbbc7f7f10 languageName: node linkType: hard -"react-router@npm:6.27.0": - version: 6.27.0 - resolution: "react-router@npm:6.27.0" +"react-router@npm:6.28.0": + version: 6.28.0 + resolution: "react-router@npm:6.28.0" dependencies: - "@remix-run/router": "npm:1.20.0" + "@remix-run/router": "npm:1.21.0" peerDependencies: react: ">=16.8" - checksum: 10c0/440d6ee00890cec92a0c2183164149fbb96363efccf52bb132a964f44e51aec2f4b5a0520c67f6f17faddaa4097090fd76f7efe58263947532fceeb11dd4cdf3 + checksum: 10c0/b435510de78fd882bf6ca9800a73cd90cee418bd1d19efd91b8dcaebde36929bbb589e25d9f7eec24ceb84255e8d538bc1fe54e6ddb5c43c32798e2b720fa76d languageName: node linkType: hard @@ -4536,7 +4472,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.2.0": +"react@npm:^18.3.0": version: 18.3.1 resolution: "react@npm:18.3.1" dependencies: @@ -4609,7 +4545,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.22.2": +"resolve@npm:^1.1.7, resolve@npm:^1.22.8": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -4622,7 +4558,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin": +"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -4660,6 +4596,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^5.0.5": + version: 5.0.10 + resolution: "rimraf@npm:5.0.10" + dependencies: + glob: "npm:^10.3.7" + bin: + rimraf: dist/esm/bin.mjs + checksum: 10c0/7da4fd0e15118ee05b918359462cfa1e7fe4b1228c7765195a45b55576e8c15b95db513b8466ec89129666f4af45ad978a3057a02139afba1a63512a2d9644cc + languageName: node + linkType: hard + "rollup@npm:4.24.0": version: 4.24.0 resolution: "rollup@npm:4.24.0" @@ -4841,12 +4788,12 @@ __metadata: linkType: hard "sonner@npm:^1.4.41": - version: 1.5.0 - resolution: "sonner@npm:1.5.0" + version: 1.7.0 + resolution: "sonner@npm:1.7.0" peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/9561b5861bede7f874cc442e447a68c8bfa6e4eadad603bc04e38db35a8b8108741f595a12c9856742062bae230ffedf73122015940491f482c5aa9e68ee85e0 + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + checksum: 10c0/02b16249b0c6e3a62c50dbcb643bb089aecd82c4f0ae988d00903e427d5485933d6cb59047fe5da338b548b6a3311643e511f63ed60a938458295015f7a3fdfd languageName: node linkType: hard @@ -4864,12 +4811,12 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^10.0.0": - version: 10.0.6 - resolution: "ssri@npm:10.0.6" +"ssri@npm:^12.0.0": + version: 12.0.0 + resolution: "ssri@npm:12.0.0" dependencies: minipass: "npm:^7.0.3" - checksum: 10c0/e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 + checksum: 10c0/caddd5f544b2006e88fa6b0124d8d7b28208b83c72d7672d5ade44d794525d23b540f3396108c4eb9280dcb7c01f0bef50682f5b4b2c34291f7c5e211fd1417d languageName: node linkType: hard @@ -4936,7 +4883,7 @@ __metadata: languageName: node linkType: hard -"sucrase@npm:^3.32.0": +"sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" dependencies: @@ -4981,9 +4928,9 @@ __metadata: linkType: hard "tailwind-merge@npm:^2.2.2": - version: 2.5.4 - resolution: "tailwind-merge@npm:2.5.4" - checksum: 10c0/6c3d2a1d44344f373859f005e6366f0dbd7f66131d330a51dbe823dab08f71c388b2efcbb2b6a2170ca469581d27079c25cd40c234ca1356c4893ae99c2febb3 + version: 2.5.5 + resolution: "tailwind-merge@npm:2.5.5" + checksum: 10c0/32614dd2b4ddd4fab070d5ec569e6da00e2b34269b9ac2f2ff16733cef29a076c8e2210fbfc1904d7983a8fdb6b3e63d18ca117645f21b12ca7bcf8fe3507241 languageName: node linkType: hard @@ -5004,49 +4951,49 @@ __metadata: linkType: hard "tailwindcss@npm:^3.4.3": - version: 3.4.14 - resolution: "tailwindcss@npm:3.4.14" + version: 3.4.16 + resolution: "tailwindcss@npm:3.4.16" dependencies: "@alloc/quick-lru": "npm:^5.2.0" arg: "npm:^5.0.2" - chokidar: "npm:^3.5.3" + chokidar: "npm:^3.6.0" didyoumean: "npm:^1.2.2" dlv: "npm:^1.1.3" - fast-glob: "npm:^3.3.0" + fast-glob: "npm:^3.3.2" glob-parent: "npm:^6.0.2" is-glob: "npm:^4.0.3" - jiti: "npm:^1.21.0" - lilconfig: "npm:^2.1.0" - micromatch: "npm:^4.0.5" + jiti: "npm:^1.21.6" + lilconfig: "npm:^3.1.3" + micromatch: "npm:^4.0.8" normalize-path: "npm:^3.0.0" object-hash: "npm:^3.0.0" - picocolors: "npm:^1.0.0" - postcss: "npm:^8.4.23" + picocolors: "npm:^1.1.1" + postcss: "npm:^8.4.47" postcss-import: "npm:^15.1.0" postcss-js: "npm:^4.0.1" - postcss-load-config: "npm:^4.0.1" - postcss-nested: "npm:^6.0.1" - postcss-selector-parser: "npm:^6.0.11" - resolve: "npm:^1.22.2" - sucrase: "npm:^3.32.0" + postcss-load-config: "npm:^4.0.2" + postcss-nested: "npm:^6.2.0" + postcss-selector-parser: "npm:^6.1.2" + resolve: "npm:^1.22.8" + sucrase: "npm:^3.35.0" bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: 10c0/f6c23f8a3293ce3b2511bca1e50008ac94bd8562cb09fec32fe4f8e8a4f54d9e9fc10e567b7f974abdd4b33e550564a2616d4e793c736955432f28448141ce45 + checksum: 10c0/f716ff38e0ea6f25c2b3d811e0aaac07f627193e8527d8ad945d5088d4a188ac1eb1e8ee9aef76d8525e756ffbb8369d717013d3ffc2f49fabaa94cb1e6784c1 languageName: node linkType: hard -"tar@npm:^6.1.11, tar@npm:^6.2.1": - version: 6.2.1 - resolution: "tar@npm:6.2.1" +"tar@npm:^7.4.3": + version: 7.4.3 + resolution: "tar@npm:7.4.3" dependencies: - chownr: "npm:^2.0.0" - fs-minipass: "npm:^2.0.0" - minipass: "npm:^5.0.0" - minizlib: "npm:^2.1.1" - mkdirp: "npm:^1.0.3" - yallist: "npm:^4.0.0" - checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.0.1" + mkdirp: "npm:^3.0.1" + yallist: "npm:^5.0.0" + checksum: 10c0/d4679609bb2a9b48eeaf84632b6d844128d2412b95b6de07d53d8ee8baf4ca0857c9331dfa510390a0727b550fd543d4d1a10995ad86cdf078423fbb8d99831d languageName: node linkType: hard @@ -5094,11 +5041,11 @@ __metadata: linkType: hard "ts-api-utils@npm:^1.3.0": - version: 1.3.0 - resolution: "ts-api-utils@npm:1.3.0" + version: 1.4.3 + resolution: "ts-api-utils@npm:1.4.3" peerDependencies: typescript: ">=4.2.0" - checksum: 10c0/f54a0ba9ed56ce66baea90a3fa087a484002e807f28a8ccb2d070c75e76bde64bd0f6dce98b3802834156306050871b67eec325cb4e918015a360a3f0868c77c + checksum: 10c0/e65dc6e7e8141140c23e1dc94984bf995d4f6801919c71d6dc27cf0cd51b100a91ffcfe5217626193e5bea9d46831e8586febdc7e172df3f1091a7384299e23a languageName: node linkType: hard @@ -5109,10 +5056,10 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2": - version: 2.8.0 - resolution: "tslib@npm:2.8.0" - checksum: 10c0/31e4d14dc1355e9b89e4d3c893a18abb7f90b6886b089c2da91224d0a7752c79f3ddc41bc1aa0a588ac895bd97bb99c5bc2bfdb2f86de849f31caeb3ba79bbe5 +"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.6.2, tslib@npm:^2.7.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 languageName: node linkType: hard @@ -5133,22 +5080,22 @@ __metadata: linkType: hard "typescript@npm:^5.2.2": - version: 5.6.3 - resolution: "typescript@npm:5.6.3" + version: 5.7.2 + resolution: "typescript@npm:5.7.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/44f61d3fb15c35359bc60399cb8127c30bae554cd555b8e2b46d68fa79d680354b83320ad419ff1b81a0bdf324197b29affe6cc28988cd6a74d4ac60c94f9799 + checksum: 10c0/a873118b5201b2ef332127ef5c63fb9d9c155e6fdbe211cbd9d8e65877283797cca76546bad742eea36ed7efbe3424a30376818f79c7318512064e8625d61622 languageName: node linkType: hard "typescript@patch:typescript@npm%3A^5.2.2#optional!builtin": - version: 5.6.3 - resolution: "typescript@patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=8c6c40" + version: 5.7.2 + resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin::version=5.7.2&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/7c9d2e07c81226d60435939618c91ec2ff0b75fbfa106eec3430f0fcf93a584bc6c73176676f532d78c3594fe28a54b36eb40b3d75593071a7ec91301533ace7 + checksum: 10c0/f3b8082c9d1d1629a215245c9087df56cb784f9fb6f27b5d55577a20e68afe2a889c040aacff6d27e35be165ecf9dca66e694c42eb9a50b3b2c451b36b5675cb languageName: node linkType: hard @@ -5159,21 +5106,21 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-filename@npm:3.0.0" +"unique-filename@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-filename@npm:4.0.0" dependencies: - unique-slug: "npm:^4.0.0" - checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + unique-slug: "npm:^5.0.0" + checksum: 10c0/38ae681cceb1408ea0587b6b01e29b00eee3c84baee1e41fd5c16b9ed443b80fba90c40e0ba69627e30855570a34ba8b06702d4a35035d4b5e198bf5a64c9ddc languageName: node linkType: hard -"unique-slug@npm:^4.0.0": - version: 4.0.0 - resolution: "unique-slug@npm:4.0.0" +"unique-slug@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-slug@npm:5.0.0" dependencies: imurmurhash: "npm:^0.1.4" - checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + checksum: 10c0/d324c5a44887bd7e105ce800fcf7533d43f29c48757ac410afd42975de82cc38ea2035c0483f4de82d186691bf3208ef35c644f73aa2b1b20b8e651be5afd293 languageName: node linkType: hard @@ -5258,8 +5205,8 @@ __metadata: linkType: hard "vite@npm:^5.2.0": - version: 5.4.10 - resolution: "vite@npm:5.4.10" + version: 5.4.11 + resolution: "vite@npm:5.4.11" dependencies: esbuild: "npm:^0.21.3" fsevents: "npm:~2.3.3" @@ -5296,7 +5243,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/4ef4807d2fd166a920de244dbcec791ba8a903b017a7d8e9f9b4ac40d23f8152c1100610583d08f542b47ca617a0505cfc5f8407377d610599d58296996691ed + checksum: 10c0/d536bb7af57dd0eca2a808f95f5ff1d7b7ffb8d86e17c6893087680a0448bd0d15e07475270c8a6de65cb5115592d037130a1dd979dc76bcef8c1dda202a1874 languageName: node linkType: hard @@ -5318,14 +5265,14 @@ __metadata: languageName: node linkType: hard -"which@npm:^4.0.0": - version: 4.0.0 - resolution: "which@npm:4.0.0" +"which@npm:^5.0.0": + version: 5.0.0 + resolution: "which@npm:5.0.0" dependencies: isexe: "npm:^3.1.1" bin: node-which: bin/which.js - checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + checksum: 10c0/e556e4cd8b7dbf5df52408c9a9dd5ac6518c8c5267c8953f5b0564073c66ed5bf9503b14d876d0e9c7844d4db9725fb0dcf45d6e911e17e26ab363dc3965ae7b languageName: node linkType: hard @@ -5379,12 +5326,19 @@ __metadata: languageName: node linkType: hard +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 + languageName: node + linkType: hard + "yaml@npm:^2.3.4": - version: 2.6.0 - resolution: "yaml@npm:2.6.0" + version: 2.6.1 + resolution: "yaml@npm:2.6.1" bin: yaml: bin.mjs - checksum: 10c0/9e74cdb91cc35512a1c41f5ce509b0e93cc1d00eff0901e4ba831ee75a71ddf0845702adcd6f4ee6c811319eb9b59653248462ab94fa021ab855543a75396ceb + checksum: 10c0/aebf07f61c72b38c74d2b60c3a3ccf89ee4da45bcd94b2bfb7899ba07a5257625a7c9f717c65a6fc511563d48001e01deb1d9e55f0133f3e2edf86039c8c1be7 languageName: node linkType: hard