diff --git a/fe/package-lock.json b/fe/package-lock.json index 7ec20c2..f2770ed 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -12,6 +12,7 @@ "@react-spring/web": "^9.7.5", "@use-gesture/react": "^10.3.1", "axios": "^1.7.7", + "classnames": "^2.5.1", "dotenv": "^16.4.5", "express": "^4.21.0", "lodash.debounce": "^4.0.8", @@ -20,6 +21,7 @@ "next-auth": "^4.24.8", "react": "^18", "react-dom": "^18", + "react-swipeable": "^7.0.2", "tailwind-scrollbar-hide": "^1.1.7", "uuid": "^10.0.0", "winston": "^3.14.2", @@ -1595,6 +1597,11 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5768,6 +5775,14 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-swipeable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz", + "integrity": "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w==", + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/fe/package.json b/fe/package.json index 058d895..9d15806 100644 --- a/fe/package.json +++ b/fe/package.json @@ -15,6 +15,7 @@ "@react-spring/web": "^9.7.5", "@use-gesture/react": "^10.3.1", "axios": "^1.7.7", + "classnames": "^2.5.1", "dotenv": "^16.4.5", "express": "^4.21.0", "lodash.debounce": "^4.0.8", @@ -23,6 +24,7 @@ "next-auth": "^4.24.8", "react": "^18", "react-dom": "^18", + "react-swipeable": "^7.0.2", "tailwind-scrollbar-hide": "^1.1.7", "uuid": "^10.0.0", "winston": "^3.14.2", diff --git a/fe/public/gif/popup.gif b/fe/public/gif/popup.gif new file mode 100644 index 0000000..ab9cab0 Binary files /dev/null and b/fe/public/gif/popup.gif differ diff --git a/fe/public/images/ArrowBack.svg b/fe/public/images/ArrowBack.svg index 74f7ab2..17e2e9e 100644 --- a/fe/public/images/ArrowBack.svg +++ b/fe/public/images/ArrowBack.svg @@ -1,3 +1,6 @@ - - + + + + + diff --git a/fe/public/images/Check.svg b/fe/public/images/Check.svg deleted file mode 100644 index cac6cec..0000000 --- a/fe/public/images/Check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/fe/public/images/Search.svg b/fe/public/images/Search.svg index 01aea08..ab0cc28 100644 --- a/fe/public/images/Search.svg +++ b/fe/public/images/Search.svg @@ -1,6 +1,6 @@ - - - + + + diff --git a/fe/public/images/graytooltip.svg b/fe/public/images/graytooltip.svg new file mode 100644 index 0000000..d0f9ca3 --- /dev/null +++ b/fe/public/images/graytooltip.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fe/public/images/tooltip.svg b/fe/public/images/tooltip.svg new file mode 100644 index 0000000..d0f9ca3 --- /dev/null +++ b/fe/public/images/tooltip.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fe/public/images/whitedelete.svg b/fe/public/images/whitedelete.svg new file mode 100644 index 0000000..2b463e9 --- /dev/null +++ b/fe/public/images/whitedelete.svg @@ -0,0 +1,3 @@ + + + diff --git a/fe/public/svg/LocationPin.svg b/fe/public/svg/LocationPin.svg new file mode 100644 index 0000000..f51ec69 --- /dev/null +++ b/fe/public/svg/LocationPin.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/fe/public/svg/bluecheck.svg b/fe/public/svg/bluecheck.svg new file mode 100644 index 0000000..e8c1eb0 --- /dev/null +++ b/fe/public/svg/bluecheck.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/fe/public/svg/delete.svg b/fe/public/svg/delete.svg index dfe8107..0e1bd60 100644 --- a/fe/public/svg/delete.svg +++ b/fe/public/svg/delete.svg @@ -1,5 +1,7 @@ - - - - + + + + + + diff --git a/fe/public/svg/graycancel.svg b/fe/public/svg/graycancel.svg new file mode 100644 index 0000000..1811acf --- /dev/null +++ b/fe/public/svg/graycancel.svg @@ -0,0 +1,3 @@ + + + diff --git a/fe/public/svg/graycheck.svg b/fe/public/svg/graycheck.svg new file mode 100644 index 0000000..976e454 --- /dev/null +++ b/fe/public/svg/graycheck.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/fe/public/svg/infomation.svg b/fe/public/svg/infomation.svg deleted file mode 100644 index cfb0f6d..0000000 --- a/fe/public/svg/infomation.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/fe/public/svg/information.svg b/fe/public/svg/information.svg new file mode 100644 index 0000000..4b7b777 --- /dev/null +++ b/fe/public/svg/information.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fe/public/svg/linkAdd.svg b/fe/public/svg/linkAdd.svg index 80d4b7f..fce34d2 100644 --- a/fe/public/svg/linkAdd.svg +++ b/fe/public/svg/linkAdd.svg @@ -1,5 +1,7 @@ - - - - + + + + + + diff --git a/fe/public/svg/loding1.svg b/fe/public/svg/loding1.svg new file mode 100644 index 0000000..d6d298e --- /dev/null +++ b/fe/public/svg/loding1.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fe/public/svg/loding2.svg b/fe/public/svg/loding2.svg new file mode 100644 index 0000000..01f4548 --- /dev/null +++ b/fe/public/svg/loding2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fe/public/svg/loding3.svg b/fe/public/svg/loding3.svg new file mode 100644 index 0000000..c93b373 --- /dev/null +++ b/fe/public/svg/loding3.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/public/svg/loding4.svg b/fe/public/svg/loding4.svg new file mode 100644 index 0000000..a180f92 --- /dev/null +++ b/fe/public/svg/loding4.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/public/svg/modal.svg b/fe/public/svg/modal.svg new file mode 100644 index 0000000..0b35902 --- /dev/null +++ b/fe/public/svg/modal.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/fe/public/svg/tip.svg b/fe/public/svg/tip.svg new file mode 100644 index 0000000..5baaf27 --- /dev/null +++ b/fe/public/svg/tip.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/src/app/components/common/Button.tsx b/fe/src/app/components/common/Button.tsx index 0df21b0..670c29d 100644 --- a/fe/src/app/components/common/Button.tsx +++ b/fe/src/app/components/common/Button.tsx @@ -7,20 +7,18 @@ function Button({ label, onClick, type = "start", - className, + className = "", disabled = false, }: ButtonProps) { - let buttonStyle = "bg-gray-200 text-gray-500 cursor-not-allowed"; - - if (!disabled) { - buttonStyle = - type === "start" ? "bg-[#F73A2C] text-white" : "bg-[#1D1D1D] text-white"; - } + // 기본 버튼 스타일 설정 + const buttonStyle = disabled + ? "bg-gray-200 text-gray-500 cursor-not-allowed" + : "bg-[#1D1D1D] text-white"; return ( -
+
- - -
-
- ); -} -export default ExitModal; diff --git a/fe/src/app/event-maps/[id]/[nonMemberId]/components/PincheckInput.tsx b/fe/src/app/event-maps/[id]/[nonMemberId]/components/PincheckInput.tsx index 8e8f30e..1e7b1f4 100644 --- a/fe/src/app/event-maps/[id]/[nonMemberId]/components/PincheckInput.tsx +++ b/fe/src/app/event-maps/[id]/[nonMemberId]/components/PincheckInput.tsx @@ -1,18 +1,23 @@ "use client"; import React, { useRef, useState, useEffect, useCallback } from "react"; +import Image from "next/image"; // Import 순서 수정 import { useRouter, useParams } from "next/navigation"; import { v4 as uuidv4 } from "uuid"; -import { useUserDataStore } from "../stores/useUserDataStore"; // zustand store import +import { useUserDataStore } from "../stores/useUserDataStore"; -export default function PasswordInput() { +interface PasswordInputProps { + iconUrl: string | null; // iconUrl prop 타입 정의 +} + +export default function PasswordInput({ iconUrl }: PasswordInputProps) { const [password, setPassword] = useState(["", "", "", ""]); const [currentIndex, setCurrentIndex] = useState(0); const [hasError, setHasError] = useState(false); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const router = useRouter(); const { id, nonMemberId } = useParams(); - const setUserData = useUserDataStore((state) => state.setUserData); // Use zustand's setUserData + const setUserData = useUserDataStore((state) => state.setUserData); const submitPassword = useCallback(async () => { const fullPassword = password.join(""); @@ -31,31 +36,27 @@ export default function PasswordInput() { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify({ - nonMemberId, - password: fullPassword, - }), + body: JSON.stringify({ nonMemberId, password: fullPassword }), } ); if (response.ok) { - const data = await response.json(); // Get the JSON response - - // Save data in zustand store + const data = await response.json(); setUserData({ nonMemberId: data.nonMemberId, name: data.name, bookmarkUrls: data.bookmarkUrls || [], storeUrls: data.storeUrls || [], }); - + localStorage.setItem("userData", JSON.stringify(data)); router.push(`/event-maps/${id}/${nonMemberId}/load-mappin-edit`); } else { setHasError(true); setPassword(["", "", "", ""]); setCurrentIndex(0); } - } catch (error) { + } catch { + // 서버 오류 처리 시 console.error 대신 메시지 로깅 또는 적절한 에러 핸들링 수행 setHasError(true); } }, [id, nonMemberId, password, router, setUserData]); @@ -100,19 +101,49 @@ export default function PasswordInput() { } }; + useEffect(() => { + const timer = setTimeout(() => { + inputRefs.current[0]?.focus(); + }, 100); + return () => clearTimeout(timer); + }, []); + useEffect(() => { inputRefs.current[currentIndex]?.focus(); }, [currentIndex]); return (
-
+ {/* 아이콘 */} +
+ {iconUrl ? ( + Lock Icon + ) : ( +
+ )} +
+ + {/* 안내 텍스트 */} +

