Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const nextConfig: NextConfig = {
},
],
},

compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
Comment on lines +21 to +23
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

프로덕션 관측성 유지: console 전부 제거 대신 error/warn는 보존 권장

compiler.removeConsole: true는 클라이언트뿐 아니라 서버 번들에서도 모든 console.* 호출을 제거합니다. 운영 장애 파악에 중요한 console.error/console.warn까지 사라질 수 있어 관측성이 급격히 저하됩니다. 아래처럼 예외 목록을 두는 편이 안전합니다.

   compiler: {
-    removeConsole: process.env.NODE_ENV === 'production',
+    removeConsole:
+      process.env.NODE_ENV === 'production'
+        ? { exclude: ['error', 'warn'] }
+        : false,
   },
🤖 Prompt for AI Agents
In next.config.ts around lines 21 to 23, the current boolean
compiler.removeConsole removes all console.* (including
console.error/console.warn); change this so production builds do not strip
error/warn. Specifically, stop using a plain boolean removeConsole in production
and instead preserve error/warn by either (A) disabling removeConsole and adding
the babel plugin transform-remove-console configured with exclude:
['error','warn'] for production, or (B) keep removeConsole false and integrate a
build-time transform (e.g., babel plugin or a custom minifier config) that only
removes verbose logs (console.log/debug/info) while leaving console.error and
console.warn intact; implement the chosen approach and ensure it only applies to
production builds.

💡 Verification agent

🧩 Analysis chain

Preview 배포 로그까지 제거되는 부작용 가능성(Vercel 사용 시) — VERCEL_ENV 기준 전환 검토

Vercel의 Preview 환경에서도 NODE_ENVproduction이므로 현재 설정으로는 QA 단계 로그까지 제거됩니다. Vercel을 사용한다면 VERCEL_ENV === 'production'일 때만 제거하도록 전환을 고려하세요. (동시에 error/warn 보존도 적용한 예)

   compiler: {
-    removeConsole: process.env.NODE_ENV === 'production',
+    removeConsole:
+      process.env.VERCEL_ENV === 'production'
+        ? { exclude: ['error', 'warn'] }
+        : false,
   },

다음 스크립트로 (1) 코드베이스에서 console.error/console.warn 사용처를 파악하고, (2) Vercel 사용 여부를 대략 확인할 수 있습니다. 결과에 따라 위 옵션 적용을 결정해 주세요.


🏁 Script executed:

#!/bin/bash
# 1) error/warn 사용 현황
rg -nP --type=ts --type=tsx -C2 '\bconsole\.(error|warn)\s*\('

# 2) Vercel 관련 설정/언급 탐색
fd -a -HI 'vercel*' 2>/dev/null || true
rg -nPI 'vercel|VERCEL_ENV|@vercel' -g '!node_modules/**' README* package.json .github 2>/dev/null || true

Length of output: 347


콘솔 에러/경고 호출(console.error/console.warn) 사용 현황과 VERCEL_ENV 참조 여부를 정확히 파악하기 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# 1) console.error/console.warn 사용 현황 (TS/TSX 파일 대상)
rg -nP -C2 '\bconsole\.(error|warn)\s*\(' --glob '*.ts' --glob '*.tsx' -g '!node_modules/**' || echo "(none found)"

# 2) VERCEL_ENV 참조 여부
rg -nP -C2 'VERCEL_ENV' -g '!node_modules/**' || echo "(none found)"

Length of output: 5010


콘솔 에러/경고 로그 보존 조건 전환 검토 필요

검증 결과:

  • console.error/console.warn 사용처가 다수(src/components/…, src/app/api/… 등 10여 개 파일 이상)에 존재하므로, Preview 배포에서 중요한 에러·경고 로그가 사라질 수 있습니다.
  • 코드베이스 내에 VERCEL_ENV 레퍼런스는 전혀 없습니다.

