Skip to content

Commit a3ce753

Browse files
authored
Merge pull request #624 from depromeet/develop
배포용 PR
2 parents 0c7cc1d + 2e4b13a commit a3ce753

18 files changed

+791
-517
lines changed

src/apis/notifications.ts

+15
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,26 @@ const NOTIFICATION_APIS = {
1717
},
1818
};
1919

20+
const NOTIFICATION_MISSIONS_APIS = {
21+
// POST /notifications/missions/remind - 미션 알림
22+
remind: (seconds: number) => {
23+
return apiInstance.post('/notifications/missions/remind', { seconds });
24+
},
25+
};
26+
2027
export default NOTIFICATION_APIS;
28+
export { NOTIFICATION_MISSIONS_APIS };
2129

2230
export const useNotifyUrging = (options?: UseMutationOptions<unknown, unknown, NotifyUrgingRequest>) => {
2331
return useMutationHandleError({
2432
mutationFn: NOTIFICATION_APIS.notifyUrging,
2533
...options,
2634
});
2735
};
36+
37+
export const useRemind = (options?: UseMutationOptions<unknown, unknown, number>) => {
38+
return useMutationHandleError({
39+
mutationFn: NOTIFICATION_MISSIONS_APIS.remind,
40+
...options,
41+
});
42+
};

