Skip to content
Merged
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
971 changes: 451 additions & 520 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

308 changes: 268 additions & 40 deletions src/app/Intro.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
import { appLogin } from "@apps-in-toss/web-framework";
import { getScreenings } from "@features/survey/service/surveyParticipation/api";
import { mapOngoingSurveySummaryToSurveyListItem } from "@features/survey-list/hooks/useProcessedOngoingSurveys";
import { useSurveyOpenStats } from "@features/survey-list/hooks/useSurveyOpenStats";
import { getAllOngoingSurveys } from "@features/survey-list/service/surveyList";
import type { OngoingSurveySummary } from "@features/survey-list/service/surveyList/types";
import { queryClient } from "@shared/contexts/queryClient";
import { pushGtmEvent } from "@shared/lib/gtm";
import { trackEvent } from "@shared/lib/mixpanel";
import { saveTokens } from "@shared/lib/tokenManager";
import { sendUserInfoEvent } from "@shared/lib/userInfoEvent";
import { loginApi } from "@shared/service/login";
import { getMemberInfo } from "@shared/service/userInfo/api";
import type { LocationStateWithReturnTo } from "@shared/types/navigation";
import { colors } from "@toss/tds-colors";
import { Asset, FixedBottomCTA, StepperRow, Top } from "@toss/tds-mobile";
import { adaptive } from "@toss/tds-colors";
import { Asset, Button, FixedBottomCTA, Text } from "@toss/tds-mobile";
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";

/** 세그먼트 조건을 만족하는 설문만 대상으로 최고 프로모션가 설문 선택 */
function pickHighestPriceAmongEligible(
list: OngoingSurveySummary[],
): OngoingSurveySummary | undefined {
const eligible = list.filter((s) => s.isEligible ?? false);
if (eligible.length === 0) return undefined;
const maxPrice = Math.max(...eligible.map((s) => s.price ?? 0));
const tied = eligible.filter((s) => (s.price ?? 0) === maxPrice);
tied.sort((a, b) => a.surveyId - b.surveyId);
return tied[0];
}

