Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions fe/src/app/components/common/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use client";

import React from "react";
import React, { useState } from "react";

export interface ButtonProps {
label: string;
onClick: () => void;
type?: "start" | "next" | "submit" | "default";
className?: string;
disabled?: boolean;
isSubmitting?: boolean;
isFormComplete?: boolean;
}

function Button({
Expand All @@ -16,17 +18,49 @@ function Button({
type = "default",
className = "",
disabled = false,
isSubmitting = false,
isFormComplete = true,
}: ButtonProps) {
const [isActive, setIsActive] = useState(false);
const [isClicked, setIsClicked] = useState(false);

const handleClick = () => {
setIsActive(true);
setIsClicked(true);
onClick();
};

const getButtonStyle = () => {
if (disabled) {
return "bg-[#E4E4E4] text-[#8E8E8E] cursor-not-allowed";
if (isSubmitting) {
return "bg-[#000000] text-white cursor-not-allowed";
}

if (!isFormComplete) {
return "bg-[#E4E4E4] cursor-not-allowed text-[#8E8E8E]";
}

if (isClicked && type !== "start") {
return "bg-[#000000] text-white";
}

if (isActive) {
switch (type) {
case "start":
return "bg-[#A6251B] text-white";
case "next":
case "submit":
case "default":
default:
return "bg-[#1D1D1D] text-white";
}
}

switch (type) {
case "start":
return "bg-[#F73A2C] text-white";
case "next":
case "submit":
return "bg-[#1D1D1D] text-white";
case "default":
default:
return "bg-[#1D1D1D] text-white";
}
Expand All @@ -36,9 +70,9 @@ function Button({
<div className="w-full fixed bottom-[20px] left-0 right-0 flex justify-center">
<button
type={type === "submit" ? "submit" : "button"}
onClick={onClick}
onClick={handleClick}
className={`${getButtonStyle()} ${className} w-[328px] h-[60px] py-[17px] rounded-lg justify-center items-center inline-flex`}
disabled={disabled}
disabled={disabled || isSubmitting}
>
<div className="text-center text-lg font-medium font-['Pretendard'] leading-relaxed">
{label}
Expand Down
60 changes: 33 additions & 27 deletions fe/src/app/event-maps/[id]/[nonMemberId]/load-mappin-edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ export default function LinkEditPage() {
const [isSaveButtonEnabled, setIsSaveButtonEnabled] = useState(false);
const [showExitModal, setShowExitModal] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isClicked, setIsClicked] = useState(false);

const router = useRouter();
const { id, nonMemberId } = useParams();

const isValidLink = (link: string) => {
const urlPattern = /^(https?:\/\/[^\s]+)/g;
return urlPattern.test(link.trim());
};

useEffect(() => {
if (!id || !nonMemberId) {
console.error("누락된 라우트 매개변수:", { id, nonMemberId });
Expand All @@ -41,8 +47,6 @@ export default function LinkEditPage() {
);
setUserName(parsedData.name || "");
setUserData(parsedData);
} else {
console.warn("비회원 ID가 일치하지 않습니다.");
}
} else {
setMapLinks(userData.bookmarkUrls.length ? userData.bookmarkUrls : [""]);
Expand All @@ -54,23 +58,22 @@ export default function LinkEditPage() {
}, [id, nonMemberId, setUserData, userData, router]);

useEffect(() => {
const allMapLinksValid = mapLinks.every((link) => link.trim() !== "");
const allStoreLinksValid = storeLinks.every((link) => link.trim() !== "");
const allMapLinksValid = mapLinks.length > 0 && mapLinks.every(isValidLink);
const allStoreLinksValid =
storeLinks.length > 0 && storeLinks.every(isValidLink);

const isComplete = (allMapLinksValid || allStoreLinksValid) && isChecked;
setIsSaveButtonEnabled(isComplete);
}, [mapLinks, storeLinks, isChecked]);

const handleSubmit = async (e?: React.FormEvent) => {
if (e) e.preventDefault();

const filteredMapLinks = mapLinks.filter((link) => link.trim() !== "");
const filteredStoreLinks = storeLinks.filter((link) => link.trim() !== "");

const updatedData = {
nonMemberId: userData.nonMemberId || Number(nonMemberId),
name: userName,
bookmarkUrls: filteredMapLinks,
storeUrls: filteredStoreLinks,
bookmarkUrls: mapLinks,
storeUrls: storeLinks,
};

try {
Expand All @@ -91,18 +94,18 @@ export default function LinkEditPage() {

setUserData(updatedData);
localStorage.setItem("userData", JSON.stringify(updatedData));

router.push(`/event-maps/${id}`);
} catch (error) {
console.error("API 호출 오류:", error);
}
};

const handleBack = () => {
setShowExitModal(true); // ExitModal을 표시
setShowExitModal(true);
};

const handleExitConfirm = () => {
// 데이터 삭제 후 뒤로가기
const handleExit = () => {
localStorage.removeItem("userData");
localStorage.removeItem("formData");
router.push(`/event-maps/${id}`);
Expand All @@ -116,17 +119,21 @@ export default function LinkEditPage() {
return <div />;
}

// Button class 설정
let buttonClass =
"w-fixl h-[60px] py-[17px] rounded-lg text-base font-medium text-white";

if (isSaveButtonEnabled) {
buttonClass += isClicked ? " bg-[#000000]" : " bg-[#1D1D1D]";
} else {
buttonClass += " bg-[#e0e0e0] cursor-not-allowed";
}

return (
<div className="w-[360px] h-screen bg-white mx-auto flex flex-col">
<Navigation onBack={handleBack} />

<div
className="flex-1 px-4 mt-[75px] overflow-y-auto"
style={{
maxHeight: "calc(100vh - 60px)",
paddingBottom: "120px",
}}
>
<div className="flex-1 px-[16px] w-full overflow-y-auto pb-[100px]">
<div className="mt-[72px] mb-[36px]" />
{userName && (
<div className="text-darkGray text-title-md">
{userName}님의 맵핀 모음이에요
Expand Down Expand Up @@ -166,17 +173,16 @@ export default function LinkEditPage() {
>
<Button
label="저장"
className={`w-[328px] h-[60px] py-[17px] rounded-lg text-lg font-['Pretendard'] font-medium bg-[#F73A2C] text-white ${
isSaveButtonEnabled
? "bg-black"
: "bg-[#e0e0e0] pointer-events-none"
}`}
onClick={handleSubmit}
className={buttonClass}
onClick={() => {
setIsClicked(true);
handleSubmit();
}}
disabled={!isSaveButtonEnabled}
/>
</div>
{showExitModal && (
<ExitModal onCancel={handleCancel} onExit={handleExitConfirm} />
<ExitModal onCancel={handleCancel} onExit={handleExit} />
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function BottomSheet() {

return (
<div
className={`fixed bottom-0 left-1/2 transform -translate-x-1/2 w-full h-[556px] bg-[#1d1d1d] rounded-t-[20px] transition-transform z-[1000] duration-700 ${
className={`fixed bottom-0 left-1/2 transform -translate-x-1/2 w-[400px] min-w-[400px] h-[556px] bg-[#1d1d1d] rounded-t-[20px] transition-transform z-[1000] duration-700 ${
isVisible ? "translate-y-0" : "translate-y-full"
}`}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { nanoid } from "nanoid";
import Image from "next/image";
import { useRouter, useParams } from "next/navigation";

interface LinkFieldEditProps {
interface LinkFieldProps {
label: string;
placeholder: string;
value: string[];
onChange: (value: string[]) => void;
showTooltip?: boolean;
onInfoClick?: () => void;
}

interface InputField {
Expand All @@ -18,14 +20,15 @@ interface InputField {
error: string;
isValid: boolean;
isTyping: boolean;
canEdit: boolean;
}

export default function LinkFieldEdit({
export default function LinkField({
label,
placeholder,
value,
onChange,
}: LinkFieldEditProps) {
}: LinkFieldProps) {
const [inputFields, setInputFields] = useState<InputField[]>(
value.length > 0
? value.map((val) => ({
Expand All @@ -34,6 +37,7 @@ export default function LinkFieldEdit({
error: "",
isValid: true,
isTyping: false,
canEdit: true,
}))
: [
{
Expand All @@ -42,6 +46,7 @@ export default function LinkFieldEdit({
error: "",
isValid: false,
isTyping: false,
canEdit: true,
},
]
);
Expand All @@ -50,18 +55,18 @@ export default function LinkFieldEdit({
const router = useRouter();
const { id } = useParams();

const cleanURL = (url: string): string => {
const match = url.match(/https?:\/\/[^\s]+/);
return match ? match[0].trim() : "";
};

useEffect(() => {
const validLinks = inputFields
.filter((field) => field.isValid)
.map((field) => field.text);
onChange(validLinks);
}, [inputFields, onChange]);

const cleanURL = (url: string): string => {
const match = url.match(/https?:\/\/[^\s]+/);
return match ? match[0].trim() : "";
};

const validateLink = async (fieldId: string, url: string, type: string) => {
const endpoint =
type === "북마크 공유 링크" ? "/pings/bookmark" : "/pings/store";
Expand Down Expand Up @@ -113,17 +118,16 @@ export default function LinkFieldEdit({
const handlePasteFromClipboard = async (fieldId: string) => {
try {
const clipboardText = await navigator.clipboard.readText();
const cleanedValue = cleanURL(clipboardText);
if (cleanedValue) {
if (clipboardText.trim()) {
const cleanedText = cleanURL(clipboardText);
setInputFields((prevFields) =>
prevFields.map((fieldItem) =>
fieldItem.id === fieldId
? { ...fieldItem, text: cleanedValue, isValid: false }
? { ...fieldItem, text: cleanedText, isValid: false }
: fieldItem
)
);

validateLink(fieldId, cleanedValue, label);
validateLink(fieldId, cleanedText, label);
}
} catch (error) {
console.error("클립보드에서 텍스트를 읽는 데 실패했습니다:", error);
Expand All @@ -139,10 +143,7 @@ export default function LinkFieldEdit({
: fieldItem
)
);

if (cleanedValue) {
validateLink(fieldId, cleanedValue, label);
}
validateLink(fieldId, cleanedValue, label);
};

const handleFocus = (fieldId: string) => {
Expand Down Expand Up @@ -172,6 +173,7 @@ export default function LinkFieldEdit({
error: "",
isValid: false,
isTyping: false,
canEdit: true,
},
]);
};
Expand Down Expand Up @@ -248,9 +250,7 @@ export default function LinkFieldEdit({
{inputFields.map((item, index) => (
<div
key={item.id}
className={`relative w-full ${
index === inputFields.length - 1 ? "" : "mb-[16px]"
}`}
className={`relative w-full ${index === inputFields.length - 1 ? "" : "mb-[16px]"}`}
>
<div
className={`w-[296px] h-[52px] px-4 py-3.5 pr-[40px] rounded-md inline-flex relative ${getClassNames(
Expand Down
Loading