Skip to content

Commit 8dbe3d8

Browse files
imddoyseong-hui
andauthored
feat: 멤버 추천 카드 CTA 쪽지 모달 연결 (#2068)
* feat: 멤버 추천 카드 CTA 쪽지 모달 연결 * feat: 홈 배너 추가 * fix: recommendations activity 배열 -> 객체 수정 * feat: 멤버 목록 페이지 밸런스게임 모달 추가 * feat: 배너 정책 변경 반영 * feat: 홈팝업 정책 변경 반영 * fix: WorkPreferneceMemberCard storybook 필드 수정 --------- Co-authored-by: moonseonghui <msh9980@naver.com>
1 parent 93286c5 commit 8dbe3d8

15 files changed

Lines changed: 438 additions & 280 deletions

File tree

1.47 MB
Loading
179 KB
Loading

src/api/endpoint/members/getRecommendations.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,12 @@ export const getRecommendations = createEndpoint({
2828
workPlace: z.string(),
2929
feedbackStyle: z.string(),
3030
}),
31-
activities: z
32-
.array(
33-
z.object({
34-
id: z.number(),
35-
generation: z.number(),
36-
part: z.string(),
37-
team: z.string().nullable(),
38-
}),
39-
)
40-
.optional()
41-
.default([]),
31+
activity: z.object({
32+
id: z.number(),
33+
generation: z.number(),
34+
part: z.string(),
35+
team: z.string().nullable(),
36+
}),
4237
}),
4338
),
4439
}),

src/components/common/Banner/ActiveBannerSlot.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,25 @@ import { FC } from 'react';
44
import AdsBanner from '@/components/common/Banner/AdsBanner';
55
import { ClosingBanner } from '@/components/common/Banner/ClosingBanner';
66
import WelcomeBannerContainer from '@/components/common/Banner/WelcomeBanner/WelcomeBannerContainer';
7+
import BalanceGameBanner from '@/components/common/Banner/BalanceGameBanner';
8+
import { useGetMemberOfMe } from '@/api/endpoint/members/getMemberOfMe';
79
interface ActiveBannerSlotProps {}
810