export const Intro = () => {
const navigate = useNavigate();
const location = useLocation();
const returnTo = (location.state as LocationStateWithReturnTo)?.returnTo;

const { data: openStats, isPending: isOpenStatsPending } =
useSurveyOpenStats();

const openSurveyCount = openStats?.openSurveyCount ?? 0;
const maxRewardCoin = openStats?.maxRewardCoin ?? 0;

const hasOngoingSurvey = !isOpenStatsPending && openSurveyCount > 0;

const surveyPromoText = `${maxRewardCoin.toLocaleString()}코인 설문 ${openSurveyCount}개 오픈,\n바로 설문에 참여해 보세요!`;

// 이전에 로그인한 사용자인지 확인
useEffect(() => {
const checkAuth = async () => {
Expand Down Expand Up @@ -79,46 +108,245 @@ export const Intro = () => {
}
};

return (
<section className="flex flex-col w-full mx-auto">
<Top
title={
<Top.TitleParagraph size={22} color={colors.grey900}>
설문조사 참여하고 포인트 받아가세요
</Top.TitleParagraph>
const navigateToSurveyInfo = (
surveyItem: ReturnType<typeof mapOngoingSurveySummaryToSurveyListItem>,
) => {
pushGtmEvent({
event: "survey_start",
pagePath: "/survey",
survey_id: String(surveyItem.id),
source: "intro",
});
trackEvent("Survey Started", {
pagePath: "/survey",
surveyId: String(surveyItem.id),
source: "intro",
hasScreening: false,
});
const searchParams = new URLSearchParams({ surveyId: surveyItem.id });
navigate(
{
pathname: "/survey",
search: `?${searchParams.toString()}`,
},
{
replace: true,
state: { surveyId: surveyItem.id, survey: surveyItem },
},
);
};

const handleParticipateLogin = async () => {
pushGtmEvent({
event: "login",
pagePath: "/intro",
method: "로그인 수단 (Toss)",
});

try {
const { authorizationCode, referrer } = await appLogin();
const loginApiResponse = await loginApi(authorizationCode, referrer);
if (!loginApiResponse.accessToken || !loginApiResponse.refreshToken) {
return;
}

await saveTokens(
loginApiResponse.accessToken,
loginApiResponse.refreshToken,
);
void sendUserInfoEvent("Toss");

let targetSummary: OngoingSurveySummary | undefined;
try {
const result = await getAllOngoingSurveys({
lastSurveyId: 0,
size: 100,
});
const surveys = result.surveys ?? [];
targetSummary = pickHighestPriceAmongEligible(surveys);
} catch (error) {
console.error("진행 설문 목록 조회 실패:", error);
navigate("/home", { replace: true });
return;
}

if (!targetSummary) {
navigate("/home", { replace: true });
return;
}
Comment thread
chldsbdud marked this conversation as resolved.

const summary = targetSummary;

void queryClient.invalidateQueries({ queryKey: ["allOngoingSurveys"] });

const surveyItem = mapOngoingSurveySummaryToSurveyListItem(summary);
let hasScreening = false;
let screeningLookupFailed = false;
try {
const screeningsData = await getScreenings({
lastSurveyId: 0,
size: 100,
});
hasScreening = (screeningsData.data ?? []).some(
(s) => s.surveyId === summary.surveyId,
);
} catch (error) {
console.error("스크리닝 목록 조회 실패:", error);
screeningLookupFailed = true;
}

if (screeningLookupFailed) {
navigate("/home", { replace: true });
return;
}

const surveyReturnTo = hasScreening
? {
path: `/oxScreening?surveyId=${surveyItem.id}`,
state: undefined,
}
: {
path: `/survey?surveyId=${surveyItem.id}`,
state: {
surveyId: surveyItem.id,
survey: surveyItem,
} as Record<string, unknown>,
};

if (loginApiResponse.onboardingCompleted) {
if (hasScreening) {
navigate(`/oxScreening?surveyId=${surveyItem.id}`, {
replace: true,
});
} else {
navigateToSurveyInfo(surveyItem);
Comment thread
chldsbdud marked this conversation as resolved.
}
/>
<div className="flex justify-center items-center my-10">
<Asset.Image
frameShape={{ width: 160, height: 160 }}
backgroundColor="transparent"
src="https://static.toss.im/ml-product/typing-laptop-apng.png"
aria-hidden={true}
/>
</div>
<StepperRow
left={<StepperRow.NumberIcon number={1} />}
center={
<StepperRow.Texts
type="B"
title="설문조사에 참여해요"
description=""
} else {
navigate("/onboarding", {
replace: true,
state: { returnTo: surveyReturnTo },
});
}
} catch (error) {
console.error("토스 로그인 실패:", error);
}
};
Comment thread
chldsbdud marked this conversation as resolved.

return (
<section
className="flex flex-col w-full mx-auto min-h-screen justify-center"
style={{
background:
"linear-gradient(to bottom, #FFFFFF 0%, #FFFFFF 15%, #C4E4D8 65%, #FFFFFF 100%)",
}}
>
<div
className={`px-4 ${hasOngoingSurvey ? "my-8" : "flex-1 flex flex-col justify-center"}`}
>
<div className="flex justify-center items-center mb-6">
<Asset.Image
frameShape={{ width: 160, height: 160 }}
backgroundColor="transparent"
src="https://static.toss.im/ml-product/typing-laptop-apng.png"
aria-hidden={true}
/>
}
/>
<StepperRow
left={<StepperRow.NumberIcon number={2} />}
center={
<StepperRow.Texts type="B" title="성실하게 응답하고" description="" />
}
/>
<StepperRow
left={<StepperRow.NumberIcon number={3} />}
center={
<StepperRow.Texts type="B" title="포인트를 적립해요" description="" />
}
hideLine={true}
/>
</div>

<Text
display="block"
color={adaptive.grey800}
typography="st5"
fontWeight="bold"
textAlign="center"
>
설문 통합 플랫폼 온서베이
</Text>

<div className="mt-6 grid grid-cols-3 gap-3">
<div className="rounded-3xl! bg-white px-3 py-5 flex flex-col items-center gap-3">
<Asset.Icon
frameShape={Asset.frameShape.CleanW40}
backgroundColor="transparent"
name="icon-document-folder-yellow-check"
aria-hidden={true}
ratio="1/1"
/>
<Text
display="block"
color={adaptive.grey800}
typography="t5"
fontWeight="semibold"
textAlign="center"
>
설문 업로드 시 빠른 응답
</Text>
</div>
<div className="rounded-3xl bg-white px-3 py-5 flex flex-col items-center gap-3">
<Asset.Icon
frameShape={Asset.frameShape.CleanW40}
backgroundColor="transparent"
name="icon-money-bag-green-weak"
aria-hidden={true}
ratio="1/1"
/>
<Text
display="block"
color={adaptive.grey800}
typography="t5"
fontWeight="semibold"
textAlign="center"
>
설문 참여 시 리워드
</Text>
</div>
<div className="rounded-3xl bg-white px-3 py-5 flex flex-col items-center gap-3">
<Asset.Image
frameShape={Asset.frameShape.CleanW40}
backgroundColor="transparent"
src="https://static.toss.im/2d-emojis/png/4x/u1F517.png"
aria-hidden={true}
style={{ aspectRatio: "1/1" }}
/>
<Text
display="block"
color={adaptive.grey800}
typography="t5"
fontWeight="semibold"
textAlign="center"
>
부담없는 구글폼 변환
</Text>
</div>
</div>

{hasOngoingSurvey && (
<div
className="mt-6 relative rounded-3xl bg-white px-5 py-5 flex items-center justify-between gap-4"
style={{ boxShadow: "0 8px 20px #D7EDE4" }}
>
<Text
color={adaptive.grey700}
typography="t6"
fontWeight="semibold"
>
{surveyPromoText}
</Text>
<Button
variant="weak"
size="medium"
style={
{
"--button-background-color": "#E8F8F0",
"--button-text-color": "#15c67f",
} as React.CSSProperties
}
onClick={handleParticipateLogin}
>
참여하기
</Button>
</div>
)}
</div>

<FixedBottomCTA
loading={false}
Expand Down
21 changes: 16 additions & 5 deletions src/features/screening/components/IneligibleSurveyBottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,38 @@ import { useNavigate } from "react-router-dom";
interface IneligibleSurveyBottomSheetProps {
open: boolean;
onClose: () => void;
/** 미입력 시 목록에서 단일 설문 비대상 안내 문구 */
title?: string;
description?: string;
/** false면 확인 시 홈으로 이동하지 않고 닫기만 (인트로 등) */
confirmNavigatesHome?: boolean;
}

const DEFAULT_TITLE = "해당 설문에 참여할 수 없어요";
const DEFAULT_DESCRIPTION = "조건이 맞지 않아 설문 참여가 불가능해요";

export const IneligibleSurveyBottomSheet = ({
open,
onClose,
title = DEFAULT_TITLE,
description = DEFAULT_DESCRIPTION,
confirmNavigatesHome = true,
}: IneligibleSurveyBottomSheetProps) => {
const navigate = useNavigate();

const handleConfirm = () => {
if (confirmNavigatesHome) {
navigate("/home");
}
onClose();
navigate("/home");
};

return (
<BottomSheet
header={
<BottomSheet.Header>해당 설문에 참여할 수 없어요</BottomSheet.Header>
}
header={<BottomSheet.Header>{title}</BottomSheet.Header>}
headerDescription={
<BottomSheet.HeaderDescription>
조건이 맞지 않아 설문 참여가 불가능해요
{description}
</BottomSheet.HeaderDescription>
}
open={open}
Expand Down
3 changes: 2 additions & 1 deletion src/features/survey-list/hooks/useAllOngoingSurveys.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { getAllOngoingSurveys } from "../service/surveyList";

export const useAllOngoingSurveys = () => {
export const useAllOngoingSurveys = (options?: { enabled?: boolean }) => {
return useQuery({
queryKey: ["allOngoingSurveys"],
queryFn: () => getAllOngoingSurveys({ lastSurveyId: 0, size: 100 }), // 충분히 큰 size로 모든 설문 가져오기
enabled: options?.enabled ?? true,
});
};
Loading
Loading