위 사항을 바탕으로, Vercel Preview 환경에서만 로그를 보존하도록 next.config.tsremoveConsole 조건을 다음과 같이 전환 적용할 것을 권장드립니다.

   compiler: {
-    removeConsole: process.env.NODE_ENV === 'production',
+    removeConsole:
+      process.env.VERCEL_ENV === 'production'
+        ? { exclude: ['error', 'warn'] }
+        : false,
   },
  • Vercel 환경변수인 VERCEL_ENV는 배포 시 자동으로 production, preview, development 중 하나로 설정됩니다.
  • Preview(검증) 단계에서는 VERCEL_ENV === 'preview'이므로 removeConsole: false가 되어 에러/경고 로그가 보존됩니다.
  • 만약 로컬 개발 환경에서도 동일하게 로그를 유지하려면, .env.local 등에 VERCEL_ENV=development를 설정해 주세요.
  • 위 설정 변경 전후로 빌드·배포 로그를 확인하여 기대 동작이 맞는지 검증해 주시기 바랍니다.
📝 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
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
compiler: {
removeConsole:
process.env.VERCEL_ENV === 'production'
? { exclude: ['error', 'warn'] }
: false,
},
🤖 Prompt for AI Agents
next.config.ts around lines 21-23: 현재 removeConsole이 NODE_ENV==='production'로 되어
있어 Vercel Preview에서 console.error/warn가 제거될 수 있으니 조건을 VERCEL_ENV를 우선 사용하도록
변경하세요; 구체적으로 removeConsole이 VERCEL_ENV==='production'일 때만 true가 되도록 하고(단,
VERCEL_ENV가 없을 수 있으니 이전 동작을 보장하려면 fallback으로 NODE_ENV==='production'을 사용) 설정을
변경한 뒤 빌드·배포에서 Preview와 Production 환경에서 로그 보존이 기대대로 동작하는지 검증하세요.

};

