diff --git a/package.json b/package.json index 2bade08d..cb39b7e5 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@egjs/react-grid": "^1.16.0", + "@hello-pangea/dnd": "^17.0.0", "@mui/material": "^5.15.9", "@next/third-parties": "^14.1.0", "@tanstack/react-query": "^5.17.12", @@ -45,11 +46,10 @@ "next": "14.0.4", "next-pwa": "^5.6.0", "open-graph": "^0.2.6", - "react": "^18", + "react": "^18.3.1", "react-apexcharts": "^1.4.1", - "react-beautiful-dnd": "^13.1.1", "react-cookie": "^7.0.2", - "react-dom": "^18", + "react-dom": "^18.3.1", "react-hook-form": "^7.50.0", "react-ios-pwa-prompt": "^1.8.4", "react-lottie": "^1.2.4", @@ -73,9 +73,8 @@ "@types/jest": "^29.5.11", "@types/node": "^20", "@types/open-graph": "^0.2.5", - "@types/react": "^18", - "@types/react-beautiful-dnd": "^13.1.8", - "@types/react-dom": "^18", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "eslint": "^8", "eslint-config-next": "14.0.4", "eslint-config-prettier": "^9.1.0", diff --git a/public/icons/add.svg b/public/icons/add.svg index 014aa172..7c4b5441 100644 --- a/public/icons/add.svg +++ b/public/icons/add.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/icons/check_white.svg b/public/icons/check_white.svg new file mode 100644 index 00000000..4573e94e --- /dev/null +++ b/public/icons/check_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/close_button.svg b/public/icons/close_button.svg index c75ab004..ccb514f5 100644 --- a/public/icons/close_button.svg +++ b/public/icons/close_button.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/icons/collapse.svg b/public/icons/collapse.svg new file mode 100644 index 00000000..5458eda7 --- /dev/null +++ b/public/icons/collapse.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/dnd.svg b/public/icons/dnd.svg index 64b5ec78..e37a7994 100644 --- a/public/icons/dnd.svg +++ b/public/icons/dnd.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/public/icons/expand.svg b/public/icons/expand.svg new file mode 100644 index 00000000..f4ed2244 --- /dev/null +++ b/public/icons/expand.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/image.svg b/public/icons/image.svg new file mode 100644 index 00000000..a45f33e9 --- /dev/null +++ b/public/icons/image.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/link.svg b/public/icons/link.svg index c6244796..f4fa2f6a 100644 --- a/public/icons/link.svg +++ b/public/icons/link.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/src/app/list/[listId]/_components/ListDetailInner/index.tsx b/src/app/list/[listId]/_components/ListDetailInner/index.tsx index 12bc6c50..188e0bc0 100644 --- a/src/app/list/[listId]/_components/ListDetailInner/index.tsx +++ b/src/app/list/[listId]/_components/ListDetailInner/index.tsx @@ -8,7 +8,7 @@ import { ListDetailType } from '@/lib/types/listType'; import Header from '@/app/list/[listId]/_components/ListDetailInner/Header'; import RankList from '@/app/list/[listId]/_components/ListDetailInner/RankList'; import Footer from '@/app/list/[listId]/_components/ListDetailInner/Footer'; -import { BACKGROUND_COLOR_PALETTE_TYPE, BACKGROUND_COLOR_READ } from '@/styles/Color'; +import { BACKGROUND_COLOR_READ } from '@/styles/Color'; interface OptionsProps { value: string; diff --git a/src/app/list/[listId]/edit/page.tsx b/src/app/list/[listId]/edit/page.tsx index 5eba0bd4..aeeaee41 100644 --- a/src/app/list/[listId]/edit/page.tsx +++ b/src/app/list/[listId]/edit/page.tsx @@ -1,45 +1,45 @@ 'use client'; import { useEffect, useState } from 'react'; -import { FieldErrors, FormProvider, useForm } from 'react-hook-form'; +import { FormProvider, useForm } from 'react-hook-form'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useRouter, useParams } from 'next/navigation'; +import { useParams, useRouter } from 'next/navigation'; -import CreateItem from '@/app/list/create/_components/CreateItem'; -import CreateList from '@/app/list/create/_components/CreateList'; +import { ItemImagesType, ListCreateType, ListDetailType, ListEditType } from '@/lib/types/listType'; +import { CategoryType } from '@/lib/types/categoriesType'; +import { QUERY_KEYS } from '@/lib/constants/queryKeys'; +import toasting from '@/lib/utils/toasting'; +import toastMessage from '@/lib/constants/toastMessage'; +import { useLanguage } from '@/store/useLanguage'; +import { useUser } from '@/store/useUser'; -import updateList from '@/app/_api/list/updateList'; import getListDetail from '@/app/_api/list/getListDetail'; import getCategories from '@/app/_api/category/getCategories'; -import { QUERY_KEYS } from '@/lib/constants/queryKeys'; -import { ItemImagesType, ListDetailType, ListEditType } from '@/lib/types/listType'; -import { CategoryType } from '@/lib/types/categoriesType'; -import { useUser } from '@/store/useUser'; +import updateList from '@/app/_api/list/updateList'; -export type FormErrors = FieldErrors; +import StepOne from '@/app/list/create/_components/StepOne'; +import StepThree from '@/app/list/create/_components/StepThree'; +import StepTwo from '@/app/list/create/_components/StepTwo'; export default function EditPage() { const router = useRouter(); + const { user } = useUser(); + const { language } = useLanguage(); const queryClient = useQueryClient(); const param = useParams<{ listId: string }>(); - const { user } = useUser(); - - const [step, setStep] = useState<'list' | 'item'>('list'); - const { data: listDetailData } = useQuery({ - queryKey: [QUERY_KEYS.getListDetail, param?.listId], - queryFn: () => getListDetail(Number(param?.listId)), - }); + /** step 관리 */ + const [step, setStep] = useState(1); - const { data: categories } = useQuery({ - queryKey: [QUERY_KEYS.getCategories], - queryFn: () => getCategories(), - }); + const handleNext = () => setStep((prev) => prev + 1); + const handleBack = () => setStep((prev) => prev - 1); - const methods = useForm({ + /** React Hook Form */ + //-- 초기세팅 + const methods = useForm({ mode: 'onChange', defaultValues: { - category: 'culture', + category: '', labels: [], collaboratorIds: [], title: '', @@ -51,8 +51,49 @@ export default function EditPage() { }, }); - //request용 데이터 만드는 함수. + //--- 기존 데이터 불러오기 + //기존 리스트 데이터 + const { data: listDetailData } = useQuery({ + queryKey: [QUERY_KEYS.getListDetail, param?.listId], + queryFn: () => getListDetail(Number(param?.listId)), + }); + + // 카테고리 목록 + const { data: categories } = useQuery({ + queryKey: [QUERY_KEYS.getCategories], + queryFn: () => getCategories(), + }); + //데이터 채워넣기 + useEffect(() => { + if (listDetailData) { + methods.reset({ + category: categories?.find((category) => category.korName === listDetailData.categoryKorName)?.engName, + labels: listDetailData.labels.map((obj) => obj.name), + collaboratorIds: listDetailData.collaborators.filter((c) => c.id !== user.id).map((c) => c.id), + title: listDetailData.title, + description: listDetailData.description, + isPublic: listDetailData.isPublic, + backgroundPalette: listDetailData.backgroundPalette, + backgroundColor: listDetailData.backgroundColor, + items: listDetailData.items.map(({ id, rank, title, comment, link, imageUrl }) => { + return { + rank: rank, + id: id, + title: title, + comment: comment ? comment : '', + link: link ? link : '', + imageUrl: typeof imageUrl === 'string' ? imageUrl : '', + }; + }), + }); + } + + methods.trigger(['title']); + }, [listDetailData, categories, methods]); + + /** Request 보내기 */ + //--- 포맷 맞추기 const formatData = () => { const originData = methods.getValues(); @@ -80,6 +121,7 @@ export default function EditPage() { }), }; + //이미지rank,extension & 이미지파일 배열 const imageData: ItemImagesType = { listId: Number(param?.listId), extensionRanks: originData.items @@ -100,46 +142,16 @@ export default function EditPage() { return { listData, imageData, imageFileList }; }; - const handleStepChange = (step: 'list' | 'item') => { - setStep(step); + //--아이템 중복 확인 + const getIsAllUnique = () => { + const allTitles = methods.getValues().items.map((item, itemIndex) => { + return item.title === '' ? itemIndex : item.title; + }); //TODO: 필요한 코드인지 다시 확인하기 + const isAllUnique = new Set(allTitles).size === allTitles.length; + return isAllUnique; }; - const handleSubmit = () => { - const { listData, imageData, imageFileList } = formatData(); - const updateData = { - listId: Number(param?.listId) || 0, - listData: listData, - imageData: imageData, - imageFileList: imageFileList, - }; - updateListMutation(updateData); - }; - - useEffect(() => { - if (listDetailData) { - methods.reset({ - category: categories?.find((c) => c.korName === listDetailData.categoryKorName)?.engName || 'culture', - labels: listDetailData.labels.map((obj) => obj.name), - collaboratorIds: listDetailData.collaborators.filter((c) => c.id !== user.id).map((c) => c.id), - title: listDetailData.title, - description: listDetailData.description, - isPublic: listDetailData.isPublic, - backgroundPalette: listDetailData.backgroundPalette, - backgroundColor: listDetailData.backgroundColor, - items: listDetailData.items.map(({ id, rank, title, comment, link, imageUrl }) => { - return { - rank: rank, - id: id, - title: title, - comment: comment ? comment : '', - link: link ? link : '', - imageUrl: typeof imageUrl === 'string' ? imageUrl : '', - }; - }), - }); - } - }, [listDetailData, categories, methods]); - + //--- 리스트 수정하기 const { mutate: updateListMutation, isPending: isUpdatingList, @@ -154,27 +166,34 @@ export default function EditPage() { }, }); + //--- 제출 + const handleSubmit = () => { + if (getIsAllUnique()) { + const { listData, imageData, imageFileList } = formatData(); + const updateData = { + listId: Number(param?.listId) || 0, + listData: listData, + imageData: imageData, + imageFileList: imageFileList, + }; + updateListMutation(updateData); + } else { + toasting({ type: 'error', txt: toastMessage[language].duplicatedItemError }); + } + }; + return ( - <> - - {step === 'list' ? ( - { - handleStepChange('item'); - }} - type="edit" - /> - ) : ( - { - handleStepChange('list'); - }} - onSubmitClick={handleSubmit} - isSubmitting={isUpdatingList || isSuccess} - type="edit" - /> - )} - - + + {step === 1 && } + {step === 2 && } + {step === 3 && ( + + )} + ); } diff --git a/src/app/list/create/_components/CreateItem.css.ts b/src/app/list/create/_components/CreateItem.css.ts deleted file mode 100644 index 3615d8f7..00000000 --- a/src/app/list/create/_components/CreateItem.css.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { vars } from '@/styles/__theme.css'; -import * as fonts from '@/styles/__font.css'; - -export const article = style({ - padding: '16px 20px 35px', -}); - -export const label = style([ - fonts.labelLarge, - { - marginBottom: '1rem', - }, -]); - -export const required = style({ - marginLeft: '5px', - - fontSize: '1.7rem', - fontWeight: '400', - lineHeight: '2.2rem', - color: vars.color.red, -}); - -export const description = style([ - fonts.bodyMedium, - { - marginBottom: '2.4rem', - - color: vars.color.gray9, - }, -]); diff --git a/src/app/list/create/_components/CreateItem.tsx b/src/app/list/create/_components/CreateItem.tsx deleted file mode 100644 index dd7b3c0b..00000000 --- a/src/app/list/create/_components/CreateItem.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useFormContext } from 'react-hook-form'; - -import BlueButton from '@/components/BlueButton/BlueButton'; -import Header from '@/components/Header/__Header'; -import Items from './item/Items'; -import * as styles from './CreateItem.css'; -import { useLanguage } from '@/store/useLanguage'; -import { itemLocale } from '@/app/list/create/locale'; - -interface CreateItemProps { - onBackClick: () => void; - onSubmitClick: () => void; - isSubmitting: boolean; - type: 'create' | 'edit'; -} - -export default function CreateItem({ onBackClick, onSubmitClick, isSubmitting, type }: CreateItemProps) { - const { language } = useLanguage(); - const { - formState: { isValid }, - } = useFormContext(); - - return ( -
-
- {itemLocale[language].complete} - - } - /> -
-

