Skip to content

Commit

Permalink
Feature/85 login (#88)
Browse files Browse the repository at this point in the history
* [feature] ์ถ”๊ฐ€ api ์—ฐ๋™

* [feature] ์ถ”์ฒœ ๋ชจํ•‘ ์„ ํƒํ• ๋•Œ ๋ฆฌํ”„๋ ˆ์‹œ ๋ฒ„ํŠผ ์œ ์ง€

* [feature] ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

* [feature] ๋นŒ๋“œ ์˜ค๋ฅ˜ ์ˆ˜์ • ๋ฐ ๋กœ์ง ๋ณ€๊ฒฝ
  • Loading branch information
choihooo authored Nov 28, 2024
1 parent 99368d6 commit d98c1be
Show file tree
Hide file tree
Showing 13 changed files with 414 additions and 19 deletions.
4 changes: 4 additions & 0 deletions fe/public/svg/alreadyPing.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions fe/src/app/components/ClientStorageHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import { useEffect, useCallback } from "react";
import useLastUpdateStore from "../stores/useLastUpdateStore";

function ClientStorageHandler() {
const { lastUpdated } = useLastUpdateStore();

const clearLocalStorageIfExpired = useCallback(() => {
if (lastUpdated) {
const elapsedTime = Date.now() - lastUpdated;
if (elapsedTime > 24 * 60 * 60 * 1000) {
localStorage.removeItem("nonMemberId");
localStorage.removeItem("authToken");
localStorage.removeItem("lastUpdated");
}
}
}, [lastUpdated]);

useEffect(() => {
const interval = setInterval(() => {
clearLocalStorageIfExpired();
}, 60 * 1000);

return () => clearInterval(interval);
}, [clearLocalStorageIfExpired]);

return null;
}

export default ClientStorageHandler;
8 changes: 5 additions & 3 deletions fe/src/app/event-maps/[id]/components/BottomDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RecommendInActive } from "./RecommendInActive";
import LocationButton from "./LocationButton";
import { RecommendActive } from "./RecommendActive";
import useUpdateTimeStore from "../stores/useUpdateTime";
import useTriggerStore from "../stores/useTriggerStore";

