Skip to content

Commit 038053e

Browse files
Merge pull request #205 from SOPT-36-NINEDOT/refactor/#204/aiFailModal
2 parents cf41fac + a020c9e commit 038053e

File tree

7 files changed

+91
-53
lines changed

7 files changed

+91
-53
lines changed

src/common/component/AiFailModal/AiFailModal.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Button from '@/common/component/Button/Button';
55
interface AiFailModalProps {
66
onClose: () => void;
77
message?: string;
8+
onRetry?: () => void;
89
}
910

1011
const TEXT = {
@@ -13,15 +14,24 @@ const TEXT = {
1314
retryButton: '다시 시도',
1415
} as const;
1516

16-
const AiFailModal = ({ onClose, message = TEXT.defaultMessage }: AiFailModalProps) => (
17-
<AiModalBase
18-
onClose={onClose}
19-
title={TEXT.title}
20-
description={message}
21-
descriptionClassName={modalStyles.failDescription}
22-
titleId="ai-fail-title"
23-
footer={<Button text={TEXT.retryButton} onClick={onClose} />}
24-
/>
25-
);
17+
const AiFailModal = ({ onClose, message = TEXT.defaultMessage, onRetry }: AiFailModalProps) => {
18+
const handleRetry = () => {
19+
onClose();
20+
if (onRetry) {
21+
setTimeout(onRetry, 0);
22+
}
23+
};
24+
25+
return (
26+
<AiModalBase
27+
onClose={onClose}
28+
title={TEXT.title}
29+
description={message}
30+
descriptionClassName={modalStyles.failDescription}
31+
titleId="ai-fail-title"
32+
footer={<Button text={TEXT.retryButton} onClick={handleRetry} />}
33+
/>
34+
);
35+
};
2636

2737
export default AiFailModal;

src/common/component/AiRecommendModal/AiRecommendModal.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import AiModalBase from '@/common/component/AiModalBase/AiModalBase';
88

99
interface AiRecommendModalProps {
1010
onClose: () => void;
11+
onBeforeClose?: () => void;
1112
onSubmit: (goals: { title: string }[]) => void;
1213
values: readonly string[];
1314
options?: readonly string[];
@@ -20,7 +21,13 @@ const TEXT = {
2021
confirmButton: '내 만다라트에 넣기',
2122
} as const;
2223

23-
const AiRecommendModal = ({ onClose, onSubmit, values, options }: AiRecommendModalProps) => {
24+
const AiRecommendModal = ({
25+
onClose,
26+
onBeforeClose,
27+
onSubmit,
28+
values,
29+
options,
30+
}: AiRecommendModalProps) => {
2431
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
2532

2633
const emptyCount = values.filter((v) => v.trim() === '').length;
@@ -34,16 +41,21 @@ const AiRecommendModal = ({ onClose, onSubmit, values, options }: AiRecommendMod
3441
);
3542
};
3643

44+
const handleClose = () => {
45+
onBeforeClose?.();
46+
onClose();
47+
};
48+
3749
const handleClick = () => {
3850
const titles = selectedOptions.slice(0, emptyCount);
3951
const goals = titles.map((title) => ({ title }));
4052
onSubmit(goals);
41-
onClose();
53+
handleClose();
4254
};
4355

4456
return (
4557
<AiModalBase
46-
onClose={onClose}
58+
onClose={handleClose}
4759
title={TEXT.title}
4860
description={
4961
<>

src/common/component/Mandalart/Square/Square.css.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ const createBaseCell = (type: keyof typeof SQUARE_TYPES) =>
6262
height: SQUARE_TYPES[type].height,
6363
padding: SQUARE_TYPES[type].padding,
6464
boxSizing: 'border-box',
65+
wordBreak: 'break-word',
66+
overflowWrap: 'anywhere',
67+
whiteSpace: 'pre-wrap',
6568
cursor:
6669
type === 'MY_MANDAL' || type === 'MY_MANDAL_CENTER' || type === 'TODO_SUB_COLORED'
6770
? 'default'
@@ -145,9 +148,6 @@ export const subCell = {
145148
{
146149
color: colors.grey8,
147150
background: colors.grey3,
148-
':hover': {
149-
background: colors.grey2,
150-
},
151151
selectors: {
152152
'&[data-completed="true"]': {
153153
border: `4px solid ${colors.blue08}`,

src/page/todo/upperTodo/UpperTodo.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { useState } from 'react';
12
import { useNavigate } from 'react-router-dom';
23
import Mandalart from '@common/component/Mandalart/Mandalart';
34

45
import * as styles from './UpperTodo.css';
56
import { SubGoalFields, UpperTodoHeader, MandalCompleteButton } from './component';
67
import { useUpperTodoState, useUpperTodoAI } from './hook';
78
import { toMainSubGoals } from './utils/goal';
8-
import { DEFAULT_TEXT, ALERT } from './constants';
9+
import { DEFAULT_TEXT, ALERT, GOAL_COUNT } from './constants';
910

1011
import GradientBackground from '@/common/component/Background/GradientBackground';
1112
import { PATH } from '@/route';
@@ -41,17 +42,25 @@ const UpperTodo = () => {
4142
navigate(PATH.TODO_LOWER);
4243
};
4344

44-
const { isAiUsed, handleOpenAiModal } = useUpperTodoAI({
45+
const [hasAiRecommendUsed, setHasAiRecommendUsed] = useState(false);
46+
47+
const { isLoading: isUpperAiLoading, handleOpenAiModal } = useUpperTodoAI({
4548
mandalartId,
4649
mainGoal,
4750
subGoals,
4851
setSubGoals,
4952
refetch,
5053
refetchCoreGoalIds,
5154
setIsTooltipOpen,
55+
hasAiBeenUsed: hasAiRecommendUsed,
56+
markAiUsed: () => setHasAiRecommendUsed(true),
5257
});
5358

54-
const hasFilledSubGoals = subGoals.filter((v) => v.trim() !== '').length > 0;
59+
const filledSubGoalCount = subGoals.filter((v) => v.trim() !== '').length;
60+
const hasFilledSubGoals = filledSubGoalCount > 0;
61+
const isAllSubGoalsFilled = filledSubGoalCount >= GOAL_COUNT;
62+
63+
const isAiUsed = hasAiRecommendUsed || isUpperAiLoading || isAllSubGoalsFilled;
5564

5665
const handleEnter = (index: number, value: string) => {
5766
handleSubGoalEnter(index, value);

src/page/todo/upperTodo/component/SubGoalFields.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const SubGoalFields = ({ values, onChange, idPositions, onEnter }: SubGoalFields
3737
onChange={(val) => handleChange(index, val)}
3838
onCommit={getHandleFieldCommit(index, idPositions?.[index]?.id)}
3939
placeholder={`${ORDER_LABELS[index]} ${DEFAULT_PLACEHOLDER.subGoal}`}
40+
maxLength={30}
4041
/>
4142
))}
4243
</div>

src/page/todo/upperTodo/constants/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ export const DEFAULT_TEXT = {
66

77
export const ALERT = {
88
noMandalartId: '전체 목표가 설정되지 않았습니다.',
9-
goalsAlreadyFilled: '이미 모든 목표가 채워져 있습니다.',
10-
aiSaveFail: 'AI 추천 목표 저장 실패',
11-
aiFetchFail: 'AI 추천을 불러오지 못했어요. 잠시 후 다시 시도해주세요.',
129
} as const;
1310

1411
export const ORDER_LABELS = [
Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useState } from 'react';
2-
import { isAxiosError } from 'axios';
32

43
import { extractTitles, updateSubGoalsWithAiResponse } from '../utils/goal';
5-
import { ALERT, GOAL_COUNT } from '../constants';
4+
import { GOAL_COUNT } from '../constants';
65

76
import AiRecommendModal from '@/common/component/AiRecommendModal/AiRecommendModal';
7+
import AiFailModal from '@/common/component/AiFailModal/AiFailModal';
88
import { useOverlayModal } from '@/common/hook/useOverlayModal';
99
import {
1010
usePostAiRecommendCoreGoal,
@@ -19,6 +19,8 @@ interface UseUpperTodoAIParams {
1919
refetch: () => void;
2020
refetchCoreGoalIds: () => void;
2121
setIsTooltipOpen: (open: boolean) => void;
22+
hasAiBeenUsed: boolean;
23+
markAiUsed: () => void;
2224
}
2325

2426
interface CoreGoalResponse {
@@ -27,22 +29,6 @@ interface CoreGoalResponse {
2729
title: string;
2830
}
2931

30-
interface ApiErrorResponse {
31-
message?: string;
32-
}
33-
34-
const getServerMessage = (error: unknown, fallback: string) => {
35-
if (isAxiosError<ApiErrorResponse>(error)) {
36-
return error.response?.data?.message ?? fallback;
37-
}
38-
39-
if (error instanceof Error && error.message) {
40-
return error.message;
41-
}
42-
43-
return fallback;
44-
};
45-
4632
export const useUpperTodoAI = ({
4733
mandalartId,
4834
mainGoal,
@@ -51,41 +37,64 @@ export const useUpperTodoAI = ({
5137
refetch,
5238
refetchCoreGoalIds,
5339
setIsTooltipOpen,
40+
hasAiBeenUsed,
41+
markAiUsed,
5442
}: UseUpperTodoAIParams) => {
5543
const { openModal, closeModal } = useOverlayModal();
5644
const postAiRecommend = usePostAiRecommendCoreGoal();
5745
const postRecommendToCore = usePostAiRecommendToCoreGoals();
5846

59-
const [isAiUsed, setIsAiUsed] = useState(false);
47+
const [isLoading, setIsLoading] = useState(false);
6048

61-
const handleAiSubmit = (goals: { title: string }[]) => {
49+
const openFailModal = (retry?: () => void) => {
50+
openModal(<AiFailModal onClose={closeModal} onRetry={retry} />);
51+
};
52+
53+
const runSubmitMutation = (titles: string[]) => {
54+
if (titles.length === 0) {
55+
return;
56+
}
57+
58+
setIsLoading(true);
6259
postRecommendToCore.mutate(
63-
{ mandalartId, goals: goals.map((g) => g.title) },
60+
{ mandalartId, goals: titles },
6461
{
6562
onSuccess: (response) => {
6663
const responseData: CoreGoalResponse[] = response.coreGoals;
6764
setSubGoals((prev) => updateSubGoalsWithAiResponse(prev, responseData));
6865
refetchCoreGoalIds();
6966
refetch();
67+
setIsLoading(false);
7068
},
71-
onError: (error) => {
72-
const message = getServerMessage(error, ALERT.aiSaveFail);
73-
alert(message);
69+
onError: () => {
70+
openFailModal(() => {
71+
runSubmitMutation(titles);
72+
});
73+
setIsLoading(false);
7474
},
7575
},
7676
);
7777
};
7878

79+
const handleAiSubmit = (goals: { title: string }[]) => {
80+
const titles = goals.map((goal) => goal.title);
81+
runSubmitMutation(titles);
82+
};
83+
7984
const handleOpenAiModal = async () => {
8085
const currentFilledCount = subGoals.filter((v) => v.trim() !== '').length;
8186
const maxGoals = GOAL_COUNT;
8287

8388
if (currentFilledCount >= maxGoals) {
84-
alert(ALERT.goalsAlreadyFilled);
89+
openFailModal();
90+
return;
91+
}
92+
93+
if (hasAiBeenUsed || isLoading) {
8594
return;
8695
}
8796

88-
setIsAiUsed(true);
97+
setIsLoading(true);
8998
setIsTooltipOpen(false);
9099

91100
try {
@@ -102,20 +111,20 @@ export const useUpperTodoAI = ({
102111
const aiModalContent = (
103112
<AiRecommendModal
104113
onClose={closeModal}
114+
onBeforeClose={markAiUsed}
105115
onSubmit={handleAiSubmit}
106116
values={subGoals}
107117
options={titles}
108118
/>
109119
);
110120

111121
openModal(aiModalContent);
112-
} catch (error) {
113-
const message = getServerMessage(error, ALERT.aiFetchFail);
114-
alert(message);
122+
} catch {
123+
openFailModal(handleOpenAiModal);
115124
} finally {
116-
setIsAiUsed(false);
125+
setIsLoading(false);
117126
}
118127
};
119128

120-
return { isAiUsed, handleOpenAiModal } as const;
129+
return { isLoading, handleOpenAiModal } as const;
121130
};

0 commit comments

Comments
 (0)