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
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default [
{
rules: {
'prettier/prettier': 'warn',
'no-console': ['error', { allow: ['warn', 'error'] }],
},
},
tsConfig,
Expand Down
12 changes: 10 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import './App.css';
import '@/App.css';

import { useMemo } from 'react';
import { RouterProvider } from 'react-router-dom';

import { DeviceTokenProvider } from '@/providers/deviceTokenProvider';
import { alarmKeys } from '@/queryKey/queryKey';
import router from '@/routes/routes';

function App() {
return <RouterProvider router={router} />;
const refetchKeys = useMemo(() => [alarmKeys._def], []);
return (
<DeviceTokenProvider refetchKeys={refetchKeys}>
<RouterProvider router={router} />
</DeviceTokenProvider>
);
}

export default App;
22 changes: 9 additions & 13 deletions src/api/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,26 @@ axiosInstance.interceptors.response.use(
const refreshResponse = await refresh();

if (refreshResponse.code === '200') {
console.log('refreshToken이 재발급 되었습니다');
isRedirecting = false;

return axiosInstance(error.config);
}
} catch (errors) {
if (axios.isAxiosError(errors)) {
const refreshError = error as AxiosError<IRefreshResponse>;
const refreshError = errors as AxiosError<IRefreshResponse>;
if (refreshError.response?.data.message === 'The token is null.') {
console.log('refreshToken이 없습니다. 로그인 페이지로 이동합니다.');
console.error('refreshToken이 없습니다. 로그인 페이지로 이동합니다.');
void logout();
} else if (refreshError.response?.data.message === 'The token is invalid.') {
console.log('refreshToken이 만료되었습니다. 로그인 페이지로 이동합니다.');
logout();
console.error('refreshToken이 만료되었습니다. 로그인 페이지로 이동합니다.');
void logout();
} else {
if (refreshError.response?.data.message === 'Incorrect password.') {
alert('Your email or password is incorrect.');
} else {
console.log('알 수 없는 오류가 발생했습니다', errors);
logout();
}
console.error('알 수 없는 오류가 발생했습니다', errors);
void logout();
}
} else {
console.log('알 수 없는 오류가 발생했습니다', errors);
logout();
console.error('알 수 없는 오류가 발생했습니다', errors);
void logout();
}

return Promise.reject(errors);
Expand Down
19 changes: 1 addition & 18 deletions src/components/auth/commonAuthInput.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { InputHTMLAttributes } from 'react';
import React from 'react';

import formatInputNumber from '@/utils/formatPhoneNumber';

import Button from '@/components/common/Button';

import AlertCircle from '@/assets/icons/alert-circle_Fill.svg?react';
Expand Down Expand Up @@ -58,21 +56,6 @@ const CommonAuthInput = React.forwardRef<HTMLInputElement, TCommonAuthInputProps
className={`flex-1 relative flex bg-default-gray-100 rounded-[4px] max-h-[56px] min-w-0 h-[8vh] pl-[16px] text-default-gray-800 focus:outline-none focus:ring-0
${error ? 'border-[2px] border-warning caret-warning' : `${validation ? 'border-primary-500 border-[2px] ' : 'border-default-gray-700 border-[1px]'}`}
`}
onChange={(e) => {
const rawValue = e.target.value;
const formatted = type === 'phoneNum' ? formatInputNumber(rawValue) : rawValue;

// 외부에서 넘긴 onChange 핸들러에 적용된 값 전달
if (rest.onChange) {
rest.onChange({
...e,
target: {
...e.target,
value: formatted,
},
});
}
}}
{...rest}
/>
{short && <div className="flex px-[16px] py-[8px] text-default-gray-100 w-[80px]" />}
Expand All @@ -96,7 +79,7 @@ const CommonAuthInput = React.forwardRef<HTMLInputElement, TCommonAuthInputProps
className={`hover:!cursor-default ${validation ? 'hover:!bg-primary-500 hover:!text-[#ffffff]' : 'hover:!bg-default-gray-400'}`}
/>
)}
{error && <div className="absolute top-[62px] font-caption text-warning left-[16px] select-none">{errorMessage}</div>}
{error && <div className="absolute top-[58px] font-caption text-warning left-[16px] select-none">{errorMessage}</div>}
{error && <AlertCircle fill="#ff517c" className={`absolute ${button || short || validationState ? 'right-30' : ' right-3'}`} />}
</div>
);
Expand Down
32 changes: 18 additions & 14 deletions src/components/common/PasswordEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import type { AxiosError } from 'axios';
import { z } from 'zod';

