Skip to content
Merged
2 changes: 2 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,

experimental: {
turbo: {
rules: {
Expand Down
6 changes: 4 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ export default function RootLayout({
}) {
return (
<html lang="ko">
<head></head>
<body>{children}</body>
<body>
<div id="modal-root"></div>
<main>{children}</main>
</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { satisfactionData } from "@/constants/satisfactionData";
import BadEmoji from "@/assets/icons/emoji_bad.svg";
import PoorEmoji from "@/assets/icons/emoji_poor.svg";
import NeutralEmoji from "@/assets/icons/emoji_neutral.svg";
import GoodEmoji from "@/assets/icons/emoji_good.svg";
import ExcellentEmoji from "@/assets/icons/emoji_excellent.svg";

const emojis = [BadEmoji, PoorEmoji, NeutralEmoji, GoodEmoji, ExcellentEmoji];
const selectedColor = ["#F84B5F", "#3C98A4", "#53B3C0", "#577DD1", "#3560C0"];

interface SatisfactionFormProps {
scores: number[];
setScores: (scores: number[]) => void;
}

export default function SatisfactionForm({
scores,
setScores,
}: SatisfactionFormProps) {
const handleScoreChange = (questionIndex: number, score: number) => {
const updatedScores = [...scores];
updatedScores[questionIndex] = score;
setScores(updatedScores);
};

return (
<>
{satisfactionData.map((data, questionIndex) => (
<div key={questionIndex} className="flex flex-col gap-4 sm:gap-6">
<h3 className="text-title-xsmall sm:text-title-small text-[#353A46]">
{data.question}
</h3>
<div className="flex justify-between gap-1">
{data.scores.map((scoreText, scoreIndex) => {
const Emoji = emojis[scoreIndex];
const isSelected = scores[questionIndex] === scoreIndex + 1;
return (
<button
key={scoreIndex}
onClick={() =>
handleScoreChange(questionIndex, scoreIndex + 1)
}
className="flex flex-1 cursor-pointer flex-col items-center gap-2 sm:gap-3"
>
<Emoji
className="size-6 sm:size-10"
style={{
color: isSelected ? selectedColor[scoreIndex] : "#969EB0",
}}
/>
<p
className="sm:text-body-medium text-link-small"
style={{
color: isSelected ? selectedColor[scoreIndex] : "#969EB0",
}}
>
{scoreText}
</p>
</button>
);
})}
</div>
</div>
))}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from "react";
import { UpdateRequest } from "@/lib/type";
import { updateSatisfactionScore } from "@/lib/apis/survey";

export function useSatisfactionSubmit(onClose: () => void) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<null | string>(null);

const handleSubmit = async (satisfactions: number[]) => {
setIsLoading(true);
setError(null);

const clientId = localStorage.getItem("userId") || "";

const payload: UpdateRequest = {
clientId,
satisfactions,
};

try {
await updateSatisfactionScore(payload);
onClose();
} catch (err) {
console.error(err);
setError("만족도 정보를 전송하는 데 실패했어요.");
} finally {
setIsLoading(false);
}
};

return { handleSubmit, isLoading, error };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useState } from "react";
import { useSatisfactionSubmit } from "./hooks/useSatisfactionSubmit";
import SatisfactionForm from "./SatisfactionForm";
import Button from "@/components/Button";

export default function SatisfactionModalContent({
onClose,
}: {
onClose: () => void;
}) {
const [satisfactionScores, setSatisfactionScores] = useState([0, 0, 0]);

const { handleSubmit, isLoading, error } = useSatisfactionSubmit(() => {
setSatisfactionScores([0, 0, 0]);
onClose();
});

if (isLoading) return <div>Loading</div>; // 로딩 페이지 시안 완성되면 변경
if (error) return <div>{error}</div>; // 에러 페이지 시안 완성되면 변경

return (
<div className="flex h-full flex-col gap-8 sm:gap-16">
<div className="flex shrink-0 flex-col gap-1 sm:gap-2.5">
<h1 className="text-title-small sm:text-heading-small text-[#1F2229]">
추천받는 과정은 어떠셨나요?
</h1>
<p className="text-body-small sm:text-body-medium text-[#79839A]">
작은 의견 하나가 더 나은 새길을 만드는 데 큰 힘이 돼요 :)
</p>
</div>
<div className="flex flex-1 flex-col gap-10 overflow-y-auto sm:gap-12">
<SatisfactionForm
scores={satisfactionScores}
setScores={setSatisfactionScores}
/>
</div>
<div className="flex shrink-0 justify-center gap-3">
<Button
color="gray"
onClick={onClose}
className="text-body-large h-[62px] w-full max-w-[150px] rounded-xl sm:w-[150px]"
>
다음에 하기
</Button>
<Button
color="blue"
onClick={() => handleSubmit(satisfactionScores)}
className="text-body-large h-[62px] w-full max-w-[150px] rounded-xl sm:w-[150px]"
disabled={false}
>
보내기
</Button>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions src/app/recommend/_components/MapView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default function MapView({
{/* 시작 지점 마커 */}
<MapMarker
position={origin}
zIndex={50}
image={{
src: "/icons/marker_origin.svg",
size: {
Expand All @@ -62,6 +63,7 @@ export default function MapView({
{/* 도착 지점 마커 */}
<MapMarker
position={destination}
zIndex={50}
image={{
src: "/icons/marker_destination.svg",
size: {
Expand Down
8 changes: 6 additions & 2 deletions src/app/recommend/_components/SatisfactionModalButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import Button from "@/components/Button";

export default function SatisfactionModalButton() {
export default function SatisfactionModalButton({
onOpen,
}: {
onOpen: () => void;
}) {
return (
<Button
color="blue"
onClick={() => {}}
onClick={onOpen}
className="text-body-small sm:text-body-large h-[37px] w-[107px] rounded-md sm:h-[62px] sm:w-[149px] sm:rounded-xl"
>
서비스 만족도
Expand Down
34 changes: 18 additions & 16 deletions src/app/recommend/_hooks/useSurveyRecommendation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ export function useSurveyRecommendation() {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<null | string>(null);

useEffect(() => {
const userId = localStorage.getItem("userId") || "";
const fetchData = async () => {
setIsLoading(true);
setError(null);

const clientId = localStorage.getItem("userId") || "";
const onboarding = JSON.parse(
localStorage.getItem("onboardingAnswers") ?? "[]"
);

const payload: RecommendationRequest = {
clientId: userId,
clientId,
age: Number(onboarding[0]?.substr(0, 2) || 0),
gender: onboarding[1],
resident: onboarding[2],
Expand All @@ -23,20 +26,19 @@ export function useSurveyRecommendation() {
mood: onboarding[6],
};

setIsLoading(true);
setError(null);
try {
const res = await fetchRecommendation(payload);
setSpaceData(res);
} catch (err) {
console.error(err);
setError("추천 정보를 불러오는 데 실패했어요.");
} finally {
setIsLoading(false);
}
};

fetchRecommendation(payload)
.then((data) => {
setSpaceData(data);
})
.catch((err) => {
console.error(err);
setError("추천 정보를 불러오는 데 실패했어요.");
})
.finally(() => {
setIsLoading(false);
});
useEffect(() => {
fetchData();
}, []);

return { spaceData, isLoading, error };
Expand Down
44 changes: 26 additions & 18 deletions src/app/recommend/page.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,46 @@
"use client";

import { useState } from "react";
import { useSurveyRecommendation } from "./_hooks/useSurveyRecommendation";
import NavBar from "./_components/NavBar";
import RecommendationPanel from "./_components/RecommendationPanel";
import MapView from "./_components/MapView";
import RetrySurveyButton from "./_components/RetrySurveyButton";
import SatisfactionModalButton from "./_components/SatisfactionModalButton";
import TransitionScreen from "@/app/_components/TransitionScreen";
import NavBar from "./_components/NavBar";
import Modal from "@/components/Modal";
import SatisfactionModalContent from "./_components/MapView/SatisfactionModalContent";

export default function RecommendPage() {
const [isOpen, setIsOpen] = useState(false);

const { spaceData, isLoading, error } = useSurveyRecommendation();

if (isLoading) return <TransitionScreen type="toRecommend" />;

// 에러 페이지 시안 완성되면 변경
if (error) return <div>{error}</div>;
if (error) return <div>{error}</div>; // 에러 페이지 시안 완성되면 변경

return (
<div className="relative h-screen overflow-hidden bg-white">
<div className="absolute inset-0 z-0 pt-10 sm:pt-0">
<div className="flex h-[50vh] w-screen sm:ml-auto sm:h-screen sm:w-[50vw] sm:min-w-[calc(100vw-750px)]">
<MapView spaceData={spaceData} />
</div>
</div>
<div className="pointer-events-none relative z-10">
<div className="flex h-screen flex-col sm:flex-row">
<NavBar />
<RecommendationPanel spaceData={spaceData} />
<>
<div className="relative h-screen overflow-hidden bg-white">
<div className="absolute inset-0 z-0 pt-10 sm:pt-0">
<div className="flex h-[50vh] w-screen sm:ml-auto sm:h-screen sm:w-[50vw] sm:min-w-[calc(100vw-750px)]">
<MapView spaceData={spaceData} />
</div>
</div>
<div className="pointer-events-auto fixed top-14 right-4 flex gap-2 sm:top-5 sm:right-5 sm:gap-5">
<RetrySurveyButton />
<SatisfactionModalButton />
<div className="pointer-events-none relative z-10">
<div className="flex h-screen flex-col sm:flex-row">
<NavBar />
<RecommendationPanel spaceData={spaceData} />
</div>
<div className="pointer-events-auto fixed top-14 right-4 flex gap-2 sm:top-5 sm:right-5 sm:gap-5">
<RetrySurveyButton />
<SatisfactionModalButton onOpen={() => setIsOpen(true)} />
</div>
</div>
</div>
</div>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<SatisfactionModalContent onClose={() => setIsOpen(false)} />
</Modal>
</>
);
}
2 changes: 1 addition & 1 deletion src/assets/icons/emoji_bad.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/icons/emoji_excellent.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/icons/emoji_good.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/icons/emoji_neutral.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/icons/emoji_poor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ReactNode } from "react";
import { createPortal } from "react-dom";

interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
}

export default function Modal({ isOpen, onClose, children }: ModalProps) {
if (!isOpen) return null;

return createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={onClose}
>
<div
className="relative m-4 h-[80vh] max-h-[560px] w-full max-w-[655px] rounded-2xl bg-white px-5 py-6 shadow-lg sm:max-h-[824px] sm:rounded-4xl sm:p-10"
onClick={(e) => e.stopPropagation()} // 모달 내부 클릭 시 닫힘 방지
>
{children}
</div>
</div>,
document.getElementById("modal-root") as HTMLElement
);
}
Loading