@@ -60,13 +60,13 @@ const InformationViewer = ({ informationId, data }: Props) => {
{data.title}
-
+
diff --git a/src/components/mypage/MyPageUserImage.tsx b/src/components/mypage/MyPageUserImage.tsx
index 6d555b3c..d176d574 100644
--- a/src/components/mypage/MyPageUserImage.tsx
+++ b/src/components/mypage/MyPageUserImage.tsx
@@ -1,4 +1,5 @@
import { dragAndDropProps } from "@/types/DragAndDrop";
+import { ModalState } from "@/types/ModalState";
import Image from "next/image";
import UserImage from "../auth/UserImage";
import CropperComponent from "../common/cropper/CropperComponent";
@@ -9,7 +10,7 @@ interface IMyPageUserImage {
imageBase64Data: string;
userImageUrl: string;
userSex: string | null;
- isModalOpen: boolean;
+ modalState: ModalState,
closeCropModal: () => void;
onChangeImageUrl: (_: string) => void;
deleteImage: () => void;
@@ -71,9 +72,7 @@ const MyPageUserImage = (props: IMyPageUserImage) => {
void;
- userDeleteText: string;
- userDeleteHandler: () => void;
}
const NICKNAME_LENGTH = 30;
@@ -30,49 +28,6 @@ const MyProfile = (props: IMyProfileProps) => {
userImageUrl={props.userInfo.userImage.address}
userSex={props.userInfo.sex}
/>
-
-
-
-
- 1. 회원 탈퇴 후에는 복구가 불가능하며, 현재 진행 중인 모임
- 서비스나 여행일기 서비스 이용 내역이 있을 경우, 관련 정보도 함께
- 삭제됩니다.
-
-
- 2. 정보 게시글은 삭제되지 않지만 사용자와 관련된 내용은 전부
- 비공개 처리되고 이후에는 수정이나 삭제는 불가능해집니다.
-
-
3. 필요한 정보는 회원탈퇴하기전에 따로 보관해주시기 바랍니다.
-
-
- 회원탈퇴를 하겠습니다.
- 라고 입력해주세요.
-
-
props.changeUserDeleteText(e.target.value)}
- />
-
-
-
@@ -165,25 +120,37 @@ const MyProfile = (props: IMyProfileProps) => {
{props.userInfo.userImage.createdDate}
-
- {props.userInfo.provider == "kakao" && (
-
- )}
- {props.userInfo.provider == "google" && (
+ {props.userInfo.provider == "kakao" &&
+
+
+
+ }
+ {/* {props.userInfo.provider == "google" &&
+
- )}
-
+ />
+
+ } */}
+ {props.userInfo.provider == "naver" &&
+
+
+
+ }
@@ -197,6 +164,11 @@ const MyProfile = (props: IMyProfileProps) => {
회원탈퇴
+
+
+
);
};
diff --git a/src/components/mypage/UserDeleteConfirmModal.tsx b/src/components/mypage/UserDeleteConfirmModal.tsx
new file mode 100644
index 00000000..c52396be
--- /dev/null
+++ b/src/components/mypage/UserDeleteConfirmModal.tsx
@@ -0,0 +1,90 @@
+import useAuthStore from "@/store/authStore";
+import useToastifyStore from "@/store/toastifyStore";
+import { IModalComponent } from "@/types/ModalState";
+import { userResponseDto } from "@/types/UserDto";
+import { fetchWithAuth } from "@/utils/fetchWithAuth";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import ModalTemplate from "../common/modal/ModalTemplate";
+
+interface IUserDeleteConfirmModal extends IModalComponent {
+ userInfo: userResponseDto
+}
+const UserDeleteConfirmModal = (props: IUserDeleteConfirmModal) => {
+ const [userDeleteText, setUserDeleteText] = useState("");
+ const authStore = useAuthStore();
+ const toastifyStore = useToastifyStore();
+ const router = useRouter();
+
+ const changeUserDeleteText = (value: string) => {
+ setUserDeleteText(value);
+ };
+
+ const userDeleteHandler = async () => {
+ const response = await fetchWithAuth(
+ `/api/auth/user?type=${props.userInfo.provider}`,
+ {
+ method: "DELETE",
+ "Content-Type": "application/json",
+ },
+ );
+
+ if (response.ok) {
+ authStore.initialize();
+ await toastifyStore.setToastify({
+ type: "success",
+ message: "회원탈퇴에 성공했습니다.",
+ });
+ props.closeModal && props.closeModal();
+ setTimeout(() => {
+ router.replace("/");
+ }, 300);
+ } else {
+ toastifyStore.setToastify({
+ type: "error",
+ message: "회원탈퇴에 실패했습니다.",
+ });
+ }
+ };
+
+ return (
+
+ {props.closeButtonComponent}
+
+
+ 1. 회원 탈퇴 후에는 복구가 불가능하며, 현재 진행 중인 모임
+ 서비스나 여행일기 서비스 이용 내역이 있을 경우, 관련 정보도 함께
+ 삭제됩니다.
+
+
+ 2. 정보 게시글은 삭제되지 않지만 사용자와 관련된 내용은 전부
+ 비공개 처리되고 이후에는 수정이나 삭제는 불가능해집니다.
+
+
3. 필요한 정보는 회원탈퇴하기전에 따로 보관해주시기 바랍니다.
+
+
+ 회원탈퇴를 하겠습니다.
+ 라고 입력해주세요.
+
+ changeUserDeleteText(e.target.value)}
+ />
+
+
+ );
+};
+export default UserDeleteConfirmModal;
\ No newline at end of file
diff --git a/src/components/skeleton/informations/detail/CommentItemSkeleton.tsx b/src/components/skeleton/informations/detail/CommentItemSkeleton.tsx
new file mode 100644
index 00000000..1e34671e
--- /dev/null
+++ b/src/components/skeleton/informations/detail/CommentItemSkeleton.tsx
@@ -0,0 +1,19 @@
+const CommentItemSkeleton = () => {
+ return (
+
+ );
+};
+
+export default CommentItemSkeleton;
diff --git a/src/containers/auth/AuthKaKaoContainer.tsx b/src/containers/auth/AuthKaKaoContainer.tsx
index f0a34e01..63ec3d6a 100644
--- a/src/containers/auth/AuthKaKaoContainer.tsx
+++ b/src/containers/auth/AuthKaKaoContainer.tsx
@@ -1,20 +1,96 @@
"use client";
+import AddUserInformationInitForm from "@/components/auth/AddUserInformationInitForm";
import AuthLoading from "@/components/auth/AuthLoading";
+import { AddUserInformationFormSchema } from "@/lib/zod/schema/AddUserInformationFormSchema";
import useAuthStore from "@/store/authStore";
+import useToastifyStore from "@/store/toastifyStore";
import UrlQueryStringToObject from "@/utils/UrlQueryStringToObject";
+import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
+import { FormProvider, useForm } from "react-hook-form";
const AuthKaKaoContainer = () => {
const router = useRouter();
const authStore = useAuthStore();
+ const [loading, setLoading] = useState(true);
+
+ const methods = useForm({
+ resolver: zodResolver(AddUserInformationFormSchema),
+ defaultValues: {
+ name: "",
+ age: 0,
+ sex: "",
+ isCheckTerm: false,
+ isCheckPrivacy: false,
+ },
+ });
+
+ const handleSubmit = async (isAgree: boolean) => {
+ setLoading(true);
+
+ const requestData = isAgree ? {
+ name: methods.getValues("name"),
+ age: methods.getValues("age"),
+ sex: methods.getValues("sex"),
+ termConditionAgreement: methods.getValues("isCheckTerm"),
+ privacyPolicyAgreement: methods.getValues("isCheckPrivacy")
+ } : {
+ termConditionAgreement: methods.getValues("isCheckTerm"),
+ privacyPolicyAgreement: methods.getValues("isCheckPrivacy")
+ };
+
+ try {
+ const response = await fetch(
+ isAgree ? "/api/auth/user/info/agree" : "/api/auth/user/info/disagree",
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ cache: "no-store",
+ body: JSON.stringify(requestData)
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error("Failed to login");
+ }
+
+ // 액세스 토큰을 이용해서 사용자 정보 조회
+ const userDataResponse = await fetch("/api/auth/user");
+ if (userDataResponse.status == 200) {
+ const userData = await userDataResponse.json();
+ authStore.setUser(userData);
+ router.push("/");
+ } else {
+ throw new Error("Failed to fetch user data");
+ }
+ } catch (error) {
+ console.error("로그인 실패", error);
+ router.push("/auth/signin");
+ }
+ };
+
+ const handleHomeButtonClick = async () => {
+ await fetch("/api/auth/logout", {method: "POST"});
+ router.push("/");
+ };
+
+ // 숫자만 입력되게 필터링
+ const handleInputChange = (e: React.ChangeEvent
) => {
+ const { value } = e.target;
+ if (!/^\d*$/.test(value)) return;
+ methods.setValue("age", Number(value));
+ methods.trigger();
+ };
useEffect(() => {
const _queryStringObject = UrlQueryStringToObject<{
[key: string]: string;
}>(window.location.href);
- const kakaoLogin = async () => {
+ const kakaoInitLogin = async () => {
try {
const response = await fetch(
`/api/auth/kakao/getToken?code=${_queryStringObject?.code}`,
@@ -27,10 +103,15 @@ const AuthKaKaoContainer = () => {
credentials: "include",
},
);
-
- if (response.status !== 200) {
+
+ if (!response.ok) {
throw new Error("Failed to login");
}
+ const data = await response.json();
+ if (data == "PENDING") {
+ setLoading(false);
+ return;
+ }
// 액세스 토큰을 이용해서 사용자 정보 조회
const userDataResponse = await fetch("/api/auth/user");
@@ -47,11 +128,24 @@ const AuthKaKaoContainer = () => {
}
};
- kakaoLogin();
+ kakaoInitLogin();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- return ;
+ return (
+ <>
+ {
+ loading ? :
+
+
+
+ }
+ >
+ );
};
export default AuthKaKaoContainer;
diff --git a/src/containers/auth/AuthNaverContainer.tsx b/src/containers/auth/AuthNaverContainer.tsx
new file mode 100644
index 00000000..c15c5916
--- /dev/null
+++ b/src/containers/auth/AuthNaverContainer.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import AuthLoading from "@/components/auth/AuthLoading";
+import useAuthStore from "@/store/authStore";
+import { useRouter } from "next/navigation";
+
+const AuthNaverContainer = () => {
+ const router = useRouter();
+ const authStore = useAuthStore();
+
+ const url = new URL(window.location.href);
+ const naverLogin = async () => {
+ try {
+ const response = await fetch(
+ `/api/auth/naver/getToken?code=${url.searchParams.get("code")}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ cache: "no-store",
+ credentials: "include",
+ },
+ );
+
+
+ if (!response.ok) {
+ throw new Error("Failed to login");
+ }
+
+ // 액세스 토큰을 이용해서 사용자 정보 조회
+ const userDataResponse = await fetch("/api/auth/user");
+ if (userDataResponse.status == 200) {
+ const userData = await userDataResponse.json();
+ authStore.setUser(userData);
+ router.push("/");
+ } else {
+ throw new Error("Failed to fetch user data");
+ }
+ } catch (error) {
+ console.error("로그인 실패", error);
+ router.push("/auth/signin");
+ }
+ };
+
+ naverLogin();
+
+ return ;
+};
+
+export default AuthNaverContainer;
diff --git a/src/containers/common/FloatingButtonContainer.tsx b/src/containers/common/FloatingButtonContainer.tsx
index 0121c62a..cdaba83b 100644
--- a/src/containers/common/FloatingButtonContainer.tsx
+++ b/src/containers/common/FloatingButtonContainer.tsx
@@ -52,11 +52,9 @@ const FloatingButtonContainer = () => {
return (
<>
-
+
{
const pathname = usePathname();
const [visible, setVisible] = useState(false);
const [transparent, setTransparent] = useState(true);
const authStore = useAuthStore();
- const router = useRouter();
+
+ usePreventBodyScroll(visible);
const onScroll = () => {
if (window.scrollY >= 500) {
@@ -29,19 +31,7 @@ const HeaderContainer = () => {
setVisible(false);
};
- const logoutHandler = async () => {
- // api로 로그아웃 요청해서 쿠키제거
- const response = await fetch("/api/auth/logout", {
- method: "POST",
- });
- if (!response.ok) {
- throw new Error(response.statusText);
- }
- authStore.initialize();
- router.push("/");
- router.refresh();
- };
useEffect(() => {
// 모달창이 열린 상태로 새로고침을 하게되는 경우 히스토리 스택을 제거하기 위해서 뒤로가기 실행
@@ -59,16 +49,26 @@ const HeaderContainer = () => {
// 자동 로그인
const login = async () => {
try {
- const data = await fetchWithAuth("/api/auth/user");
- if (data.status == 200) {
- data.json().then((res) => {
- authStore.setUser(res);
- });
- } else {
- authStore.setUser({
- id: -1,
- });
+ const res = await fetchWithAuth("/api/auth/user");
+ if (res.status == 200) {
+ const data = await res.json();
+ // 유저 상태가 '대기'인 경우는 쿠키를 제거하기 위해 로그아웃 처리
+ if (data.userStatus == "대기") {
+ await fetchWithAuth("/api/auth/logout", {
+ method: "POST"
+ });
+ authStore.setUser({
+ id: -1,
+ });
+ }
+ else {
+ authStore.setUser(data);
+ }
+ return;
}
+ authStore.setUser({
+ id: -1,
+ });
} catch {
authStore.setUser({
id: -1,
@@ -87,9 +87,6 @@ const HeaderContainer = () => {
onMenuClicked={onMenuClicked}
onClose={onClose}
userId={authStore.id}
- userSex={authStore.sex}
- userProfile={authStore.userImage.address}
- logoutHandler={logoutHandler}
/>
);
};
diff --git a/src/containers/gathering/read/GatheringCardListContainer.tsx b/src/containers/gathering/read/GatheringCardListContainer.tsx
index 1c26ddbc..3f5a0007 100644
--- a/src/containers/gathering/read/GatheringCardListContainer.tsx
+++ b/src/containers/gathering/read/GatheringCardListContainer.tsx
@@ -98,11 +98,9 @@ const GatheringCardListContainer = () => {
) : (
<>
-
+
{
- const [isModal, setIsModal] = useState(false);
+ const modalState = useModalState();
const [loading, setLoading] = useState(true);
useEffect(() => {
@@ -21,17 +22,15 @@ const GatheringFilterContainer = (props: IGatheringFilterContainer) => {
<>
setIsModal(false)}
- isHeaderBar={true}
+ modalState={modalState}
>
- setIsModal(false)} />
+
>
);
diff --git a/src/containers/gathering/read/detail/GatheringStatusChangeModalContainer.tsx b/src/containers/gathering/read/detail/GatheringStatusChangeModalContainer.tsx
index 1eaf045d..540ca4de 100644
--- a/src/containers/gathering/read/detail/GatheringStatusChangeModalContainer.tsx
+++ b/src/containers/gathering/read/detail/GatheringStatusChangeModalContainer.tsx
@@ -2,18 +2,18 @@
import RemoveModal from "@/components/common/RemoveModal";
import useToastifyStore from "@/store/toastifyStore";
+import { IModalComponent } from "@/types/ModalState";
import { fetchWithAuth } from "@/utils/fetchWithAuth";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
-interface IGatheringStatusChangeModalContainer {
- closeModal: () => void;
+interface IGatheringStatusChangeModalContainer extends IModalComponent {
isFinish: boolean;
}
const GatheringStatusChangeModalContainer = ({
- closeModal,
isFinish,
+ ...props
}: IGatheringStatusChangeModalContainer) => {
const [loading, setLoading] = useState(false);
const router = useRouter();
@@ -38,7 +38,7 @@ const GatheringStatusChangeModalContainer = ({
message: "모임 마감에 실패했습니다.",
});
setLoading(false);
- closeModal();
+ props.closeModal!();
return;
// throw new Error(response.statusText);
}
@@ -51,7 +51,7 @@ const GatheringStatusChangeModalContainer = ({
closeModal()}
+ onCancelClick={() => props.closeModal!()}
mainMessage={["모임을 마감하시겠습니까?"]}
subMessage={[
"모임을 마감하시면 추가적으로 인원을 받을 수 없고 검색에서 제외됩니다.",
diff --git a/src/containers/informations/detail/CommentListContainer.tsx b/src/containers/informations/detail/CommentListContainer.tsx
new file mode 100644
index 00000000..0eeeac6b
--- /dev/null
+++ b/src/containers/informations/detail/CommentListContainer.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import CommentList from "@/components/informations/detail/CommentList";
+import { InformationCommentResponseDto } from "@/types/InformationDto";
+import { useEffect, useState } from "react";
+
+interface Props {
+ informationId: number;
+}
+
+const CommentListContainer = ({ informationId }: Props) => {
+ const [loading, setLoading] = useState(true);
+ const [comments, setComments] = useState([]);
+
+ useEffect(() => {
+ // TODO: API 연동 필요
+ // (async function () {
+ // const response = await fetch(`/api/informations/comment/${informationId}`, {
+ // method: "GET",
+ // cache: "no-store",
+ // });
+ //
+ // setLoading(false);
+ //
+ // if (!response.ok) {
+ // throw new Error(response.statusText);
+ // }
+ // setComments(
+ // await (response.json() as Promise),
+ // );
+ // })();
+
+ // TODO: 임시 코드입니다.
+ (async function () {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ setLoading(false);
+ setComments([
+ {
+ userImage: "/user/default-female.svg",
+ nickname: "하몽",
+ createdDate: new Date(),
+ content: "좋아보여요~",
+ },
+ {
+ userImage: "/user/default-female.svg",
+ nickname: "유저d0f",
+ createdDate: new Date(),
+ content: "추천 메뉴 있을까요?",
+ },
+ ]);
+ })();
+ }, []);
+
+ return ;
+};
+
+export default CommentListContainer;
diff --git a/src/containers/mypage/MyPageGatheringContainer.tsx b/src/containers/mypage/MyPageGatheringContainer.tsx
index 5b468503..389800ca 100644
--- a/src/containers/mypage/MyPageGatheringContainer.tsx
+++ b/src/containers/mypage/MyPageGatheringContainer.tsx
@@ -113,11 +113,9 @@ const MyPageGatheringContainer = (props: IMyPageGatheringContainer) => {
return (
-
+
{
}}
userImageUrl={imageUrl}
userSex={props.userSex}
- isModalOpen={modalState.isOpen}
+ modalState={modalState}
imageBase64Data={imageBase64Data}
closeCropModal={closeCropModal}
onChangeImageUrl={onChangeImageUrl}
diff --git a/src/containers/mypage/MyProfileContainer.tsx b/src/containers/mypage/MyProfileContainer.tsx
index 32e43c9c..6a2952bf 100644
--- a/src/containers/mypage/MyProfileContainer.tsx
+++ b/src/containers/mypage/MyProfileContainer.tsx
@@ -2,11 +2,8 @@
import MyProfile from "@/components/mypage/MyProfile";
import useModalState from "@/hooks/useModalState";
-import useAuthStore from "@/store/authStore";
-import useToastifyStore from "@/store/toastifyStore";
import { userResponseDto } from "@/types/UserDto";
import { fetchWithAuth } from "@/utils/fetchWithAuth";
-import { useRouter } from "next/navigation";
import { useState } from "react";
interface IMyProfileContainer {
@@ -18,11 +15,7 @@ const MyProfileContainer = ({ userInfo }: IMyProfileContainer) => {
const [defaultNickname, setDefaultNickname] = useState(userInfo.nickname);
const [message, setMessage] = useState("");
const modalState = useModalState();
- const [userDeleteText, setUserDeleteText] = useState("");
- const router = useRouter();
- const toastifyStore = useToastifyStore();
- const authStore = useAuthStore();
-
+
const submitChangeNicknameHandler = async () => {
if (nickname == "" && nickname == defaultNickname) return;
const res = await fetchWithAuth("/api/mypage/change-nickname", {
@@ -47,37 +40,6 @@ const MyProfileContainer = ({ userInfo }: IMyProfileContainer) => {
setMessage("");
};
- const changeUserDeleteText = (value: string) => {
- setUserDeleteText(value);
- };
-
- const userDeleteHandler = async () => {
- const response = await fetchWithAuth(
- `/api/auth/user?type=${userInfo.provider}`,
- {
- method: "DELETE",
- "Content-Type": "application/json",
- },
- );
-
- if (response.ok) {
- modalState.closeModal();
- await toastifyStore.setToastify({
- type: "success",
- message: "회원탈퇴에 성공했습니다.",
- });
- authStore.initialize();
- setTimeout(() => {
- router.replace("/");
- }, 300);
- } else {
- toastifyStore.setToastify({
- type: "error",
- message: "회원탈퇴에 실패했습니다.",
- });
- }
- };
-
return (
<>
{
defaultNickname={defaultNickname}
message={message}
modalState={modalState}
- changeUserDeleteText={changeUserDeleteText}
- userDeleteText={userDeleteText}
- userDeleteHandler={userDeleteHandler}
/>
>
);
diff --git a/src/types/InformationDto.ts b/src/types/InformationDto.ts
index 7168d9cf..173febd6 100644
--- a/src/types/InformationDto.ts
+++ b/src/types/InformationDto.ts
@@ -153,3 +153,13 @@ export interface BestInformationResponseDto {
thumbNailImage: string; // 썸네일 이미지 주소
likeCount: number; // 좋아요 수
}
+
+/**
+ * 정보 댓글 조회 결과 DTO
+ */
+export interface InformationCommentResponseDto {
+ userImage: string;
+ nickname: string;
+ createdDate: Date;
+ content: string;
+}
diff --git a/src/types/ModalState.ts b/src/types/ModalState.ts
index f7375a1c..43ef7fcd 100644
--- a/src/types/ModalState.ts
+++ b/src/types/ModalState.ts
@@ -1,5 +1,13 @@
+import { ReactNode } from "react";
+
export type ModalState = {
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
};
+
+
+export interface IModalComponent {
+ closeModal?: () => void;
+ closeButtonComponent?: ReactNode;
+}
\ No newline at end of file