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
30 changes: 20 additions & 10 deletions src/common/component/AiFailModal/AiFailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Button from '@/common/component/Button/Button';
interface AiFailModalProps {
onClose: () => void;
message?: string;
onRetry?: () => void;
}

const TEXT = {
Expand All @@ -13,15 +14,24 @@ const TEXT = {
retryButton: '다시 시도',
} as const;

const AiFailModal = ({ onClose, message = TEXT.defaultMessage }: AiFailModalProps) => (
<AiModalBase
onClose={onClose}
title={TEXT.title}
description={message}
descriptionClassName={modalStyles.failDescription}
titleId="ai-fail-title"
footer={<Button text={TEXT.retryButton} onClick={onClose} />}
/>
);
const AiFailModal = ({ onClose, message = TEXT.defaultMessage, onRetry }: AiFailModalProps) => {
const handleRetry = () => {
onClose();
if (onRetry) {
setTimeout(onRetry, 0);
}
};

return (
<AiModalBase
onClose={onClose}
title={TEXT.title}
description={message}
descriptionClassName={modalStyles.failDescription}
titleId="ai-fail-title"
footer={<Button text={TEXT.retryButton} onClick={handleRetry} />}
/>
);
};

export default AiFailModal;
18 changes: 15 additions & 3 deletions src/common/component/AiRecommendModal/AiRecommendModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import AiModalBase from '@/common/component/AiModalBase/AiModalBase';

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

const AiRecommendModal = ({ onClose, onSubmit, values, options }: AiRecommendModalProps) => {
const AiRecommendModal = ({
onClose,
onBeforeClose,
onSubmit,
values,
options,
}: AiRecommendModalProps) => {
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);

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

const handleClose = () => {
onBeforeClose?.();
onClose();
};

const handleClick = () => {
const titles = selectedOptions.slice(0, emptyCount);
const goals = titles.map((title) => ({ title }));
onSubmit(goals);
onClose();
handleClose();
};

return (
<AiModalBase
onClose={onClose}
onClose={handleClose}
title={TEXT.title}
description={
<>
Expand Down
6 changes: 3 additions & 3 deletions src/common/component/Mandalart/Square/Square.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ const createBaseCell = (type: keyof typeof SQUARE_TYPES) =>
height: SQUARE_TYPES[type].height,
padding: SQUARE_TYPES[type].padding,
boxSizing: 'border-box',
wordBreak: 'break-word',
overflowWrap: 'anywhere',
whiteSpace: 'pre-wrap',
cursor:
type === 'MY_MANDAL' || type === 'MY_MANDAL_CENTER' || type === 'TODO_SUB_COLORED'
? 'default'
Expand Down Expand Up @@ -145,9 +148,6 @@ export const subCell = {
{
color: colors.grey8,
background: colors.grey3,
':hover': {
background: colors.grey2,
},
selectors: {
'&[data-completed="true"]': {
border: `4px solid ${colors.blue08}`,
Expand Down
15 changes: 12 additions & 3 deletions src/page/todo/upperTodo/UpperTodo.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Mandalart from '@common/component/Mandalart/Mandalart';

import * as styles from './UpperTodo.css';
import { SubGoalFields, UpperTodoHeader, MandalCompleteButton } from './component';
import { useUpperTodoState, useUpperTodoAI } from './hook';
import { toMainSubGoals } from './utils/goal';
import { DEFAULT_TEXT, ALERT } from './constants';
import { DEFAULT_TEXT, ALERT, GOAL_COUNT } from './constants';

import GradientBackground from '@/common/component/Background/GradientBackground';
import { PATH } from '@/route';
Expand Down Expand Up @@ -41,17 +42,25 @@ const UpperTodo = () => {
navigate(PATH.TODO_LOWER);
};

const { isAiUsed, handleOpenAiModal } = useUpperTodoAI({
const [hasAiRecommendUsed, setHasAiRecommendUsed] = useState(false);

const { isLoading: isUpperAiLoading, handleOpenAiModal } = useUpperTodoAI({
mandalartId,
mainGoal,
subGoals,
setSubGoals,
refetch,
refetchCoreGoalIds,
setIsTooltipOpen,
hasAiBeenUsed: hasAiRecommendUsed,
markAiUsed: () => setHasAiRecommendUsed(true),
});

const hasFilledSubGoals = subGoals.filter((v) => v.trim() !== '').length > 0;
const filledSubGoalCount = subGoals.filter((v) => v.trim() !== '').length;
const hasFilledSubGoals = filledSubGoalCount > 0;
const isAllSubGoalsFilled = filledSubGoalCount >= GOAL_COUNT;

const isAiUsed = hasAiRecommendUsed || isUpperAiLoading || isAllSubGoalsFilled;

const handleEnter = (index: number, value: string) => {
handleSubGoalEnter(index, value);
Expand Down
1 change: 1 addition & 0 deletions src/page/todo/upperTodo/component/SubGoalFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const SubGoalFields = ({ values, onChange, idPositions, onEnter }: SubGoalFields
onChange={(val) => handleChange(index, val)}
onCommit={getHandleFieldCommit(index, idPositions?.[index]?.id)}
placeholder={`${ORDER_LABELS[index]} ${DEFAULT_PLACEHOLDER.subGoal}`}
maxLength={30}
/>
))}
</div>
Expand Down
3 changes: 0 additions & 3 deletions src/page/todo/upperTodo/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ export const DEFAULT_TEXT = {

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

export const ORDER_LABELS = [
Expand Down
71 changes: 40 additions & 31 deletions src/page/todo/upperTodo/hook/useUpperTodoAI.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useState } from 'react';
import { isAxiosError } from 'axios';

import { extractTitles, updateSubGoalsWithAiResponse } from '../utils/goal';
import { ALERT, GOAL_COUNT } from '../constants';
import { GOAL_COUNT } from '../constants';

import AiRecommendModal from '@/common/component/AiRecommendModal/AiRecommendModal';
import AiFailModal from '@/common/component/AiFailModal/AiFailModal';
import { useOverlayModal } from '@/common/hook/useOverlayModal';
import {
usePostAiRecommendCoreGoal,
Expand All @@ -19,6 +19,8 @@ interface UseUpperTodoAIParams {
refetch: () => void;
refetchCoreGoalIds: () => void;
setIsTooltipOpen: (open: boolean) => void;
hasAiBeenUsed: boolean;
markAiUsed: () => void;
}

interface CoreGoalResponse {
Expand All @@ -27,22 +29,6 @@ interface CoreGoalResponse {
title: string;
}

interface ApiErrorResponse {
message?: string;
}

const getServerMessage = (error: unknown, fallback: string) => {
if (isAxiosError<ApiErrorResponse>(error)) {
return error.response?.data?.message ?? fallback;
}

if (error instanceof Error && error.message) {
return error.message;
}

return fallback;
};

export const useUpperTodoAI = ({
mandalartId,
mainGoal,
Expand All @@ -51,41 +37,64 @@ export const useUpperTodoAI = ({
refetch,
refetchCoreGoalIds,
setIsTooltipOpen,
hasAiBeenUsed,
markAiUsed,
}: UseUpperTodoAIParams) => {
const { openModal, closeModal } = useOverlayModal();
const postAiRecommend = usePostAiRecommendCoreGoal();
const postRecommendToCore = usePostAiRecommendToCoreGoals();

const [isAiUsed, setIsAiUsed] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const handleAiSubmit = (goals: { title: string }[]) => {
const openFailModal = (retry?: () => void) => {
openModal(<AiFailModal onClose={closeModal} onRetry={retry} />);
};

const runSubmitMutation = (titles: string[]) => {
if (titles.length === 0) {
return;
}

setIsLoading(true);
postRecommendToCore.mutate(
{ mandalartId, goals: goals.map((g) => g.title) },
{ mandalartId, goals: titles },
{
onSuccess: (response) => {
const responseData: CoreGoalResponse[] = response.coreGoals;
setSubGoals((prev) => updateSubGoalsWithAiResponse(prev, responseData));
refetchCoreGoalIds();
refetch();
setIsLoading(false);
},
onError: (error) => {
const message = getServerMessage(error, ALERT.aiSaveFail);
alert(message);
onError: () => {
openFailModal(() => {
runSubmitMutation(titles);
});
setIsLoading(false);
},
},
);
};

const handleAiSubmit = (goals: { title: string }[]) => {
const titles = goals.map((goal) => goal.title);
runSubmitMutation(titles);
};

const handleOpenAiModal = async () => {
const currentFilledCount = subGoals.filter((v) => v.trim() !== '').length;
const maxGoals = GOAL_COUNT;

if (currentFilledCount >= maxGoals) {
alert(ALERT.goalsAlreadyFilled);
openFailModal();
return;
}

if (hasAiBeenUsed || isLoading) {
return;
}

setIsAiUsed(true);
setIsLoading(true);
setIsTooltipOpen(false);

try {
Expand All @@ -102,20 +111,20 @@ export const useUpperTodoAI = ({
const aiModalContent = (
<AiRecommendModal
onClose={closeModal}
onBeforeClose={markAiUsed}
onSubmit={handleAiSubmit}
values={subGoals}
options={titles}
/>
);

openModal(aiModalContent);
} catch (error) {
const message = getServerMessage(error, ALERT.aiFetchFail);
alert(message);
} catch {
openFailModal(handleOpenAiModal);
} finally {
setIsAiUsed(false);
setIsLoading(false);
}
};

return { isAiUsed, handleOpenAiModal } as const;
return { isLoading, handleOpenAiModal } as const;
};
Loading