Skip to content

Commit 91e31c1

Browse files
authored
Merge pull request #20 from CarToi/feat/#17/satisfaction-modal
[feat, design] #17 서비스 만족도 조사 모달 레이아웃 및 API 연결
2 parents 71a4968 + 724dfa0 commit 91e31c1

File tree

17 files changed

+272
-44
lines changed

17 files changed

+272
-44
lines changed

next.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
3+
reactStrictMode: false,
4+
35
experimental: {
46
turbo: {
57
rules: {

src/app/layout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ export default function RootLayout({
1212
}) {
1313
return (
1414
<html lang="ko">
15-
<head></head>
16-
<body>{children}</body>
15+
<body>
16+
<div id="modal-root"></div>
17+
<main>{children}</main>
18+
</body>
1719
</html>
1820
);
1921
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { satisfactionData } from "@/constants/satisfactionData";
2+
import BadEmoji from "@/assets/icons/emoji_bad.svg";
3+
import PoorEmoji from "@/assets/icons/emoji_poor.svg";
4+
import NeutralEmoji from "@/assets/icons/emoji_neutral.svg";
5+
import GoodEmoji from "@/assets/icons/emoji_good.svg";
6+
import ExcellentEmoji from "@/assets/icons/emoji_excellent.svg";
7+
8+
const emojis = [BadEmoji, PoorEmoji, NeutralEmoji, GoodEmoji, ExcellentEmoji];
9+
const selectedColor = ["#F84B5F", "#3C98A4", "#53B3C0", "#577DD1", "#3560C0"];
10+
11+
interface SatisfactionFormProps {
12+
scores: number[];
13+
setScores: (scores: number[]) => void;
14+
}
15+
16+
export default function SatisfactionForm({
17+
scores,
18+
setScores,
19+
}: SatisfactionFormProps) {
20+
const handleScoreChange = (questionIndex: number, score: number) => {
21+
const updatedScores = [...scores];
22+
updatedScores[questionIndex] = score;
23+
setScores(updatedScores);
24+
};
25+
26+
return (
27+
<>
28+
{satisfactionData.map((data, questionIndex) => (
29+
<div key={questionIndex} className="flex flex-col gap-4 sm:gap-6">
30+
<h3 className="text-title-xsmall sm:text-title-small text-[#353A46]">
31+
{data.question}
32+
</h3>
33+
<div className="flex justify-between gap-1">
34+
{data.scores.map((scoreText, scoreIndex) => {
35+
const Emoji = emojis[scoreIndex];
36+
const isSelected = scores[questionIndex] === scoreIndex + 1;
37+
return (
38+
<button
39+
key={scoreIndex}
40+
onClick={() =>
41+
handleScoreChange(questionIndex, scoreIndex + 1)
42+
}
43+
className="flex flex-1 cursor-pointer flex-col items-center gap-2 sm:gap-3"
44+
>
45+
<Emoji
46+
className="size-6 sm:size-10"
47+
style={{
48+
color: isSelected ? selectedColor[scoreIndex] : "#969EB0",
49+
}}
50+
/>
51+
<p
52+
className="sm:text-body-medium text-link-small"
53+
style={{
54+
color: isSelected ? selectedColor[scoreIndex] : "#969EB0",
55+
}}
56+
>
57+
{scoreText}
58+
</p>
59+
</button>
60+
);
61+
})}
62+
</div>
63+
</div>
64+
))}
65+
</>
66+
);
67+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useState } from "react";
2+
import { UpdateRequest } from "@/lib/type";
3+
import { updateSatisfactionScore } from "@/lib/apis/survey";
4+
5+
export function useSatisfactionSubmit(onClose: () => void) {
6+
const [isLoading, setIsLoading] = useState(false);
7+
const [error, setError] = useState<null | string>(null);
8+
9+
const handleSubmit = async (satisfactions: number[]) => {
10+
setIsLoading(true);
11+
setError(null);
12+
13+
const clientId = localStorage.getItem("userId") || "";
14+
15+
const payload: UpdateRequest = {
16+
clientId,
17+
satisfactions,
18+
};
19+
20+
try {
21+
await updateSatisfactionScore(payload);
22+
onClose();
23+
} catch (err) {
24+
console.error(err);
25+
setError("만족도 정보를 전송하는 데 실패했어요.");
26+
} finally {
27+
setIsLoading(false);
28+
}
29+
};
30+
31+
return { handleSubmit, isLoading, error };
32+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useState } from "react";
2+
import { useSatisfactionSubmit } from "./hooks/useSatisfactionSubmit";
3+
import SatisfactionForm from "./SatisfactionForm";
4+
import Button from "@/components/Button";
5+
6+
export default function SatisfactionModalContent({
7+
onClose,
8+
}: {
9+
onClose: () => void;
10+
}) {
11+
const [satisfactionScores, setSatisfactionScores] = useState([0, 0, 0]);
12+
13+
const { handleSubmit, isLoading, error } = useSatisfactionSubmit(() => {
14+
setSatisfactionScores([0, 0, 0]);
15+
onClose();
16+
});
17+
18+
if (isLoading) return <div>Loading</div>; // 로딩 페이지 시안 완성되면 변경
19+
if (error) return <div>{error}</div>; // 에러 페이지 시안 완성되면 변경
20+
21+
return (
22+
<div className="flex h-full flex-col gap-8 sm:gap-16">
23+
<div className="flex shrink-0 flex-col gap-1 sm:gap-2.5">
24+
<h1 className="text-title-small sm:text-heading-small text-[#1F2229]">
25+
추천받는 과정은 어떠셨나요?
26+
</h1>
27+
<p className="text-body-small sm:text-body-medium text-[#79839A]">
28+
작은 의견 하나가 더 나은 새길을 만드는 데 큰 힘이 돼요 :)
29+
</p>
30+
</div>
31+
<div className="flex flex-1 flex-col gap-10 overflow-y-auto sm:gap-12">
32+
<SatisfactionForm
33+
scores={satisfactionScores}
34+
setScores={setSatisfactionScores}
35+
/>
36+
</div>
37+
<div className="flex shrink-0 justify-center gap-3">
38+
<Button
39+
color="gray"
40+
onClick={onClose}
41+
className="text-body-large h-[62px] w-full max-w-[150px] rounded-xl sm:w-[150px]"
42+
>
43+
다음에 하기
44+
</Button>
45+
<Button
46+
color="blue"
47+
onClick={() => handleSubmit(satisfactionScores)}
48+
className="text-body-large h-[62px] w-full max-w-[150px] rounded-xl sm:w-[150px]"
49+
disabled={false}
50+
>
51+
보내기
52+
</Button>
53+
</div>
54+
</div>
55+
);
56+
}

src/app/recommend/_components/MapView/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default function MapView({
5050
{/* 시작 지점 마커 */}
5151
<MapMarker
5252
position={origin}
53+
zIndex={50}
5354
image={{
5455
src: "/icons/marker_origin.svg",
5556
size: {
@@ -62,6 +63,7 @@ export default function MapView({
6263
{/* 도착 지점 마커 */}
6364
<MapMarker
6465
position={destination}
66+
zIndex={50}
6567
image={{
6668
src: "/icons/marker_destination.svg",
6769
size: {

src/app/recommend/_components/SatisfactionModalButton.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import Button from "@/components/Button";
22

3-
export default function SatisfactionModalButton() {
3+
export default function SatisfactionModalButton({
4+
onOpen,
5+
}: {
6+
onOpen: () => void;
7+
}) {
48
return (
59
<Button
610
color="blue"
7-
onClick={() => {}}
11+
onClick={onOpen}
812
className="text-body-small sm:text-body-large h-[37px] w-[107px] rounded-md sm:h-[62px] sm:w-[149px] sm:rounded-xl"
913
>
1014
서비스 만족도

src/app/recommend/_hooks/useSurveyRecommendation.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ export function useSurveyRecommendation() {
77
const [isLoading, setIsLoading] = useState(true);
88
const [error, setError] = useState<null | string>(null);
99

10-
useEffect(() => {
11-
const userId = localStorage.getItem("userId") || "";
10+
const fetchData = async () => {
11+
setIsLoading(true);
12+
setError(null);
13+
14+
const clientId = localStorage.getItem("userId") || "";
1215
const onboarding = JSON.parse(
1316
localStorage.getItem("onboardingAnswers") ?? "[]"
1417
);
1518

1619
const payload: RecommendationRequest = {
17-
clientId: userId,
20+
clientId,
1821
age: Number(onboarding[0]?.substr(0, 2) || 0),
1922
gender: onboarding[1],
2023
resident: onboarding[2],
@@ -23,20 +26,19 @@ export function useSurveyRecommendation() {
2326
mood: onboarding[6],
2427
};
2528

26-
setIsLoading(true);
27-
setError(null);
29+
try {
30+
const res = await fetchRecommendation(payload);
31+
setSpaceData(res);
32+
} catch (err) {
33+
console.error(err);
34+
setError("추천 정보를 불러오는 데 실패했어요.");
35+
} finally {
36+
setIsLoading(false);
37+
}
38+
};
2839

29-
fetchRecommendation(payload)
30-
.then((data) => {
31-
setSpaceData(data);
32-
})
33-
.catch((err) => {
34-
console.error(err);
35-
setError("추천 정보를 불러오는 데 실패했어요.");
36-
})
37-
.finally(() => {
38-
setIsLoading(false);
39-
});
40+
useEffect(() => {
41+
fetchData();
4042
}, []);
4143

4244
return { spaceData, isLoading, error };

src/app/recommend/page.tsx

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,46 @@
11
"use client";
22

3+
import { useState } from "react";
34
import { useSurveyRecommendation } from "./_hooks/useSurveyRecommendation";
5+
import NavBar from "./_components/NavBar";
46
import RecommendationPanel from "./_components/RecommendationPanel";
57
import MapView from "./_components/MapView";
68
import RetrySurveyButton from "./_components/RetrySurveyButton";
79
import SatisfactionModalButton from "./_components/SatisfactionModalButton";
810
import TransitionScreen from "@/app/_components/TransitionScreen";
9-
import NavBar from "./_components/NavBar";
11+
import Modal from "@/components/Modal";
12+
import SatisfactionModalContent from "./_components/MapView/SatisfactionModalContent";
1013

1114
export default function RecommendPage() {
15+
const [isOpen, setIsOpen] = useState(false);
16+
1217
const { spaceData, isLoading, error } = useSurveyRecommendation();
1318

1419
if (isLoading) return <TransitionScreen type="toRecommend" />;
15-
16-
// 에러 페이지 시안 완성되면 변경
17-
if (error) return <div>{error}</div>;
20+
if (error) return <div>{error}</div>; // 에러 페이지 시안 완성되면 변경
1821

1922
return (
20-
<div className="relative h-screen overflow-hidden bg-white">
21-
<div className="absolute inset-0 z-0 pt-10 sm:pt-0">
22-
<div className="flex h-[50vh] w-screen sm:ml-auto sm:h-screen sm:w-[50vw] sm:min-w-[calc(100vw-750px)]">
23-
<MapView spaceData={spaceData} />
24-
</div>
25-
</div>
26-
<div className="pointer-events-none relative z-10">
27-
<div className="flex h-screen flex-col sm:flex-row">
28-
<NavBar />
29-
<RecommendationPanel spaceData={spaceData} />
23+
<>
24+
<div className="relative h-screen overflow-hidden bg-white">
25+
<div className="absolute inset-0 z-0 pt-10 sm:pt-0">
26+
<div className="flex h-[50vh] w-screen sm:ml-auto sm:h-screen sm:w-[50vw] sm:min-w-[calc(100vw-750px)]">
27+
<MapView spaceData={spaceData} />
28+
</div>
3029
</div>
31-
<div className="pointer-events-auto fixed top-14 right-4 flex gap-2 sm:top-5 sm:right-5 sm:gap-5">
32-
<RetrySurveyButton />
33-
<SatisfactionModalButton />
30+
<div className="pointer-events-none relative z-10">
31+
<div className="flex h-screen flex-col sm:flex-row">
32+
<NavBar />
33+
<RecommendationPanel spaceData={spaceData} />
34+
</div>
35+
<div className="pointer-events-auto fixed top-14 right-4 flex gap-2 sm:top-5 sm:right-5 sm:gap-5">
36+
<RetrySurveyButton />
37+
<SatisfactionModalButton onOpen={() => setIsOpen(true)} />
38+
</div>
3439
</div>
3540
</div>
36-
</div>
41+
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
42+
<SatisfactionModalContent onClose={() => setIsOpen(false)} />
43+
</Modal>
44+
</>
3745
);
3846
}

src/assets/icons/emoji_bad.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)