diff --git a/eslint.config.js b/eslint.config.js index 387006f5..fde404ef 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -40,7 +40,7 @@ export default tseslint.config( ...reactHooks.configs.recommended.rules, ...jsxA11y.configs.recommended.rules, 'react/react-in-jsx-scope': 'off', - 'no-console': 'warn', + 'no-console': 'off', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'warn', curly: 'error', @@ -52,7 +52,7 @@ export default tseslint.config( }, ], 'react/self-closing-comp': 'warn', - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'off', 'react/jsx-pascal-case': 'error', 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 'prettier/prettier': [ diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index 631ab46e..3f28a0e7 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -1,4 +1,5 @@ import axios from 'axios'; + import { HTTP_STATUS } from '@/api/constant/httpStatus'; const axiosInstance = axios.create({ @@ -28,11 +29,11 @@ axiosInstance.interceptors.response.use( const { status } = error.response; if (status === HTTP_STATUS.UNAUTHORIZED) { - console.warn('인증 실패'); + // 인증 실패 처리 } if (status === HTTP_STATUS.INTERNAL_SERVER_ERROR) { - console.error('서버 오류가 발생'); + // 서버 오류 처리 } } return Promise.reject(error); diff --git a/src/api/constant/queryKey.ts b/src/api/constant/queryKey.ts index ff0f7e30..92cdbfd4 100644 --- a/src/api/constant/queryKey.ts +++ b/src/api/constant/queryKey.ts @@ -1,5 +1,5 @@ export const QUERY_KEY = { - OVERALL_TODO: ['overallTodo'], + ENTIRE_TODO: ['entireTodo'], MANDALART_CORE_GOALS: (mandalartId: number) => ['mandalartCoreGoals', mandalartId], MANDALART_SUB_GOALS: (mandalartId: number, coreGoalId?: number, cycle?: string) => ['mandalartSubGoals', mandalartId, coreGoalId, cycle].filter(Boolean), @@ -22,6 +22,6 @@ export const QUERY_KEY = { PERSONA: 'persona', UPDATE_SUB_GOAL: (id: number) => ['updateSubGoal', id], DELETE_SUB_GOAL: (id: number) => ['deleteSubGoal', id], - OVERALL_GOAL: ['overallGoal'], + ENTIRE_GOAL: ['entireGoal'], USER_INFO: ['userInfo'], } as const; diff --git a/src/api/domain/edit/hook.ts b/src/api/domain/edit/hook.ts index ed5b87db..aa05ea8a 100644 --- a/src/api/domain/edit/hook.ts +++ b/src/api/domain/edit/hook.ts @@ -43,7 +43,7 @@ export const useUpdateSubGoal = (mandalartId: number) => { return useMutation({ mutationFn: (data: UpdateSubGoalRequest) => updateSubGoal(mandalartId, data), - onSuccess: (_) => { + onSuccess: () => { queryClient.invalidateQueries({ queryKey: [...QUERY_KEY.MANDAL_ALL, mandalartId], exact: true, diff --git a/src/api/domain/entireTodo/hook.ts b/src/api/domain/entireTodo/hook.ts new file mode 100644 index 00000000..317cca9c --- /dev/null +++ b/src/api/domain/entireTodo/hook.ts @@ -0,0 +1,18 @@ +import { useMutation, type UseMutationOptions } from '@tanstack/react-query'; + +import { + postEntireTodo, + type CreateEntireTodoRequest, + type CreateEntireTodoResponse, +} from '@/api/domain/entireTodo'; +import { QUERY_KEY } from '@/api/constant/queryKey'; + +export const useCreateEntireTodo = ( + options?: UseMutationOptions, +) => { + return useMutation({ + mutationKey: [...QUERY_KEY.ENTIRE_TODO, 'create'], + mutationFn: postEntireTodo, + ...options, + }); +}; diff --git a/src/api/domain/entireTodo/hook/useCreateMandalart.tsx b/src/api/domain/entireTodo/hook/useCreateMandalart.tsx deleted file mode 100644 index fab33742..00000000 --- a/src/api/domain/entireTodo/hook/useCreateMandalart.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; - -import { postOverallTodo } from '@/api/domain/entireTodo'; -import type { - CreateOverallTodoRequest, - CreateOverallTodoResponse, -} from '@/api/domain/entireTodo/type/entireTodo'; -import type { BaseResponse, ErrorResponse } from '@/type/api'; -import { QUERY_KEY } from '@/api/constant/queryKey'; - -export const useCreateOverallTodo = () => { - const queryClient = useQueryClient(); - return useMutation< - BaseResponse, - ErrorResponse, - CreateOverallTodoRequest - >({ - mutationFn: postOverallTodo, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: QUERY_KEY.OVERALL_TODO }); - }, - }); -}; diff --git a/src/api/domain/entireTodo/index.ts b/src/api/domain/entireTodo/index.ts index fd045a89..0098adae 100644 --- a/src/api/domain/entireTodo/index.ts +++ b/src/api/domain/entireTodo/index.ts @@ -1,17 +1,20 @@ import axiosInstance from '@/api/axiosInstance'; import { END_POINT } from '@/api/constant/endPoint'; -import type { BaseResponse } from '@/type/api'; -import type { - CreateOverallTodoRequest, - CreateOverallTodoResponse, -} from '@/api/domain/entireTodo/type/entireTodo'; -export const postOverallTodo = async ( - body: CreateOverallTodoRequest, -): Promise> => { - const res = await axiosInstance.post>( - END_POINT.MANDALART, - body, - ); - return res.data; +export interface CreateEntireTodoRequest { + title: string; +} + +export interface CreateEntireTodoResponse { + id: number; + title: string; +} + +export const postEntireTodo = async ( + body: CreateEntireTodoRequest, +): Promise => { + const res = await axiosInstance.post<{ + data: CreateEntireTodoResponse; + }>(END_POINT.MANDALART, body); + return res.data.data; }; diff --git a/src/api/domain/entireTodo/type/entireTodo.ts b/src/api/domain/entireTodo/type/entireTodo.ts deleted file mode 100644 index 56e3ed3a..00000000 --- a/src/api/domain/entireTodo/type/entireTodo.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type CreateOverallTodoRequest = { - title: string; -}; - -export type CreateOverallTodoResponse = { - id: number; - title: string; -}; diff --git a/src/api/domain/lowerTodo/hook/useOverallGoal.ts b/src/api/domain/lowerTodo/hook/useOverallGoal.ts index a7428e28..5469312e 100644 --- a/src/api/domain/lowerTodo/hook/useOverallGoal.ts +++ b/src/api/domain/lowerTodo/hook/useOverallGoal.ts @@ -6,7 +6,7 @@ import { QUERY_KEY } from '@/api/constant/queryKey'; export const useOverallGoal = (mandalartId: number) => { return useQuery({ - queryKey: [QUERY_KEY.OVERALL_GOAL, mandalartId], + queryKey: [QUERY_KEY.ENTIRE_GOAL, mandalartId], queryFn: () => getOverallGoal(mandalartId), enabled: !!mandalartId, }); diff --git a/src/api/domain/myTodo/hook/useMyMandal.ts b/src/api/domain/myTodo/hook/useMyMandal.ts index 700740ba..9fe949de 100644 --- a/src/api/domain/myTodo/hook/useMyMandal.ts +++ b/src/api/domain/myTodo/hook/useMyMandal.ts @@ -17,7 +17,7 @@ type MandalCoreGoalsResponse = BaseResponse<{ export const useGetMandalAll = (mandalartId: number) => { return useQuery({ - queryKey: [QUERY_KEY.OVERALL_TODO, mandalartId], + queryKey: [QUERY_KEY.ENTIRE_GOAL, mandalartId], queryFn: () => getMandalAll(mandalartId), }); }; diff --git a/src/api/domain/upperTodo/hook.ts b/src/api/domain/upperTodo/hook.ts index 7e87a0e5..fdfe1a4a 100644 --- a/src/api/domain/upperTodo/hook.ts +++ b/src/api/domain/upperTodo/hook.ts @@ -14,7 +14,7 @@ import { QUERY_KEY } from '@/api/constant/queryKey'; export const useGetMandalAll = (mandalartId: number) => { return useQuery({ - queryKey: [QUERY_KEY.OVERALL_TODO, mandalartId], + queryKey: [QUERY_KEY.ENTIRE_GOAL, mandalartId], queryFn: () => getMandalAll(mandalartId), }); }; diff --git a/src/common/component/CycleDropDown/CycleDropDown.tsx b/src/common/component/CycleDropDown/CycleDropDown.tsx index daebd069..00eb3cbf 100644 --- a/src/common/component/CycleDropDown/CycleDropDown.tsx +++ b/src/common/component/CycleDropDown/CycleDropDown.tsx @@ -23,7 +23,7 @@ type CycleDropDownProps = { const CycleDropDown = ({ initialType = 'DAILY', onChange }: CycleDropDownProps) => { const [isOpen, setIsOpen] = useState(false); const [selectedType, setSelectedType] = useState( - (Object.entries(CYCLE_MAPPING).find(([_, v]) => v === initialType)?.[0] as DisplayCycleType) || + (Object.entries(CYCLE_MAPPING).find(([, v]) => v === initialType)?.[0] as DisplayCycleType) || '매일', ); diff --git a/src/common/component/GoButton/GoButton.tsx b/src/common/component/GoButton/GoButton.tsx index 75ba40df..94665686 100644 --- a/src/common/component/GoButton/GoButton.tsx +++ b/src/common/component/GoButton/GoButton.tsx @@ -3,14 +3,27 @@ import { goButtonContainer, goIcon } from '@/common/component/GoButton/GoButton. type GoButtonProps = { isActive: boolean; + disabled?: boolean; onClick?: React.MouseEventHandler; + type?: 'button' | 'submit' | 'reset'; }; -const GoButton = ({ isActive = true, onClick }: GoButtonProps) => { - const state = isActive ? 'active' : 'disabled'; +const GoButton = ({ + isActive = true, + disabled = false, + onClick, + type = 'button', +}: GoButtonProps) => { + const state = isActive && !disabled ? 'active' : 'disabled'; + const isDisabled = disabled || !isActive; return ( - ); diff --git a/src/common/component/Loading/Loading.tsx b/src/common/component/Loading/Loading.tsx index 9f39a57b..4ac41058 100644 --- a/src/common/component/Loading/Loading.tsx +++ b/src/common/component/Loading/Loading.tsx @@ -4,7 +4,7 @@ import loadingAnimation from '@/assets/lottie/loading.json'; import * as styles from '@/common/component/Loading/Loading.css'; type LoadingProps = { - type: 'goal' | 'todo' | 'history'; + type: 'goal' | 'todo' | 'history' | 'entireTodo'; }; const Loading = ({ type }: LoadingProps) => { @@ -20,6 +20,9 @@ const Loading = ({ type }: LoadingProps) => { case 'history': message = '내가 한 일을 불러오고 있어요'; break; + case 'entireTodo': + message = '목표를 작성중입니다'; + break; } return ( diff --git a/src/common/component/UserModal/UserModal.tsx b/src/common/component/UserModal/UserModal.tsx index ff635010..a0514588 100644 --- a/src/common/component/UserModal/UserModal.tsx +++ b/src/common/component/UserModal/UserModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { IcDivider } from '@/assets/svg'; import * as styles from '@/common/component/UserModal/UserModal.css'; @@ -14,11 +14,14 @@ const UserModal = ({ onClose }: UserModalProps) => { const { data: user, isLoading, isError } = useGetUser(); const { mutate: logoutMutate } = usePostLogout(); - const handleClickOutside = (e: MouseEvent) => { - if (modalRef.current && !modalRef.current.contains(e.target as Node)) { - onClose(); - } - }; + const handleClickOutside = useCallback( + (e: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(e.target as Node)) { + onClose(); + } + }, + [onClose], + ); const handleLogout = () => { logoutMutate(undefined, { @@ -39,7 +42,7 @@ const UserModal = ({ onClose }: UserModalProps) => { return () => { document.removeEventListener('mousedown', handleClickOutside); }; - }, []); + }, [handleClickOutside]); if (isLoading || isError || !user) { return null; diff --git a/src/common/hook/useTypingEffect.ts b/src/common/hook/useTypingEffect.ts index cf22dd82..cbff8e57 100644 --- a/src/common/hook/useTypingEffect.ts +++ b/src/common/hook/useTypingEffect.ts @@ -1,38 +1,37 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; const useTypingEffect = (fullText: string, duration: number) => { const [displayedText, setDisplayedText] = useState(''); + const rafIdRef = useRef(null); useEffect(() => { let startTime: number | null = null; const charArray = Array.from(fullText); const totalChars = charArray.length; - let isMounted = true; const step = (timestamp: number) => { - if (!isMounted) { - return; - } if (startTime === null) { startTime = timestamp; } const elapsed = timestamp - startTime; - const progress = Math.min(elapsed / duration, 1); - const charsToShow = Math.floor(progress * totalChars); + const progress = duration <= 0 ? 1 : Math.min(elapsed / duration, 1); + const charsToShow = Math.round(progress * totalChars); setDisplayedText(charArray.slice(0, charsToShow).join('')); if (progress < 1) { - requestAnimationFrame(step); + rafIdRef.current = requestAnimationFrame(step); } }; - const rafId = requestAnimationFrame(step); + rafIdRef.current = requestAnimationFrame(step); return () => { - isMounted = false; - cancelAnimationFrame(rafId); + if (rafIdRef.current !== null) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } }; }, [fullText, duration]); diff --git a/src/page/home/hook/useFadeInOnView.ts b/src/page/home/hook/useFadeInOnView.ts index 4d797a49..28d35a69 100644 --- a/src/page/home/hook/useFadeInOnView.ts +++ b/src/page/home/hook/useFadeInOnView.ts @@ -24,11 +24,12 @@ export const useFadeInOnView = ({ } return () => { - if (ref.current) { - observer.unobserve(ref.current); + if (current) { + observer.unobserve(current); } + observer.disconnect(); }; - }, []); + }, [rootMargin, threshold]); return { ref, visible }; }; diff --git a/src/page/intro/Intro.tsx b/src/page/intro/Intro.tsx index 72b08296..a3eff13c 100644 --- a/src/page/intro/Intro.tsx +++ b/src/page/intro/Intro.tsx @@ -1,4 +1,5 @@ import { useLocation, useNavigate } from 'react-router-dom'; + import * as styles from '@/page/intro/Intro.css'; const MESSAGE = { diff --git a/src/page/signup/SignUp.tsx b/src/page/signup/SignUp.tsx index 8d453c26..e59f3ac3 100644 --- a/src/page/signup/SignUp.tsx +++ b/src/page/signup/SignUp.tsx @@ -51,9 +51,7 @@ const SignUp = () => { onSuccess: () => { navigate(PATH.INTRO); }, - onError: (err) => { - console.error('회원가입 실패:', err); - }, + onError: () => {}, }); }; diff --git a/src/page/todo/entireTodo/Todo.css.ts b/src/page/todo/entireTodo/Todo.css.ts index 99976b82..ec4f49cf 100644 --- a/src/page/todo/entireTodo/Todo.css.ts +++ b/src/page/todo/entireTodo/Todo.css.ts @@ -1,33 +1,26 @@ import { style } from '@vanilla-extract/css'; -import { colors } from '@/style/token'; -import { fonts } from '@/style/token/typography.css'; +import { colors, fonts, layout } from '@/style/token'; export const todoContainer = style({ + ...layout.columnCenter, height: '100vh', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', backgroundColor: colors.bg_black01, position: 'relative', - overflow: 'hidden', }); export const todoTitle = style({ - color: colors.white01, ...fonts.display01, + color: colors.white01, textAlign: 'center', + whiteSpace: 'pre-line', marginBottom: '5.6rem', position: 'relative', - zIndex: 1, height: '15.2rem', }); export const todoInputContainer = style({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', + ...layout.rowCenter, gap: '2rem', position: 'relative', }); diff --git a/src/page/todo/entireTodo/Todo.tsx b/src/page/todo/entireTodo/Todo.tsx index 3e766c7e..775e75be 100644 --- a/src/page/todo/entireTodo/Todo.tsx +++ b/src/page/todo/entireTodo/Todo.tsx @@ -1,67 +1,60 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { FULL_TEXT, TYPING_DURATION, PLACEHOLDER_TEXT } from './constant/constants'; +import { FULL_TEXT, TYPING_DURATION, PLACEHOLDER_TEXT } from './constant/typing'; import * as styles from './Todo.css'; -import { useCreateOverallTodo } from '@/api/domain/entireTodo/hook/useCreateMandalart'; -import useTypingEffect from '@/common/hook/useTypingEffect'; -import GoButton from '@/common/component/GoButton/GoButton'; import GradientBackground from '@/common/component/Background/GradientBackground'; +import GoButton from '@/common/component/GoButton/GoButton'; +import Loading from '@/common/component/Loading/Loading'; import TextField from '@/common/component/MandalartTextField/MandalartTextField'; +import useTypingEffect from '@/common/hook/useTypingEffect'; +import { useCreateEntireTodo } from '@/api/domain/entireTodo/hook'; import { PATH } from '@/route'; const Todo = () => { const [inputText, setInputText] = useState(''); + const trimmed = inputText.trim(); + const isValid = trimmed.length > 0; const displayedText = useTypingEffect(FULL_TEXT, TYPING_DURATION); - const navigate = useNavigate(); - const { mutate } = useCreateOverallTodo(); + const { mutate, isPending } = useCreateEntireTodo(); + const navigate = useNavigate(); - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleGoNext(); + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!isValid || isPending) { + return; } - }; - const handleGoNext = () => { - if (inputText.trim().length > 0) { - mutate( - { title: inputText.trim() }, - { - onSuccess: () => { - navigate(PATH.TODO_UPPER); - }, - onError: () => { - // 생성 실패 시 처리 로직 - }, - }, - ); - } + const title = trimmed; + mutate( + { title }, + { + onSuccess: () => navigate(PATH.TODO_UPPER), + }, + ); }; - const renderTextWithLineBreaks = () => - displayedText.split('\n').map((line, idx) => ( - - {line} -
-
- )); + if (isPending) { + return ; + } return (
-

{renderTextWithLineBreaks()}

-
+

{displayedText}

+
- 0} onClick={handleGoNext} /> -
+ +
); }; diff --git a/src/page/todo/entireTodo/constant/constants.ts b/src/page/todo/entireTodo/constant/typing.ts similarity index 100% rename from src/page/todo/entireTodo/constant/constants.ts rename to src/page/todo/entireTodo/constant/typing.ts diff --git a/src/page/todo/lowerTodo/LowerTodo.tsx b/src/page/todo/lowerTodo/LowerTodo.tsx index f44e5826..dd776c67 100644 --- a/src/page/todo/lowerTodo/LowerTodo.tsx +++ b/src/page/todo/lowerTodo/LowerTodo.tsx @@ -234,7 +234,7 @@ const LowerTodo = ({ userName = '김도트' }: LowerTodoProps) => { return newTodos; }); } - }, [subGoalsData, selectedGoalIndex]); + }, [subGoalsData, selectedGoalIndex, allTodos]); useEffect(() => { if (coreGoalsData && selectedGoalIndex !== -1) { @@ -417,7 +417,7 @@ const LowerTodo = ({ userName = '김도트' }: LowerTodoProps) => { try { await completeMandalart(mandalartId); navigate(PATH.TODO_MY); - } catch (error) { + } catch { alert('만다라트 완성 처리 중 오류가 발생했습니다.'); } }; diff --git a/src/page/todo/upperTodo/UpperTodo.tsx b/src/page/todo/upperTodo/UpperTodo.tsx index e4935014..eb1eef42 100644 --- a/src/page/todo/upperTodo/UpperTodo.tsx +++ b/src/page/todo/upperTodo/UpperTodo.tsx @@ -103,8 +103,7 @@ const UpperTodo = ({ userName = '김도트' }: UpperTodoProps) => { ); openModal(aiModalContent); - } catch (error) { - console.error('AI 추천 호출 실패:', error); + } catch { setIsAiUsed(false); } }; diff --git a/src/page/todo/upperTodo/hook/useUpperTodoState.ts b/src/page/todo/upperTodo/hook/useUpperTodoState.ts index b9b9645c..b136a275 100644 --- a/src/page/todo/upperTodo/hook/useUpperTodoState.ts +++ b/src/page/todo/upperTodo/hook/useUpperTodoState.ts @@ -59,8 +59,8 @@ export const useUpperTodoState = (mandalartId: number) => { } await refetchCoreGoalIds(); - } catch (error) { - console.error('상위 목표 저장 실패 또는 삭제 실패:', error); + } catch { + /* empty */ } }; diff --git a/src/style/token/index.ts b/src/style/token/index.ts index 792ef73b..ac69af63 100644 --- a/src/style/token/index.ts +++ b/src/style/token/index.ts @@ -1,3 +1,4 @@ export * from './color.css'; export * from './typography.css'; export * from './zIndex.css'; +export * from './layout.css'; diff --git a/src/style/token/layout.css.ts b/src/style/token/layout.css.ts new file mode 100644 index 00000000..803146da --- /dev/null +++ b/src/style/token/layout.css.ts @@ -0,0 +1,19 @@ +export const layout = { + flexCenter: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + columnCenter: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + rowCenter: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, +} as const;