Skip to content
Open
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
Binary file modified public/img_pravel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/quest/quest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/record/travel/default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 14 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use client';

import { useEffect } from 'react';
import { useEffect, useState } from 'react';

import WishList from '@/components/main/AddOption/WishList';
import ScheduleList from '@/components/main/Schedule/ScheduleList';
import QuestModal from '@/components/quest/modal/QuestModal';
import useModal, { MODAL } from '@/hook/useModal';
import { useFetchPlan } from '@/hook/usePlan';
import Header from '@/layout/header/Header';
Expand All @@ -16,9 +17,11 @@ import FloatingBar from '../components/main/FloatingBar';
import Gnb from '../layout/navigation/Gnb';

const Home = () => {
const [questId, setQuestId] = useState<number | null>(null);
const [modalState, { openModal, closeModal }] = useModal({
[MODAL.ADD_OPTION]: false,
[MODAL.WISH_LIST]: false,
[MODAL.QUEST]: false,
});

const { data, isError, isFetching } = useFetchPlan();
Expand Down Expand Up @@ -52,7 +55,13 @@ const Home = () => {
<DateViewer />
<ScheduleList />
</>
<FloatingBar openAddOption={() => openModal(MODAL.ADD_OPTION)} />
<FloatingBar
openAddOption={() => openModal(MODAL.ADD_OPTION)}
openQuest={(id) => {
setQuestId(id);
openModal(MODAL.QUEST);
}}
/>
</main>
{modalState.addOption && (
<AddOption
Expand All @@ -71,6 +80,9 @@ const Home = () => {
}}
/>
)}
{modalState.quest && questId && (
<QuestModal id={questId} close={() => closeModal(MODAL.QUEST)} />
)}
</>
);
};
Expand Down
15 changes: 14 additions & 1 deletion src/app/travel-record/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Image from 'next/image';