interface NonMember {
nonMemberId: number;
Expand All @@ -22,6 +23,7 @@ interface Ping {
url: string;
type: string;
nonMembers: NonMember[];
sid: string;
}

interface BottomDrawerProps {
Expand Down Expand Up @@ -63,7 +65,7 @@ export function BottomDrawer({
const [isRecommended, setIsRecommended] = useState<boolean>(false);
const [neighborhood, setNeighborhood] = useState<string>("");
const [nonRecommend, setNonRecommend] = useState<boolean>(false);
const [trigger, setTrigger] = useState<boolean>(false);
const { trigger, toggleTrigger } = useTriggerStore();
const { setUpdateTime } = useUpdateTimeStore();

const { setCustomMarkers } = useMarkerStore();
Expand Down Expand Up @@ -157,7 +159,7 @@ export function BottomDrawer({

const result = await response.json();
console.log("Recommended Data Response:", result);
setTrigger((prev) => !prev);
toggleTrigger();
setIsRecommend(false);
} catch (error) {
console.error("Error fetching recommended data:", error);
Expand Down Expand Up @@ -197,7 +199,7 @@ export function BottomDrawer({
};

const handleRecommendCancle = () => {
setTrigger((prev) => !prev);
toggleTrigger();
setIsRecommend(false);
};

Expand Down
201 changes: 201 additions & 0 deletions fe/src/app/event-maps/[id]/components/LoginModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"use client";

import React, { useEffect, useState } from "react";
import Image from "next/image";
import { useSwipeable } from "react-swipeable";
import { useRouter } from "next/navigation";
import { useLoginModalStore } from "../stores/useLoginModalStore";

interface NonMember {
nonMemberId: number;
name: string;
profileSvg: string;
}

interface LoginModalProps {
eventId: string;
}

export default function LoginModal({ eventId }: LoginModalProps) {
const [profiles, setProfiles] = useState<NonMember[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(0);
const profilesPerPage = 4;

const { isLoginModalOpen, closeLoginModal } = useLoginModalStore();

const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
const router = useRouter();

const handleNextPage = () => {
if (currentPage < Math.ceil(profiles.length / profilesPerPage) - 1) {
setCurrentPage((prev) => prev + 1);
}
};

const handlePrevPage = () => {
if (currentPage > 0) {
setCurrentPage((prev) => prev - 1);
}
};

useEffect(() => {
const fetchProfiles = async () => {
setLoading(true);
setError(null);

try {
const response = await fetch(`${apiUrl}/pings?uuid=${eventId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});

if (!response.ok) {
throw new Error(`Failed to fetch profiles: ${response.status}`);
}

const data = await response.json();
setProfiles(data.nonMembers || []);
} catch (err: unknown) {
setError("ํ”„๋กœํ•„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
} finally {
setLoading(false);
}
};

fetchProfiles();
}, [eventId, apiUrl]);

const closeModal = () => {
closeLoginModal();
};

const navigateToPasswordPage = (nonMemberId: number) => {
closeLoginModal();
router.push(
`/event-maps/${eventId}/login/pin-check?nonMemberId=${nonMemberId}`
);
};

const swipeHandlers = useSwipeable({
onSwipedLeft: handleNextPage,
onSwipedRight: handlePrevPage,
preventScrollOnSwipe: true,
});

const currentProfiles = profiles.slice(
currentPage * profilesPerPage,
(currentPage + 1) * profilesPerPage
);

const navigateToProfileCreation = () => {
closeLoginModal();
router.push(`/event-maps/${eventId}/load-mappin/forms/name-pin`);
};

if (!isLoginModalOpen) return null;

return (
<>
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="w-[272px] h-[380px] relative">
<div className="w-[272px] h-[380px] left-0 top-0 absolute bg-white rounded-lg" />

<div className="left-[64px] top-[22px] absolute text-center text-[#1d1d1d] text-xl font-semibold font-['Pretendard'] leading-7">
๋กœ๊ทธ์ธํ•  ํ”„๋กœํ•„์„
<br />
์„ ํƒํ•˜์„ธ์š”.
</div>

<div
{...swipeHandlers}
className="h-[214px] left-0 right-0 top-[96px] absolute flex flex-col justify-center items-center gap-4"
>
{loading && <p className="text-center text-gray-500">๋กœ๋”ฉ ์ค‘...</p>}
{!loading && error && (
<p className="text-center text-red-500">{error}</p>
)}
{!loading && !error && profiles.length === 0 && (
<p className="text-center text-gray-500">
์ €์žฅ๋œ ํ”„๋กœํ•„์ด ์—†์Šต๋‹ˆ๋‹ค.
</p>
)}
{!loading && !error && profiles.length > 0 && (
<div className="grid grid-cols-2 gap-4">
{currentProfiles.map((profile) => (
<div
key={profile.nonMemberId}
onClick={() => navigateToPasswordPage(profile.nonMemberId)}
role="button"
tabIndex={0}
className="w-[68px] flex flex-col items-center cursor-pointer"
onKeyDown={(e) =>
e.key === "Enter" &&
navigateToPasswordPage(profile.nonMemberId)
}
>
<div className="w-16 h-16 bg-gray-200 rounded-md flex justify-center items-center">
<Image
src={profile.profileSvg}
alt={profile.name}
width={64}
height={64}
className="rounded"
/>
</div>
<p className="text-[#717171] text-xs font-medium mt-1">
{profile.name}
</p>
</div>
))}
</div>
)}
</div>

<div className="absolute bottom-[70px] left-0 right-0 flex justify-center space-x-2">
{Array.from(
{ length: Math.ceil(profiles.length / profilesPerPage) },
(_, index) => (
<button
key={`profile-nav-${index}`}
type="button"
onClick={() => setCurrentPage(index)}
className={`w-2 h-2 rounded-full cursor-pointer ${
index === currentPage ? "bg-black" : "bg-[#d9d9d9]"
}`}
/>
)
)}
</div>

<div
onClick={navigateToProfileCreation}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === "Enter" && navigateToProfileCreation()}
className="left-[66px] top-[348px] absolute text-center text-[#8e8e8e] text-xs font-medium font-['Pretendard'] underline leading-none cursor-pointer"
>
์•„์ง ํ”„๋กœํ•„ ์ƒ์„ฑ์„ ์•ˆํ–ˆ๋‹ค๋ฉด?
</div>

<button
onClick={closeModal}
type="button"
className="absolute top-2 right-2 w-6 h-6"
>
<Image
src="/svg/ModalDelete.svg"
alt="๋‹ซ๊ธฐ"
width={18}
height={18}
className="object-cover"
/>
</button>
</div>
</div>
</>
);
}
Loading

0 comments on commit d98c1be

Please sign in to comment.