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
18 changes: 18 additions & 0 deletions public/assets/svg/moreOptionsIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

const MoreOptionsIcon = ({ size = 24, ...props }) => (
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

TypeScript 타입 정의와 접근성 개선이 필요합니다

컴포넌트에 적절한 타입 정의가 없고, 접근성을 위한 속성이 누락되어 있습니다.

다음과 같이 개선하세요:

+interface MoreOptionsIconProps extends React.SVGProps<SVGSVGElement> {
+  size?: number;
+}
+
-const MoreOptionsIcon = ({ size = 24, ...props }) => (
+const MoreOptionsIcon = ({ size = 24, ...props }: MoreOptionsIconProps) => (
   <svg
     xmlns='http://www.w3.org/2000/svg'
     width={size}
     height={size}
     fill='none'
     viewBox='0 0 40 40'
+    role='img'
+    aria-label='더보기 옵션'
     {...props}
   >
📝 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 MoreOptionsIcon = ({ size = 24, ...props }) => (
interface MoreOptionsIconProps extends React.SVGProps<SVGSVGElement> {
size?: number;
}
const MoreOptionsIcon = ({ size = 24, ...props }: MoreOptionsIconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill="none"
viewBox="0 0 40 40"
role="img"
aria-label="더보기 옵션"
{...props}
>
{/* existing icon paths/circles */}
</svg>
);
🤖 Prompt for AI Agents
In public/assets/svg/moreOptionsIcon.tsx at line 3, the MoreOptionsIcon
component lacks TypeScript type definitions and accessibility attributes. Define
proper TypeScript types for the component props, including size and any other
SVG attributes. Add accessibility attributes such as role="img" and an
appropriate aria-label or title to improve screen reader support.

<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 40 40'
{...props}
>
<circle cx='20' cy='9' r='3' fill='#79747E' />
<circle cx='20' cy='20' r='3' fill='#79747E' />
<circle cx='20' cy='31' r='3' fill='#79747E' />
</svg>
);

export default MoreOptionsIcon;
28 changes: 28 additions & 0 deletions src/apis/myActivities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { privateInstance } from './privateInstance';
import { MyActivitiesResponse } from '@/types/dashboardTypes';

/**
* 내 체험 리스트 조회 (무한 스크롤용)
* GET /my-activities
*/
export const getMyActivitiesWithPagination = async (params?: {
cursorId?: number;
size?: number;
}): Promise<MyActivitiesResponse> => {
const queryParams = new URLSearchParams();
if (params?.cursorId)
queryParams.append('cursorId', params.cursorId.toString());
if (params?.size) queryParams.append('size', params.size.toString());

const url = `/my-activities${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
const response = await privateInstance.get(url);
return response.data;
};
Comment on lines +8 to +20
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

API 함수 구현을 승인하며 쿼리 파라미터 처리 개선을 제안합니다

전반적인 구현이 잘 되어 있습니다. 쿼리 파라미터 생성 부분을 약간 개선할 수 있습니다.

더 간결한 구현을 위해 다음과 같이 개선할 수 있습니다:

 export const getMyActivitiesWithPagination = async (params?: {
   cursorId?: number;
   size?: number;
 }): Promise<MyActivitiesResponse> => {
-  const queryParams = new URLSearchParams();
-  if (params?.cursorId)
-    queryParams.append('cursorId', params.cursorId.toString());
-  if (params?.size) queryParams.append('size', params.size.toString());
-
-  const url = `/my-activities${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
+  const queryParams = new URLSearchParams();
+  if (params?.cursorId !== undefined) {
+    queryParams.append('cursorId', params.cursorId.toString());
+  }
+  if (params?.size !== undefined) {
+    queryParams.append('size', params.size.toString());
+  }
+  
+  const queryString = queryParams.toString();
+  const url = `/my-activities${queryString ? `?${queryString}` : ''}`;
   const response = await privateInstance.get(url);
   return response.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 getMyActivitiesWithPagination = async (params?: {
cursorId?: number;
size?: number;
}): Promise<MyActivitiesResponse> => {
const queryParams = new URLSearchParams();
if (params?.cursorId)
queryParams.append('cursorId', params.cursorId.toString());
if (params?.size) queryParams.append('size', params.size.toString());
const url = `/my-activities${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
const response = await privateInstance.get(url);
return response.data;
};
export const getMyActivitiesWithPagination = async (params?: {
cursorId?: number;
size?: number;
}): Promise<MyActivitiesResponse> => {
const queryParams = new URLSearchParams();
if (params?.cursorId !== undefined) {
queryParams.append('cursorId', params.cursorId.toString());
}
if (params?.size !== undefined) {
queryParams.append('size', params.size.toString());
}
const queryString = queryParams.toString();
const url = `/my-activities${queryString ? `?${queryString}` : ''}`;
const response = await privateInstance.get(url);
return response.data;
};
🤖 Prompt for AI Agents
In src/apis/myActivities.ts around lines 8 to 20, the current query parameter
construction manually appends each parameter, which can be simplified. Refactor
the code to build the URLSearchParams object more concisely by conditionally
adding parameters in a cleaner way or by using a helper function to filter and
append only defined parameters. This will make the code more readable and
maintainable without changing its behavior.


/**
* 내 체험 삭제
* DELETE /deleteActivity/{id}
*/
export const deleteMyActivity = async (id: number): Promise<void> => {
await privateInstance.delete(`/deleteActivity/${id}`);
};
74 changes: 74 additions & 0 deletions src/app/(with-header)/myactivity/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import { ProfileNavigation } from '@/app/(with-header)/mypage/components';
import useResponsiveRouting from '@/hooks/useResponsiveRouting';
import { useMyProfile } from '@/hooks/useMyPageQueries';

export default function MyActivityLayout({
children,
}: {
children: React.ReactNode;
}) {
const { mounted } = useResponsiveRouting();
const { isLoading, error } = useMyProfile();

// mounted + API 로딩 상태 모두 체크
if (!mounted || isLoading) {
return (
<div className='min-h-screen bg-gray-100'>
<div className='mx-auto max-w-1200 px-20 py-24 lg:py-72'>
<div className='flex gap-24'>
{/* 좌측 프로필 네비게이션 스켈레톤 - 데스크톱/태블릿 */}
<div className='hidden flex-shrink-0 animate-pulse md:block'>
<div className='h-432 w-251 rounded border border-gray-300 bg-white p-24 lg:w-384'>
{/* 프로필 이미지 영역 */}
<div className='mb-32 flex justify-center'>
<div className='h-160 w-160 rounded-full bg-gray-200'></div>
</div>
{/* 메뉴 리스트 영역 */}
<div className='space-y-2'>
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className='h-44 w-203 rounded-xl bg-gray-200 lg:w-336'
></div>
))}
</div>
</div>
</div>
{/* 메인 스켈레톤 */}
<div className='flex-grow animate-pulse rounded bg-gray-200'></div>
</div>
</div>
</div>
);
}

if (error) {
return (
<div className='flex min-h-screen items-center justify-center bg-gray-100'>
<div className='text-center'>
<h2 className='mb-2 text-xl font-bold text-red-500'>
로그인이 필요합니다
</h2>
<p className='text-gray-600'>다시 로그인해주세요.</p>
</div>
</div>
);
}

// API 로딩 완료 + mounted 상태일 때만 실행
return (
<div className='min-h-screen bg-gray-100'>
<div className='mx-auto max-w-1200 px-20 py-24 lg:py-72'>
<div className='flex gap-24'>
{/* 좌측 프로필 네비게이션 섹션 - 데스크톱/태블릿에서만 표시 */}
<ProfileNavigation />

{/* 우측 메인 콘텐츠 섹션 */}
<div className='flex-grow'>{children}</div>
</div>
</div>
</div>
);
}
102 changes: 102 additions & 0 deletions src/app/(with-header)/mypage/activities/components/ActivityCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use client';

import Image from 'next/image';
import { useState } from 'react';
import { MyActivity } from '@/types/dashboardTypes';
import { useRouter } from 'next/navigation';
import Star from '@assets/svg/star';
import MoreOptionsIcon from '@assets/svg/moreOptionsIcon';

interface ActivityCardProps {
activity: MyActivity;
onDelete: (activityId: number) => void;
}

export default function ActivityCard({
activity,
onDelete,
}: ActivityCardProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const router = useRouter();

const { id, title, price, bannerImageUrl, rating, reviewCount } = activity;

const handleEdit = () => {
router.push(`/myactivity/${id}`);
};

const handleDelete = () => {
onDelete(id);
setIsMenuOpen(false);
};

return (
<div className='flex h-204 w-792 rounded-3xl border border-gray-300 bg-white'>
{/* 이미지 영역 */}
<div className='relative h-204 w-204 flex-shrink-0 overflow-hidden rounded-l-xl'>
<Image src={bannerImageUrl} alt={title} fill className='object-cover' />
</div>

{/* 콘텐츠 영역 */}
<div className='flex flex-1 flex-col justify-start px-24 py-14'>
{/* 별점 및 리뷰 */}
<div className='flex items-center gap-6'>
<div className='flex items-center gap-2'>
<Star size={19} />
<span className='font-base font-normal text-black'>{rating}</span>
<span className='font-base font-normal text-black'>
({reviewCount})
</span>
</div>
</div>

{/* 제목 */}
<div className='mt-6'>
<h3 className='text-nomad text-xl font-bold'>{title}</h3>
</div>

<div className='mt-auto flex items-center justify-between'>
{/* 가격 */}
<p className='text-2xl font-medium text-gray-900'>
₩{price.toLocaleString()} / 인
</p>

{/* 더보기 옵션 */}
<div className='relative'>
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className='flex h-40 w-40 items-center justify-center rounded-full hover:bg-gray-100'
>
<MoreOptionsIcon size={40} />
</button>
Comment on lines +67 to +71
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

버튼에 명시적인 type 속성을 추가하세요

모든 버튼에 type="button" 속성을 추가하여 의도하지 않은 폼 제출을 방지하세요.

             <button
+              type="button"
               onClick={() => setIsMenuOpen(!isMenuOpen)}
               className='flex h-40 w-40 items-center justify-center rounded-full hover:bg-gray-100'
             >

                   <button
+                    type="button"
                     onClick={handleEdit}
                     className='flex h-62 w-full items-center justify-center border-b border-gray-300 px-46 py-18 text-center text-lg font-medium text-gray-900 hover:bg-gray-50'
                   >

                   <button
+                    type="button"
                     onClick={handleDelete}
                     className='flex h-62 w-full items-center justify-center px-46 py-18 text-center text-lg font-medium text-gray-900 hover:bg-gray-50'
                   >

Also applies to: 83-87, 89-93

🧰 Tools
🪛 Biome (2.1.2)

[error] 67-71: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/activities/components/ActivityCard.tsx around
lines 67 to 71, 83 to 87, and 89 to 93, the button elements lack an explicit
type attribute, which can cause unintended form submissions. Add type="button"
to each button element in these line ranges to clearly specify their behavior
and prevent accidental form submissions.


{isMenuOpen && (
<>
<div
className='fixed inset-0 z-40'
onClick={() => setIsMenuOpen(false)}
/>

{/* 드롭다운 메뉴 */}
<div className='absolute top-full right-0 z-50 w-160 rounded-md border border-gray-300 bg-white shadow-lg'>
<button
onClick={handleEdit}
className='flex h-62 w-full items-center justify-center border-b border-gray-300 px-46 py-18 text-center text-lg font-medium text-gray-900 hover:bg-gray-50'
>
수정하기
</button>
<button
onClick={handleDelete}
className='flex h-62 w-full items-center justify-center px-46 py-18 text-center text-lg font-medium text-gray-900 hover:bg-gray-50'
>
삭제하기
</button>
</div>
</>
)}
Comment on lines +73 to +96
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

🛠️ Refactor suggestion

드롭다운 메뉴의 접근성을 개선하세요

현재 구현은 키보드 네비게이션을 지원하지 않습니다. 접근성을 위해 다음을 고려하세요:

  1. ESC 키로 메뉴 닫기
  2. 포커스 트랩 구현
  3. 적절한 ARIA 속성 추가

Headless UI의 Menu 컴포넌트나 Radix UI의 DropdownMenu를 사용하는 것을 추천합니다. 이미 구현된 접근성 기능을 활용할 수 있습니다.

🧰 Tools
🪛 Biome (2.1.2)

[error] 76-81: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 76-81: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)


[error] 84-88: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)


[error] 90-94: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/activities/components/ActivityCard.tsx around
lines 73 to 96, the dropdown menu lacks keyboard accessibility features such as
closing with the ESC key, focus trapping, and ARIA attributes. To fix this,
replace the current custom dropdown implementation with a well-supported
accessible component like Headless UI's Menu or Radix UI's DropdownMenu, which
provide built-in keyboard navigation, focus management, and ARIA roles. This
will ensure the dropdown is fully accessible without manually implementing these
features.

</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import Modal from '@/components/Modal';
import Button from '@/components/Button';
import CheckIcon from '@assets/svg/check';

interface DeleteActivityModalProps {
isOpen: boolean;
onCancel: () => void;
onConfirm: () => void;
isLoading?: boolean;
}

export default function DeleteActivityModal({
isOpen,
onCancel,
onConfirm,
isLoading = false,
}: DeleteActivityModalProps) {
return (
<Modal isOpen={isOpen} onOpenChange={(open) => !open && onCancel()}>
<Modal.Content
className='!h-184 !w-298 !max-w-none !min-w-0 !rounded-xl !p-0'
zIndex={999}
backdropClassName='!z-999'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

잘못된 Tailwind 클래스명을 수정하세요

backdropClassName='!z-999'는 올바르지 않은 Tailwind 클래스명입니다. 대괄호 표기법을 사용해야 합니다.

다음과 같이 수정하세요:

-        backdropClassName='!z-999'
+        backdropClassName='!z-[999]'
📝 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
backdropClassName='!z-999'
backdropClassName='!z-[999]'
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/activities/components/DeleteActivityModal.tsx at
line 25, the Tailwind class 'backdropClassName="!z-999"' is invalid. Replace it
with the correct bracket notation for arbitrary values by changing it to
'backdropClassName="!z-[999]"' to ensure proper styling.

>
<div className='flex h-full w-full flex-col items-center justify-center gap-24 rounded-xl bg-white p-16 shadow-[0px_4px_16px_0px_rgba(17,34,17,0.05)]'>
{/* 체크 아이콘 */}
<div className='flex justify-center'>
<CheckIcon size={24} />
</div>

{/* 메시지 */}
<p className='text-nomad text-center text-lg font-medium'>
체험을 삭제하시겠어요?
</p>

{/* 버튼 */}
<div className='flex gap-12'>
<Button
variant='secondary'
className='text-md h-38 w-80 rounded-lg border border-gray-300 font-medium'
onClick={onCancel}
disabled={isLoading}
>
아니오
</Button>
<Button
variant='primary'
className='text-md bg-nomad h-38 w-80 rounded-lg font-medium text-white'
onClick={onConfirm}
disabled={isLoading}
>
{isLoading ? '삭제 중...' : '삭제하기'}
</Button>
</div>
</div>
</Modal.Content>
</Modal>
);
}
Comment on lines +14 to +61
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

코드 중복을 줄이기 위한 공통 컴포넌트 고려

DeleteActivityModalCancelReservationModal이 거의 동일한 구조를 가지고 있습니다. 메시지와 버튼 텍스트만 다를 뿐입니다. 공통 ConfirmationModal 컴포넌트를 만들어 재사용성을 높이는 것을 고려해보세요.

공통 컴포넌트 예시:

interface ConfirmationModalProps {
  isOpen: boolean;
  onCancel: () => void;
  onConfirm: () => void;
  isLoading?: boolean;
  message: string;
  confirmText: string;
  cancelText?: string;
  loadingText?: string;
}
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/activities/components/DeleteActivityModal.tsx
from lines 14 to 61, the DeleteActivityModal component shares almost identical
structure with CancelReservationModal except for message and button texts. To
reduce code duplication, create a reusable ConfirmationModal component that
accepts props for isOpen, onCancel, onConfirm, isLoading, message, confirmText,
cancelText, and loadingText. Replace DeleteActivityModal and
CancelReservationModal with this common component by passing the appropriate
texts as props.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import EmptyDocumentIcon from '@assets/svg/empty-document';

export default function EmptyActivities() {
return (
<div className='flex flex-col items-center justify-center py-120'>
{/* 빈 상태 아이콘 */}
<div className='mb-24'>
<EmptyDocumentIcon size={131} />
</div>

{/* 빈 상태 메시지 */}
<p className='text-2xl font-normal text-gray-700'>
아직 등록한 체험이 없어요
</p>
</div>
);
}
Loading