src/app/feed/FeedItem.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ const missionNameCss = css({
140140
const remarkCss = css({
141141
textStyle: 'body2',
142142
color: 'text.primary',
143+
whiteSpace: 'pre-wrap',
144+
wordBreak: 'break-word',
143145
});
144146

145147
const captionCss = css({

src/app/guest/mission/stopwatch/page.tsx

+13-11
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,30 @@ import Header from '@/components/Header/Header';
88
import Stopwatch from '@/components/Stopwatch/Stopwatch';
99
import { EVENT_LOG_CATEGORY, EVENT_LOG_NAME } from '@/constants/eventLog';
1010
import { ROUTER } from '@/constants/router';
11-
import useStopwatch from '@/hooks/mission/stopwatch/useStopwatch';
12-
import useStopwatchStatus from '@/hooks/mission/stopwatch/useStopwatchStatus';
11+
import useStopwatchLogic from '@/hooks/mission/stopwatch/useStopwatchLogic';
12+
import useStopwatchStatus, { StopwatchStep } from '@/hooks/mission/stopwatch/useStopwatchStatus';
1313
import useModal from '@/hooks/useModal';
1414
import useSearchParamsTypedValue from '@/hooks/useSearchParamsTypedValue';
1515
import { eventLogger } from '@/utils';
16+
import { formatMMSS } from '@/utils/time';
1617
import { css } from '@styled-system/css';
1718

18-
const GUEST_MISSION_ID = '';
19-
2019
export default function GuestMissionStopwatchPage() {
2120
const router = useRouter();
2221
const category = useGetCategory();
2322

2423
const { step, prevStep, stepLabel, onNextStep } = useStopwatchStatus();
25-
const { seconds, minutes, stepper } = useStopwatch(step, GUEST_MISSION_ID);
24+
const { second } = useStopwatchLogic({ status: step });
25+
26+
const { formattedMinutes: minutes, formattedSeconds: seconds } = formatMMSS(second);
27+
const stepper = second < 60 ? 0 : Math.floor(second / 60 / 10);
2628

2729
const { isOpen, openModal, closeModal } = useModal();
2830

2931
const onFinishButtonClick = () => {
3032
eventLogger.logEvent(EVENT_LOG_NAME.STOPWATCH.CLICK_FINISH_BUTTON, EVENT_LOG_CATEGORY.STOPWATCH, { category });
3133
openModal();
32-
onNextStep('stop');
34+
onNextStep(StopwatchStep.stop);
3335
};
3436

3537
const onFinish = () => {
@@ -56,15 +58,15 @@ export default function GuestMissionStopwatchPage() {
5658
stopTime: Number(minutes) * 60 + Number(seconds),
5759
isGuest: true,
5860
});
59-
onNextStep('stop');
61+
onNextStep(StopwatchStep.stop);
6062
};
6163

6264
const onStart = () => {
6365
eventLogger.logEvent(EVENT_LOG_NAME.STOPWATCH.CLICK_START, EVENT_LOG_CATEGORY.STOPWATCH, {
6466
category,
6567
isGuest: true,
6668
});
67-
onNextStep('progress');
69+
onNextStep(StopwatchStep.progress);
6870
};
6971

7072
return (
@@ -90,14 +92,14 @@ export default function GuestMissionStopwatchPage() {
9092
/>
9193
</section>
9294
<section className={buttonContainerCss}>
93-
{step === 'ready' && (
95+
{step === StopwatchStep.ready && (
9496
<div className={fixedButtonContainerCss}>
9597
<Button variant="primary" size="large" type="button" onClick={onStart}>
9698
시작
9799
</Button>
98100
</div>
99101
)}
100-
{step === 'progress' && (
102+
{step === StopwatchStep.progress && (
101103
<>
102104
<Button size="medium" variant="secondary" type="button" onClick={onStop}>
103105
일시 정지
@@ -109,7 +111,7 @@ export default function GuestMissionStopwatchPage() {
109111
)}
110112
{step === 'stop' && (
111113
<>
112-
<Button size="medium" variant="secondary" type="button" onClick={() => onNextStep('progress')}>
114+
<Button size="medium" variant="secondary" type="button" onClick={() => onNextStep(StopwatchStep.progress)}>
113115
다시 시작
114116
</Button>
115117
<Button size="medium" variant="primary" type="button" onClick={onFinishButtonClick}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { NOTIFICATION_MISSIONS_APIS } from '@/apis/notifications';
5+
import Button from '@/components/Button/Button';
6+
import { EVENT_LOG_CATEGORY, EVENT_LOG_NAME } from '@/constants/eventLog';
7+
import { StopwatchStep } from '@/hooks/mission/stopwatch/useStopwatchStatus';
8+
import { eventLogger } from '@/utils';
9+
import {
10+
checkPrevProgressMission,
11+
getPrevProgressMissionStatus,
12+
getProgressMissionTime,
13+
setMissionData,
14+
setMissionTimeStack,
15+
} from '@/utils/storage/progressMission';
16+
import { css, cx } from '@styled-system/css';
17+
18+
import { useVisibilityStateVisible } from './index.hooks';
19+
import { useStopwatchModalContext } from './Modal.context';
20+
import { useStopwatchStepContext, useStopwatchTimeContext } from './Stopwatch.context';
21+
22+
function ButtonSection({ missionId }: { missionId: string }) {
23+
const { step } = useStopwatchStepContext();
24+
const { minutes, time } = useStopwatchTimeContext();
25+
26+
const logData = { finishTime: time };
27+
28+
const { onInitStart, onMidStart, onStop, onMidOut, onFinish, onRestart } = useStopwatch(missionId);
29+
const { isPending: isStopwatchPending } = useInitTimeSetting({ missionId });
30+
31+
const onStartButtonClick = () => {
32+
if (time > 0) {
33+
onMidStart();
34+
eventLogger.logEvent(EVENT_LOG_NAME.STOPWATCH.CLICK_RESTART, EVENT_LOG_CATEGORY.STOPWATCH);
35+
return;
36+
}
37+
onInitStart();
38+
eventLogger.logEvent(EVENT_LOG_NAME.STOPWATCH.CLICK_START, EVENT_LOG_CATEGORY.STOPWATCH);
39+
40+
NOTIFICATION_MISSIONS_APIS.remind(600);
41+
};
42+
43+
const onStopButtonClick = () => {
44+
onStop();
45+
eventLogger.logEvent(EVENT_LOG_NAME.STOPWATCH.CLICK_STOP, EVENT_LOG_CATEGORY.STOPWATCH, {
46+
stopTime: time,
47+
});
48+
};
49+
50+
const onRestartButtonClick = () => {
51+
onRestart();
52+
eventLogger.logEvent(EVENT_LOG_NAME.STOPWATCH.CLICK_RESTART, EVENT_LOG_CATEGORY.STOPWATCH);
53+
};
54+
55+
const onFinishButtonClick = () => {
56+
if (Number(minutes) < 10) {
57+
eventLogger.logEvent(
58+
EVENT_LOG_NAME.STOPWATCH.CLICK_FINISH_BUTTON_BEFORE_10MM,
59+
EVENT_LOG_CATEGORY.STOPWATCH,
60+
logData,
61+
);
62+
onMidOut();
63+
return;
64+
}
65+
eventLogger.logEvent(EVENT_LOG_NAME.STOPWATCH.CLICK_FINISH_BUTTON, EVENT_LOG_CATEGORY.STOPWATCH, logData);
66+
onFinish();
67+
};
68+
69+
return (
70+
<section className={cx(buttonContainerCss, opacityAnimation)}>
71+
{step === StopwatchStep.ready && (
72+
<div className={fixedButtonContainerCss}>
73+
<Button
74+
variant="primary"
75+
size="large"
76+
type="button"
77+
onClick={onStartButtonClick}
78+
disabled={isStopwatchPending}
79+
>
80+
시작
81+
</Button>
82+
</div>
83+
)}
84+
{step === 'progress' && (
85+
<>
86+
<Button size="medium" variant="secondary" type="button" onClick={onStopButtonClick}>
87+
일시 정지
88+
</Button>
89+
<Button size="medium" variant="primary" type="button" onClick={onFinishButtonClick}>
90+
끝내기
91+
</Button>
92+
</>
93+
)}
94+
{step === 'stop' && (
95+
<>
96+
<Button size="medium" variant="secondary" type="button" onClick={onRestartButtonClick}>
97+
다시 시작
98+
</Button>
99+
<Button size="medium" variant="primary" type="button" onClick={onFinishButtonClick}>
100+
끝내기
101+
</Button>
102+
</>
103+
)}
104+
</section>
105+
);
106+
}
107+
108+
export default ButtonSection;
109+
110+
const useStopwatch = (missionId: string) => {
111+
const { onNextStep } = useStopwatchStepContext();
112+
const { openMidOutModal, openFinalModal } = useStopwatchModalContext();
113+
114+
const startAction = () => {
115+
onNextStep(StopwatchStep.progress);
116+
117+
// 이전 미션 기록 삭제 - 강제 접근 이슈
118+
checkPrevProgressMission(missionId);
119+
setMissionTimeStack(missionId, 'start');
120+
};
121+
122+
const onInitStart = () => {
123+
startAction();
124+
setMissionData(missionId);
125+
};
126+
127+
const onMidStart = () => {
128+
startAction();
129+
};
130+
131+
const onStop = () => {
132+
onNextStep(StopwatchStep.stop);
133+
setMissionTimeStack(missionId, 'stop');
134+
};
135+
136+
const onRestart = () => {
137+
setMissionTimeStack(missionId, 'restart');
138+
onNextStep(StopwatchStep.progress);
139+
};
140+
141+
const onMidOut = () => {
142+
onNextStep(StopwatchStep.stop);
143+
openMidOutModal();
144+
};
145+
146+
const onFinish = () => {
147+
onNextStep(StopwatchStep.stop);
148+
openFinalModal();
149+
};
150+
151+
return {
152+
onInitStart,
153+
onMidStart,
154+
onStop,
155+
onMidOut,
156+
onFinish,
157+
onRestart,
158+
};
159+
};
160+
161+
const buttonContainerCss = css({
162+
margin: '28px auto',
163+
display: 'flex',
164+
justifyContent: 'center',
165+
gap: '12px',
166+
});
167+
168+
const opacityAnimation = css({
169+
animation: 'fadeIn .7s',
170+
});
171+
172+
const fixedButtonContainerCss = css({
173+
position: 'fixed',
174+
left: '16px',
175+
right: '16px',
176+
bottom: '16px',
177+
width: '100%',
178+
maxWidth: 'calc(475px - 48px)',
179+
margin: '0 auto',
180+
'@media (max-width: 475px)': {
181+
maxWidth: 'calc(100vw - 48px)',
182+
},
183+
});
184+
185+
const MAX_SECONDS = 3600; // max 1 hour
186+
187+
const useInitTimeSetting = ({ missionId }: { missionId: string }) => {
188+
const { onNextStep } = useStopwatchStepContext();
189+
const { setTime: setSecond } = useStopwatchTimeContext();
190+
191+
const [isPending, setIsPending] = useState(true);
192+
const settingInitTime = () => {
193+
const initSeconds = getProgressMissionTime(missionId);
194+
195+
if (!initSeconds) return false;
196+
if (initSeconds >= MAX_SECONDS) {
197+
setSecond(MAX_SECONDS);
198+
} else {
199+
setSecond(initSeconds);
200+
}
201+
return true;
202+
};
203+
204+
// 화면 visible 상태로 변경 시, 시간을 다시 세팅
205+
useVisibilityStateVisible(() => {
206+
setIsPending(true);
207+
settingInitTime();
208+
setIsPending(false);
209+
});
210+
211+
useEffect(() => {
212+
// 해당 미션을 이어 가는 경우. init time setting
213+
const isSettingInit = settingInitTime();
214+
setIsPending(false);
215+
if (!isSettingInit) return;
216+
217+
const prevStatus = getPrevProgressMissionStatus(missionId);
218+
prevStatus && onNextStep?.(prevStatus); // 바로 재시작
219+
}, []);
220+
221+
return { isPending };
222+
};
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Header from '@/components/Header/Header';
2+
import { StopwatchStep } from '@/hooks/mission/stopwatch/useStopwatchStatus';
3+
4+
import { useStopwatchModalContext } from './Modal.context';
5+
import { useStopwatchStepContext } from './Stopwatch.context';
6+
7+
function StopwatchHeader() {
8+
const { onNextStep } = useStopwatchStepContext();
9+
const { openBackModal } = useStopwatchModalContext();
10+
11+
const onBackAction = () => {
12+
onNextStep(StopwatchStep.stop);
13+
openBackModal();
14+
};
15+
16+
return <Header rightAction="none" onBackAction={onBackAction} />;
17+
}
18+
19+
export default StopwatchHeader;

0 commit comments

Comments
 (0)