-
회원정보 수정
-
-
-
-
-
+
+ {!checkKey &&
}
+ {getUserInfoIsLoading ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/src/app/update-my-password/page.tsx b/src/app/update-my-password/page.tsx
index 7ce5e51..98cef9d 100644
--- a/src/app/update-my-password/page.tsx
+++ b/src/app/update-my-password/page.tsx
@@ -1,77 +1,82 @@
-'use client';
+"use client";
-import React, { useState } from 'react';
-import { useRouter } from 'next/navigation';
-
-import { url } from '../store';
+import { Button, Input } from "@nextui-org/react";
+import { useForm } from "react-hook-form";
+import { ErrorMessage } from "@hookform/error-message";
+import { passwordV, passwordConfirmV } from "../validationRules";
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import CheckPasswordModal from "../components/CheckPasswordModal";
+import useUpdateUserPassword from "@/hooks/userHooks/useUpdateUserPassword";
+import { UpdateUserPassword } from "@/types/userTypes/updateInfo";
export default function UpdateMyPassword() {
- const router = useRouter();
+ const router = useRouter();
+ const [checkKey, setCheckKey] = useState(false);
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm
({
+ mode: "onChange", // 입력 값이 변경될 때마다 유효성 검사
+ reValidateMode: "onChange", // 입력 값이 변경될 때마다 유효성 검사
+ });
+
+ const { mutate, isPending } = useUpdateUserPassword();
+
+ const onSubmit = (updateData: UpdateUserPassword) => {
+ if (!updateData) return;
+
+ mutate(updateData, {
+ onSuccess: () => {
+ router.back();
+ },
+ });
+ };
- const [updateData, setUpdateData] = useState({
- newPassword: "",
- password: ""
- })
-
- const handleChange = (e: React.ChangeEvent) => {
- const { name, value } = e.target;
- setUpdateData({
- ...updateData,
- [name]: value
- });
- };
-
- // jwt 만료 되었을 때 설정을 안해줬습니다! api 요청에서 리팩토링이 필요해 보여서 jwt 토큰이 만료 안된 상황만 처리했습니다.
- async function patchMember() {
- try {
- const response = await fetch(`${url}/member/authed/password`, {
- method: "PATCH",
- headers: {
- 'Authorization': `${localStorage.getItem("accessToken")}`,
- 'Content-Type': 'application/json' // 데이터 형식 설정
- },
- body: JSON.stringify(updateData)})
- if (response.status === 200) {
- alert("비밀번호가 수정되었습니다")
- router.push('/check-my-info'); // 로그인 페이지로 리다이렉트
- }
- else if (response.status === 400) {
- alert("입력값을 확인해주세요");
- }
- } catch (error) {
- alert(error);
- }
- }
-
- return (
-
- );
+ const errorStyle = "text-sm text-red-500 font-semibold";
+ return (
+
+ );
}
diff --git a/src/app/update/[id]/page.tsx b/src/app/update/[id]/page.tsx
index 3feec59..113d81e 100644
--- a/src/app/update/[id]/page.tsx
+++ b/src/app/update/[id]/page.tsx
@@ -1,6 +1,6 @@
-'use client';
+"use client";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import Swal from "sweetalert2";
import { useForm } from "react-hook-form";
@@ -12,6 +12,21 @@ import PlaceSearch from "@/app/components/PlaceSearch";
import LoadingSpinner from "@/app/components/Loading";
import ErrorShow from "@/app/components/Error";
import QuillEditor from "@/app/components/Quill";
+import { Button } from "@nextui-org/react";
+import PostModal from "@/app/components/PostModal";
+import { FaAngleDoubleDown, FaAngleDoubleUp } from "react-icons/fa";
+import fromUnixTime from "@/util/fromUnixTime";
+import { FaCircleArrowDown } from "react-icons/fa6";
+import formatTravelTime from "@/util/formatTravelTime";
+import formatStartTime from "@/util/formatStartTime";
+import useGetPlanner from "@/hooks/calender/useGetPlanner";
+import useConfirmPageLeave from "@/util/useConfirmPageLeave";
+import { useQueryClient } from "@tanstack/react-query";
+import usePostImages from "@/hooks/usePostImages";
+import Image from "next/image";
+import { MdDeleteForever } from "react-icons/md";
+import useDeleteImage from "@/hooks/useDeleteImage";
+import useAccessCheck from "@/hooks/TokenHooks/useAccessCheck";
type SetLocalData = {
setLocation: React.Dispatch>;
@@ -20,16 +35,48 @@ type SetLocalData = {
setLongitude: React.Dispatch>;
};
+type LocationInfo = {
+ id: number;
+ place: string;
+ address: string;
+ transportation: string;
+ transportationNote: string;
+ phoneNumber: string;
+ memo: string;
+ unixTime: number;
+ travelTime: number;
+};
+
+interface Image {
+ url: string;
+}
+
const Update = ({ params }: { params: ParamsId }) => {
+ const queryClient = useQueryClient();
const router = useRouter();
const { id } = params;
const { data, isLoading, isError, error } = usePost(id);
const [location, setLocation] = useState(data?.locationName || "");
- const [formattedAddress, setFormattedAddress] = useState(data?.formattedAddress || "");
+ const [formattedAddress, setFormattedAddress] = useState(
+ data?.formattedAddress || ""
+ );
const [latitude, setLatitude] = useState(data?.latitude || 0);
const [longitude, setLongitude] = useState(data?.longitude || 0);
const updateMutation = useUpdatePost(id);
const [html, setHtml] = useState(data?.body || "");
+ const [showModal, setShowModal] = useState(false);
+ const [plannerId, setPlannerId] = useState("");
+ const { data: plannerData } = useGetPlanner(plannerId, !!plannerId);
+ const [calendarView, setCalendarView] = useState(false);
+ const { data: accessCheck, isLoading: accessCheckLoading } = useAccessCheck();
+ const [images, setImages] = useState([]);
+ const imageRef = useRef(null);
+ const { mutate: postImages, isPending: postImagesIsPending } =
+ usePostImages();
+ const { mutate: deleteImage, isPending: deleteImageIsPending } =
+ useDeleteImage();
+
+ useConfirmPageLeave();
const {
register,
@@ -44,15 +91,32 @@ const Update = ({ params }: { params: ParamsId }) => {
});
useEffect(() => {
- if(data?.body) {
+ if (data?.planner !== 0) {
+ setPlannerId(data?.planner);
+ }
+ }, [data?.planner]);
+
+ useEffect(() => {
+ if (data?.body) {
setHtml(data.body);
}
+ if (data?.images.length > 0) {
+ data?.images.map((image: string) => {
+ setImages((prev) => {
+ if (!prev.some((img) => img.url === image)) {
+ return [...prev, { url: image }];
+ }
+ return prev;
+ });
+ });
+ }
+
reset({
title: data?.title,
body: data?.body,
});
- }, [data, reset]);
+ }, [data, reset]);
const setLocalData: SetLocalData = {
setLocation,
@@ -62,17 +126,28 @@ const Update = ({ params }: { params: ParamsId }) => {
};
const onSubmitForm = (formData: WriteUpdateType) => {
+ // submit을 누르면 기존 게시글 이미지 배열과 최종 state images 배열을 비교하여 삭제된 이미지를 찾아 삭제하는 로직 추가하여야 함
if (
formData.title === defaultValues?.title &&
- html === defaultValues?.body &&
- data.locationName === location
+ html === defaultValues?.body &&
+ data.locationName === location &&
+ data.planner === plannerId
) {
Swal.fire({
- icon: "warning",
- title: "게시글에 변경사항이 없습니다",
- showConfirmButton: false,
- timer: 1000,
+ icon: "warning",
+ title: "게시글에 변경사항이 없습니다",
+ showConfirmButton: false,
+ timer: 1000,
+ });
+ return;
+ }
+ if (!accessCheck && !accessCheckLoading) {
+ Swal.fire({
+ icon: "error",
+ title: "로그인 필요",
+ text: "로그인이 필요한 서비스입니다. 로그인 페이지로 이동합니다",
});
+ router.push(`/login`);
return;
}
const postData = {
@@ -82,18 +157,71 @@ const Update = ({ params }: { params: ParamsId }) => {
formattedAddress,
latitude,
longitude,
+ plannerId: Number(plannerId),
+ images: images.map((img) => img.url),
};
updateMutation.mutate(postData);
};
const handleCancel = () => router.back();
+ const handleImageChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ if (
+ !file.type.includes("png") &&
+ !file.type.includes("jpg") &&
+ !file.type.includes("jpeg")
+ ) {
+ Swal.fire({
+ icon: "error",
+ title: "이미지 형식 오류",
+ text: "png 또는 jpg 형식의 이미지만 업로드 가능합니다.",
+ });
+ return;
+ }
+
+ if (file.size > 2 * 1024 * 1024) {
+ Swal.fire({
+ icon: "error",
+ title: "이미지 용량 초과",
+ text: "이미지 용량은 2MB 이하여야 합니다.",
+ });
+ return;
+ }
+ postImages(file, {
+ onSuccess: (data) => {
+ setImages((prev) => [...prev, { url: data.data.url }]);
+ },
+ });
+ };
+
+ const handleImageClick = () => {
+ if (imageRef.current) {
+ imageRef.current.click();
+ }
+ };
+
+ const handleDeleteImage = (url: string) => {
+ setImages((prev) => prev.filter((img) => img.url !== url));
+ // deleteImage(url);
+ };
+
+ const btnStyle =
+ "p-1 px-3 sm:px-6 sm:p-2 border text-gray-900 rounded-lg text-sm sm:text-base";
return (
-
- {isError || updateMutation?.isError ?
: null}
- {isLoading || updateMutation?.isPending || isError || updateMutation?.isError ? null :
}
-
+
+ )}
+
);
};
-export default Update;
\ No newline at end of file
+export default Update;
diff --git a/src/app/validationRules.ts b/src/app/validationRules.ts
index 972e5ff..cf8530b 100644
--- a/src/app/validationRules.ts
+++ b/src/app/validationRules.ts
@@ -1,62 +1,66 @@
export const usernameV = {
- required: '사용자명을 입력하세요',
+ required: "사용자명을 입력하세요",
minLength: {
value: 5,
- message: '사용자명은 5글자 이상이어야 합니다.',
+ message: "사용자명은 5글자 이상이어야 합니다.",
},
maxLength: {
value: 30,
- message: '사용자명은 30글자 이하여야 합니다.',
+ message: "사용자명은 30글자 이하여야 합니다.",
},
};
export const nicknameV = {
- required: '닉네임을 입력하세요',
+ required: "닉네임을 입력하세요",
minLength: {
value: 1,
- message: '닉네임은 1글자 이상이어야 합니다.',
+ message: "닉네임은 1글자 이상이어야 합니다.",
},
maxLength: {
value: 10,
- message: '닉네임은 10글자 이하여야 합니다.',
+ message: "닉네임은 10글자 이하여야 합니다.",
},
};
export const emailV = {
- required: '이메일을 입력하세요',
+ required: "이메일을 입력하세요",
pattern: {
value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
- message: '이메일 형식이 아닙니다.',
+ message: "이메일 형식이 아닙니다.",
},
};
export const emailConfirmV = {
- required: '이메일 인증번호를 입력하세요'
-}
+ required: "이메일 인증번호를 입력하세요",
+};
export const passwordV = {
- required: '비밀번호를 입력하세요',
+ required: "비밀번호를 입력하세요",
minLength: {
value: 8,
- message: '비밀번호는 8글자 이상이어야 하며, 영문 및 숫자를 포함해야 합니다.',
- }
+ message: "8글자 이상, 영문 및 숫자를 포함해야 합니다.",
+ },
+ maxLength: {
+ value: 20,
+ message: "20글자 이하, 영문 및 숫자를 포함해야 합니다.",
+ },
};
export const passwordConfirmV = {
- required: '비밀번호를 다시 입력하세요',
+ required: "비밀번호를 다시 입력하세요",
validate: (value: string, values: any) => {
- return value === values.password || '비밀번호가 일치하지 않습니다.';
+ return value === values.password || "비밀번호가 일치하지 않습니다.";
},
};
export const ageV = {
- required: '나이를 입력하세요',
+ required: "나이를 입력하세요",
min: {
value: 1,
- message: '나이는 1세 이상이어야 합니다.',
+ message: "나이는 1세 이상이어야 합니다.",
},
max: {
value: 100,
- message: '100세 이상은 가입할 수 없습니다.',
+ message: "100세 이상은 가입할 수 없습니다.",
},
-};
\ No newline at end of file
+};
diff --git a/src/app/write/page.tsx b/src/app/write/page.tsx
index 95015c1..f5c7d46 100644
--- a/src/app/write/page.tsx
+++ b/src/app/write/page.tsx
@@ -1,27 +1,108 @@
-'use client';
+"use client";
import useWritePost from "@/hooks/useWritePost";
import { WriteUpdateType } from "@/types/board";
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import PlaceSearch from "../components/PlaceSearch";
import { useRouter } from "next/navigation";
import LoadingSpinner from "../components/Loading";
import ErrorShow from "../components/Error";
-import 'react-quill/dist/quill.snow.css';
+import "react-quill/dist/quill.snow.css";
import QuillEditor from "../components/Quill";
import Swal from "sweetalert2";
import { SetLocalData } from "@/types/write";
+import { Button } from "@nextui-org/react";
+import useGetPlanner from "@/hooks/calender/useGetPlanner";
+import { FaAngleDoubleDown, FaAngleDoubleUp } from "react-icons/fa";
+import fromUnixTime from "@/util/fromUnixTime";
+import formatTravelTime from "@/util/formatTravelTime";
+import formatStartTime from "@/util/formatStartTime";
+import { FaCircleArrowDown } from "react-icons/fa6";
+import PostModal from "../components/PostModal";
+import useConfirmPageLeave from "@/util/useConfirmPageLeave";
+import Image from "next/image";
+import usePostImages from "@/hooks/usePostImages";
+import { MdDeleteForever } from "react-icons/md";
+import useDeleteImage from "@/hooks/useDeleteImage";
+import useAccessCheck from "@/hooks/TokenHooks/useAccessCheck";
+
+interface LocationInfo {
+ address: string;
+ id: number;
+ memo: string;
+ place: string;
+ plannerId: number;
+ travelTime: number;
+ unixTime: number;
+ transportation: string;
+ transportationNote: string;
+ phoneNumber: string;
+}
+
+interface Image {
+ url: string;
+}
const Write = () => {
const { register, handleSubmit } = useForm
();
const writeMutation = useWritePost();
- const [location, setLocation] = useState('');
- const [formattedAddress, setFormattedAddress] = useState('');
+ const [location, setLocation] = useState("");
+ const [formattedAddress, setFormattedAddress] = useState("");
const [latitude, setLatitude] = useState(0);
const [longitude, setLongitude] = useState(0);
- const [html, setHtml] = useState('');
+ const [html, setHtml] = useState("");
const router = useRouter();
+ const localStoragePlannerId =
+ typeof window !== "undefined" ? localStorage.getItem("plannerId") : null;
+ const [plannerId, setPlannerId] = useState("");
+ const { data, isLoading } = useGetPlanner(plannerId, !!plannerId);
+ const [calendarView, setCalendarView] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+ const {
+ data: cacheData,
+ isLoading: accessIsLoading,
+ refetch,
+ } = useAccessCheck();
+ const imageRef = useRef(null);
+ const [images, setImages] = useState([]);
+ const { mutate: postImages, isPending: postImagesIsPending } =
+ usePostImages();
+ const { mutate: deleteImage, isPending: deleteImageIsPending } =
+ useDeleteImage();
+
+ useConfirmPageLeave();
+
+ useEffect(() => {
+ // 모달 켜졌을 때 배경 스크롤 막기
+ if (showModal) {
+ document.body.style.overflow = "hidden";
+ document.body.style.touchAction = "none";
+ } else {
+ document.body.style.touchAction = "auto";
+ document.body.style.overflow = "auto";
+ }
+ }, [showModal]);
+
+ useEffect(() => {
+ refetch();
+ if (!cacheData && !accessIsLoading) {
+ Swal.fire({
+ icon: "error",
+ title: "로그인 필요",
+ text: "로그인이 필요한 서비스입니다. 로그인 페이지로 이동합니다",
+ });
+ router.push(`/login`);
+ return;
+ }
+ if (localStoragePlannerId) {
+ setPlannerId(localStoragePlannerId);
+ }
+
+ return () => {
+ localStorage.removeItem("plannerId");
+ };
+ }, []);
const setLocalData: SetLocalData = {
setLocation,
@@ -31,70 +112,335 @@ const Write = () => {
};
const onSubmitForm = (title: WriteUpdateType) => {
- if (title.title === '' || html === '') {
+ if (title.title === "" || html === "" || formattedAddress === "") {
Swal.fire({
- icon: 'error',
- title: '제목과 본문을 입력해주세요.',
+ icon: "error",
+ title: "제목/본문을 입력하고, 지역을 선택해주세요.",
});
return;
- };
+ }
+ if (!cacheData) {
+ Swal.fire({
+ icon: "error",
+ title: "로그인 필요",
+ text: "로그인이 필요한 서비스입니다. 로그인 페이지로 이동합니다",
+ });
+ router.push(`/login`);
+ return;
+ }
+ const imageUrls = images.map((image) => image.url);
const locationData = {
title: title.title,
body: html,
locationName: location,
formattedAddress,
latitude,
- longitude
- }
+ longitude,
+ plannerId: data?.id || 0,
+ images: imageUrls,
+ };
writeMutation.mutate(locationData);
};
const handleCancel = () => router.back();
+ const handleShowModal = () => {
+ if (!cacheData) {
+ Swal.fire({
+ icon: "error",
+ title: "로그인 필요",
+ text: "로그인이 필요한 서비스입니다. 로그인 페이지로 이동합니다",
+ });
+ router.push(`/login`);
+ return;
+ }
+ setShowModal(true);
+ };
+
+ const handleImageClick = () => {
+ if (imageRef.current) {
+ imageRef.current.click();
+ }
+ };
+
+ const handleImageChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ if (
+ !file.type.includes("png") &&
+ !file.type.includes("jpg") &&
+ !file.type.includes("jpeg")
+ ) {
+ Swal.fire({
+ icon: "error",
+ title: "이미지 형식 오류",
+ text: "png 또는 jpg 형식의 이미지만 업로드 가능합니다.",
+ });
+ return;
+ }
+
+ if (file.size > 2 * 1024 * 1024) {
+ Swal.fire({
+ icon: "error",
+ title: "이미지 용량 초과",
+ text: "이미지 용량은 2MB 이하여야 합니다.",
+ });
+ return;
+ }
+ postImages(file, {
+ onSuccess: (data) => {
+ setImages((prev) => [...prev, { url: data.data.url }]);
+ },
+ });
+ };
+
+ const handleDeleteImage = (url: string) => {
+ setImages((prev) => prev.filter((img) => img.url !== url));
+ deleteImage(url);
+ };
+
+ const btnStyle =
+ "p-1 px-3 sm:p-2 sm:px-6 border text-gray-900 hover:bg-gray-100 rounded-lg text-sm sm:text-base";
return (
<>
-
-
- {writeMutation.isError &&
}
-