export default nextConfig;
Binary file added public/assets/img/default-bg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default function ActivityDetailForm() {
<LocationMap address={activityData.address} />

<ReviewSection
activityId={id as string}
activityId={Number(id)}
reviewCount={activityData.reviewCount}
rating={activityData.rating}
/>
Expand Down
68 changes: 53 additions & 15 deletions src/app/(with-header)/activities/[id]/components/ImageGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,41 @@ import Image from 'next/image';
import React, { useState } from 'react';
import { ImageGridProps } from '@/types/activityDetailType';
import { AnimatePresence, motion } from 'framer-motion';
import Modal from '@/components/Modal';
import { DEFAULT_BG } from '@/constants/AvatarConstants';

function ImageGrid({ mainImage, subImages }: ImageGridProps) {
const images = [mainImage, ...subImages];

const [image, setImage] = useState([mainImage, ...subImages]);
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);

Comment on lines +11 to +16
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

props 변경 시 이미지 상태 동기화 필요

초기 렌더에서만 [mainImage, ...subImages]로 state를 설정하므로, props가 갱신되어도 UI가 반영되지 않습니다. 상세 페이지에서 데이터가 비동기로 로드/변경될 수 있다면 동기화 로직을 추가하세요.

아래 수정으로 props 변화에 동기화할 수 있습니다.

-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 ...
-  const [image, setImage] = useState([mainImage, ...subImages]);
+  const [image, setImage] = useState([mainImage, ...subImages]);
   const [currentIndex, setCurrentIndex] = useState(0);
   const [direction, setDirection] = useState(0);
   const [isOpen, setIsOpen] = useState(false);
   const [selectedImage, setSelectedImage] = useState<string | null>(null);
 
+  useEffect(() => {
+    setImage([mainImage, ...subImages]);
+  }, [mainImage, subImages]);
📝 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 [image, setImage] = useState([mainImage, ...subImages]);
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
import React, { useEffect, useState } from 'react';
...
const [image, setImage] = useState([mainImage, ...subImages]);
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
useEffect(() => {
setImage([mainImage, ...subImages]);
}, [mainImage, subImages]);
...
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ImageGrid.tsx around lines
11 to 16, the image state is initialized only on first render with [mainImage,
...subImages] so updates to mainImage/subImages props are not reflected; add a
useEffect that watches mainImage and subImages and calls setImage([mainImage,
...subImages]) (optionally reset currentIndex and selectedImage as needed) to
synchronize state when props change.

const handleImageClick = (image: string) => {
setSelectedImage(image);
setIsOpen(true);
};

const handleImageError = (index: number) => {
setImage((prev) => prev.map((src, i) => (i === index ? DEFAULT_BG : src)));
};

const prevSlide = () => {
setDirection(-1);
setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
setCurrentIndex((prev) => (prev === 0 ? image.length - 1 : prev - 1));
};

const nextSlide = () => {
setDirection(1);
setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
setCurrentIndex((prev) => (prev === image.length - 1 ? 0 : prev + 1));
};

const variants = {
enter: (direction: number) => ({
x: direction > 0 ? 300 : -300,
opacity: 0,
}),
center: {
x: 0,
opacity: 1,
},
center: { x: 0, opacity: 1 },
exit: (direction: number) => ({
x: direction > 0 ? -300 : 300,
opacity: 0,
Expand All @@ -55,11 +64,13 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) {
className='absolute inset-0'
>
<Image
src={images[currentIndex]}
alt={` ${currentIndex + 1}`}
src={image[currentIndex]}
alt={`${currentIndex + 1}`}
fill
className='rounded-lg object-cover'
priority
unoptimized
onError={() => handleImageError(currentIndex)}
/>
</motion.div>
</AnimatePresence>
Expand All @@ -81,7 +92,7 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) {
</button>

<div className='absolute bottom-2 left-1/2 flex -translate-x-1/2 gap-1'>
{images.map((_, i) => (
{image.map((_, i) => (
<div
key={i}
className={`h-10 w-10 rounded-full ${
Expand All @@ -94,28 +105,55 @@ function ImageGrid({ mainImage, subImages }: ImageGridProps) {

{/* PC/태블릿 */}
<div className='hidden h-[500px] grid-cols-4 grid-rows-4 gap-6 md:grid'>
<div className='relative col-span-2 row-span-4 hover:animate-pulse'>
<div
onClick={() => handleImageClick(mainImage)}
className='relative col-span-2 row-span-4 hover:animate-pulse'
>
<Image
src={mainImage}
src={image[0]}
alt='메인이미지'
fill
className='rounded-lg object-cover'
onError={() => handleImageError(0)}
/>
</div>
Comment on lines +108 to 119
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

a11y: 클릭 가능한 정적 요소(div) 교체 및 클릭 소스 일관성 수정

  • 정적 요소에 onClick을 부여해 접근성 린트 에러가 발생합니다. 버튼 요소로 교체해 키보드 접근성을 확보하세요.
  • 메인 타일 클릭 시 mainImage(원본)을 넘기고, 표시 소스는 image[0](오류 시 대체)라 불일치합니다. 클릭에도 image[0]을 사용해 일관성을 맞추세요.
  • map 콜백 파라미터명이 state 변수 image와 동일해 가독성이 떨어집니다. src 등으로 변경하세요.

아래 diff를 적용하면 접근성 경고를 해결하고, 클릭 동작의 일관성을 맞출 수 있습니다.

-        <div
-          onClick={() => handleImageClick(mainImage)}
-          className='relative col-span-2 row-span-4 hover:animate-pulse'
-        >
+        <button
+          type='button'
+          aria-label='메인 이미지 확대 보기'
+          onClick={() => handleImageClick(image[0])}
+          className='relative col-span-2 row-span-4 hover:animate-pulse focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'
+        >
           <Image
             src={image[0]}
             alt='메인이미지'
             fill
             className='rounded-lg object-cover'
+            sizes="(min-width: 768px) 50vw, 100vw"
             onError={() => handleImageError(0)}
           />
-        </div>
+        </button>
-        {image.slice(1, 5).map((image, index) => (
-          <div
-            key={index + 1}
-            onClick={() => handleImageClick(image)}
-            className='relative col-span-1 row-span-2 h-full hover:animate-pulse'
-          >
-            <Image
-              src={image}
-              alt={`서브이미지 ${index + 1}`}
-              fill
-              className='rounded-lg object-cover'
-              onError={() => handleImageError(index + 1)}
-            />
-          </div>
-        ))}
+        {image.slice(1, 5).map((src, index) => (
+          <button
+            type='button'
+            key={index + 1}
+            aria-label={`서브 이미지 ${index + 1} 확대 보기`}
+            onClick={() => handleImageClick(src)}
+            className='relative col-span-1 row-span-2 h-full hover:animate-pulse focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'
+          >
+            <Image
+              src={src}
+              alt={`서브이미지 ${index + 1}`}
+              fill
+              className='rounded-lg object-cover'
+              sizes="(min-width: 768px) 25vw, 100vw"
+              onError={() => handleImageError(index + 1)}
+            />
+          </button>
+        ))}

Also applies to: 120-134

🧰 Tools
🪛 Biome (2.1.2)

[error] 109-113: 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] 109-113: 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)

🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ImageGrid.tsx around lines
108-119 (also apply same change to 120-134): replace the clickable static <div>
with a <button> to satisfy accessibility/keyboard interaction, update the click
handler to pass the displayed source (use image[0] / the mapped src) instead of
mainImage so the click payload matches the shown image, and rename the map
callback parameter from image to a neutral name like src to avoid shadowing the
state variable and improve readability; ensure the button keeps the same
classes/visuals and still calls handleImageError with the correct index on image
load error.

{subImages.slice(0, 4).map((image, index) => (
{image.slice(1, 5).map((image, index) => (
<div
key={index}
key={index + 1}
onClick={() => handleImageClick(image)}
className='relative col-span-1 row-span-2 h-full hover:animate-pulse'
>
<Image
src={image}
alt={`서브이미지 ${index + 1}`}
fill
className='rounded-lg object-cover'
onError={() => handleImageError(index + 1)}
/>
</div>
))}
</div>
<Modal onOpenChange={setIsOpen} isOpen={isOpen}>
<Modal.Content className='rounded-md'>
<Modal.Header>
<Modal.Close />
</Modal.Header>
<Modal.Item className='flex items-center justify-center'>
<div className='relative aspect-square w-[1200px]'>
{selectedImage && (
<Image
src={selectedImage}
alt='확대 이미지'
fill
className='rounded-lg object-cover p-18'
/>
)}
</div>
</Modal.Item>

<Modal.Footer></Modal.Footer>
</Modal.Content>
</Modal>
</>
);
}
Expand Down
19 changes: 2 additions & 17 deletions src/app/(with-header)/activities/[id]/components/ReviewSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,9 @@ import { privateInstance } from '@/apis/privateInstance';
import ReviewTitle from './ReviewTitle';
import useUserStore from '@/stores/authStore';
import cn from '@/lib/cn';

import ReviewCardSkeleton from './Skeletons/ReviewCardSkeleton';

interface ReviewSectionProps {
activityId: string;
reviewCount: number;
rating: number;
}

interface ReviewProps {
id: string;
user: {
nickname: string;
profileImageUrl: string;
};
createdAt: string;
content: string;
}
import { ReviewSectionProps } from '@/types/activityDetailType';
import { ReviewProps } from '@/types/activityDetailType';

function ReviewSection({
activityId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

import Star from '@assets/svg/star';
import { useState, useEffect } from 'react';
import { ReviewTitleProps } from '@/types/activityDetailType';
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

타입 전용 임포트 제안

타입만 사용하므로 import type으로 가져오면 불필요한 번들 영향이 줄어듭니다.

-import { ReviewTitleProps } from '@/types/activityDetailType';
+import type { ReviewTitleProps } from '@/types/activityDetailType';
📝 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 { ReviewTitleProps } from '@/types/activityDetailType';
import type { ReviewTitleProps } from '@/types/activityDetailType';
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx around line
5, the import of ReviewTitleProps is used only as a type and should be a
type-only import to avoid affecting the runtime bundle; change the import to use
the TypeScript "import type" form for ReviewTitleProps so it is erased at
compile time and does not impact bundle size.


interface ReviewTitleProps {
reviewCount: number;
rating: number;
}
export default function ReviewTitle({
reviewCount = 0,
rating = 0,
Expand Down
14 changes: 3 additions & 11 deletions src/app/(with-header)/activities/[id]/components/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,8 @@ import { useParams, useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { useDeleteActivity } from '../hooks/useDeleteActivity';
import Popup from '@/components/Popup';

interface TitleProps {
title: string;
category: string;
rating: number;
reviewCount: number;
address: string;
isOwner: boolean;
}
import { toast } from 'sonner';
import { TitleProps } from '@/types/activityDetailType';

function Title({
title,
Expand All @@ -43,10 +36,9 @@ function Title({

const handleDeleteConfirm = () => {
if (!id) return;
mutate(id as string);
mutate(Number(id));
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

id 타입 변환 검증 필요

useParams()에서 반환되는 idstring | string[] 타입인데, Number(id)로 변환할 때 배열인 경우 NaN이 될 수 있습니다.

다음 스크립트로 id 사용 패턴을 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# useParams의 id 사용 패턴 확인
rg -n -A3 -B3 "useParams.*id" --type=tsx --type=ts

Length of output: 80


Ripgrep가 기본적으로 tsx 파일 타입을 인식하지 못해 검색이 실패했습니다. 아래 스크립트로 *.tsx*.ts 파일 전체에서 useParams 사용 패턴을 다시 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# useParams의 id 사용 패턴을 tsx/ts 파일에서 재검색
rg -n -C3 "useParams.*id" -g '*.tsx' -g '*.ts'

Length of output: 48


useParams 사용 위치와 id 구조분해 할당 패턴을 다시 확인하기 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# 1. useParams 사용 위치 확인
rg -n "useParams" -g '*.tsx' -g '*.ts'

# 2. useParams에서 id를 구조분해 할당하는 패턴 확인
rg -n -C3 "const\s*{\s*id\s*}\s*=\s*useParams" -g '*.tsx' -g '*.ts'

Length of output: 1210


Title.tsx 파일에서 id 구조분해 및 mutate(Number(id)) 호출 부분을 정확히 확인하기 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# Title.tsx 상단과 사용 부분 출력
sed -n '1,200p' src/app/\(with-header\)/activities/\[id\]/components/Title.tsx

Length of output: 2500


Title.tsx에서 useParams 타입 지정 및 배열 검사 추가 필요

useParams()는 기본적으로 string | string[] 타입을 반환하기 때문에, 현재 코드처럼 바로 Number(id)를 호출하면 배열인 경우 NaN이 될 수 있습니다. 아래와 같이 제네릭으로 idstring 타입으로 지정하고, 배열 여부를 검사하는 로직을 추가해주세요.

• 파일: src/app/(with-header)/activities/[id]/components/Title.tsx
• 위치: const { id } = useParams();handleDeleteConfirm 내부

-  const { id } = useParams();
+  // 단일 세그먼트인 [id]이므로, 제네릭을 통해 string임을 보장합니다.
+  const { id } = useParams<{ id: string }>();

...

const handleDeleteConfirm = () => {
-   if (!id) return;
-   mutate(Number(id));
+   // id가 없거나 배열인 경우 처리 중단
+   if (!id || Array.isArray(id)) return;
+   // id는 string이 보장되므로 안전하게 변환
+   mutate(Number(id));
    setIsPopupOpen(false);
};

위 변경으로

  1. TypeScript 레벨에서 idstring임을 명시
  2. 런타임에서 배열일 경우를 방어
    두 가지 모두 보완됩니다.
📝 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
mutate(Number(id));
// 단일 세그먼트인 [id]이므로, 제네릭을 통해 string임을 보장합니다.
const { id } = useParams<{ id: string }>();
const handleDeleteConfirm = () => {
// id가 없거나 배열인 경우 처리 중단
if (!id || Array.isArray(id)) return;
// id는 string이 보장되므로 안전하게 변환
mutate(Number(id));
setIsPopupOpen(false);
};
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/Title.tsx around line 38,
useParams is untyped and you call Number(id) directly which can produce NaN if
id is string[]; change const { id } = useParams(); to const { id } = useParams<{
id: string }>(); and in handleDeleteConfirm add a runtime guard: if
(Array.isArray(id) || typeof id !== 'string') { /* handle invalid id: return,
show error, or abort */ } else { mutate(Number(id)); } so TypeScript knows id is
a string and you defend against array values at runtime.

setIsPopupOpen(false);
};
Comment on lines 36 to 40
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

삭제 팝업은 성공 시에만 닫기

현재는 API 결과와 무관하게 팝업이 닫힙니다. 호출 단에서 mutate 옵션을 사용해 성공 시에만 닫도록 변경하면 UX와 상태 정합성이 좋아집니다.

   const handleDeleteConfirm = () => {
     if (!id) return;
-    mutate(Number(id));
-    setIsPopupOpen(false);
+    mutate(Number(id), {
+      onSuccess: () => setIsPopupOpen(false),
+      // 실패 시 팝업 유지(에러 토스트는 훅에서 처리)
+    });
   };
📝 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 handleDeleteConfirm = () => {
if (!id) return;
mutate(id as string);
mutate(Number(id));
setIsPopupOpen(false);
};
const handleDeleteConfirm = () => {
if (!id) return;
mutate(Number(id), {
onSuccess: () => setIsPopupOpen(false),
// 실패 시 팝업 유지(에러 토스트는 훅에서 처리)
});
};
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/Title.tsx around lines 37 to
41, the delete popup is being closed immediately regardless of API outcome;
change mutate(Number(id)); setIsPopupOpen(false); to call mutate with options so
the popup is closed only on success (e.g., mutate(Number(id), { onSuccess: () =>
setIsPopupOpen(false), onError: /* optional error handling */ })), preserving
the early-return check for id.



return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';

const deleteActivity = async (id: string) => {
const deleteActivity = async (id: number) => {
const response = await privateInstance.delete(`/deleteActivity/${id}`);
return response.data;
};
Comment on lines +7 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)

숫자형 ID 전환 OK—함수·뮤테이션 제네릭으로 타입 안정성 보강 권장

런타임은 문제없지만 반환·에러·변수 타입을 명시하면 호출부 안전성이 올라갑니다.

-const deleteActivity = async (id: number) => {
+const deleteActivity = async (id: number): Promise<unknown> => {
   const response = await privateInstance.delete(`/deleteActivity/${id}`);
   return response.data;
 };
 
 export const useDeleteActivity = () => {
   const queryClient = useQueryClient();
   const router = useRouter();
 
-  return useMutation({
+  return useMutation<unknown, AxiosError, number>({
     mutationFn: deleteActivity,
📝 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 deleteActivity = async (id: number) => {
const response = await privateInstance.delete(`/deleteActivity/${id}`);
return response.data;
};
const deleteActivity = async (id: number): Promise<unknown> => {
const response = await privateInstance.delete(`/deleteActivity/${id}`);
return response.data;
};
export const useDeleteActivity = () => {
const queryClient = useQueryClient();
const router = useRouter();
return useMutation<unknown, AxiosError, number>({
mutationFn: deleteActivity,
// …other options (e.g., onSuccess, onError)
});
};
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts around lines
7 to 10, the deleteActivity function currently lacks explicit return and error
typing; change it to a typed, generic async function (e.g., export async
function deleteActivity<T = void>(id: number): Promise<T>) and annotate the
axios call with the response generic (use the axios delete generic to infer
response data type) so the function returns Promise<T>; ensure callers pass or
infer the correct T and update any mutation typing to accept the same generic
for full type-safety and clearer error/response typing.

Expand All @@ -23,6 +23,7 @@ export const useDeleteActivity = () => {
});
queryClient.invalidateQueries({ queryKey: ['popularExperiences'] });
router.push(`/`);
toast.success('체험이 삭제되었습니다!');
},
onError: (error: AxiosError) => {
const responseData = error.response?.data as
Expand Down
58 changes: 0 additions & 58 deletions src/app/(with-header)/activities/[id]/mock/mock.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import { ScheduleSelectForm } from '../../components/ScheduleSelectForm';
import { ImageSection } from '../../components/ImageSection';
import { useEditActivityForm } from '../hooks/useEditActivityForm';
import EditActivityFormSkeleton from '../../loading';

interface SubImageType {
id?: number;
url: string | File;
}
import { SubImageType } from '@/types/addEditExperienceType';

export default function EditActivityForm() {
const {
Expand Down
22 changes: 12 additions & 10 deletions src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ import { ActivityDetailEdit, Schedule } from '@/types/activityDetailType';
import { AxiosError } from 'axios';
import { toast } from 'sonner';
import { notFound } from 'next/navigation';
import { validateSchedules } from '../../utils/dateValidatoin';

interface SubImageType {
id?: number;
url: string | File;
}
import { SubImageType } from '@/types/addEditExperienceType';
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

타입 전용 임포트 제안

타입은 import type으로 가져오는 것이 모범사례입니다.

-import { SubImageType } from '@/types/addEditExperienceType';
+import type { SubImageType } from '@/types/addEditExperienceType';
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts around
line 12, the SubImageType is imported as a regular value import; change it to a
type-only import by replacing the statement with "import type { SubImageType }
from '@/types/addEditExperienceType';" so the compiler treats it as a type-only
import and it is erased from emitted JS (avoids accidental runtime dependency).


export const useEditActivityForm = () => {
const { id } = useParams() as { id: string };
const { id } = useParams();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

useParams 타입 미지정으로 인한 id의 string | string[] 가능성 (템플릿 리터럴 등에서 타입 오류 위험)

Next.js App Router의 useParams는 제네릭으로 파라미터 스키마를 지정하지 않으면 id가 string | string[]로 추론됩니다. 현재처럼 템플릿 리터럴(/activities/${id})에 바로 사용하면 타입 체커에서 걸리거나 런타임 이상 동작 가능성이 있습니다. 제네릭을 명시해 단일 string으로 고정하세요.

-  const { id } = useParams();
+  const params = useParams<{ id: string }>();
+  const id = params.id;
📝 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 { id } = useParams();
const params = useParams<{ id: string }>();
const id = params.id;
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts around
line 15, useParams is inferred as string | string[] which can break
template-literal usages; update the call to useParams with an explicit generic
like useParams<{ id: string }> so id is typed as a single string, and add a
small runtime guard (throw or return early) if id is undefined to satisfy the
typechecker and avoid runtime issues.

const router = useRouter();
const queryClient = useQueryClient();

Expand Down Expand Up @@ -209,9 +204,16 @@ export const useEditActivityForm = () => {

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const validationMessage = validateSchedules(dates);
if (validationMessage) {
toast.error(validationMessage);
if (
!title ||
!category ||
!description ||
!address ||
!price ||
!mainImage ||
dates.length === 0
) {
toast.error('소개이미지를 제외한 모든값은 필수값입니다!');
return;
Comment on lines +207 to 217
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

검증 메시지와 로직 불일치 (메인 이미지 필수인데 “제외”) + 스케줄 필드 공란 검증 권장

메인 이미지를 필수로 요구하면서 메시지는 반대로 안내합니다. 또한 스케줄 항목의 날짜/시간 공란도 빠르게 검증하면 UX가 좋아집니다.

   if (
     !title ||
     !category ||
     !description ||
     !address ||
     !price ||
     !mainImage ||
-    dates.length === 0
+    dates.length === 0 ||
+    dates.some((d) => !d.date || !d.startTime || !d.endTime)
   ) {
-    toast.error('소개이미지를 제외한 모든값은 필수값입니다!');
+    toast.error('모든 값은 필수값입니다!');
     return;
   }

추가로, 생성 훅(useCreateActivityForm)과 본 훅의 검증 메시지를 통일하면 사용자 혼란을 줄일 수 있습니다.

원하시면 생성/수정 공통 폼 검증 유틸(예: validateActivityForm)을 만들어 두 훅에서 공유하도록 PR 보조 커밋을 제안드릴 수 있습니다.

📝 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
if (
!title ||
!category ||
!description ||
!address ||
!price ||
!mainImage ||
dates.length === 0
) {
toast.error('소개이미지를 제외한 모든값은 필수값입니다!');
return;
if (
!title ||
!category ||
!description ||
!address ||
!price ||
!mainImage ||
dates.length === 0 ||
dates.some((d) => !d.date || !d.startTime || !d.endTime)
) {
toast.error('모든 값은 필수값입니다!');
return;
}

}
try {
Expand Down
16 changes: 2 additions & 14 deletions src/app/(with-header)/myactivity/components/AddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,8 @@ import DaumPostcode from 'react-daum-postcode';
import { useState } from 'react';
import Input from '@/components/Input';
import Button from '@/components/Button';

interface AddressInputProps {
onAddressChange: (address: string) => void;
address: string;
}

interface PostcodeData {
address: string;
addressType: 'R' | 'J';
bname: string;
buildingName: string;
zonecode: string;
userSelectedType: string;
}
import { AddressInputProps } from '@/types/addEditExperienceType';
import { PostcodeData } from '@/types/addEditExperienceType';

export default function AddressInput({
onAddressChange,
Expand Down
Loading
Loading