import SwiperCarousel from '@/components/common/carousel/SwiperCarousel';
import TravelList from '@/components/record/TravelList';
import Gnb from '@/layout/navigation/Gnb';
Expand Down Expand Up @@ -33,7 +35,18 @@ const TravelRecord = async () => {
{list.length ? (
<SwiperCarousel data={list} />
) : (
<div className="mx-auto w-[246px] h-[362px] bg-gray-200 rounded-[25px]" />
<div className="mx-auto flex flex-col justify-center items-center w-[246px] h-[362px] bg-gray-200 rounded-[25px]">
<Image
className="mx-auto"
src="/record/travel/default.png"
width={130}
height={130}
alt="default"
/>
<p className="mt-[4px] font-semibold text-gray-500">
여행 기록이 없어요 :(
</p>
</div>
)}
</div>
<div className="h-[9px] bg-gray-200 mt-[40px]"></div>
Expand Down
6 changes: 5 additions & 1 deletion src/components/main/FloatingBar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import QuestButton from './QuestButton';

interface FloatingBarProps {
openAddOption: () => void;
openQuest: (questId: number) => void;
}

const FloatingBar = ({ openAddOption }: FloatingBarProps) => {
const FloatingBar = ({ openAddOption, openQuest }: FloatingBarProps) => {
return (
<ul className="fixed bottom-[101px] right-[16px] w-[56px]">
<QuestButton openQuest={openQuest} />
<li className="bg-white rounded-full shadow-[0_0_10px_0_#0000001A] mb-[8px]">
<button className="ico_pravel ico_plus56" onClick={openAddOption}>
여행계획 추가하기
Expand Down
46 changes: 46 additions & 0 deletions src/components/main/QuestButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { useState } from 'react';

import { useFetchQuestList } from '@/hook/useQuest';

interface QuestButtonProps {
openQuest: (questId: number) => void;
}

const QuestButton = ({ openQuest }: QuestButtonProps) => {
const [enabled, setEnabled] = useState(false);
const { data } = useFetchQuestList(enabled);

return (
<li className="relative shadow-[0_0_10px_0_#0000001A] mb-[8px] rounded-full">
<button
className="w-[56px] h-[56px] bg-primary text-white text-[36px] font-rajdhani rounded-full font-bold"
onClick={() => setEnabled(true)}
>
Q
</button>
{enabled && (
<ul className="absolute flex flex-col gap-[6px] top-[10px] -left-[14px] -translate-x-full">
{data?.map((quest, i) => (
<li
key={quest.id}
className="cursor-pointer bg-gray-900 w-[38px] h-[38px] text-white font-rajdhani rounded-full text-[20px] font-bold flex items-center justify-center"
onClick={() => openQuest(quest.id)}
>
{`Q${i + 1}`}
</li>
))}

<li className="cursor-pointer bg-gray-600 w-[38px] h-[38px] text-white font-rajdhani rounded-full text-[20px] font-bold flex items-center justify-center hover:bg-gray-700 transition-colors">
<button className="w-full" onClick={() => setEnabled(false)}>
X
</button>
</li>
</ul>
)}
</li>
);
};

export default QuestButton;
86 changes: 86 additions & 0 deletions src/components/quest/modal/QuestModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use client';

import { Bounce, toast } from 'react-toastify';
import Image from 'next/image';

import ModalBackdrop from '@/components/mypage/modal/ModalBackdrop';
import { useFetchQuest, useStartQuestMutation } from '@/hook/useQuest';
import { usePlanStateStore } from '@/store';

interface QuestModalProps {
id: number;
close: () => void;
}

const QuestModal = ({ id }: QuestModalProps) => {
const { data: quest, isLoading, isError } = useFetchQuest(id);
const { currentDate, planId } = usePlanStateStore();
const startQuestMutation = useStartQuestMutation();

if (isLoading) return null;
if (isError) {
close();
return null;
}

const handleStartQuest = () => {
startQuestMutation.mutate({
id,
planId,
date: currentDate,
});
};

return (
<div className="fixed top-0 left-0 w-full h-dvh pt-[106px] z-50">
<ModalBackdrop blur />

<div className="max-w-[780px] w-full absolute left-1/2 -translate-x-1/2 flex justify-center flex-col z-30">
<h1 className="text-white font-bold text-[34px] text-center font-rajdhani">
Quest
</h1>
<p className="text-center mt-[25px]">
<span className="text-white font-semibold leading-[19px]">
<span className="text-primary">[{quest!.title}]</span>하고 기념품을
받아보세요!
<br /> 일정에 참여해 보실래요?
</span>
</p>

<div className="max-w-[780px] px-[20px]">
<div className="font-semibold mt-[35px] w-full h-[46px] bg-[rgba(11,197,141,0.2)] border-[1px] rounded-[5px] border-solid border-[rgba(11,197,141,0.3)] px-[20px] py-[11px] flex justify-between">
<span className="text-white">퀘스트 보상</span>
<span className="text-primary">
<i className="ico_pravel ico_energy_fill24 mr-[4px]" />
에너지 {quest!.energy}%
</span>
</div>
</div>

<div className="relative">
<Image
className="mx-auto"
src="/quest/quest.png"
width={385}
height={385}
alt="퀘스트 이미지"
/>
</div>

<div className="mt-[19px] flex justify-center gap-[8px]">
<button className="w-[170px] h-[43px] bg-gray-200 rounded-[20px_20px_20px_12px] text-gray-600">
별로예요
</button>
<button
className="w-[170px] h-[43px] bg-primary rounded-[20px_20px_20px_12px] text-white"
onClick={handleStartQuest}
>
참여하기
</button>
</div>
</div>
</div>
);
};

export default QuestModal;
1 change: 1 addition & 0 deletions src/hook/useModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum MODAL {
ONBOARDING_CALENDAR = 'onboardingCalendar',
ONBOARDING_SEARCH_LOCATION = 'onboardingSearchLocation',
SHARE_LINK = 'shareLink',
QUEST = 'quest',
}

const useModal = (initState: ModalState = {}): [ModalState, ModalActions] => {
Expand Down
46 changes: 46 additions & 0 deletions src/hook/useQuest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

import getLocation from '@/services/api/location.api';
import * as questApi from '@/services/api/quest.api';
import { LocationData } from '@/types/search.type';
import { boundToast } from '@/utils/toastUtils';

export const useFetchQuestList = (enabled: boolean) => {
const queryClient = useQueryClient();

return useQuery({
queryKey: ['quest'],
queryFn: async () => {
let location = queryClient.getQueryData(['location']) as LocationData;

if (!location) {
location = await getLocation();
}

return questApi.getQuestList(location);
},
staleTime: 5 * 3_600 * 1000,
enabled,
});
};

export const useFetchQuest = (questId: number) =>
useQuery({
queryKey: ['quest', questId],
queryFn: async () => {
return questApi.getQuest(questId);
},
});

export const useStartQuestMutation = () =>
useMutation({
mutationFn: questApi.startQuest,

onSuccess() {
boundToast('추가되었습니다');
},

onError() {
boundToast('퀘스트를 시작할 수 없습니다.', 'warning');
},
});
45 changes: 45 additions & 0 deletions src/services/api/quest.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Quest, StartQuestRequest } from '@/types/quest.type';
import { LocationData } from '@/types/search.type';

import { baseURL, setDefaultHeader } from '.';

const QUEST = '/quest';

export const getQuestList = async ({
lat,
lng,
}: LocationData): Promise<Quest[]> =>
fetch(`${baseURL}${QUEST}?x=${lat}&y=${lng}`, {
headers: await setDefaultHeader(),
}).then((res) => {
if (!res.ok) {
throw new Error('Network response was not ok');
}

return res.json();
});

export const getQuest = async (questId: number): Promise<Quest> =>
fetch(`${baseURL}${QUEST}/${questId}`, {
headers: await setDefaultHeader(),
}).then((res) => {
if (!res.ok) {
throw new Error('Network response was not ok');
}

return res.json();
});

export const startQuest = async ({
id,
...props
}: StartQuestRequest): Promise<void> =>
fetch(`${baseURL}${QUEST}/${id}`, {
method: 'POST',
headers: await setDefaultHeader(),
body: JSON.stringify(props),
}).then((res) => {
if (!res.ok) {
throw new Error('Network response was not ok');
}
});
5 changes: 5 additions & 0 deletions src/styles/icons.css
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,8 @@
height: 56px;
background-position: 0 -240px;
}
.ico_energy_fill24 {
width: 24px;
height: 24px;
background-position: -240px -40px;
}
11 changes: 11 additions & 0 deletions src/types/quest.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface Quest {
id: number;
energy: number;
title: string;
}

export interface StartQuestRequest {
id: number;
planId: number;
date: string;
}
16 changes: 16 additions & 0 deletions src/utils/toastUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Bounce, toast, TypeOptions } from 'react-toastify';

export const boundToast = (message: string, type: TypeOptions = 'success') => {
toast(message, {
position: 'top-center',
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: 'colored',
transition: Bounce,
type,
});
};