911
const ActiveBannerSlot: FC<ActiveBannerSlotProps> = ({}) => {
1012
const isTimecapsopOpen = false; // 타임캡솝 오픈 기간에만 이 값을 true로 변경
13+
const { data: myData } = useGetMemberOfMe();
14+
const isBalanceGameOpen = true;
15+
1116
return (
1217
<StyledActiveBanner>
1318
{/* 이 밑에 노출할 배너를 넣으세요. */}
14-
{isTimecapsopOpen ? <WelcomeBannerContainer /> : <AdsBanner />}
19+
{isTimecapsopOpen ? (
20+
<WelcomeBannerContainer />
21+
) : isBalanceGameOpen && myData?.enableWorkPreferenceEvent ? (
22+
<BalanceGameBanner />
23+
) : (
24+
<AdsBanner />
25+
)}
1526
</StyledActiveBanner>
1627
);
1728
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import styled from '@emotion/styled';
2+
import { useRouter } from 'next/router';
3+
import { MB_BASE_MEDIA_QUERY } from '@/styles/mediaQuery';
4+
import matchMemberBannerDesktop from '@/public/icons/img/banner_balancegame_desktop.png';
5+
import matchMemberBannerMobile from '@/public/icons/img/banner_balancegame_mobile.png';
6+
import { playgroundLink } from 'playground-common/export';
7+
8+
const BalanceGameBanner = () => {
9+
const router = useRouter();
10+
11+
return (
12+
<>
13+
<BannerContainer onClick={() => router.push(playgroundLink.memberList())}>
14+
<BannerImageWrapper>
15+
<picture>
16+
<source media={MB_BASE_MEDIA_QUERY} srcSet={matchMemberBannerMobile.src} />
17+
<BannerImage src={matchMemberBannerDesktop.src} alt='매칭 배너' />
18+
</picture>
19+
</BannerImageWrapper>
20+
</BannerContainer>
21+
</>
22+
);
23+
};
24+
25+
export default BalanceGameBanner;
26+
27+
const BannerContainer = styled.header`
28+
display: flex;
29+
flex-direction: column;
30+
align-items: center;
31+
justify-content: center;
32+
margin-bottom: 16px;
33+
width: 100%;
34+
min-height: 168px;
35+
overflow: hidden;
36+
`;
37+
38+
const BannerImageWrapper = styled.div`
39+
@media ${MB_BASE_MEDIA_QUERY} {
40+
width: 100%;
41+
height: auto;
42+
}
43+
`;
44+
45+
const BannerImage = styled.img`
46+
width: 100%;
47+
height: 168px;
48+
object-fit: cover;
49+
50+
@media ${MB_BASE_MEDIA_QUERY} {
51+
width: 100%;
52+
height: auto;
53+
}
54+
`;

src/components/common/HomePopup/HomePopupContainer.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,33 @@ import { useGetHomePopup } from '@/api/endpoint/homePopup/getHomePopup';
22
import { useGetMemberOfMe } from '@/api/endpoint/members/getMemberOfMe';
33
import { HomePopup } from '@/components/common/HomePopup';
44
import useModalState from '@/components/common/Modal/useModalState';
5+
import { useMatchMemberEvent } from '@/components/matchmember/hooks/useMatchMemberEvent';
56
import MatchMemberModal from '@/components/matchmember/MatchMemberModal';
67
import { LATEST_GENERATION } from '@/constants/generation';
78
import { useEffect, useState } from 'react';
89

910
const HomePopupContainer = () => {
1011
const { data: myData } = useGetMemberOfMe();
1112
const { data: homePopupData } = useGetHomePopup();
12-
13+
const { isSpecialPopupPeriod, canOpenModal, handleCloseForToday } = useMatchMemberEvent();
1314
const isLastGeneration = myData?.generation === LATEST_GENERATION;
1415

15-
const hasWorkPreference = myData?.hasWorkPreference ?? false;
16-
const isSpecialPopupPeriod = true; // 이벤트용 팝업 오픈
1716
const { isOpen, onOpen, onClose } = useModalState();
18-
const [isQA, setIsQA] = useState(false);
19-
const shouldShowMatchModal =
20-
isQA ||
21-
(myData && isSpecialPopupPeriod && isLastGeneration && !hasWorkPreference && myData.enableWorkPreferenceEvent);
22-
useEffect(() => {
23-
setIsQA(localStorage.getItem('BALANCEGAME_OPEN') === 'true');
24-
}, []);
2517
useEffect(() => {
26-
if (shouldShowMatchModal) {
18+
if (canOpenModal && isSpecialPopupPeriod) {
2719
onOpen();
2820
}
29-
}, [shouldShowMatchModal]);
21+
}, [canOpenModal, isSpecialPopupPeriod]);
3022
if (isSpecialPopupPeriod) {
31-
if (!shouldShowMatchModal) return null;
32-
return <MatchMemberModal isOpen={isOpen} onClose={onClose} />;
23+
if (!canOpenModal) return null;
24+
return (
25+
<MatchMemberModal
26+
isOpen={isOpen}
27+
onClose={onClose}
28+
handleCloseForToday={handleCloseForToday}
29+
hasWorkPreference={myData?.hasWorkPreference}
30+
/>
31+
);
3332
}
3433

3534
if (!homePopupData || typeof homePopupData === 'string') {

src/components/matchmember/MatchContent.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,30 @@ import { usePostWorkPreferenceMutation } from '@/api/endpoint/members/postWorkPr
1818
import Loading from '@/components/common/Loading';
1919
import { LoggingClick } from '@/components/eventLogger/components/LoggingClick';
2020
import useEventLogger from '@/components/eventLogger/hooks/useEventLogger';
21-
import { LoggingImpression } from '@/components/eventLogger/components/LoggingImpression';
2221

2322
interface MatchContentProps {
2423
step: number;
2524
value: BalanceGameValue;
2625
onChange: (key: QuestionKey, side: ChoiceSide) => void;
2726
onNextStep: () => void;
27+
handleClose: () => void;
28+
handleCloseForToday: () => void;
2829
}
2930

30-
export const MatchContent = ({ step, value, onChange, onNextStep }: MatchContentProps) => {
31+
export const MatchContent = ({
32+
step,
33+
value,
34+
onChange,
35+
onNextStep,
36+
handleClose,
37+
handleCloseForToday,
38+
}: MatchContentProps) => {
3139
const router = useRouter();
3240
const { mutateAsync, isPending } = usePostWorkPreferenceMutation();
3341
const isValid = QUESTIONS.every((q) => value[q.key] !== undefined);
3442
const { logSubmitEvent } = useEventLogger();
3543

36-
if (step === 1)
44+
if (step === 1) {
3745
return (
3846
<>
3947
<Text typography='SUIT_14_SB' color='#FFCA00'>
@@ -50,8 +58,19 @@ export const MatchContent = ({ step, value, onChange, onNextStep }: MatchContent
5058
5초만에 소울메이트 찾기
5159
</Button>
5260
</LoggingClick>
61+
<CloseForTodayButton
62+
onClick={() => {
63+
handleCloseForToday();
64+
handleClose();
65+
}}
66+
>
67+
<Text typography='SUIT_14_SB' color={colors.gray200}>
68+
다시 보지 않기
69+
</Text>
70+
</CloseForTodayButton>
5371
</>
5472
);
73+
}
5574

5675
if (step === 2) {
5776
const handleSubmit = async () => {
@@ -118,7 +137,7 @@ export const MatchContent = ({ step, value, onChange, onNextStep }: MatchContent
118137
<Spacing size={24} />
119138
<LoggingClick eventKey='newmember'>
120139
<Button size='lg' onClick={() => router.push(playgroundLink.memberList())}>
121-
나랑 찰떡 멤버 더 찾아보기
140+
다른 찰떡 멤버도 확인하기
122141
</Button>
123142
</LoggingClick>
124143
</>
@@ -142,3 +161,9 @@ const QuestionRow = styled.div`
142161
flex: 1;
143162
}
144163
`;
164+
165+
const CloseForTodayButton = styled.button`
166+
margin-top: 16px;
167+
text-decoration: underline;
168+
color: ${colors.gray200};
169+
`;

src/components/matchmember/MatchMemberModal.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,58 @@ import { LoggingImpression } from '@/components/eventLogger/components/LoggingIm
1717
interface MatchMemberModalProps {
1818
onClose: () => void;
1919
isOpen: boolean;
20+
handleCloseForToday: () => void;
21+
hasWorkPreference?: boolean;
2022
}
2123

22-
const MatchMemberModal = ({ onClose, isOpen }: MatchMemberModalProps) => {
23-
const router = useRouter();
24+
const MatchMemberModal = ({ onClose, isOpen, handleCloseForToday, hasWorkPreference }: MatchMemberModalProps) => {
2425
const [step, setStep] = useState(1);
2526
const [value, setValue] = useState<BalanceGameValue>({});
2627

2728
const handleClose = () => {
2829
onClose();
29-
router.push(playgroundLink.feedList());
3030
};
3131

3232
const handleChange = (key: QuestionKey, side: ChoiceSide) => {
3333
setValue((prev) => ({ ...prev, [key]: side }));
3434
};
3535

3636
const handleNextStep = () => {
37-
setStep((prev) => prev + 1);
37+
if (hasWorkPreference) {
38+
setStep(3);
39+
} else {
40+
setStep((prev) => prev + 1);
41+
}
3842
};
3943

4044
return (
4145
<LoggingImpression eventKey='balancegame'>
4246
<Responsive only='desktop'>
4347
<StyledModal isOpen={isOpen} onClose={handleClose} zIndex={zIndex.헤더 + 100} step={step}>
4448
<StyledContent>
45-
<MatchContent step={step} value={value} onChange={handleChange} onNextStep={handleNextStep} />
49+
<MatchContent
50+
step={step}
51+
value={value}
52+
onChange={handleChange}
53+
onNextStep={handleNextStep}
54+
handleClose={handleClose}
55+
handleCloseForToday={handleCloseForToday}
56+
/>
4657
</StyledContent>
4758
</StyledModal>
4859
</Responsive>
4960

5061
<Responsive only='mobile'>
5162
<StyledModalBottomSheet isOpen={isOpen} onClose={handleClose} step={step}>
5263
<StyledContent>
53-
<MatchContent step={step} value={value} onChange={handleChange} onNextStep={handleNextStep} />
64+
<MatchContent
65+
step={step}
66+
value={value}
67+
onChange={handleChange}
68+
onNextStep={handleNextStep}
69+
handleClose={handleClose}
70+
handleCloseForToday={handleCloseForToday}
71+
/>
5472
</StyledContent>
5573
</StyledModalBottomSheet>
5674
</Responsive>

src/components/matchmember/MemberCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const MemberCard = () => {
7575
</ImageBox>
7676
<Tag>
7777
<Circle />
78-
{member.activities[0].generation}{member.activities[0].part}
78+
{member.activity?.generation}{member.activity?.part}
7979
</Tag>
8080
<InfoBox>
8181
<Text typography='SUIT_20_SB' color={colors.gray10}>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
3+
import { useGetMemberOfMe } from '@/api/endpoint/members/getMemberOfMe';
4+
import { LATEST_GENERATION } from '@/constants/generation';
5+
6+
export const useMatchMemberEvent = () => {
7+
const { data: myData } = useGetMemberOfMe();
8+
const [isQA, setIsQA] = useState(false);
9+
const [isClosedToday, setIsClosedToday] = useState(false);
10+
11+
const getKoreanDate = (): string => {
12+
const koreanTime = new Date();
13+
return `${koreanTime.getFullYear()}-${String(koreanTime.getMonth() + 1).padStart(2, '0')}-${String(
14+
koreanTime.getDate(),
15+
).padStart(2, '0')}`;
16+
};
17+
18+
useEffect(() => {
19+
setIsQA(typeof window !== 'undefined' && localStorage.getItem('BALANCEGAME_OPEN') === 'true');
20+
}, []);
21+
22+
const isLastGeneration = myData?.generation === LATEST_GENERATION;
23+
const hasWorkPreference = myData?.hasWorkPreference ?? false;
24+
const isSpecialPopupPeriod = true;
25+
26+
const handleCloseForToday = useCallback(() => {
27+
const today = getKoreanDate();
28+
if (typeof window !== 'undefined') {
29+
localStorage.setItem('popupClosedDate', today);
30+
}
31+
setIsClosedToday(true);
32+
}, []);
33+
34+
useEffect(() => {
35+
setIsQA(typeof window !== 'undefined' && localStorage.getItem('BALANCEGAME_OPEN') === 'true');
36+
if (typeof window !== 'undefined') {
37+
const storedDate = localStorage.getItem('popupClosedDate');
38+
const today = getKoreanDate();
39+
40+
if (storedDate === today) {
41+
setIsClosedToday(true);
42+
} else {
43+
setIsClosedToday(false);
44+
}
45+
}
46+
}, []);
47+
48+
const canOpenModal =
49+
!!myData &&
50+
!isClosedToday &&
51+
(isQA || (isSpecialPopupPeriod && isLastGeneration && !hasWorkPreference && myData.enableWorkPreferenceEvent));
52+
53+
return {
54+
hasWorkPreference,
55+
isSpecialPopupPeriod,
56+
canOpenModal,
57+
handleCloseForToday,
58+
};
59+
};

0 commit comments

Comments
 (0)