+ 암호를 입력하세요 +

+ + {/* 입력 필드 */} +
{password.map((_, i) => (
{ @@ -131,9 +162,10 @@ export default function PasswordInput() { ))}
+ {/* 에러 메시지 */} {hasError && ( -

- 비밀번호가 일치하지 않아요 +

+ 암호가 일치하지 않아요

)}
diff --git a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx new file mode 100644 index 0000000..e875d0b --- /dev/null +++ b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/EditLink.tsx @@ -0,0 +1,297 @@ +"use client"; + +import React, { useState, useEffect, useRef } from "react"; +import { nanoid } from "nanoid"; +import Image from "next/image"; +import { useRouter, useParams } from "next/navigation"; + +interface LinkFieldEditProps { + label: string; + placeholder: string; + value: string[]; + onChange: (value: string[]) => void; + showTooltip?: boolean; + onInfoClick?: () => void; +} + +interface InputField { + id: string; + text: string; + error: string; + isValid: boolean; + isTyping: boolean; + canEdit: boolean; +} + +export default function LinkFieldEdit({ + label, + placeholder, + value, + onChange, +}: LinkFieldEditProps) { + const [inputFields, setInputFields] = useState( + value.length > 0 + ? value.map((val) => ({ + id: nanoid(), + text: val, + error: "", + isValid: true, + isTyping: false, + canEdit: true, + })) + : [ + { + id: nanoid(), + text: "", + error: "", + isValid: false, + isTyping: false, + canEdit: true, + }, + ] + ); + + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + const router = useRouter(); + const { id } = useParams(); + + useEffect(() => { + const validLinks = inputFields + .filter((field) => field.isValid) + .map((field) => field.text); + onChange(validLinks); + }, [inputFields, onChange]); + + const validateLink = async (fieldId: string, url: string, type: string) => { + const endpoint = + type === "북마크 공유 링크" ? "/pings/bookmark" : "/pings/store"; + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}${endpoint}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url }), + } + ); + + if (response.ok) { + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId + ? { ...fieldItem, error: "", isValid: true } + : fieldItem + ) + ); + } else { + const errorResponse = await response.json(); + const errorMessage = + errorResponse?.message || "링크가 유효하지 않아요."; + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId + ? { ...fieldItem, error: errorMessage, isValid: false } + : fieldItem + ) + ); + } + } catch (error) { + console.error("API 요청 실패:", error); + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId + ? { + ...fieldItem, + error: "URL 검증에 실패했습니다.", + isValid: false, + } + : fieldItem + ) + ); + } + }; + + const handleInputChange = (fieldId: string, inputValue: string) => { + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId + ? { ...fieldItem, text: inputValue, isValid: false, isTyping: true } + : fieldItem + ) + ); + + validateLink(fieldId, inputValue, label); + }; + + const handleFocus = (fieldId: string) => { + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId ? { ...fieldItem, isTyping: true } : fieldItem + ) + ); + }; + + const handleBlur = (fieldId: string) => { + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId ? { ...fieldItem, isTyping: false } : fieldItem + ) + ); + }; + + const addInputField = () => { + setInputFields((prevFields) => [ + ...prevFields, + { + id: nanoid(), + text: "", + error: "", + isValid: false, + isTyping: false, + canEdit: true, + }, + ]); + }; + + const clearInput = (fieldId: string) => { + setInputFields((prevFields) => + prevFields.map((fieldItem) => + fieldItem.id === fieldId + ? { ...fieldItem, text: "", error: "", isValid: false } + : fieldItem + ) + ); + }; + + const navigateToTooltipPage = () => { + if (id) { + router.push(`/event-maps/${id}/load-mappin/forms/tooltip`); + } else { + console.error("ID not found for navigation"); + } + }; + + const handleNaverMove = () => { + window.location.href = "https://m.map.naver.com/"; + }; + + const getClassNames = (item: InputField): string => { + if (item.error && !item.isTyping) { + return "border-2 border-[#f73a2c] bg-[#F8F8F8]"; + } + if (item.isValid) { + return "bg-[#EBF4FD] text-[#3a91ea]"; + } + if (item.isTyping) { + return "border-2 border-[#555555] bg-[#F8F8F8]"; + } + return "bg-[#F8F8F8]"; + }; + + return ( +
+ +
+ {inputFields.map((item, index) => ( +
+
+ { + inputRefs.current[index] = el; + }} + type="text" + value={item.text} + onFocus={() => handleFocus(item.id)} + onChange={(e) => handleInputChange(item.id, e.target.value)} + onBlur={() => handleBlur(item.id)} + placeholder={placeholder} + className={`flex-1 bg-transparent outline-none placeholder:text-[#8e8e8e] text-sm font-medium font-['Pretendard'] ${ + item.isValid ? "text-[#3A91EA]" : "" + }`} + /> + {item.text && ( + + )} +
+ {item.error && ( +
+ {item.error} +
+ )} +
+ ))} + +
+
+ ); +} diff --git a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/ExitModal.tsx b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/ExitModal.tsx new file mode 100644 index 0000000..74bd569 --- /dev/null +++ b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/ExitModal.tsx @@ -0,0 +1,66 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; + +interface ExitModalProps { + onCancel: () => void; + onExit: () => void; +} + +const ExitModal: React.FC = function ExitModal({ + onCancel, + onExit, +}) { + return ( +
+
+ {/* Top Section */} +
+
+ Warning Icon +
+
+
+ 저장하지 않고 나갈까요? +
+
+ 이대로 나가면 +
작성하던 내용이 사라져요. +
+
+
+ + {/* Buttons */} +
+ + +
+
+
+ ); +}; + +export default ExitModal; diff --git a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/Form.tsx b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/Form.tsx deleted file mode 100644 index 0773d2d..0000000 --- a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/Form.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; - -import React, { useState, useEffect, FormEvent } from "react"; -import { useRouter, useParams } from "next/navigation"; -import LinkField from "./LinkField"; -import { useUserDataStore } from "../../stores/useUserDataStore"; - -export default function Form() { - const { userData } = useUserDataStore(); // Access userData from zustand - const [mapLinks, setMapLinks] = useState([]); - const [storeLinks, setStoreLinks] = useState([]); - const [isTooltipVisible, setIsTooltipVisible] = useState(true); - const router = useRouter(); - const { id } = useParams(); // Retrieve `id` from the route parameters - - // Load mapLinks and storeLinks from the store when the component mounts - useEffect(() => { - if (userData) { - setMapLinks( - userData.bookmarkUrls?.filter((link) => link.trim() !== "") || [] - ); - setStoreLinks( - userData.storeUrls?.filter((link) => link.trim() !== "") || [] - ); - } - }, [userData]); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - - // Filter out any empty strings just before submitting - const filteredMapLinks = mapLinks.filter((link) => link.trim() !== ""); - const filteredStoreLinks = storeLinks.filter((link) => link.trim() !== ""); - - const formData = { - nonMemberId: userData?.nonMemberId, // Assuming nonMemberId is available in userData - bookmarkUrls: filteredMapLinks, - storeUrls: filteredStoreLinks, - }; - - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE_URL}/nonmembers/pings`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(formData), - } - ); - - if (response.ok) { - console.log("데이터가 성공적으로 전송되었습니다."); - router.push(`/event-maps/${id}`); // Redirect to the specified route on success - } else { - console.error("데이터 전송 실패:", response.status); - } - } catch (error) { - console.error("서버 오류 발생:", error); - } - }; - useEffect(() => { - const hideTooltip = (e: MouseEvent) => { - if ((e.target as HTMLElement).closest(".group") === null) { - setIsTooltipVisible(false); - } - }; - - if (isTooltipVisible) { - window.addEventListener("click", hideTooltip); - } - - return () => { - window.removeEventListener("click", hideTooltip); - }; - }, [isTooltipVisible]); - return ( -
-
- - setMapLinks(links.filter((link) => link.trim() !== "")) - } - showTooltip={isTooltipVisible} - onInfoClick={() => setIsTooltipVisible(true)} - /> - - - setStoreLinks(links.filter((link) => link.trim() !== "")) - } - /> - - - -
- ); -} diff --git a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/LinkField.tsx b/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/LinkField.tsx deleted file mode 100644 index 57a87a8..0000000 --- a/fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/components/LinkField.tsx +++ /dev/null @@ -1,159 +0,0 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { nanoid } from "nanoid"; -import Image from "next/image"; -import { useUserDataStore } from "../../stores/useUserDataStore"; - -interface LinkFieldProps { - label: string; - placeholder: string; - value: string[]; - onChange: (value: string[]) => void; - showTooltip?: boolean; - onInfoClick?: () => void; -} - -export default function LinkField({ - label, - placeholder, - value, - onChange, - showTooltip = true, - onInfoClick, -}: LinkFieldProps) { - const userData = useUserDataStore((state) => state.userData); - const [inputFields, setInputFields] = useState( - value.length > 0 - ? value.map((val) => ({ id: nanoid(), text: val })) - : [{ id: nanoid(), text: "" }] - ); - - // Load data from localStorage when the component mounts - useEffect(() => { - const initialData = - label === "맵핀 모음 링크" ? userData.bookmarkUrls : userData.storeUrls; - - const storedLinks = localStorage.getItem(label); - if (storedLinks) { - const linksArray = JSON.parse(storedLinks); - setInputFields( - linksArray.length > 0 - ? linksArray.map((val: string) => ({ id: nanoid(), text: val })) - : [{ id: nanoid(), text: "" }] - ); - } else { - setInputFields( - initialData.length > 0 - ? initialData.map((val) => ({ id: nanoid(), text: val })) - : [{ id: nanoid(), text: "" }] - ); - } - }, [label, userData]); - - // Save links to localStorage when inputFields change - useEffect(() => { - localStorage.setItem( - label, - JSON.stringify(inputFields.map((field) => field.text)) - ); - }, [inputFields, label]); - - const handleInputChange = (id: string, inputValue: string) => { - const newInputs = inputFields.map((field) => - field.id === id ? { ...field, text: inputValue } : field - ); - setInputFields(newInputs); - onChange( - newInputs.map((field) => field.text).filter((text) => text.trim() !== "") - ); - }; - - const clearInput = (id: string) => { - const newInputs = inputFields.map((field) => - field.id === id ? { ...field, text: "" } : field - ); - setInputFields(newInputs); - onChange( - newInputs.map((field) => field.text).filter((text) => text.trim() !== "") - ); - }; - - const addInputField = () => { - const newField = { id: nanoid(), text: "" }; - const updatedInputs = [...inputFields, newField]; - setInputFields(updatedInputs); - onChange( - updatedInputs - .map((field) => field.text) - .filter((text) => text.trim() !== "") - ); - }; - - return ( -
-