- {itemLocale[language].addItem} * -

-

- {itemLocale[language].itemCreateMessage1}
- {itemLocale[language].itemCreateMessage2} -

- -
-
- ); -} diff --git a/src/app/list/create/_components/CreateList.css.ts b/src/app/list/create/_components/CreateList.css.ts deleted file mode 100644 index 6e0b7423..00000000 --- a/src/app/list/create/_components/CreateList.css.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { style } from '@vanilla-extract/css'; - -export const body = style({ - width: '100vw', - padding: '16px 20px 50px', - - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - rowGap: '50px', -}); - -export const headerNextButton = style({ - fontSize: '1.6rem', - color: '#AFB1B6', - cursor: 'default', -}); - -export const headerNextButtonActive = style({ - fontSize: '1.6rem', -}); diff --git a/src/app/list/create/_components/CreateList.tsx b/src/app/list/create/_components/CreateList.tsx deleted file mode 100644 index c7a130b9..00000000 --- a/src/app/list/create/_components/CreateList.tsx +++ /dev/null @@ -1,220 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useFormContext, useWatch } from 'react-hook-form'; -import { useRouter } from 'next/navigation'; -import { useSearchParams } from 'next/navigation'; -import { useQuery } from '@tanstack/react-query'; - -import Header from '@/components/Header/__Header'; -import Section from './list/Section'; -import SimpleInput from './list/SimpleInput'; -import ButtonSelector from './list/ButtonSelector'; -import LabelInput from './list/LabelInput'; -import MemberSelector from './list/MemberSelector'; -import PaletteSelector from './list/PaletteSelector'; -import ColorSelector from './list/ColorSelector'; -import RadioInput from './list/RadioInput'; - -import { useUser } from '@/store/useUser'; -import { QUERY_KEYS } from '@/lib/constants/queryKeys'; -import getCategories from '@/app/_api/category/getCategories'; -import getFollowingList from '@/app/_api/follow/getFollowingList'; -import { CategoryType } from '@/lib/types/categoriesType'; -import { FollowingListType } from '@/lib/types/followType'; -import { BACKGROUND_COLOR_PALETTE_TYPE } from '@/styles/Color'; -import { listPlaceholder } from '@/lib/constants/placeholder'; -import { listDescriptionRules, listLabelRules, listTitleRules } from '@/lib/constants/formInputValidationRules'; - -import * as styles from './CreateList.css'; -import { useLanguage } from '@/store/useLanguage'; -import { listLocale } from '@/app/list/create/locale'; - -interface CreateListProps { - onNextClick: () => void; - type: 'create' | 'edit'; -} - -interface OptionsProps { - value: string; - label: string; -} - -/** - * CreateList 컴포넌트: - * 리스트 생성 과정 중 - * 리스트에 대한 정보를 입력하는 페이지 - * - * @param props.onNextClick - 헤더의 '다음'버튼을 클릭했을때 동작시킬 함수 - */ -function CreateList({ onNextClick, type }: CreateListProps) { - const { language } = useLanguage(); - const { user: me } = useUser(); - - const { - setValue, - getValues, - control, - formState: { errors }, - } = useFormContext(); - const collaboIDs = useWatch({ control, name: 'collaboratorIds' }); - const title = useWatch({ control, name: 'title' }); - const category = useWatch({ control, name: 'category' }); - const palette = useWatch({ control, name: 'backgroundPalette' }); - const [selectedPalette, setSelectedPalette] = useState(palette); - - const router = useRouter(); - const searchParams = useSearchParams(); - const isTemplateCreation = searchParams?.has('title') && searchParams?.has('category'); - - const { data: followingList } = useQuery({ - queryKey: [QUERY_KEYS.getFollowingList, me.id], - queryFn: () => getFollowingList(me.id), - }); - - const { data: categories } = useQuery({ - queryKey: [QUERY_KEYS.getCategories], - queryFn: getCategories, - }); - - useEffect(() => { - const handleQueryParams = () => { - if (isTemplateCreation) { - setValue('title', searchParams?.get('title')); - const c = categories?.find((c) => c.korName === searchParams?.get('category'))?.engName; - setValue('category', c); - } - }; - handleQueryParams(); - }, [categories]); - - const isValid = - title && - category && - !errors.title && - !errors.category && - !errors.labels && - !errors.collaboratorIds && - !errors.description; - - return ( -
- {/* 헤더 */} -
{ - router.back(); - }} - right={ - - } - /> - -
- {/* 리스트 제목 */} -
- -
- - {/* 리스트 소개 */} -
- -
- - {/* 카테고리 */} -
- category.code !== '0') || []} - onClick={(item: CategoryType) => { - setValue('category', item.engName); - }} - defaultValue={category} - /> -
- - {/* 라벨 */} -
- -
- - {/* 콜라보레이터 */} -
- { - setValue('collaboratorIds', [...collaboIDs, userId]); - }} - onClickDelete={(userId: number) => { - setValue( - 'collaboratorIds', - collaboIDs.filter((collaboId: number) => collaboId !== userId) - ); - }} - rules={{ - maxNum: { - value: 20, - errorMessage: listLocale[language].addCollaboratorError, - }, - }} - /> -
- - {/* 배경 색상 */} -
- { - const value: BACKGROUND_COLOR_PALETTE_TYPE = target.value as BACKGROUND_COLOR_PALETTE_TYPE; - setSelectedPalette(value); - }} - /> - { - setValue('backgroundColor', color); - setValue('backgroundPalette', palette); - }} - /> -
- - {/* 공개 설정 */} -
- { - setValue('isPublic', b); - }} - /> -
-
-
- ); -} - -export default CreateList; diff --git a/src/app/list/create/_components/ItemAccordion.css.ts b/src/app/list/create/_components/ItemAccordion.css.ts new file mode 100644 index 00000000..c75100af --- /dev/null +++ b/src/app/list/create/_components/ItemAccordion.css.ts @@ -0,0 +1,129 @@ +import { style, styleVariants } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; +import * as fonts from '@/styles/font.css'; + +export const accordion = style({ + display: 'flex', + flexDirection: 'column', + gap: '1rem', + padding: '1.1rem 1.6rem ', + + backgroundColor: vars.color.white, + borderRadius: '1.2rem', +}); + +//헤더 +export const header = style({ + display: 'flex', + alignItems: 'center', + gap: '1.2rem', + color: vars.color.red, +}); + +export const rank = style([ + fonts.Label, + { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: '4.2rem', + height: '2.6rem', + + color: vars.color.blue, + backgroundColor: vars.color.lightblue, + borderRadius: '1.5rem', + }, +]); + +export const variantRank = styleVariants({ + default: [rank], + first: [rank, { color: vars.color.white, backgroundColor: vars.color.blue }], +}); + +export const titleInput = style([ + fonts.BodyBold, + { + flexGrow: 1, + color: vars.color.bluegray10, + '::placeholder': { color: vars.color.bluegray6 }, + }, +]); + +//TODO: 모바일에서 아코디언 아이콘 잘 눌리는지 확인하기 +export const accordionIconWrapper = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + width: '2rem', + height: '2.6rem', +}); + +//콘텐트 +export const hr = style({ + width: '100%', + strokeWidth: '0.4rem ', + stroke: vars.color.bluegray8, +}); + +export const content = style({ + display: 'flex', + flexDirection: 'column', + gap: '1rem', +}); + +export const commentTextarea = style([ + fonts.Label, + { + resize: 'none', + border: 'none', + outline: 'none', + '::-webkit-scrollbar': { + width: '0', // 스크롤바 너비를 0으로 설정하여 숨김 + }, + + color: vars.color.bluegray10, + '::placeholder': { color: vars.color.bluegray6 }, + }, +]); + +export const length = style([ + fonts.Label, + { + width: '100%', + + display: 'flex', + justifyContent: 'end', + alignItems: 'center', + + color: vars.color.bluegray6, + }, +]); + +export const toolsContainer = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', +}); + +export const toolsWrapper = style({ + display: 'flex', + alignItems: 'center', + gap: '1rem', +}); + +export const imageInput = style({ + display: 'none', +}); + +export const deleteButton = style([ + fonts.Label, + { + color: vars.color.blue, + }, +]); + +export const previewContainer = style({ + display: 'flex', + gap: '10px', +}); diff --git a/src/app/list/create/_components/ItemAccordion.tsx b/src/app/list/create/_components/ItemAccordion.tsx new file mode 100644 index 00000000..41119336 --- /dev/null +++ b/src/app/list/create/_components/ItemAccordion.tsx @@ -0,0 +1,183 @@ +import { ChangeEvent } from 'react'; +import Image from 'next/image'; +import { useParams } from 'next/navigation'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { useQuery } from '@tanstack/react-query'; + +import CollapseIcon from '/public/icons/collapse.svg'; +import ExpandIcon from '/public/icons/expand.svg'; + +import { useLanguage } from '@/store/useLanguage'; +import useResizeTextarea from '@/hooks/useResizeTextarea'; +import { QUERY_KEYS } from '@/lib/constants/queryKeys'; +import { itemCommentRules, itemTitleRules } from '@/lib/constants/formInputValidationRules'; +import { ListDetailType } from '@/lib/types/listType'; + +import toasting from '@/lib/utils/toasting'; +import toastMessage from '@/lib/constants/toastMessage'; + +import getListDetail from '@/app/_api/list/getListDetail'; + +import ItemImagePreview from './ItemImagePreview'; +import ItemLinkUploader from './ItemLinkUploader'; +import ItemLinkPreview from './ItemLinkPreview'; +import ItemImageUploader from './ItemImageUploader'; + +import * as styles from './ItemAccordion.css'; + +interface ItemAccordionProps { + type: 'create' | 'edit'; + index: number; + handleToggleItem: (index: number) => void; + isExpand: boolean; + handleDeleteItem: (id: number) => void; +} + +export default function ItemAccordion({ + type, + index, + handleToggleItem, + isExpand, + handleDeleteItem, +}: ItemAccordionProps) { + const { language } = useLanguage(); + //props로 받아야하는 항목 mock data + const rank = index + 1; + + //--- (수정)기존 데이터 가져오기 + const param = useParams<{ listId: string }>(); + const listId = param?.listId; + + const { data: listDetailData } = useQuery({ + queryKey: [QUERY_KEYS.getListDetail, listId], + queryFn: () => getListDetail(Number(listId)), + enabled: type === 'edit', + }); + + /** react-hook-form */ + const { register, setValue, getValues, control } = useFormContext(); + const titleRegister = register(`items.${index}.title`, itemTitleRules); + const commentRegister = register(`items.${index}.comment`, itemCommentRules); + const imageRegister = register(`items.${index}.imageUrl`); + + const watchComment = useWatch({ control, name: `items.${index}.comment` }); + const watchImage = useWatch({ control, name: `items.${index}.imageUrl` }); + const watchLink = useWatch({ control, name: `items.${index}.link` }); + + //--- 글자 길이에 맞춘 높이 조절 + const { textareaRef, handleResizeHeight } = useResizeTextarea(); + + /** Tool */ + //--- 이미지 업로드 & 미리보기 + const MAX_IMAGE_INPUT_SIZE_MB = 50 * 1024 * 1024; //50MB + + const handleImageChange = async (e: ChangeEvent) => { + if (e.target.files) { + const targetFile = e.target.files[0]; + if (targetFile?.size > MAX_IMAGE_INPUT_SIZE_MB) { + toasting({ type: 'error', txt: toastMessage[language].imageSizeError }); + } else { + imageRegister.onChange(e); + } + } + }; + + const handleImageClear = () => { + setValue(`items.${index}.imageUrl`, ''); + }; + + const handleLinkClear = () => { + setValue(`items.${index}.link`, ''); + }; + + return ( +
+
+ drag and drop +
{rank}위
+ item.id === getValues(`items.${index}.id`))} + autoComplete="off" + onClick={() => { + if (type === 'edit' && listDetailData?.items.some((item) => item.id === getValues(`items.${index}.id`))) { + toasting({ type: 'default', txt: '이미 등록한 아이템명은 수정이 불가능해요' }); + } + }} + /> +
{ + handleToggleItem(index); + }} + > + {isExpand ? : } +
+ {/** end-아코디언 확장축소 아이콘 */} +
+ {/** end-header */} + {isExpand ? ( + <> +
+
+