-
Notifications
You must be signed in to change notification settings - Fork 1
Fix/158 리팩토링 #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/158 리팩토링 #159
Changes from 6 commits
18ca00c
9d222c3
9c7f66a
2b0e3c3
c5a2456
14d8e19
93acc70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) props 변경 시 이미지 상태 동기화 필요 초기 렌더에서만 아래 수정으로 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||||
|
|
@@ -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> | ||||||||||||||||||||||||||||||||||||
|
|
@@ -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 ${ | ||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a11y: 클릭 가능한 정적 요소(div) 교체 및 클릭 소스 일관성 수정
아래 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 |
||||||||||||||||||||||||||||||||||||
| {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> | ||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,11 +2,8 @@ | |||||
|
|
||||||
| import Star from '@assets/svg/star'; | ||||||
| import { useState, useEffect } from 'react'; | ||||||
| import { ReviewTitleProps } from '@/types/activityDetailType'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 타입 전용 임포트 제안 타입만 사용하므로 -import { ReviewTitleProps } from '@/types/activityDetailType';
+import type { ReviewTitleProps } from '@/types/activityDetailType';📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| interface ReviewTitleProps { | ||||||
| reviewCount: number; | ||||||
| rating: number; | ||||||
| } | ||||||
| export default function ReviewTitle({ | ||||||
| reviewCount = 0, | ||||||
| rating = 0, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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, | ||||||||||||||||||||||||||||
|
|
@@ -43,10 +36,9 @@ function Title({ | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const handleDeleteConfirm = () => { | ||||||||||||||||||||||||||||
| if (!id) return; | ||||||||||||||||||||||||||||
| mutate(id as string); | ||||||||||||||||||||||||||||
| mutate(Number(id)); | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainid 타입 변환 검증 필요
다음 스크립트로 id 사용 패턴을 확인해보겠습니다: 🏁 Script executed: #!/bin/bash
# useParams의 id 사용 패턴 확인
rg -n -A3 -B3 "useParams.*id" --type=tsx --type=tsLength of output: 80 Ripgrep가 기본적으로 🏁 Script executed: #!/bin/bash
# useParams의 id 사용 패턴을 tsx/ts 파일에서 재검색
rg -n -C3 "useParams.*id" -g '*.tsx' -g '*.ts'Length of output: 48
🏁 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
🏁 Script executed: #!/bin/bash
# Title.tsx 상단과 사용 부분 출력
sed -n '1,200p' src/app/\(with-header\)/activities/\[id\]/components/Title.tsxLength of output: 2500 Title.tsx에서 useParams 타입 지정 및 배열 검사 추가 필요
• 파일: - 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);
};위 변경으로
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| setIsPopupOpen(false); | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
Comment on lines
36
to
40
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
@@ -23,6 +23,7 @@ export const useDeleteActivity = () => { | |||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: ['popularExperiences'] }); | ||||||||||||||||||||||||||||||||||||||
| router.push(`/`); | ||||||||||||||||||||||||||||||||||||||
| toast.success('체험이 삭제되었습니다!'); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| onError: (error: AxiosError) => { | ||||||||||||||||||||||||||||||||||||||
| const responseData = error.response?.data as | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 타입 전용 임포트 제안 타입은 -import { SubImageType } from '@/types/addEditExperienceType';
+import type { SubImageType } from '@/types/addEditExperienceType';🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export const useEditActivityForm = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { id } = useParams() as { id: string }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { id } = useParams(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion useParams 타입 미지정으로 인한 id의 string | string[] 가능성 (템플릿 리터럴 등에서 타입 오류 위험) Next.js App Router의 useParams는 제네릭으로 파라미터 스키마를 지정하지 않으면 id가 string | string[]로 추론됩니다. 현재처럼 템플릿 리터럴( - const { id } = useParams();
+ const params = useParams<{ id: string }>();
+ const id = params.id;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const queryClient = useQueryClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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
💡 Verification agent
🧩 Analysis chain
Preview 배포 로그까지 제거되는 부작용 가능성(Vercel 사용 시) — VERCEL_ENV 기준 전환 검토
Vercel의 Preview 환경에서도
NODE_ENV는production이므로 현재 설정으로는 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:
Length of output: 347
콘솔 에러/경고 호출(
console.error/console.warn) 사용 현황과VERCEL_ENV참조 여부를 정확히 파악하기 위해 아래 스크립트를 실행해 주세요.🏁 Script executed:
Length of output: 5010
콘솔 에러/경고 로그 보존 조건 전환 검토 필요
검증 결과:
console.error/console.warn사용처가 다수(src/components/…,src/app/api/…등 10여 개 파일 이상)에 존재하므로, Preview 배포에서 중요한 에러·경고 로그가 사라질 수 있습니다.VERCEL_ENV레퍼런스는 전혀 없습니다.위 사항을 바탕으로, Vercel Preview 환경에서만 로그를 보존하도록
next.config.ts의removeConsole조건을 다음과 같이 전환 적용할 것을 권장드립니다.compiler: { - removeConsole: process.env.NODE_ENV === 'production', + removeConsole: + process.env.VERCEL_ENV === 'production' + ? { exclude: ['error', 'warn'] } + : false, },VERCEL_ENV는 배포 시 자동으로production,preview,development중 하나로 설정됩니다.VERCEL_ENV === 'preview'이므로removeConsole: false가 되어 에러/경고 로그가 보존됩니다..env.local등에VERCEL_ENV=development를 설정해 주세요.📝 Committable suggestion
🤖 Prompt for AI Agents