import { useAccount } from '@/hooks/auth/useAccount';
Expand Down Expand Up @@ -28,16 +29,7 @@ export default function PasswordEditSection() {
setIsEditing(false);
};

const { mutate: changePw, isPending } = useChangePassword({
onSuccess: () => {
alert('비밀번호가 변경되었습니다.');
handleCancel();
},
onError: (err) => {
const msg = (err as any)?.response?.data?.message ?? '비밀번호 변경에 실패했습니다.';
alert(msg);
},
});
const { mutate: changePw, isPending } = useChangePassword();

// 제출
const handleSubmit = () => {
Expand All @@ -58,10 +50,22 @@ export default function PasswordEditSection() {
if (Object.keys(nextErrors).length > 0) return;

// 제출
changePw({
currentPassword: currentPw,
newPassword: newPw,
});
changePw(
{
currentPassword: currentPw,
newPassword: newPw,
},
{
onSuccess: () => {
alert('비밀번호가 변경되었습니다.');
handleCancel();
},
onError: (err: AxiosError) => {
const msg = (err as any)?.response?.data?.message ?? '비밀번호 변경에 실패했습니다.';
alert(msg);
},
},
);
};

// 공통 인풋 스타일
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function Modal({ isOpen = true, title, children, onClose, positio
<div className="font-heading2 text-default-gray-800">{title}</div>
<GraySvgButton type="cancle" onClick={onClose} />
</div>
<div className="flex">{children}</div>
<div className="flex px-[12px]">{children}</div>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion src/components/dateCourse/dateCourse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ function DateCourse({ defaultOpen = false }: { defaultOpen?: boolean }) {
} else {
setIsBookmarked(!isBookmarked);
}
// console.log('북마크 해제');
};

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/dateCourse/dateCourseSearchFilterOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function DateCourseSearchFilterOption({ options, type, value, onC
mode="search"
onSearchClick={handleSearch}
placeholder="ex: 서울시 강남구"
className="w-full"
className="!w-full min-w-full"
value={inputValue}
onChange={handleInputChange}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/home/dateRecommend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function DateRecommend() {
<div className="flex flex-col sm:px-[40px] px-[20px] py-[40px] justify-center h-full gap-[30px] sm:gap-[20px] w-full">
{/* 상단 텍스트 */}
<div className="flex items-center justify-between sm:flex-row flex-col">
<div className="text-2xl font-bold whitespace-nowrap">이번 주 {forecastData?.result?.region?.regionName ?? '지역'} 데이트 추천</div>
<div className="text-2xl font-bold break-keep text-center">이번 주 {forecastData?.result?.region?.regionName ?? '지역'} 데이트 추천</div>
<button
type="button"
className="sm:mt-1 mt-4 text-sm text-black underline sm:self-center self-end"
Expand Down
56 changes: 29 additions & 27 deletions src/components/modal/SettingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import InfoSetting from '@/components/settingTab/InfoSetting';
import MembershipSetting from '@/components/settingTab/MembershipSetting';

import Modal from '../common/modal';
import MobileSettingTab from '../settingTab/mobileSettingTab';

import LogoutSvg from '@/assets/icons/Logout_Blank.svg?react';

Expand Down Expand Up @@ -38,35 +39,36 @@ export default function SettingsModal({ onClose, defaultTab = '알람' }: ISetti

return (
<Modal onClose={onClose} position="main" title="설정">
{/* 좌측 탭 메뉴 */}
<div className="w-[130px] sm:w-[170px] flex flex-col justify-between pr-4 sm:pr-6">
<div className="flex flex-col space-y-3">
{['알람', '멤버십', '정보'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab as '알람' | '멤버십' | '정보')}
className={`w-full h-[35px] px-4 rounding-16 font-body1 text-left transition
<div className="sm:flex-row flex-col flex min-w-[300px]">
<div className="w-[130px] sm:w-[170px] hidden sm:flex flex-col justify-between pr-4 sm:pr-6">
<div className="flex flex-col space-y-3">
{['알람', '멤버십', '정보'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab as '알람' | '멤버십' | '정보')}
className={`w-full h-[35px] px-4 rounding-16 font-body1 text-left transition
${activeTab === tab ? 'bg-primary-500 text-white' : 'bg-default-gray-400 text-default-gray-700'}`}
>
{tab} 설정
</button>
))}
</div>

<button
onClick={handleLogout}
disabled={logoutPending}
className="font-body2 text-default-gray-800 cursor-pointer flex items-center space-x-2 mt-6 disabled:opacity-60"
>
<LogoutSvg className="w-4 h-4" fill="none" stroke="CurrentColor" />
<span>{logoutPending ? '로그아웃 중...' : '로그아웃'}</span>
</button>
</div>
>
{tab} 설정
</button>
))}
</div>

<div className="flex-1 pl-6 overflow-y-auto">
{activeTab === '알람' && <AlarmSetting />}
{activeTab === '멤버십' && <MembershipSetting />}
{activeTab === '정보' && <InfoSetting />}
<button
onClick={handleLogout}
disabled={logoutPending}
className="font-body2 text-default-gray-800 cursor-pointer flex items-center space-x-2 mt-6 disabled:opacity-60"
>
<LogoutSvg className="w-4 h-4" fill="none" stroke="CurrentColor" />
<span>{logoutPending ? '로그아웃 중...' : '로그아웃'}</span>
</button>
</div>
<MobileSettingTab setActiveTab={setActiveTab} activeTab={activeTab} handleLogout={handleLogout} logoutPending={logoutPending} />
<div className="flex-1 sm:pl-6 overflow-y-auto min-w-[300px]">
{activeTab === '알람' && <AlarmSetting />}
{activeTab === '멤버십' && <MembershipSetting />}
{activeTab === '정보' && <InfoSetting />}
</div>
</div>
</Modal>
);
Expand Down
1 change: 0 additions & 1 deletion src/components/modal/dateCourseSearchFilterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export default function DateCourseSearchFilterModal({ onClose }: TDateCourseSear
const [errorMessages, setErrorMessages] = useState<string[]>(Array(7).fill(''));

const handleSearch = () => {
console.log('선택된 필터:', answers);
onClose();
};

Expand Down
1 change: 0 additions & 1 deletion src/components/modal/deleteBookmarkModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ function DeleteBookmarkModal({ onClose, isOpen, changeState }: TDeleteBookmarkMo
}, [isOpen]);

const handleDelete = () => {
// Logic to delete the bookmarked date course
changeState(false);
onClose();
};
Expand Down
4 changes: 4 additions & 0 deletions src/components/modal/regionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { useUserRegion } from '@/hooks/home/useUserRegion';
import EditableInputBox from '@/components/common/EditableInputBox';
import Modal from '@/components/common/modal';

import { queryClient } from '@/api/queryClient';
import { homeKeys } from '@/queryKey/queryKey';

interface IRegionModalProps {
onClose: () => void;
}
Expand All @@ -23,6 +26,7 @@ function RegionModal({ onClose }: IRegionModalProps) {
},
{
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: homeKeys.userRegion().queryKey });
onClose();
},
},
Expand Down
39 changes: 22 additions & 17 deletions src/components/settingTab/AlarmSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { useGetAlarmSettings, usePatchAlarmSettings } from '@/hooks/settingAlarm

import ToggleSwitch from '@/components/common/ToggleSwitch';

import { queryClient } from '@/api/queryClient';
import { alarmKeys } from '@/queryKey/queryKey';

type TAlarmType = 'email' | 'push' | 'sms';

interface IAlarmSettingState {
Expand Down Expand Up @@ -36,32 +39,34 @@ export default function AlarmSetting() {

// 토글 핸들러
const handleToggle = (key: TAlarmType) => {
const prev = alarmSetting;
const next = { ...prev, [key]: !prev[key] };
setAlarmSetting(next);

patchAlarm(
{
emailAlarm: next.email,
pushAlarm: next.push,
smsAlarm: next.sms,
},
{
onError: () => setAlarmSetting(prev),
},
);
setAlarmSetting((prev) => {
const next = { ...prev, [key]: !prev[key] };
patchAlarm(
{
emailAlarm: next.email,
pushAlarm: next.push,
smsAlarm: next.sms,
},
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: alarmKeys.alarmSettings().queryKey });
},
onError: () => setAlarmSetting(prev),
},
);
return next;
});
};

const items: { label: string; key: TAlarmType }[] = [
{ label: 'Email 알람', key: 'email' },
{ label: '푸쉬 알람', key: 'push' },
{ label: 'SMS 알람', key: 'sms' },
];

return (
<div className="mt-5 flex flex-col gap-10 p-8">
<div className="mt-5 flex flex-col gap-10">
{items.map(({ label, key }) => (
<div key={key} className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-6 w-full">
<div key={key} className="flex sm:items-center justify-between gap-3 sm:gap-6 w-full sm:px-0 px-[20px]">
<p className="font-heading3 text-default-gray-800 truncate overflow-hidden">{label}</p>
<ToggleSwitch value={alarmSetting[key]} onChange={() => handleToggle(key)} onLabel="ON" offLabel="OFF" />
</div>
Expand Down
Loading