diff --git a/public/svgs/support/consumer/Ic_Alert.svg b/public/svgs/support/consumer/Ic_Alert.svg new file mode 100644 index 0000000..e8dfb3b --- /dev/null +++ b/public/svgs/support/consumer/Ic_Alert.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/svgs/support/consumer/Ic_SupportMessage.svg b/public/svgs/support/consumer/Ic_SupportMessage.svg new file mode 100644 index 0000000..e2bc0a4 --- /dev/null +++ b/public/svgs/support/consumer/Ic_SupportMessage.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/pages/join/components/GoHome.jsx b/src/pages/join/components/GoHome.jsx index 135a733..6e5d7e1 100644 --- a/src/pages/join/components/GoHome.jsx +++ b/src/pages/join/components/GoHome.jsx @@ -21,7 +21,7 @@ const GoHome = ({ onNext, onBack }) => {
-

+

가치있는 동네여행,
시작해볼까요? @@ -30,7 +30,7 @@ const GoHome = ({ onNext, onBack }) => { diff --git a/src/pages/join/components/LocationStep.jsx b/src/pages/join/components/LocationStep.jsx index 0a7022b..86b5b95 100644 --- a/src/pages/join/components/LocationStep.jsx +++ b/src/pages/join/components/LocationStep.jsx @@ -43,14 +43,14 @@ const LocationStep = ({ onNext, onBack }) => {

-

어느 동네에 사세요?

-

+

어느 동네에 사세요?

+

가까운 사회적 기업을 추천해드릴게요.

@@ -61,7 +61,7 @@ const LocationStep = ({ onNext, onBack }) => { } py-2`} > {!checked && ( - + 서울특별시  )} @@ -74,7 +74,7 @@ const LocationStep = ({ onNext, onBack }) => { value={location} onChange={handleChange} readOnly={checked} - className={`flex-1 bg-transparent text-h2 font-semibold focus:outline-none placeholder-gray-6 ${ + className={`flex-1 bg-transparent h2 focus:outline-none placeholder-gray-6 ${ checked ? "bg-gray-6 cursor-not-allowed" : "" }`} /> @@ -91,7 +91,7 @@ const LocationStep = ({ onNext, onBack }) => { {(showWarningForSeoul || showNotSeoulMessage) && ( -

+

현재는 서울에 한해 사회적 기업들을 소개하고 있습니다.

)} @@ -105,7 +105,7 @@ const LocationStep = ({ onNext, onBack }) => { ) : ( )} - + 현재 서울에 살고 있지 않습니다. @@ -121,7 +121,7 @@ const LocationStep = ({ onNext, onBack }) => { - +
-

- 이름을 입력해주세요. -

-

투명한 리뷰에 사용됩니다.

+

이름을 입력해주세요.

+

투명한 리뷰에 사용됩니다.

@@ -78,8 +78,10 @@ const NameStep = ({ onNext, onBack }) => {
-

+

사회적기업의 사장님이신가요?

diff --git a/src/pages/join/components/ProfileImageStep.jsx b/src/pages/join/components/ProfileImageStep.jsx index 0873fb1..fb1d04d 100644 --- a/src/pages/join/components/ProfileImageStep.jsx +++ b/src/pages/join/components/ProfileImageStep.jsx @@ -31,7 +31,7 @@ const ProfileImageStep = ({ onNext, onBack }) => { -

+

프로필사진을 설정해주세요.

diff --git a/src/pages/kakaoAuth/kakaoAuth.jsx b/src/pages/kakaoAuth/kakaoAuth.jsx index 6f073eb..70a0d4e 100644 --- a/src/pages/kakaoAuth/kakaoAuth.jsx +++ b/src/pages/kakaoAuth/kakaoAuth.jsx @@ -67,7 +67,7 @@ const KakaoAuth = () => { ) : ( -
카카오 로그인 처리 중입니다...
+
카카오 로그인 처리 중입니다...
)} ); diff --git a/src/pages/map/MapPage.jsx b/src/pages/map/MapPage.jsx index 36107c6..250d943 100644 --- a/src/pages/map/MapPage.jsx +++ b/src/pages/map/MapPage.jsx @@ -172,7 +172,7 @@ const MapPage = () => { onClick={handleSearchClick} className="absolute top-20 sm:top-24 left-1/2 -translate-x-1/2 z-50 w-[90%] max-w-[33.5rem] h-14 sm:h-16 px-4 sm:px-6 flex items-center justify-between bg-white rounded-2xl shadow cursor-pointer" > - 내 주변 가치가게 찾기 + 내 주변 가치가게 찾기 검색 아이콘 { const engValue = reverseBusinessNameMap[cate.name]; onSelect(engValue); }} - className="flex shrink-0 gap-2 items-center pl-3 pr-5 py-1.5 sm:pl-4 sm:pr-6 rounded-full text-b5 sm:text-b3 font-medium text-gray-12 whitespace-nowrap bg-white shadow cursor-pointer" + className="flex shrink-0 gap-2 items-center pl-3 pr-5 py-1.5 sm:pl-4 sm:pr-6 rounded-full b5 sm:b3 text-gray-12 whitespace-nowrap bg-white shadow cursor-pointer" > {
-

+

우리 동네 사회적기업 찾아보기

-

+

사회적기업은, 판매 수익을 사회문제 해결이나 {"\n"}이웃 돕기에 쓰는 특별한 기업들이에요. {"\n"}우리 동네 사회적기업을 찾아보고, 함께 참여해보세요! diff --git a/src/pages/map/components/PlaceContent.jsx b/src/pages/map/components/PlaceContent.jsx index 7c033d2..74807c5 100644 --- a/src/pages/map/components/PlaceContent.jsx +++ b/src/pages/map/components/PlaceContent.jsx @@ -115,21 +115,21 @@ const PlaceContent = ({ place, onToggleLike, showMapLink = true }) => {

-

-
+

+
{companyName} - + {businessTypeNameMap[companyCategory] ?? companyCategory}

-

+

{temperature}도 - + 방문자 리뷰 {reviewCount}

@@ -138,7 +138,7 @@ const PlaceContent = ({ place, onToggleLike, showMapLink = true }) => { {business && (
-
+

{business}

{companyType && ( @@ -148,7 +148,7 @@ const PlaceContent = ({ place, onToggleLike, showMapLink = true }) => { alt={companyType} className="w-5 h-5" /> - + {companyTypeNameMap[companyType]}
@@ -205,7 +205,7 @@ const PlaceContent = ({ place, onToggleLike, showMapLink = true }) => { )} {companyLocation && ( -
+
기업 주소 {
)} {companyTelNum && ( -
+
기업 전화번호 { )} {step === 1 && ( -
+
검색기록 없음 { className="w-6 h-6 mt-1" />
-

+

{place.name}

-

{place.address}

+

{place.address}

@@ -43,7 +43,7 @@ const PlaceCard = ({ place, onClick }) => { /> )}
- {place.formattedDistance} + {place.formattedDistance}
); diff --git a/src/pages/search/components/SearchPlaceList.jsx b/src/pages/search/components/SearchPlaceList.jsx index 6b3a9d5..881058e 100644 --- a/src/pages/search/components/SearchPlaceList.jsx +++ b/src/pages/search/components/SearchPlaceList.jsx @@ -3,7 +3,7 @@ import PlaceCard from "./PlaceCard"; const PlaceList = ({ places, onSelect, showEmptyMessage }) => { if (!places.length) { return showEmptyMessage ? ( -
+
최근 검색 결과 없음 { alt="fire" className="w-4 h-4" /> - + {story.storyLikes || 0}
-

+

{story.storyTitle}

diff --git a/src/pages/support/FinancialProductDetailPage.jsx b/src/pages/support/FinancialProductDetailPage.jsx index d7dc829..7757887 100644 --- a/src/pages/support/FinancialProductDetailPage.jsx +++ b/src/pages/support/FinancialProductDetailPage.jsx @@ -23,7 +23,7 @@ const FinancialProductDetailPage = () => { if (error) { return (
-

데이터를 불러오지 못했습니다.

+

데이터를 불러오지 못했습니다.

); } @@ -31,7 +31,7 @@ const FinancialProductDetailPage = () => { if (!data) { return (
-

+

해당 상품 정보를 찾을 수 없습니다.

@@ -60,50 +60,56 @@ const FinancialProductDetailPage = () => {
- {data.productType && ( - - {data.productType} - - )} + {data.productType && + data.recommendedCategory && + data.defaultCategory && ( + <> + + {data.productType} + + + {data.recommendedCategory} + + + {data.defaultCategory} + + + )}
-

{data.productName}

-

{data.bankName}

+

{data.productName}

+

{data.bankName}

-

- 기본 정보 -

+

기본 정보

-

가입 기간

-

- {data.period || "-"} -

+

가입 기간

+

{data.period || "-"}

-

방식

-

+

방식

+

{data.method || "-"}

-

기본 금리

-

+

기본 금리

+

{data.benefit || "-"}

-

+

금융상품 소개

-

+

{data.productDescription || "상품 설명이 없습니다."}

-
+

• 본 서비스에서 제공하는 상품 정보는 각 기관의 공고를 바탕으로 수집·정리한 참고용 자료입니다. @@ -125,7 +131,7 @@ const FinancialProductDetailPage = () => { className="w-6 h-6" alt="바로가기" /> -

+

바로가기

diff --git a/src/pages/support/FinancialProductListPage.jsx b/src/pages/support/FinancialProductListPage.jsx index 1af984b..74f988f 100644 --- a/src/pages/support/FinancialProductListPage.jsx +++ b/src/pages/support/FinancialProductListPage.jsx @@ -1,13 +1,78 @@ +import { useEffect, useState } from "react"; import RecommendationCard from "@/pages/support/components/RecommendationCard"; import { useNavigate } from "react-router-dom"; -import { useFinancialProducts } from "@/pages/support/hooks/useFinancialProducts"; import Spinner from "@components/common/Spinner"; +import { useFinancialProducts } from "@/pages/support/hooks/useFinancialProducts"; +import { getConsumptionDetail } from "@apis/consumer/getConsumptionDetail"; +import { getMyProfile } from "@apis/member/auth"; +import { KOR_TO_ENUM_MAP } from "@pages/support/constants/consumerMap"; const FinancialProductList = () => { const navigate = useNavigate(); const { data: products, isLoading, error } = useFinancialProducts(); + const [userName, setUserName] = useState(""); + + useEffect(() => { + const fetchProfile = async () => { + try { + const res = await getMyProfile(); + setUserName(res?.name || "사용자"); + } catch (e) { + console.error("프로필 로딩 실패:", e); + } + }; + + fetchProfile(); + }, []); + + const [sortedProducts, setSortedProducts] = useState([]); + + useEffect(() => { + const sortProductsByReviewCount = async () => { + if (!products || products.length === 0) return; + + const uniqueCategories = new Set(); + products.forEach((item) => { + if (item.recommendedCategory) + uniqueCategories.add(item.recommendedCategory); + if (item.defaultCategory) uniqueCategories.add(item.defaultCategory); + }); + + const reviewCountMap = new Map(); + + await Promise.all( + [...uniqueCategories].map(async (korName) => { + const enumKey = KOR_TO_ENUM_MAP[korName]; + if (!enumKey) return; + + try { + const res = await getConsumptionDetail(enumKey); + reviewCountMap.set(korName, res?.reviewCount || 0); + } catch { + reviewCountMap.set(korName, 0); + } + }) + ); + + const sorted = [...products].sort((a, b) => { + const aCount = + reviewCountMap.get(a.recommendedCategory) || + reviewCountMap.get(a.defaultCategory) || + 0; + const bCount = + reviewCountMap.get(b.recommendedCategory) || + reviewCountMap.get(b.defaultCategory) || + 0; + return bCount - aCount; + }); + + setSortedProducts(sorted); + }; + + sortProductsByReviewCount(); + }, [products]); - const safeProducts = Array.isArray(products) ? products : []; + const safeProducts = Array.isArray(sortedProducts) ? sortedProducts : []; return (
@@ -21,15 +86,21 @@ const FinancialProductList = () => { />
-
-

총 {safeProducts.length}개

+
+

+ 총 {safeProducts.length}개 +

+

+ {userName}님이 리뷰를 남긴 기업 특성과 +
연관된 금융상품 순으로 보여드려요! +

- {isLoading && } + {(isLoading || safeProducts.length === 0) && } {error &&

데이터를 불러오지 못했습니다.

}
- {safeProducts.map((item, _) => ( + {safeProducts.map((item) => ( { showDescription={false} productType={item.productType} benefit={item.benefit} + recommendedCategory={item.recommendedCategory} + defaultCategory={item.defaultCategory} /> ))}
diff --git a/src/pages/support/SupportPage.jsx b/src/pages/support/SupportPage.jsx index 5f0b0ba..80ca257 100644 --- a/src/pages/support/SupportPage.jsx +++ b/src/pages/support/SupportPage.jsx @@ -2,8 +2,8 @@ import { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import CompanyTab from "@/pages/support/components/CompanyTab"; import ConsumerTab from "@/pages/support/components/ConsumerTab"; -import HaveToLoginModal from "@components/common/HaveToLoginModal"; import { getMyProfile } from "@apis/member/auth"; +import HaveToLoginModal from "./components/HaveToLoginModal"; const SupportPage = () => { const [searchParams, setSearchParams] = useSearchParams(); diff --git a/src/pages/support/components/ConsumerTab.jsx b/src/pages/support/components/ConsumerTab.jsx index 979046c..fd57efa 100644 --- a/src/pages/support/components/ConsumerTab.jsx +++ b/src/pages/support/components/ConsumerTab.jsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useNavigate } from "react-router-dom"; import ConsumptionChart from "./ConsumptionChart"; import RecommendationCard from "./RecommendationCard"; @@ -5,8 +6,19 @@ import Spinner from "@components/common/Spinner"; import { useConsumptionData } from "../hooks/useConsumptionData"; import { useFinancialProducts } from "../hooks/useFinancialProducts"; import { companyTypeNameMap } from "@constants/categoryMap"; +import { + ENUM_TO_KOR_MAP, + RECOMMEND_MESSAGE_MAP, +} from "../constants/consumerMap"; + +import { Swiper, SwiperSlide } from "swiper/react"; +import { Autoplay, Pagination } from "swiper/modules"; +import "swiper/css"; +import "swiper/css/pagination"; +import "@/styles/swiper.css"; const ConsumerTab = () => { + const [topCategory, setTopCategory] = useState(null); const navigate = useNavigate(); const { data: consumptionData, isLoading, error } = useConsumptionData(); @@ -16,6 +28,16 @@ const ConsumerTab = () => { error: productsError, } = useFinancialProducts(); + const safeProducts = Array.isArray(consumerProducts) ? consumerProducts : []; + + const filteredProducts = topCategory + ? safeProducts.filter( + (item) => + item.recommendedCategory === ENUM_TO_KOR_MAP[topCategory] || + item.defaultCategory === ENUM_TO_KOR_MAP[topCategory] + ) + : safeProducts; + if (isLoading || isProductsLoading) return ; if (error || productsError) return

데이터를 불러오지 못했습니다.

; @@ -25,23 +47,22 @@ const ConsumerTab = () => { value: item.totalPrice, })) || []; - const safeProducts = Array.isArray(consumerProducts) ? consumerProducts : []; - return ( -
+
-

+

{consumptionData?.name || "모락 사용자"}님의 소비 가치

-

소비 가치에 맞는 금융상품

+

추천

navigate("/consumer/list")} @@ -50,22 +71,62 @@ const ConsumerTab = () => { 전체 보기
+ {topCategory && ( +
+ 추천 금융상품 안내 아이콘 +

+ {RECOMMEND_MESSAGE_MAP[ENUM_TO_KOR_MAP[topCategory]]} +

+
+ )} -
- {safeProducts.map((item, _) => ( -
- -
+ + {filteredProducts.map((item) => ( + +
+ +
+
))} -
+ + +
); diff --git a/src/pages/support/components/ConsumptionChart.jsx b/src/pages/support/components/ConsumptionChart.jsx index 199bad8..2a74171 100644 --- a/src/pages/support/components/ConsumptionChart.jsx +++ b/src/pages/support/components/ConsumptionChart.jsx @@ -9,7 +9,7 @@ import { import { getConsumptionDetail } from "@apis/consumer/getConsumptionDetail"; import CustomTooltip from "./CustomTooltip"; -const ConsumptionChart = ({ data, reviewCount }) => { +const ConsumptionChart = ({ data, reviewCount, onTopCategory }) => { const [topReviewSpeech, setTopReviewSpeech] = useState(""); const [topTwo, setTopTwo] = useState([]); const [chartData, setChartData] = useState([]); @@ -51,6 +51,7 @@ const ConsumptionChart = ({ data, reviewCount }) => { reviewCount: typeReviewCount, reviewRatio: typeReviewCount / reviewCount, speech: SPEECH_BUBBLE_MAP[korTypeName], + companyType: consumption?.companyType?.trim(), }; }) ); @@ -64,6 +65,9 @@ const ConsumptionChart = ({ data, reviewCount }) => { ); setTopTwo(ranked.slice(0, 2)); setChartData(ranked.slice(0, 6)); + if (ranked[0]?.companyType && onTopCategory) { + onTopCategory(ranked[0].companyType); + } } } catch (err) { console.error("전체 오류:", err); @@ -78,7 +82,7 @@ const ConsumptionChart = ({ data, reviewCount }) => { return () => { isMounted = false; }; - }, [reviewCount, sortedData]); + }, [reviewCount, sortedData, onTopCategory]); return (
@@ -106,16 +110,16 @@ const ConsumptionChart = ({ data, reviewCount }) => {
-

- +

+ {reviewCount} - +

-

기반

+

기반

@@ -131,7 +135,7 @@ const ConsumptionChart = ({ data, reviewCount }) => {

-
+
{topTwo.map((item, index) => (
{ + const navigate = useNavigate(); + + const handleLogin = () => { + onClose(); + navigate("/auth"); + }; + + return ( +
+
e.stopPropagation()} + > + {showClose && ( + + )} + + 로그인 필요 알림 + +

{message}

+ {subMessage &&

{subMessage}

} + + {showButton && ( + + )} +
+
+ ); +}; + +export default HaveToLoginModal; diff --git a/src/pages/support/components/RecommendationCard.jsx b/src/pages/support/components/RecommendationCard.jsx index 309bc59..f649b9a 100644 --- a/src/pages/support/components/RecommendationCard.jsx +++ b/src/pages/support/components/RecommendationCard.jsx @@ -6,7 +6,8 @@ const RecommendationCard = ({ title, description, productType, - benefit, + recommendedCategory, + defaultCategory, showDescription = true, }) => { const navigate = useNavigate(); @@ -20,8 +21,8 @@ const RecommendationCard = ({ onClick={handleClick} role="button" tabIndex={0} - aria-label={`${title} 상품 상세정보`} - className="relative w-full h-full p-5 rounded-xl bg-white flex flex-col gap-2 cursor-pointer transition-shadow shadow-shadow hover:shadow-md" + aria-label={`${title} 상품 정보`} + className="relative w-full h-full py-5 px-6 bg-white flex flex-col gap-2 cursor-pointer transition-shadow shadow-shadow hover:shadow-md" >
- {productType && ( - - {productType} - + {productType && recommendedCategory && defaultCategory && ( + <> + + {productType} + + + {recommendedCategory} + + + {defaultCategory} + + )}
-

{title}

-

{bank}

+

{title}

+

{bank}

{showDescription && ( -

+

{description}

)} diff --git a/src/pages/support/constants/consumerMap.js b/src/pages/support/constants/consumerMap.js index b92a8b4..1769d88 100644 --- a/src/pages/support/constants/consumerMap.js +++ b/src/pages/support/constants/consumerMap.js @@ -12,7 +12,16 @@ export const SPEECH_BUBBLE_MAP = { 사회서비스제공형: "돌봄과 배려가 필요한 곳에,\n당신의 소비가 닿았어요.", 혼합형: "다양한 사회문제를 동시에 돕는\n멋진 소비를 하셨네요!", "기타(창의ㆍ혁신)형": "사회에 선한 영향을 준\n당신의 소비, 함께 기억할게요.", - 예비: "예비 사회적 기업에\n 선한 영향을 준\n 당신의 소비, 함께 기억할게요.", + 예비형: "예비 사회적 기업에\n 선한 영향을 준\n 당신의 소비, 함께 기억할게요.", +}; + +export const RECOMMEND_MESSAGE_MAP = { + 일자리제공형: "고용을 살린 소비, 이 금융상품과 잘 맞아요", + 지역사회공헌형: "지역을 밝힌 소비, 이 금융상품이 닮았어요", + 사회서비스제공형: "돌봄을 실천한 소비, 이 금융과 함께 이어가보세요", + 혼합형: "다방면의 가치소비, 금융에서도 이어집니다", + "기타(창의ㆍ혁신)형": "특별한 소비, 특별한 금융 제안이에요", + 예비형: "예비 사회적기업까지 알아본 소비, 아래 금융상품을 추천해요", }; export const KOR_TO_ENUM_MAP = { @@ -21,7 +30,7 @@ export const KOR_TO_ENUM_MAP = { 혼합형: "MIXED", "기타(창의ㆍ혁신)형": "ETC", 지역사회공헌형: "COMPANY_CONTRIBUTION", - 예비: "PRE", + 예비형: "PRE", }; export const ENUM_TO_KOR_MAP = { @@ -30,5 +39,5 @@ export const ENUM_TO_KOR_MAP = { MIXED: "혼합형", ETC: "기타(창의ㆍ혁신)형", COMPANY_CONTRIBUTION: "지역사회공헌형", - PRE: "예비", + PRE: "예비형", }; diff --git a/tailwind.config.js b/tailwind.config.js index 94df9d5..ff29322 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -12,6 +12,9 @@ export default { error: "rgba(232, 58, 58, 1)", errorContainer: "rgba(255, 232, 232, 1)", Scrim: "rgba(46, 45, 43, 0.20)", + supportMessage: + "linear-gradient(90deg, rgba(255, 111, 49, 0.20) 0%, rgba(255, 255, 255, 0.20) 100%), #FFF", + gray: { 0: "rgba(255, 255, 255, 1)", 1: "rgba(253, 253, 253, 1)",