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
2 changes: 1 addition & 1 deletion src/api/alarm/alarm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TRequestGetAlarm, TRequestPostDeviceToken, TResponseGetAlarm, TResponsePostDeviceToken } from '@/types/alarm/alarm';

import { axiosInstance } from '../axiosInstance';
import { axiosInstance } from '@/api/axiosInstance';

export const getAlarm = async ({ size = 5, cursor }: TRequestGetAlarm): Promise<TResponseGetAlarm> => {
const { data } = await axiosInstance.get('/api/v1/alarms', {
Expand Down
2 changes: 1 addition & 1 deletion src/api/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
TSocialLoginValues,
} from '@/types/auth/auth';

import { axiosInstance } from '../axiosInstance';
import { axiosInstance } from '@/api/axiosInstance';

export const defaultSignup = async ({ email, password, username, gender, phoneNumber, birth, socialId }: TSignupValues): Promise<TSignupResponse> => {
const { data } = await axiosInstance.post('/api/v1/auth/sign-up', { email, password, socialId, username, gender, phoneNumber, birth });
Expand Down
2 changes: 1 addition & 1 deletion src/api/course/course.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TSearchRegionResponse, TSearchRegionValues } from '@/types/dateCourse/dateCourse';

import { axiosInstance } from '../axiosInstance';
import { axiosInstance } from '@/api/axiosInstance';

export const searchRegion = async ({ keyword }: TSearchRegionValues): Promise<TSearchRegionResponse> => {
const { data } = await axiosInstance.get('/api/v1/regions/search', {
Expand Down
8 changes: 8 additions & 0 deletions src/api/home/dateCourse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { TDateCourseSavedCountResponse } from '@/types/home/dateCourse';

import { axiosInstance } from '@/api/axiosInstance';

export const getDateCourseSavedCount = async (): Promise<TDateCourseSavedCountResponse> => {
const { data } = await axiosInstance.get('/api/v1/logs/datecourses/saved-count');
return data;
};
14 changes: 14 additions & 0 deletions src/api/home/dateTimes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { TGetDateTimeStates, TMonthlyDatePlaceResponse } from '../../types/home/datePlace';

import { axiosInstance } from '@/api/axiosInstance';

// 월별 데이트 장소 수 조회 API
export const getMonthlyDatePlaceStates = async (): Promise<TMonthlyDatePlaceResponse> => {
const { data } = await axiosInstance.get('/api/v1/logs/dateplaces/monthly');
return data;
};

export const getDateTimeStates = async (): Promise<TGetDateTimeStates> => {
const { data } = await axiosInstance.get('/api/v1/logs/datecourses/average');
return data;
};
9 changes: 9 additions & 0 deletions src/api/home/keyword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { TWeeklyKeywordResponse } from '@/types/home/keyword';

import { axiosInstance } from '@/api/axiosInstance';

// 이번 주 인기 키워드 조회 API
export const getWeeklyKeywords = async (): Promise<TWeeklyKeywordResponse> => {
const { data } = await axiosInstance.get('/api/v1/logs/keyword/weekly');
return data;
};
9 changes: 9 additions & 0 deletions src/api/home/level.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { TUserGradeResponse } from '@/types/home/level';

import { axiosInstance } from '@/api/axiosInstance';

// 사용자 등급 조회 API
export const getUserGrade = async (): Promise<TUserGradeResponse> => {
const { data } = await axiosInstance.get('/api/v1/members/grade');
return data;
Comment on lines +7 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Axios 제네릭으로 응답 타입 안전성 강화

응답 타입을 제네릭으로 명시하면 data의 타입 안정성이 올라갑니다.

아래처럼 변경을 권장합니다.

-    const { data } = await axiosInstance.get('/api/v1/members/grade');
+    const { data } = await axiosInstance.get<TUserGradeResponse>('/api/v1/members/grade');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data } = await axiosInstance.get('/api/v1/members/grade');
return data;
const { data } = await axiosInstance.get<TUserGradeResponse>('/api/v1/members/grade');
return data;
🤖 Prompt for AI Agents
In src/api/home/level.ts around lines 6 to 7, the axios call returns an untyped
data object; change the call to use Axios generics to enforce response typing
(e.g., define or import the expected response interface for the grade endpoint
and call axiosInstance.get<GradeResponse>('/api/v1/members/grade')) so the
returned data is properly typed and downstream code gains compile-time safety.

};
13 changes: 13 additions & 0 deletions src/api/home/region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { TGetUserRegionResponse, TPatchUserRegionRequest, TPatchUserRegionResponse } from '@/types/home/region';

import { axiosInstance } from '@/api/axiosInstance';

export const patchUserRegion = async ({ regionId }: TPatchUserRegionRequest): Promise<TPatchUserRegionResponse> => {
const { data } = await axiosInstance.patch('/api/v1/regions/users', { regionId });
return data;
};
Comment on lines +5 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Axios 제네릭으로 응답 타입 명시

응답 타입을 명확히 지정해 런타임 이슈를 컴파일 타임에 방지하는 것을 권장합니다.

 export const patchUserRegion = async ({ regionId }: TPatchUserRegionRequest): Promise<TPatchUserRegionResponse> => {
-    const { data } = await axiosInstance.patch('/api/v1/regions/users', { regionId });
+    const { data } = await axiosInstance.patch<TPatchUserRegionResponse>('/api/v1/regions/users', { regionId });
     return data;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const patchUserRegion = async ({ regionId }: TPatchUserRegionRequest): Promise<TPatchUserRegionResponse> => {
const { data } = await axiosInstance.patch('/api/v1/regions/users', { regionId });
return data;
};
export const patchUserRegion = async ({ regionId }: TPatchUserRegionRequest): Promise<TPatchUserRegionResponse> => {
const { data } = await axiosInstance.patch<TPatchUserRegionResponse>('/api/v1/regions/users', { regionId });
return data;
};
🤖 Prompt for AI Agents
In src/api/home/region.ts around lines 5 to 8, the axios.patch call does not
specify a response generic which can hide type mismatches; update the call to
use axiosInstance.patch<TPatchUserRegionResponse>('/api/v1/regions/users', {
regionId }) so the returned data is typed as TPatchUserRegionResponse and the
function signature stays correct, ensuring compile-time checking of the response
shape.


export const getUserRegion = async (): Promise<TGetUserRegionResponse> => {
const { data } = await axiosInstance.get('/api/v1/regions/users/current');
return data;
};
22 changes: 22 additions & 0 deletions src/api/home/weather.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {
TGetPrecipitationRequest,
TGetPrecipitationResponse,
TGetWeeklyWeatherRecommendationRequest,
TGetWeeklyWeatherRecommendationResponse,
} from '@/types/home/weather';

import { axiosInstance } from '@/api/axiosInstance';

// 주간 날씨 추천 조회 API
export const getWeeklyWeatherRecommendation = async ({
regionId,
startDate,
}: TGetWeeklyWeatherRecommendationRequest): Promise<TGetWeeklyWeatherRecommendationResponse> => {
const { data } = await axiosInstance.get(`/api/v1/weather/${regionId}/weekly`, { params: { startDate } });
return data;
};

export const getPrecipitation = async ({ regionId, startDate }: TGetPrecipitationRequest): Promise<TGetPrecipitationResponse> => {
const { data } = await axiosInstance.get(`/api/v1/weather/${regionId}/precipitation`, { params: { startDate } });
return data;
};
16 changes: 4 additions & 12 deletions src/api/notice/notice.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import type { TFetchNoticeDetailResponse, TFetchNoticesResponse } from '@/types/notice/notice';
import type { TFetchNoticeDetailResponse, TFetchNoticesResponse, TRequestGetNoticeRequest } from '@/types/notice/notice';

import { axiosInstance } from '../axiosInstance';
import { axiosInstance } from '@/api/axiosInstance';

// 공지사항 전체 조회 API
export const fetchNotices = async ({
category,
page,
size,
}: {
category: 'SERVICE' | 'SYSTEM';
page: number;
size: number;
}): Promise<TFetchNoticesResponse> => {
export const fetchNotices = async ({ noticeCategory = 'SERVICE', page, size }: TRequestGetNoticeRequest): Promise<TFetchNoticesResponse> => {
const { data } = await axiosInstance.get('/api/v1/notices', {
params: { noticeCategory: category, page, size },
params: { noticeCategory: noticeCategory, page, size },
});
return data;
};
Expand Down
3 changes: 1 addition & 2 deletions src/assets/icons/weather/rain.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/components/common/modalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import DateCourseSearchFilterModal from '@/components/modal/dateCourseSearchFilt
import ErrorModal from '@/components/modal/errorModal';
import SettingsModal from '@/components/modal/SettingModal';

import RegionModal from '../modal/regionModal';

Comment on lines +9 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

경로 alias 일관성 유지 제안

다른 모달들은 절대 경로 alias('@/components/...')를 사용하고 있는데, RegionModal만 상대 경로를 사용합니다. 팀 규칙에 맞춰 alias로 통일하면 가독성과 유지보수성이 좋아집니다.

적용 예시:

-import RegionModal from '../modal/regionModal';
+import RegionModal from '@/components/modal/regionModal';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import RegionModal from '../modal/regionModal';
-import RegionModal from '../modal/regionModal';
+import RegionModal from '@/components/modal/regionModal';
🤖 Prompt for AI Agents
In src/components/common/modalProvider.tsx around lines 9-10, the import for
RegionModal uses a relative path while other modals use the project alias;
replace the relative import with the same alias style (e.g. import RegionModal
from '@/components/modal/regionModal') to keep path conventions consistent, and
ensure the aliased path matches the project’s tsconfig/webpack path mapping.

import useModalStore from '@/store/useModalStore';

// 모달 타입 정의 -> 만약 다른 모달을 추가하고 싶다면 여기에 타입을 추가하고, MODAL_COMPONENTS에 컴포넌트를 추가하면 됩니다.
Expand All @@ -15,13 +17,15 @@ export const MODAL_TYPES = {
DateCourseSearchFilterModal: 'DateCourseSearchFilterModal',
SettingsModal: 'SettingsModal', //설정 모달 추가
AlarmModal: 'AlarmModal',
RegionModal: 'RegionModal',
};

export const MODAL_COMPONENTS = {
[MODAL_TYPES.ErrorModal]: ErrorModal,
[MODAL_TYPES.DateCourseSearchFilterModal]: DateCourseSearchFilterModal,
[MODAL_TYPES.SettingsModal]: SettingsModal,
[MODAL_TYPES.AlarmModal]: AlarmModal,
[MODAL_TYPES.RegionModal]: RegionModal,
};
Comment on lines 23 to 29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

MODAL_COMPONENTS에 명시적 타입 지정 권장

현재 ModalComponent의 타입이 느슨해 JSX 사용 시 추론이 깨질 수 있습니다. 컴포넌트 프로프 형식이 모두 onClosemodalProps를 받는 동일 인터페이스라면 명시적으로 매핑 타입을 지정해주세요.

예시(제안 코드):

import type { ComponentType } from 'react';

type ModalBaseProps = { onClose: () => void } & Record<string, any>;
export const MODAL_COMPONENTS: Record<ModalType, ComponentType<ModalBaseProps>> = {
  [MODAL_TYPES.ErrorModal]: ErrorModal,
  [MODAL_TYPES.DateCourseSearchFilterModal]: DateCourseSearchFilterModal,
  [MODAL_TYPES.SettingsModal]: SettingsModal,
  [MODAL_TYPES.AlarmModal]: AlarmModal,
  [MODAL_TYPES.RegionModal]: RegionModal,
};
🤖 Prompt for AI Agents
In src/components/common/modalProvider.tsx around lines 23 to 29, the
MODAL_COMPONENTS object lacks an explicit typed mapping which weakens JSX prop
inference; declare a ModalBaseProps type (e.g., { onClose: () => void } &
Record<string, any>), import React's ComponentType, ensure you have a
ModalType/ModalType union matching MODAL_TYPES keys, and annotate
MODAL_COMPONENTS as Record<ModalType, ComponentType<ModalBaseProps>> so each
modal component is typed to receive onClose and modalProps consistently.


export default function ModalProvider() {
Expand Down
19 changes: 12 additions & 7 deletions src/components/home/banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,50 @@ import Button from '../common/Button';

import ChevronBack from '@/assets/icons/default_arrows/chevron_back.svg?react';
import ChevronForward from '@/assets/icons/default_arrows/chevron_forward.svg?react';
import scroll from '@/images/scroll.png';
import bicycle from '@/images/banner/bicycle.png';
import bukchon from '@/images/banner/bukchon.png';
import itaewon from '@/images/banner/itaewon.png';
import sungsu from '@/images/banner/sungsu.png';

const slides = [
{
title: '서울 성수동 : 옛것과 새로운 것이 교차하는 하루',
description: '1960년대부터 조성된 오래된 공장 건물과 최근 벽돌 건물들의 분위기',
tags: ['#활발한 활동', '#레트로 감성', '#서울 핫플'],
img: sungsu,
},
{
title: '한강 자전거 데이트 : 바람 따라 달리는 낭만',
description: '도심 속 자연을 만끽하며 힐링 타임',
tags: ['#운동 데이트', '#자연과 함께', '#저녁노을'],
img: bicycle,
},
{
title: '이태원 세계 음식 투어 : 입 안 가득 여행',
description: '세계 각국의 맛을 한 자리에서 즐기기',
tags: ['#미식가 커플', '#이국적인 분위기', '#도심 속 여행'],
img: itaewon,
},
{
title: '북촌 한옥마을 산책 : 전통의 미를 따라 걷기',
description: '골목골목 숨어있는 사진 명소',
tags: ['#한옥', '#조용한 산책', '#전통과 현대'],
img: bukchon,
},
];

function Banner() {
const navigate = useNavigate();
const [currentIndex, setCurrentIndex] = useState(0);

// ⏱️ 자동 슬라이드 타이머
useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex((prevIndex) => (prevIndex + 1) % slides.length);
}, 3000); // 2초

return () => clearInterval(interval); // 언마운트 시 정리
return () => clearInterval(interval);
}, []);

// ⬅️➡️ 버튼 클릭 핸들러
const goToPrev = () => {
setCurrentIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length);
};
Expand All @@ -52,12 +57,12 @@ function Banner() {
setCurrentIndex((prevIndex) => (prevIndex + 1) % slides.length);
};

const { title, description, tags } = slides[currentIndex];
const { title, description, tags, img } = slides[currentIndex];

return (
<div className="relative w-full">
<img src={scroll} alt="배너" className="w-full h-[450px] object-cover" />

<img src={img} alt="배너" className="w-full h-[450px] object-cover" />
<div className="absolute inset-0 bg-gradient-to-b from-black/60 to-transparent z-[5]" />
{/* 내용 */}
<div className="absolute inset-0 flex flex-col justify-start px-4 sm:px-12 py-10 text-white z-10">
<div className="font-body1 mb-5 sm:mt-0 mt-[30px]">오늘의 데이트 추천</div>
Expand Down
15 changes: 14 additions & 1 deletion src/components/home/dateCourseStore.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { Navigate } from 'react-router-dom';

import { useDateCourseSavedCount } from '@/hooks/home/useDateCourseStates';

import MainCard from './mainCard';

import ArchiveBlank from '@/assets/icons/Archive_Blank.svg?react';

function DateCourseStore() {
const { data, isLoading, error } = useDateCourseSavedCount();
if (error) {
return <Navigate to="/error" replace />;
}
return (
<MainCard>
<div className="flex flex-col px-4 sm:px-8 lg:px-[20px] py-8 lg:py-[28px] h-full justify-center">
Expand All @@ -11,11 +19,16 @@ function DateCourseStore() {
</div>
<div className="flex text-sm sm:text-base lg:text-m bold-medium text-[#616161] mb-1">내 데이트 코스를</div>
<div className="flex gap-1 items-center">
<div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">2,345명</div>
{isLoading ? (
<div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">로딩...</div>
) : (
<div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">{data?.result.count}명</div>
)}
Comment on lines +22 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

데이터 미도착 시 'undefined명' 노출 가능성 및 숫자 포매팅 개선

데이터가 일시적으로 없을 때 undefined명이 표시될 수 있습니다. 기본값과 천 단위 구분을 적용해 가독성을 높이세요.

적용 예시(diff):

-                        <div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">{data?.result.count}명</div>
+                        <div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">
+                            {(data?.result.count ?? 0).toLocaleString()}명
+                        </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isLoading ? (
<div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">로딩...</div>
) : (
<div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">{data?.result.count}</div>
)}
{isLoading ? (
<div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">
로딩...
</div>
) : (
<div className="text-lg sm:text-xl font-bold text-primary-700 whitespace-nowrap">
{(data?.result.count ?? 0).toLocaleString()}
</div>
)}
🤖 Prompt for AI Agents
In src/components/home/dateCourseStore.tsx around lines 24 to 28, the count
display can show "undefined명" when data is missing and lacks thousand
separators; to fix, derive a safe numeric value (e.g., const count =
data?.result?.count ?? 0) and render the formatted string using a locale-aware
formatter (e.g., Intl.NumberFormat or toLocaleString) so the UI shows "0명" when
absent and numbers with thousand separators.

<div className="text-sm sm:text-base lg:text-m bold-medium text-[#616161] whitespace-nowrap">이 저장했어요.</div>
</div>
</div>
</MainCard>
);
}

export default DateCourseStore;
54 changes: 34 additions & 20 deletions src/components/home/dateLocation.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
import { useMemo } from 'react';
import { Navigate } from 'react-router-dom';
import ClipLoader from 'react-spinners/ClipLoader';

import { useMonthlyPlaceStates } from '@/hooks/home/useDatePlaceStates';

import MainCard from '@/components/home/mainCard';

function DateLocation() {
const { data, isLoading, error } = useMonthlyPlaceStates();
const maxCount = useMemo(() => {
return data?.result?.datePlaceLogList?.reduce((max, cur) => Math.max(max, cur.count), 0) ?? 0;
}, [data]);
if (error) {
return <Navigate to="/error" replace />;
}
if (isLoading) {
return (
<MainCard>
<ClipLoader className="self-center" />
</MainCard>
);
}
return (
<MainCard>
<div className="py-[28px] flex flex-col">
<div className="text-xl font-bold text-[#616161] mb-6">WithTime에 등록된 데이트 장소 수</div>
<div className="flex items-end gap-8 w-full justify-center">
<div className="flex flex-col items-center">
<span className="text-xs text-default-gray-500 mb-1">230</span>
<div className="h-16 w-10 bg-default-gray-400 mb-2 flex items-start justify-center" />
<div className="text-default-gray-500 mt-1">2022</div>
</div>
<div className="flex flex-col items-center">
<span className="text-xs text-default-gray-500 mb-1">430</span>
<div className="h-24 w-10 bg-default-gray-400 mb-2 flex items-start justify-center" />
<div className="text-default-gray-500 mt-1">2023</div>
</div>
<div className="flex flex-col items-center">
<span className="text-xs text-default-gray-500 mb-1">830</span>
<div className="h-36 w-10 bg-default-gray-400 mb-2 flex items-start justify-center" />
<div className="text-default-gray-500 mt-1">2024</div>
</div>
<div className="flex flex-col items-center">
<span className="text-xs text-default-gray-500 mb-1">1,230</span>
<div className="h-48 w-10 bg-default-gray-400 mb-2 flex items-start justify-center" />
<div className="text-default-gray-500 mt-1">2025</div>
</div>
{(data?.result?.datePlaceLogList ?? []).map((graph, idx) => {
const height = maxCount ? Math.max((graph.count / maxCount) * 200, 4) : 4;
return (
<div className="flex flex-col items-center" key={idx}>
<span className="text-xs text-default-gray-500 mb-1">{graph.count}</span>
<div
aria-label={`월별 데이트 장소 수: ${graph.year}년 ${graph.month}월 ${graph.count}곳`}
className="w-10 bg-default-gray-400 mb-2 flex items-start justify-center transition-all duration-300"
style={{ height: `${height}px` }}
/>
<div className="text-default-gray-500 mt-1">{graph.month}월</div>
</div>
);
})}
</div>
</div>
</MainCard>
Expand Down
Loading