Repository: GoTogether-Inc/frontend
Pull request #157: fix: 모든 페이지 기능/API 에러 해결
Author: login: Yejiin21
Labels: name: 🛠️ BugFix
description: 오류 해결 과정 기록
name: 🪄 Refactoring
description: 코드 리팩토링
Created: 2025-05-31T15:44:17Z | Merged: 2025-05-31T16:05:11Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **신규 기능**
    - 이벤트 목록과 슬라이더에서 종료된 이벤트가 자동으로 제외되어, 지난 이벤트가 더 이상 표시되지 않습니다.
    - 이벤트 카드 클릭 시 상세 페이지로 이동할 수 있습니다.
    - 이벤트 상세 페이지에서 로그인한 사용자만 북마크(좋아요) 버튼을 볼 수 있습니다.
    - 이벤트 상세 페이지의 공식 웹사이트 링크가 여러 개의 참조 링크로 동적으로 표시됩니다.
    - 티켓 옵션 드래그앤드랍 기능에 옵션 삭제 기능이 추가되었습니다.

- **버그 수정**
    - 전화번호 입력 시 하이픈 포함 13자리로 정확히 입력해야 하며, 형식에 맞지 않으면 오류 메시지가 안내됩니다.
    - 티켓 옵션 삭제 시, 이미 응답이 있는 옵션은 삭제할 수 없다는 안내가 제공됩니다.

- **스타일**
    - 대시보드 및 참여자 카드, 리스트, 차트 등 UI 요소의 간격, 폰트 크기, 배경색 등 레이아웃 및 시각적 일관성이 개선되었습니다.
    - 메뉴 및 검색 아이콘의 크기가 명확하게 지정되었습니다.

- **리팩터**
    - 이벤트, 사용자 관련 API 요청 헤더를 `skipAuth`에서 `isPublicApi`로 일괄 변경해 인증 처리 방식을 일관성 있게 조정하였습니다.
    - 대시보드 응답 리스트의 렌더링 구조가 간소화되어 유지보수성이 향상되었습니다.
    - 이벤트 정보 저장 시 페이지 이동 대신 데이터 갱신이 이루어집니다.

- **기타**
    - 불필요한 콘솔 로그 및 코드 중복이 제거되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/entities/event/api/event.ts b/src/entities/event/api/event.ts
index 30a333c1..be86163f 100644
--- a/src/entities/event/api/event.ts
+++ b/src/entities/event/api/event.ts
@@ -7,7 +7,7 @@ import { EventItem, PaginationParams } from '../model/eventDetail';
 export const eventDetail = async (dto: EventDetailRequest) => {
   const response = await axiosClient.get(`/events/${dto.eventId}`, {
     params: { userId: dto.userId },
-    headers: { skipAuth: true },
+    headers: { isPublicApi: true },
   });
   return response.data;
 };
@@ -25,7 +25,7 @@ export const getAllEventsInfinite = async ({
   params.append('size', size.toString());
 
   const response = await axiosClient.get<ApiResponse<EventItem[]>>(`/events?${params.toString()}`, {
-    headers: { skipAuth: true },
+    headers: { isPublicApi: true },
   });
 
   const items = response.data.result ?? [];
@@ -49,7 +49,7 @@ export const getCategoryEventsInfinite = async ({
   params.append('size', size.toString());
 
   const response = await axiosClient.get<ApiResponse<EventItem[]>>(`/events/categories?${params.toString()}`, {
-    headers: { skipAuth: true },
+    headers: { isPublicApi: true },
   });
 
   const items = response.data.result ?? [];
@@ -63,7 +63,7 @@ export const getCategoryEventsInfinite = async ({
 // 태그별 이벤트 목록 조회 (최신, 인기, 마감 / 기본 정보)
 export const getEventByTag = async (tag: TagType, { page, size }: PaginationParams): Promise<EventItem[]> => {
   const response = await axiosClient.get<{ result: EventItem[] }>(`/events?tags=${tag}&page=${page}&size=${size}`, {
-    headers: { skipAuth: true },
+    headers: { isPublicApi: true },
   });
   return response.data.result || [];
 };
@@ -76,7 +76,7 @@ export const getEventByCategory = async (
   const response = await axiosClient.get<EventItem[]>(
     `/events/category?category=${category}&page=${page}&size=${size}`,
     {
-      headers: { skipAuth: true },
+      headers: { isPublicApi: true },
     }
   );
   return response.data;
diff --git a/src/features/dashboard/ui/MultiplePieCharts.tsx b/src/features/dashboard/ui/MultiplePieCharts.tsx
index 0d752ffc..dc7b9a44 100644
--- a/src/features/dashboard/ui/MultiplePieCharts.tsx
+++ b/src/features/dashboard/ui/MultiplePieCharts.tsx
@@ -1,5 +1,5 @@
 import { PieChart, Pie, Cell, Legend, ResponsiveContainer } from 'recharts';
-import { TicketOption, TicketOptionAnswer} from '../../ticket/model/ticketInformation';
+import { TicketOption, TicketOptionAnswer } from '../../ticket/model/ticketInformation';
 
 const COLORS = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40'];
 
@@ -23,7 +23,7 @@ const MultiplePieCharts = ({ responses }: MultiplePieChartsProps) => {
   }
 
   return (
-    <div className="grid grid-cols-1 gap-6 p-4">
+    <div className="grid grid-cols-1 gap-6 p-4 bg-white">
       {responses.map((option, index) => {
         const data = aggregateAnswers(option.ticketOptionAnswers);
 
@@ -32,16 +32,7 @@ const MultiplePieCharts = ({ responses }: MultiplePieChartsProps) => {
             <h3 className="mb-2 font-semibold text-lg">{option.optionName}</h3>
             <ResponsiveContainer width="100%" height={300}>
               <PieChart>
-                <Pie
-                  data={data}
-                  dataKey="value"
-                  nameKey="name"
-                  cx="50%"
-                  cy="50%"
-                  outerRadius={80}
-                  label
-                  stroke="none"
-                >
+                <Pie data={data} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} label stroke="none">
                   {data.map((_, i) => (
                     <Cell key={`cell-${i}`} fill={COLORS[i % COLORS.length]} />
                   ))}
diff --git a/src/features/dashboard/ui/ParicipantCard.tsx b/src/features/dashboard/ui/ParicipantCard.tsx
index 03a030a9..3f493dae 100644
--- a/src/features/dashboard/ui/ParicipantCard.tsx
+++ b/src/features/dashboard/ui/ParicipantCard.tsx
@@ -12,38 +12,36 @@ interface ParticipantCardProps {
 }
 
 const ParticipantCard = ({ participant, onCheckClick }: ParticipantCardProps) => {
-
   const { mutate: approveParticipant } = useApproveParticipants(participant.orderId);
 
   return (
     <div className="flex items-center justify-between w-full text-xs bg-white px-2 md:px-3 py-2 shadow-sm">
       <div className="flex gap-2 md:gap-3">
-        <div className="flex items-center gap-4 md:gap-6 text-11 md:text-12">
-          <p>{participant.orderId}</p>
+        <div className="flex items-center gap-8 md:gap-20 text-10 md:text-12">
+          <p className="ml-3">{participant.orderId}</p>
           <div className="flex flex-col">
             <p>이름: {participant.participant}</p>
             <p>이메일 주소: {participant.email}</p>
             <p>휴대폰 번호: {participant.phoneNumber}</p>
-            <div>구매 일자: {formatDate(participant.purchaseDate)} {formatTime(participant.purchaseDate)}</div>
+            <div>
+              구매 일자: {formatDate(participant.purchaseDate)} {formatTime(participant.purchaseDate)}
+            </div>
             <p>티켓 이름: {participant.ticketName}</p>
           </div>
         </div>
       </div>
       <div className="flex items-center justify-center gap-3">
-        {
-          <SecondaryButton
-            label="확인하기"
-            color="pink"
-            size="small"
-            onClick={onCheckClick}
-          />
-        }
-        {participant.checkedIn ? <p className="text-[#888686]">완료</p> : <p className="text-[#888686]">미완료</p>}
+        {<SecondaryButton label="확인하기" color="pink" size="small" onClick={onCheckClick} />}
+        {participant.checkedIn ? (
+          <p className="text-[#888686] text-10 md:text-12">완료</p>
+        ) : (
+          <p className="text-[#888686] text-10 md:text-12">미완료</p>
+        )}
 
         {participant.approved ? (
-          <p className="text-[#888686]">승인됨</p>
+          <p className="text-[#888686] text-10 md:text-12">승인됨</p>
         ) : participant.ticketType === 'FIRST_COME' ? (
-          <p className="text-[#888686] invisible">승인됨</p>
+          <p className="text-[#888686] invisible text-10 md:text-12">승인됨</p>
         ) : (
           <TertiaryButton
             label="승인"
diff --git a/src/features/dashboard/ui/PariticipantsList.tsx b/src/features/dashboard/ui/PariticipantsList.tsx
index 9db92e3d..a351a56a 100644
--- a/src/features/dashboard/ui/PariticipantsList.tsx
+++ b/src/features/dashboard/ui/PariticipantsList.tsx
@@ -19,7 +19,7 @@ const ParticipantsList = ({ listType, selectedFilter = [], participants }: Parti
     selectedTicketId,
     selectedOrderId,
     setSelectedTicketId,
-    setSelectedOrderId
+    setSelectedOrderId,
   } = useParticipantStore();
 
   const { data } = usePersonalTicketOptionAnswers(selectedTicketId);
@@ -38,9 +38,7 @@ const ParticipantsList = ({ listType, selectedFilter = [], participants }: Parti
       return;
     }
 
-    const order = data.result
-      .flatMap(user => user.orders)
-      .find(order => order.orderId === selectedOrderId);
+    const order = data.result.flatMap(user => user.orders).find(order => order.orderId === selectedOrderId);
 
     if (!order) {
       alert('응답 데이터가 없습니다.');
@@ -68,7 +66,7 @@ const ParticipantsList = ({ listType, selectedFilter = [], participants }: Parti
     <div className="flex flex-col gap-2 mb-4">
       <div className="flex justify-between text-xs text-[#888686] bg-white shadow-sm px-2 md:px-3 py-3 rounded-t-lg">
         <div className="flex gap-2 md:gap-3">
-          <div className="flex items-center gap-15 md:gap-24">
+          <div className="flex items-center gap-3 md:gap-16">
             <p>주문 번호</p>
             <p>참여자 정보</p>
           </div>
diff --git a/src/features/dashboard/ui/ResponsesList.tsx b/src/features/dashboard/ui/ResponsesList.tsx
index 92cd7b08..efb651b2 100644
--- a/src/features/dashboard/ui/ResponsesList.tsx
+++ b/src/features/dashboard/ui/ResponsesList.tsx
@@ -14,85 +14,66 @@ interface ResponsesListProps {
 const ResponsesList = ({ listType, ticketOptionResponses, ticketId }: ResponsesListProps) => {
   const { data, isLoading, error } = usePersonalTicketOptionAnswers(ticketId);
 
-  const {
-    currentIndex,
-    setCurrentIndex,
-  } = useResponseStore();
+  const { currentIndex, setCurrentIndex } = useResponseStore();
 
   useEffect(() => {
     setCurrentIndex(() => 0);
   }, [listType, setCurrentIndex]);
 
-  const renderTextResponses = (responses: TicketOption[]) => {
-    const textResponses = responses.filter((res) => res.optionType === 'TEXT');
-    if (textResponses.length === 0) return null;
-
-    return (
-      <>
-        {textResponses.map((textResponse, idx) => (
-          <div className="bg-white p-4 flex flex-col gap-2 mb-4" key={`${textResponse.optionId}-${idx}`}>
-            <div className="flex justify-between items-center text-xs bg-white px-2 md:px-3 py-3">
-              <p className="text-base font-bold">{textResponse.optionName}</p>
-              <p>응답 {textResponse.ticketOptionAnswers.length}개</p>
-            </div>
-
-            {textResponse.ticketOptionAnswers.length === 0 ? (
-              <p>응답이 없습니다.</p>
-            ) : (
-              <div className="h-full max-h-48 overflow-y-auto space-y-2">
-                {textResponse.ticketOptionAnswers.map((answer) => (
-                  <div
-                    className="flex justify-between text-xs bg-gray-100 shadow-sm px-2 md:px-3 py-3 gap-2"
-                    key={answer.id}
-                  >
-                    <p>{answer.answer}</p>
-                  </div>
-                ))}
-              </div>
-            )}
-          </div>
-        ))}
-      </>
-    );
-  };
-
   const renderList = () => {
     switch (listType) {
-      case 'summary':
-        {
-          const filteredResponses = ticketOptionResponses.filter(
-            (option) => option.optionType !== 'TEXT'
-          );
-          return (
-            <>
-              <div className="flex justify-center">
-                <div style={{ minWidth: '300px', maxWidth: '600px', width: '100%' }}>
-                  <MultiplePieCharts responses={filteredResponses} />
+      case 'summary': {
+        return (
+          <div className="space-y-4">
+            {ticketOptionResponses.map(option => {
+              if (option.optionType === 'TEXT') {
+                return (
+                  <div key={option.optionId} className="bg-white p-4 rounded shadow-sm">
+                    <div className="flex justify-between items-center text-xs px-2 md:px-3 py-3">
+                      <p className="text-base font-bold">{option.optionName}</p>
+                      <p>응답 {option.ticketOptionAnswers.length}개</p>
+                    </div>
+                    {option.ticketOptionAnswers.length === 0 ? (
+                      <p className="text-sm text-gray-500">응답이 없습니다.</p>
+                    ) : (
+                      <div className="max-h-48 overflow-y-auto space-y-2">
+                        {option.ticketOptionAnswers.map(answer => (
+                          <div key={answer.id} className="text-xs bg-gray-100 px-3 py-2 rounded">
+                            {answer.answer}
+                          </div>
+                        ))}
+                      </div>
+                    )}
+                  </div>
+                );
+              }
+              return (
+                <div key={option.optionId} className="flex justify-center">
+                  <div className="w-full max-w-[600px] min-w-[300px]">
+                    <MultiplePieCharts responses={[option]} />
+                  </div>
                 </div>
-              </div>
-              {renderTextResponses(ticketOptionResponses)}
-            </>
-          );
-        }
+              );
+            })}
+          </div>
+        );
+      }
+
+      case 'individual': {
+        if (isLoading) return <p>로딩 중...</p>;
+        if (error || !data?.result) return <p>데이터를 불러오지 못했습니다.</p>;
+        const allOrders = data.result.flatMap(user => user.orders);
+        return (
+          <IndividualResponseViewer orders={allOrders} currentIndex={currentIndex} setCurrentIndex={setCurrentIndex} />
+        );
+      }
 
-      case 'individual':
-        {
-          if (isLoading) return <p>로딩 중...</p>;
-          if (error || !data?.result) return <p>데이터를 불러오지 못했습니다.</p>;
-          const allOrders = data.result.flatMap(user => user.orders);
-          return (
-            <IndividualResponseViewer
-              orders={allOrders}
-              currentIndex={currentIndex}
-              setCurrentIndex={setCurrentIndex}
-            />
-          );
-        }
       default:
         return null;
     }
   };
+
   return <div>{renderList()}</div>;
 };
-export default ResponsesList;
 
+export default ResponsesList;
diff --git a/src/features/event/ui/DatePicker.tsx b/src/features/event/ui/DatePicker.tsx
index ea398ee5..8ebded2c 100644
--- a/src/features/event/ui/DatePicker.tsx
+++ b/src/features/event/ui/DatePicker.tsx
@@ -27,10 +27,12 @@ const EventDatePicker = ({
   isLabel = false,
 }: DatePickerProps) => {
   const [startDate, setStartDate] = useState<Date | null>(
-    eventState?.startDate ? new Date(eventState.startDate) : new Date()
+    eventState?.startDate ? new Date(eventState.startDate) : initialStartDate ? new Date(initialStartDate) : new Date()
   );
-  const [endDate, setEndDate] = useState<Date | null>(eventState?.endDate ? new Date(eventState.endDate) : new Date());
 
+  const [endDate, setEndDate] = useState<Date | null>(
+    eventState?.endDate ? new Date(eventState.endDate) : initialEndDate ? new Date(initialEndDate) : new Date()
+  );
   const [startTime, setStartTime] = useState<string>('06:00');
   const [endTime, setEndTime] = useState<string>('23:00');
 
@@ -39,18 +41,18 @@ const EventDatePicker = ({
     const end = eventState?.endDate || initialEndDate;
 
     if (start && !startDate) {
-      const startDate = new Date(start);
-      setStartDate(startDate);
-      const hours = startDate.getHours().toString().padStart(2, '0');
-      const minutes = startDate.getMinutes().toString().padStart(2, '0');
+      const startDateObj = new Date(start);
+      setStartDate(prev => prev ?? startDateObj);
+      const hours = startDateObj.getHours().toString().padStart(2, '0');
+      const minutes = startDateObj.getMinutes().toString().padStart(2, '0');
       setStartTime(`${hours}:${minutes}`);
     }
 
     if (end && !endDate) {
-      const endDate = new Date(end);
-      setEndDate(endDate);
-      const hours = endDate.getHours().toString().padStart(2, '0');
-      const minutes = endDate.getMinutes().toString().padStart(2, '0');
+      const endDateObj = new Date(end);
+      setEndDate(prev => prev ?? endDateObj);
+      const hours = endDateObj.getHours().toString().padStart(2, '0');
+      const minutes = endDateObj.getMinutes().toString().padStart(2, '0');
       setEndTime(`${hours}:${minutes}`);
     }
   }, [eventState, initialStartDate, initialEndDate]);
diff --git a/src/features/event/ui/EventList.tsx b/src/features/event/ui/EventList.tsx
index 740c61b8..7a965d3d 100644
--- a/src/features/event/ui/EventList.tsx
+++ b/src/features/event/ui/EventList.tsx
@@ -3,6 +3,7 @@ import { useInfiniteScroll } from '../../../shared/hooks/useInfiniteScroll';
 import { getAllEventsInfinite, getCategoryEventsInfinite } from '../../../entities/event/api/event';
 import EventCard from '../../../shared/ui/EventCard';
 import { BaseEvent, CategoryType, TagType } from '../../../shared/types/baseEventType';
+import { useNavigate } from 'react-router-dom';
 
 interface EventListProps extends BaseEvent {
   id: number;
@@ -16,6 +17,8 @@ interface EventListComponentProps {
 }
 
 const EventList = ({ category, tag }: EventListComponentProps) => {
+  const navigate = useNavigate();
+
   const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteScroll<EventListProps>({
     queryKey: ['events', 'infinite', category ?? '', tag ?? ''],
     queryFn: params => {
@@ -63,23 +66,36 @@ const EventList = ({ category, tag }: EventListComponentProps) => {
       ) : (
         <div className="w-[90%] grid grid-cols-2 gap-4 mx-6 mt-2 md:grid-cols-2 lg:grid-cols-2">
           {data?.pages.map((page, pageIndex) =>
-            page.items.map((event: EventListProps, eventIndex) => {
-              const isLastElement = pageIndex === data.pages.length - 1 && eventIndex === page.items.length - 1;
-              return (
-                <div key={event.id} ref={isLastElement ? lastEventCardRef : null}>
-                  <EventCard
-                    id={event.id}
-                    img={event.bannerImageUrl}
-                    eventTitle={event.title}
-                    eventDate={event.startDate}
-                    location={event.address}
-                    host={event.hostChannelName}
-                    hashtags={event.hashtags}
-                    dDay={event.remainDays}
-                  />
-                </div>
-              );
-            })
+            page.items
+              .filter(event => {
+                const today = new Date();
+                today.setHours(0, 0, 0, 0);
+
+                const endDate = new Date((event as EventListProps).endDate ?? event.startDate);
+
+                return endDate >= today;
+              })
+              .map((event: EventListProps, eventIndex) => {
+                const isLastElement = pageIndex === data.pages.length - 1 && eventIndex === page.items.length - 1;
+                return (
+                  <div
+                    key={event.id}
+                    ref={isLastElement ? lastEventCardRef : null}
+                    onClick={() => navigate(`/event-details/${event.id}`)}
+                  >
+                    <EventCard
+                      id={event.id}
+                      img={event.bannerImageUrl}
+                      eventTitle={event.title}
+                      eventDate={event.startDate}
+                      location={event.address}
+                      host={event.hostChannelName}
+                      hashtags={event.hashtags}
+                      dDay={event.remainDays}
+                    />
+                  </div>
+                );
+              })
           )}
         </div>
       )}
diff --git a/src/features/home/ui/EventSliderSection.tsx b/src/features/home/ui/EventSliderSection.tsx
index ecc53a90..2214f987 100644
--- a/src/features/home/ui/EventSliderSection.tsx
+++ b/src/features/home/ui/EventSliderSection.tsx
@@ -16,8 +16,6 @@ const EventSliderSection = ({ title, events }: EventSliderSectionProps) => {
   const maxCardsToShow = 2;
   const navigate = useNavigate();
 
-  console.log(events);
-
   type SetStartIndex = Dispatch<SetStateAction<number>>;
 
   const handleNext = (setStartIndex: SetStartIndex, currentIndex: number, eventsLength: number): void => {
@@ -28,21 +26,31 @@ const EventSliderSection = ({ title, events }: EventSliderSectionProps) => {
     setStartIndex((currentIndex - 1 + eventsLength) % eventsLength);
   };
 
+  const now = new Date();
+  now.setHours(0, 0, 0, 0);
+
+  const filteredEvents = events.filter(event => {
+    const endDate = new Date(event.endDate);
+    endDate.setHours(0, 0, 0, 0);
+    return endDate >= now;
+  });
+
   const eventsToShow =
-    events.length > 0
-      ? events
+    filteredEvents.length > 0
+      ? filteredEvents
           .slice(startIndex, startIndex + maxCardsToShow)
           .concat(
-            startIndex + maxCardsToShow > events.length
-              ? events.slice(0, (startIndex + maxCardsToShow) % events.length)
+            startIndex + maxCardsToShow > filteredEvents.length
+              ? filteredEvents.slice(0, (startIndex + maxCardsToShow) % filteredEvents.length)
               : []
           )
       : [];
+
   return (
     <div className="relative w-full px-6">
       <h2 className="sm:mb-3 md:mb-3.5 lg:mb-4 font-bold sm:text-sm md:text-base lg:text-lg">{title}</h2>
       <div className="flex gap-4">
-        {events.length === 0 ? (
+        {filteredEvents.length === 0 ? (
           <div className="w-full text-center text-gray-500">표시할 이벤트가 없습니다.</div>
         ) : (
           eventsToShow.map((event: EventItem) => (
@@ -62,6 +70,22 @@ const EventSliderSection = ({ title, events }: EventSliderSectionProps) => {
             </div>
           ))
         )}
+        {eventsToShow.length === 1 && (
+          <div className="w-full h-full min-h-[200px] max-w-sm opacity-0" aria-hidden="true">
+            {/* 빈 카드로 자리 유지 */}
+            <EventCard
+              id={-1}
+              img=""
+              eventTitle=""
+              dDay=""
+              host=""
+              eventDate=""
+              location=""
+              hashtags={[]}
+              onClick={() => {}}
+            />
+          </div>
+        )}
       </div>
       {startIndex !== 0 && (
         <IconButton
diff --git a/src/features/join/api/user.ts b/src/features/join/api/user.ts
index 9ebaa02a..ba992d3e 100644
--- a/src/features/join/api/user.ts
+++ b/src/features/join/api/user.ts
@@ -3,14 +3,14 @@ import { UserInfoRequest, UserInfoResponse } from '../model/userInformation';
 
 export const readUser = async (): Promise<UserInfoResponse> => {
   const response = await axiosClient.get<{ result: UserInfoResponse }>('/users', {
-    headers: { skipAuth: true },
+    headers: { isPublicApi: true },
   });
   return response.data.result;
 };
 
 export const updateUser = async (data: UserInfoRequest): Promise<UserInfoResponse> => {
   const response = await axiosClient.put<UserInfoResponse>('/users', data, {
-    headers: { skipAuth: true },
+    headers: { isPublicApi: true },
   });
   return response.data;
 };
diff --git a/src/features/ticket/hooks/useTicketOptionDnD.ts b/src/features/ticket/hooks/useTicketOptionDnD.ts
index fe27ee28..3f5e68af 100644
--- a/src/features/ticket/hooks/useTicketOptionDnD.ts
+++ b/src/features/ticket/hooks/useTicketOptionDnD.ts
@@ -1,9 +1,14 @@
 import { DropResult } from '@hello-pangea/dnd';
-import { useAttachTicketOptionMutation, useDetachTicketOptionMutation } from './useTicketOptionHook';
+import {
+  useAttachTicketOptionMutation,
+  useDeleteTicketOptionMutation,
+  useDetachTicketOptionMutation,
+} from './useTicketOptionHook';
 
 export const useTicketOptionDnD = () => {
   const { mutate: attachOption } = useAttachTicketOptionMutation();
   const { mutate: detachOption } = useDetachTicketOptionMutation();
+  const { mutate: deleteOption } = useDeleteTicketOptionMutation();
 
   const onDragEnd = (result: DropResult) => {
     const { source, destination } = result;
@@ -24,13 +29,10 @@ export const useTicketOptionDnD = () => {
     if (source.droppableId === 'options' && destination.droppableId.startsWith('ticket-')) {
       const ticketId = parseInt(destination.droppableId.replace('ticket-', ''), 10);
       const ticketOptionId = parseInt(result.draggableId, 10);
-      
+
       if (isNaN(ticketId) || isNaN(ticketOptionId)) {
-        console.error('Invalid ID parsing:', { ticketId, ticketOptionId });
-        return;
+        attachOption({ ticketId, ticketOptionId });
       }
-
-      attachOption({ ticketId, ticketOptionId });
       return;
     }
 
@@ -40,12 +42,20 @@ export const useTicketOptionDnD = () => {
       const ticketOptionIdStr = result.draggableId.split('-').pop();
       const ticketOptionId = ticketOptionIdStr ? parseInt(ticketOptionIdStr, 10) : undefined;
 
-      if (ticketId && ticketOptionId != undefined) {
+      if (ticketId && ticketOptionId !== undefined) {
         detachOption({ ticketId, ticketOptionId });
       }
       return;
     }
-  };
 
+    // 옵션 영역에서 자체 옵션 삭제 (모든 티켓에서 제거)
+    if (source.droppableId === 'options' && destination.droppableId === 'delete') {
+      const ticketOptionId = parseInt(result.draggableId, 10);
+      if (!isNaN(ticketOptionId)) {
+        deleteOption(ticketOptionId);
+      }
+      return;
+    }
+  };
   return { onDragEnd };
 };
diff --git a/src/features/ticket/hooks/useTicketOptionHook.ts b/src/features/ticket/hooks/useTicketOptionHook.ts
index 23389189..d70b5143 100644
--- a/src/features/ticket/hooks/useTicketOptionHook.ts
+++ b/src/features/ticket/hooks/useTicketOptionHook.ts
@@ -25,7 +25,6 @@ import {
   detachTicketOption,
 } from '../api/ticketOption';
 
-
 // 티켓 옵션 조회
 export const useTicketOptions = (ticketId: number) => {
   return useQuery<{ isSuccess: boolean; result: TicketOptionResponse[] }>({
@@ -76,7 +75,7 @@ export const usePersonalTicketOptionAnswers = (ticketId: number | null) => {
   });
 };
 
-// 티켓 옵션 생성 훅 
+// 티켓 옵션 생성 훅
 export const useCreateTicketOptionMutation = () => {
   const { id } = useParams();
   const navigate = useNavigate();
@@ -125,10 +124,16 @@ export const useDeleteTicketOptionMutation = () => {
       // 티켓 옵션 목록과 상세 정보 쿼리 리패칭
       queryClient.invalidateQueries({ queryKey: ['ticketOptions', id] });
       queryClient.invalidateQueries({ queryKey: ['ticketOptionDetail', ticketOptionId] });
-      console.log('티켓 옵션이 성공적으로 삭제되었습니다.');
+      queryClient.invalidateQueries({ queryKey: ['attachedTicketOptions'] });
     },
-    onError: () => {
-      alert('티켓 옵션 삭제에 실패했습니다. 다시 시도해주세요.');
+    onError: error => {
+      const errorCode = error.message;
+
+      if (errorCode === '이미 응답된 티켓 옵션입니다.') {
+        alert('한 명 이상의 유저가 응답한 옵션은 삭제할 수 없습니다.');
+      } else {
+        alert('티켓 옵션 삭제에 실패했습니다. 다시 시도해주세요.');
+      }
     },
   });
 };
diff --git a/src/pages/dashboard/ui/EventInfoPage.tsx b/src/pages/dashboard/ui/EventInfoPage.tsx
index 6db8f9de..ffaf4686 100644
--- a/src/pages/dashboard/ui/EventInfoPage.tsx
+++ b/src/pages/dashboard/ui/EventInfoPage.tsx
@@ -3,7 +3,6 @@ import ChoiceChip from '../../../../design-system/ui/ChoiceChip';
 import DefaultTextField from '../../../../design-system/ui/textFields/DefaultTextField';
 import EventDatePicker from '../../../features/event/ui/DatePicker';
 import DashboardLayout from '../../../shared/ui/backgrounds/DashboardLayout';
-import { useNavigate } from 'react-router-dom';
 import Button from '../../../../design-system/ui/Button';
 import { useUpdateEventHook } from '../../../features/dashboard/hook/useEventHook';
 import { OnlineType } from '../../../shared/types/baseEventType';
@@ -11,9 +10,10 @@ import { AddressSearch } from '../../../shared/ui/AddressSearch';
 import KakaoMap from '../../../shared/ui/KakaoMap';
 import { UpdateEventRequest } from '../../../features/dashboard/model/event';
 import { useEventDetail } from '../../../entities/event/hook/useEventHook';
+import { useQueryClient } from '@tanstack/react-query';
 
 const EventInfoPage = () => {
-  const navigate = useNavigate();
+  const queryClient = useQueryClient();
   const [selectedOption, setSelectedOption] = useState('');
   const { data } = useEventDetail();
   const { mutate } = useUpdateEventHook();
@@ -60,7 +60,7 @@ const EventInfoPage = () => {
     mutate(requestData, {
       onSuccess: () => {
         alert('이벤트 정보가 저장되었습니다.');
-        navigate(`/dashboard/${data?.result.id}`);
+        queryClient.invalidateQueries({ queryKey: ['eventDetail', data.result.id] });
       },
       onError: error => {
         console.error('Error details:', error);
diff --git a/src/pages/event/ui/EventDetailsPage.tsx b/src/pages/event/ui/EventDetailsPage.tsx
index 9ce83b91..cafe5920 100644
--- a/src/pages/event/ui/EventDetailsPage.tsx
+++ b/src/pages/event/ui/EventDetailsPage.tsx
@@ -8,7 +8,7 @@ import share from '../../../../public/assets/event-manage/details/Share.svg';
 import like from '../../../../public/assets/event-manage/details/Like.svg';
 import liked from '../../../../public/assets/event-manage/details/ClickedLike.svg';
 import TicketInfo from '../../../widgets/event/ui/TicketInfo';
-import link from '../../../../public/assets/event-manage/details/Link.svg';
+import linkIcon from '../../../../public/assets/event-manage/details/Link.svg';
 import ShareEventModal from '../../../features/event/ui/ShareEventModal';
 import participantsImg from '../../../../public/assets/event-manage/details/People.svg';
 import dateImg from '../../../../public/assets/event-manage/details/Date.svg';
@@ -18,11 +18,13 @@ import KakaoMap from '../../../shared/ui/KakaoMap';
 import { useCreateBookmark, useDeleteBookmark } from '../../../features/bookmark/hook/useBookmarkHook';
 import { formatDate, formatTime } from '../../../shared/lib/date';
 import { useEventDetail } from '../../../entities/event/hook/useEventHook';
+import useAuthStore from '../../../app/provider/authStore';
 
 const EventDetailsPage = () => {
   const navigate = useNavigate();
   const [title, setTitle] = useState('');
   const [isModalOpen, setIsModalOpen] = useState(false);
+  const isLoggedIn = useAuthStore(state => state.isLoggedIn);
   const { data: event } = useEventDetail();
 
   const handleShareClick = (title: string) => {
@@ -61,7 +63,9 @@ const EventDetailsPage = () => {
         leftButtonClick={() => navigate(-1)}
         leftButtonLabel="<"
         centerContent="같이가요"
-        rightContent={<img src={Search} alt="검색" className="w-4" />}
+        rightContent={
+          <img src={Search} alt="검색" className="w-5 cursor-pointer z-30" onClick={() => navigate('/search')} />
+        }
       />
       {event ? (
         <>
@@ -81,10 +85,12 @@ const EventDetailsPage = () => {
                     iconPath={<img src={share} alt="공유하기 버튼" />}
                     onClick={() => handleShareClick(event.result.title)}
                   />
-                  <IconButton
-                    iconPath={<img src={event.result.bookmarked ? liked : like} alt="좋아요 버튼" />}
-                    onClick={handleLikeClick}
-                  />
+                  {isLoggedIn && (
+                    <IconButton
+                      iconPath={<img src={event.result.bookmarked ? liked : like} alt="좋아요 버튼" />}
+                      onClick={handleLikeClick}
+                    />
+                  )}
                 </div>
               </div>
               <div className="flex gap-2">
@@ -99,10 +105,14 @@ const EventDetailsPage = () => {
                   {formatTime(event.result.startDate)} ~ {formatTime(event.result.endDate)}
                 </span>
               </div>
-              <div className="flex gap-2">
-                <img src={locationImg} alt="위치 이미지" />
-                <span className="text-sm md:text-base">{event.result.address}</span>
-              </div>
+              {event.result.address && (
+                <div className="flex gap-2">
+                  <img src={locationImg} alt="위치 이미지" />
+                  <span className="text-sm md:text-base">
+                    {event.result.address} {event.result.detailAddress}
+                  </span>
+                </div>
+              )}
               <div
                 className="text-sm md:text-base py-3"
                 dangerouslySetInnerHTML={{ __html: event.result.description }}
@@ -129,8 +139,15 @@ const EventDetailsPage = () => {
             <div className="flex flex-col gap-2">
               <h2 className="font-bold text-xl">관련 링크</h2>
               <div className="flex items-center gap-3 mb-4">
-                <img src={link} alt="링크 이모지" />
-                <span>공식 웹사이트</span>
+                {event.result.referenceLinks.map((link: { title: string; url: string }, index: number) => (
+                  <div key={index} className="flex items-center gap-2 mb-3">
+                    <img src={linkIcon} alt="링크 이모지" />
+                    <span>{link.title}</span>
+                    <a href={link.url} target="_blank" rel="noopener noreferrer" className="text-blue-500 underline">
+                      {link.url}
+                    </a>
+                  </div>
+                ))}
               </div>
             </div>
           </div>
diff --git a/src/pages/menu/ui/MenuPage.tsx b/src/pages/menu/ui/MenuPage.tsx
index d0337a20..770ea081 100644
--- a/src/pages/menu/ui/MenuPage.tsx
+++ b/src/pages/menu/ui/MenuPage.tsx
@@ -20,7 +20,7 @@ const MenuPage = () => {
         centerContent="카테고리"
         rightContent={
           <button type="button" className="w-5 z-10" onClick={() => navigate('/search')}>
-            <img src={searchIcon} alt="Search Icon" />
+            <img src={searchIcon} alt="Search Icon" className="w-4" />
           </button>
         }
       />
diff --git a/src/shared/lib/formValidation.ts b/src/shared/lib/formValidation.ts
index 962a567a..b356962d 100644
--- a/src/shared/lib/formValidation.ts
+++ b/src/shared/lib/formValidation.ts
@@ -9,8 +9,10 @@ export const formSchema = z.object({
     .regex(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/, '올바른 이메일 형식이어야 합니다.'),
   phone: z
     .string()
-    .regex(/^[0-9]{3}-[0-9]{3,4}-[0-9]{4}$/, '연락처는 -을 포함한 형식이어야 합니다.')
-    .min(1, '전화번호를 입력해주세요.'),
+    .regex(/^[0-9]{3}-[0-9]{3,4}-[0-9]{4}$/, '연락처는 하이픈(-)을 포함한 형식이어야 합니다.')
+    .refine(val => val.length === 13, {
+      message: '연락처는 하이픈(-) 포함 13자 형식(예: 010-1234-5678)이어야 합니다.',
+    }),
 });
 export const organizerFormSchema = formSchema.pick({ email: true, phone: true });
 export const eventTitleSchema = z.object({
diff --git a/src/shared/types/api/http-client.ts b/src/shared/types/api/http-client.ts
index 02d2e885..72b9dc7c 100644
--- a/src/shared/types/api/http-client.ts
+++ b/src/shared/types/api/http-client.ts
@@ -14,7 +14,7 @@ export const axiosClient = axios.create({
 
 axiosClient.interceptors.request.use(
   config => {
-    if (config.headers?.skipAuth) {
+    if (config.headers?.isPublicApi) {
       return config;
     }
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>src/features/home/ui/EventSliderSection.tsx (1)</summary><blockquote>

`93-93`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**네비게이션 로직의 일관성을 확인하세요.**

네비게이션 버튼이 여전히 원본 `events.length`를 사용하고 있는데, 이는 `filteredEvents.length`와 다를 수 있습니다. 이로 인해 인덱스 범위를 벗어나는 문제가 발생할 수 있습니다.


다음 스크립트를 실행하여 이벤트 슬라이더 컴포넌트에서 필터링된 이벤트와 네비게이션 로직의 일관성을 확인하세요:




Also applies to: 98-98

---

🏁 Script executed:

```shell
#!/bin/bash
# 이벤트 슬라이더에서 네비게이션 로직과 필터링된 이벤트 사용 패턴 확인

echo "=== EventSliderSection에서 events와 filteredEvents 사용 패턴 분석 ==="
rg -A 10 -B 5 "handleNext|handlePrev" src/features/home/ui/EventSliderSection.tsx

echo "=== 유사한 슬라이더 컴포넌트에서 네비게이션 패턴 확인 ==="
rg -A 5 -B 5 "startIndex.*length" src/
```

Length of output: 3635

---



</details>

**네비게이션 로직: `filteredEvents.length` 사용으로 일관성 유지하세요.**

현재 `handlePrev`/`handleNext`에 `events.length`를 넘겨주고 있어, 전체 이벤트 개수를 기준으로 인덱스를 순환합니다. 하지만 화면에 표시되는 건 `filteredEvents`이기 때문에, 필터된 이벤트 개수(`filteredEvents.length`)로 순환해야 일부 인덱스에서 빈 배열이 반환되는 문제를 방지할 수 있습니다.

수정 위치:
- 파일: `src/features/home/ui/EventSliderSection.tsx`
- 라인: onClick 호출부 (약 93, 98번째 줄)

제안하는 변경 사항:
```diff
- <IconButton
-   onClick={() => handlePrev(setStartIndex, startIndex, events.length)}
-   ...
- />
+ <IconButton
+   onClick={() => handlePrev(setStartIndex, startIndex, filteredEvents.length)}
+   ...
+/>

- <IconButton
-   onClick={() => handleNext(setStartIndex, startIndex, events.length)}
-   ...
- />
+ <IconButton
+   onClick={() => handleNext(setStartIndex, startIndex, filteredEvents.length)}
+   ...
+/>
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a50ff9ae163f77e882979d066d02575f8a0bd929 and 63513a71bcc88f46e752d5838be47c29d0feb4c0.

</details>

<details>
<summary>📒 Files selected for processing (16)</summary>

* `src/entities/event/api/event.ts` (5 hunks)
* `src/features/dashboard/ui/MultiplePieCharts.tsx` (3 hunks)
* `src/features/dashboard/ui/ParicipantCard.tsx` (1 hunks)
* `src/features/dashboard/ui/PariticipantsList.tsx` (3 hunks)
* `src/features/dashboard/ui/ResponsesList.tsx` (1 hunks)
* `src/features/event/ui/DatePicker.tsx` (2 hunks)
* `src/features/event/ui/EventList.tsx` (3 hunks)
* `src/features/home/ui/EventSliderSection.tsx` (2 hunks)
* `src/features/join/api/user.ts` (1 hunks)
* `src/features/ticket/hooks/useTicketOptionDnD.ts` (3 hunks)
* `src/features/ticket/hooks/useTicketOptionHook.ts` (2 hunks)
* `src/pages/dashboard/ui/EventInfoPage.tsx` (2 hunks)
* `src/pages/event/ui/EventDetailsPage.tsx` (6 hunks)
* `src/pages/menu/ui/MenuPage.tsx` (1 hunks)
* `src/shared/lib/formValidation.ts` (1 hunks)
* `src/shared/types/api/http-client.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>src/features/join/api/user.ts (2)</summary><blockquote>

<details>
<summary>src/features/join/model/userInformation.ts (2)</summary>

* `UserInfoRequest` (8-12)
* `UserInfoResponse` (1-6)

</details>
<details>
<summary>src/shared/types/api/http-client.ts (1)</summary>

* `axiosClient` (6-13)

</details>

</blockquote></details>
<details>
<summary>src/features/dashboard/ui/ParicipantCard.tsx (1)</summary><blockquote>

<details>
<summary>src/shared/lib/date.ts (2)</summary>

* `formatDate` (1-9)
* `formatTime` (10-15)

</details>

</blockquote></details>
<details>
<summary>src/features/dashboard/ui/ResponsesList.tsx (1)</summary><blockquote>

<details>
<summary>src/features/dashboard/model/store/ResponseStore.tsx (1)</summary>

* `useResponseStore` (21-61)

</details>

</blockquote></details>
<details>
<summary>src/pages/event/ui/EventDetailsPage.tsx (1)</summary><blockquote>

<details>
<summary>src/app/provider/authStore.ts (1)</summary>

* `useAuthStore` (16-28)

</details>

</blockquote></details>

</details><details>
<summary>🪛 ESLint</summary>

<details>
<summary>src/features/event/ui/EventList.tsx</summary>

[error] 6-6: 'ReactQueryDevtools' is defined but never used.

(@typescript-eslint/no-unused-vars)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: storybook

</details>

<details>
<summary>🔇 Additional comments (35)</summary><blockquote>

<details>
<summary>src/pages/menu/ui/MenuPage.tsx (1)</summary>

`23-23`: **UI 일관성 개선이 적절합니다.**

검색 아이콘에 `w-4` 클래스를 추가하여 버튼 컨테이너(`w-5`) 내에서 적절한 여백을 확보한 것이 좋습니다.

</details>
<details>
<summary>src/features/dashboard/ui/MultiplePieCharts.tsx (3)</summary>

`2-2`: **코드 포맷팅 개선이 좋습니다.**

import 문에 적절한 공백을 추가하여 가독성을 향상시켰습니다.

---

`26-26`: **명시적인 배경색 설정이 좋습니다.**

`bg-white` 클래스를 추가하여 컨테이너의 배경색을 명확히 설정한 것이 UI 일관성에 도움이 됩니다.

---

`35-35`: **코드 정리가 적절합니다.**

Pie 컴포넌트의 props를 한 줄로 정리하여 코드를 더 간결하게 만들었습니다.

</details>
<details>
<summary>src/entities/event/api/event.ts (1)</summary>

`10-10`: **인증 헤더 키 통일화가 적절합니다.**

모든 공개 API 호출에서 `skipAuth: true`를 `isPublicApi: true`로 일관되게 변경한 것이 좋습니다. 이는 http-client.ts의 인터셉터 로직과 일치하는 변경사항입니다.



Also applies to: 28-28, 52-52, 66-66, 79-79

</details>
<details>
<summary>src/shared/types/api/http-client.ts (1)</summary>

`17-17`: <details>
<summary>✅ Verification successful</summary>

**인증 헤더 키 변경이 올바릅니다.**

`skipAuth`에서 `isPublicApi`로 헤더 키를 변경한 것이 다른 API 파일들의 변경사항과 일치합니다. 이 변경으로 공개 API 요청이 인증을 우회할 수 있습니다.



모든 API 파일에서 일관되게 헤더 키가 업데이트되었는지 확인하기 위해 다음 스크립트를 실행합니다:

---

🏁 Script executed:

```shell
#!/bin/bash
# 설명: skipAuth 헤더를 사용하는 다른 API 호출이 있는지 확인
# 예상 결과: skipAuth 사용 사례가 없어야 함

echo "=== skipAuth 헤더를 사용하는 파일 검색 ==="
rg -A 2 -B 2 "skipAuth.*true" --type ts --type tsx

echo "=== isPublicApi 헤더를 사용하는 파일 검색 ==="
rg -A 2 -B 2 "isPublicApi.*true" --type ts --type tsx
```

Length of output: 316

---

`rg`에서 `--type tsx`를 인식하지 못해 글롭(glob) 패턴으로 `.ts`와 `.tsx` 파일을 모두 검색하도록 스크립트를 수정합니다.

---

🏁 Script executed:

```shell
#!/bin/bash
echo "=== skipAuth 사용 여부 확인 ==="
rg -n 'skipAuth' --glob '*.ts' --glob '*.tsx'

echo "=== isPublicApi 사용 여부 확인 ==="
rg -n 'isPublicApi' --glob '*.ts' --glob '*.tsx'
```

Length of output: 794

---



</details>

**인증 헤더 키 변경 일관성 확인 완료**

`skipAuth` 관련 코드를 모두 제거했고, 모든 API 호출이 `isPublicApi`를 사용하도록 업데이트된 것을 확인했습니다.

- 발견된 `isPublicApi` 사용 위치:  
  • src/entities/event/api/event.ts (라인 10, 28, 52, 66, 79)  
  • src/features/join/api/user.ts (라인 6, 13)  
  • src/shared/types/api/http-client.ts (라인 17)  

더 이상의 조치가 필요 없습니다.

</details>
<details>
<summary>src/features/join/api/user.ts (1)</summary>

`6-6`: <details>
<summary>✅ Verification successful</summary>

**인증 헤더 키 변경이 올바르게 적용되었습니다.**

`skipAuth`에서 `isPublicApi`로의 헤더 키 변경이 일관되게 적용되었습니다. 이 변경은 HTTP 클라이언트 인터셉터의 업데이트와 일치합니다.







Also applies to: 13-13

---

🏁 Script executed:

```shell
#!/bin/bash
# HTTP 클라이언트가 새로운 isPublicApi 헤더를 올바르게 처리하는지 확인
rg -A 10 -B 5 "isPublicApi" --type ts
```

Length of output: 6617

---



</details>

**isPublicApi 헤더 처리 일관 적용 확인**

인터셉터에서 `isPublicApi` 헤더를 정상 처리하며, 관련 API 호출에도 일관되게 적용되었습니다.

- `src/shared/types/api/http-client.ts`: 요청 인터셉터에서 `config.headers?.isPublicApi` 확인
- `src/features/join/api/user.ts`, `src/entities/event/api/event.ts` 등: `headers: { isPublicApi: true }`로 일관 적용

</details>
<details>
<summary>src/features/dashboard/ui/PariticipantsList.tsx (3)</summary>

`22-22`: **구문 일관성 개선이 좋습니다.**

후행 쉼표 추가로 코드 일관성이 향상되었습니다.

---

`41-41`: **코드 가독성 향상이 좋습니다.**

다중 라인 체이닝을 단일 라인으로 리팩토링하여 가독성이 향상되었습니다. 로직은 동일하게 유지되었습니다.

---

`69-69`: **UI 간격 조정이 적절합니다.**

CSS gap 값 조정 (`gap-15 md:gap-24`에서 `gap-3 md:gap-16`으로)으로 더 적절한 간격을 제공합니다.

</details>
<details>
<summary>src/pages/dashboard/ui/EventInfoPage.tsx (2)</summary>

`13-13`: **React Query 캐시 관리 도입이 좋습니다.**

`useQueryClient` 도입으로 더 나은 상태 관리가 가능해졌습니다.




Also applies to: 16-16

---

`63-63`: **UX 동작 변경 검증이 필요합니다.**

성공 후 페이지 이동 대신 캐시 무효화로 변경되었습니다. 이로 인해 사용자가 동일한 페이지에 머무르게 되는데, 이것이 의도된 UX인지 확인이 필요합니다.


이벤트 정보 저장 후 사용자가 동일한 페이지에 머무르는 것이 올바른 UX 플로우인지 확인해주세요. 일반적으로 저장 후 대시보드나 목록 페이지로 이동하는 것이 예상되는 동작일 수 있습니다.

</details>
<details>
<summary>src/features/ticket/hooks/useTicketOptionHook.ts (3)</summary>

`78-78`: **코드 정리가 좋습니다.**

불필요한 빈 줄 제거로 코드가 더 깔끔해졌습니다.

---

`127-127`: **캐시 무효화 로직 추가가 좋습니다.**

티켓 옵션 삭제 시 `attachedTicketOptions` 쿼리도 무효화하여 데이터 일관성을 보장합니다.

---

`129-137`: **에러 처리 개선이 훌륭합니다.**

특정 에러 메시지에 따른 맞춤형 알림 제공으로 사용자 경험이 크게 향상되었습니다. "이미 응답된 티켓 옵션입니다." 에러에 대한 명확한 안내는 특히 유용합니다.

</details>
<details>
<summary>src/features/event/ui/EventList.tsx (3)</summary>

`7-7`: **네비게이션 구현이 잘 되었습니다.**

`useNavigate` 훅을 올바르게 임포트하고 초기화했습니다.



Also applies to: 21-21

---

`70-78`: **이벤트 필터링 로직이 일관성 있게 구현되었습니다.**

종료된 이벤트를 제외하는 필터링 로직이 `EventSliderSection.tsx`와 일관성 있게 구현되었습니다. 오늘 날짜를 자정으로 설정하고 이벤트의 종료일(또는 시작일)과 비교하는 방식이 적절합니다.

---

`82-98`: **클릭 가능한 이벤트 카드 구현이 우수합니다.**

각 이벤트 카드를 클릭했을 때 상세 페이지로 이동하는 기능이 올바르게 구현되었습니다. `div` 컨테이너에 `onClick` 핸들러를 추가하여 전체 카드 영역을 클릭 가능하게 만든 것이 좋은 UX 개선입니다.

</details>
<details>
<summary>src/shared/lib/formValidation.ts (1)</summary>

`12-15`: **전화번호 유효성 검사가 더 정확해졌습니다.**

기존의 `.min(1)` 검사 대신 `.refine()` 메소드를 사용하여 정확히 13자 길이를 강제하는 것이 한국 휴대폰 번호 형식("010-1234-5678")에 적합합니다. 에러 메시지도 더 구체적이고 사용자 친화적입니다.

</details>
<details>
<summary>src/features/dashboard/ui/ParicipantCard.tsx (4)</summary>

`20-21`: **레이아웃 간격 개선이 잘 되었습니다.**

요소 간 간격을 늘리고(`gap-8 md:gap-20`) 주문 ID에 왼쪽 마진을 추가하여 가독성이 향상되었습니다.

---

`26-28`: **구매 일자 표시 개선이 우수합니다.**

구매 일자를 별도 `div`로 감싸서 멀티라인으로 표시하는 것이 가독성을 향상시킵니다. `formatDate`와 `formatTime` 함수의 활용도 적절합니다.

---

`34-34`: **버튼 렌더링이 간소화되었습니다.**

불필요한 중괄호와 줄바꿈을 제거하여 코드가 더 깔끔해졌습니다.

---

`36-36`: **일관된 폰트 크기 적용이 우수합니다.**

모든 상태 텍스트("완료", "미완료", "승인됨")에 동일한 반응형 폰트 크기 클래스(`text-10 md:text-12`)를 적용하여 UI 일관성이 향상되었습니다.



Also applies to: 38-38, 42-42, 44-44

</details>
<details>
<summary>src/features/home/ui/EventSliderSection.tsx (4)</summary>

`29-36`: **이벤트 필터링 로직이 일관성 있게 구현되었습니다.**

`EventList.tsx`와 동일한 방식으로 종료된 이벤트를 필터링하는 로직이 구현되었습니다. 날짜를 자정으로 정규화하여 비교하는 방식이 적절합니다.

---

`38-47`: **이벤트 표시 로직이 올바르게 수정되었습니다.**

`filteredEvents`를 사용하도록 슬라이싱 및 연결 로직이 적절히 수정되었습니다.

---

`53-53`: **빈 상태 조건이 올바르게 수정되었습니다.**

`filteredEvents.length`를 확인하도록 변경한 것이 논리적으로 올바릅니다.

---

`73-88`: **레이아웃 일관성을 위한 placeholder 추가가 우수합니다.**

단일 이벤트일 때 invisible placeholder 카드를 추가하여 레이아웃 일관성을 유지하는 것이 좋은 UX 개선입니다.

</details>
<details>
<summary>src/features/ticket/hooks/useTicketOptionDnD.ts (1)</summary>

`51-58`: **새로운 삭제 기능이 잘 구현되었습니다.**

드래그 앤 드롭으로 옵션을 삭제하는 새로운 기능이 올바르게 구현되었습니다. ID 검증과 삭제 뮤테이션 호출이 적절합니다.

</details>
<details>
<summary>src/pages/event/ui/EventDetailsPage.tsx (3)</summary>

`66-68`: **검색 아이콘 상호작용성 개선이 훌륭합니다.**

검색 아이콘에 클릭 이벤트와 커서 스타일을 추가하여 사용자 경험이 크게 개선되었습니다.

---

`88-93`: **인증 상태에 따른 조건부 렌더링이 적절합니다.**

로그인하지 않은 사용자에게는 좋아요 버튼을 숨기는 것이 적절한 UX 개선입니다. `useAuthStore`를 활용한 구현도 올바릅니다.

---

`142-150`: **동적 참조 링크 렌더링으로 유연성이 향상되었습니다.**

하드코딩된 정적 링크에서 `referenceLinks` 배열을 순회하는 동적 렌더링으로 변경되어 데이터 기반의 유연한 UI가 구현되었습니다.

</details>
<details>
<summary>src/features/event/ui/DatePicker.tsx (2)</summary>

`29-35`: **상태 초기화 로직이 개선되었습니다.**

`initialStartDate`와 `initialEndDate`로의 폴백 로직 추가로 더 유연한 초기화가 가능해졌습니다.

---

`44-57`: **함수형 상태 업데이트와 변수 네이밍 개선이 훌륭합니다.**

`prev => prev ?? value` 패턴 사용으로 불필요한 상태 업데이트를 방지하고, `startDateObj`와 `endDateObj`로 변수명을 변경하여 상태 변수와의 섀도잉을 방지한 것이 좋은 개선입니다.

</details>
<details>
<summary>src/features/dashboard/ui/ResponsesList.tsx (2)</summary>

`17-17`: **스토어 구조분해할당 단순화가 좋습니다.**

필요한 프로퍼티만 구조분해하여 코드가 더 명확해졌습니다.

---

`25-60`: **렌더링 로직 통합으로 가독성이 향상되었습니다.**

별도의 `renderTextResponses` 함수를 제거하고 인라인으로 처리하여 코드 흐름이 더 명확해졌습니다. 텍스트 응답과 차트 렌더링이 조건부로 잘 처리되어 있습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-31T15:48:17Z
user: login: m3k0813
body: LGTM👍
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-31T16:04:36Z

Review comments:
diff_hunk: @@ -3,6 +3,8 @@ import { useInfiniteScroll } from '../../../shared/hooks/useInfiniteScroll';
 import { getAllEventsInfinite, getCategoryEventsInfinite } from '../../../entities/event/api/event';
 import EventCard from '../../../shared/ui/EventCard';
 import { BaseEvent, CategoryType, TagType } from '../../../shared/types/baseEventType';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
path: src/features/event/ui/EventList.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**사용하지 않는 임포트를 제거하세요.**

`ReactQueryDevtools`가 임포트되었지만 코드에서 사용되지 않습니다.


다음 diff를 적용하여 사용하지 않는 임포트를 제거하세요:

```diff
-import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 ESLint</summary>

[error] 6-6: 'ReactQueryDevtools' is defined but never used.

(@typescript-eslint/no-unused-vars)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/features/event/ui/EventList.tsx at line 6, the import statement for
ReactQueryDevtools is present but not used anywhere in the code. Remove the
entire line importing ReactQueryDevtools to clean up unused imports and avoid
unnecessary code.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-31T15:48:16Z
author_association: NONE
diff_hunk: @@ -24,13 +29,10 @@ export const useTicketOptionDnD = () => {
     if (source.droppableId === 'options' && destination.droppableId.startsWith('ticket-')) {
       const ticketId = parseInt(destination.droppableId.replace('ticket-', ''), 10);
       const ticketOptionId = parseInt(result.draggableId, 10);
-      
+
       if (isNaN(ticketId) || isNaN(ticketOptionId)) {
-        console.error('Invalid ID parsing:', { ticketId, ticketOptionId });
-        return;
+        attachOption({ ticketId, ticketOptionId });
       }
-
-      attachOption({ ticketId, ticketOptionId });
       return;
path: src/features/ticket/hooks/useTicketOptionDnD.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**드래그 앤 드롭 로직에 치명적인 버그가 있습니다.**

ID 파싱이 실패했을 때(`isNaN`이 true일 때) `attachOption`을 호출하는 것은 잘못된 로직입니다. NaN 값으로 API를 호출하면 예상치 못한 동작이나 오류가 발생할 수 있습니다.



다음과 같이 조건을 수정해주세요:

```diff
-      if (isNaN(ticketId) || isNaN(ticketOptionId)) {
-        attachOption({ ticketId, ticketOptionId });
-      }
+      if (!isNaN(ticketId) && !isNaN(ticketOptionId)) {
+        attachOption({ ticketId, ticketOptionId });
+      }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion
      if (!isNaN(ticketId) && !isNaN(ticketOptionId)) {
        attachOption({ ticketId, ticketOptionId });
      }
      return;
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/features/ticket/hooks/useTicketOptionDnD.ts around lines 33 to 36, the
current logic incorrectly calls attachOption when ticketId or ticketOptionId is
NaN, which can cause errors. Change the condition to only call attachOption if
both ticketId and ticketOptionId are valid numbers (i.e., not NaN). This
prevents API calls with invalid IDs and avoids unexpected behavior.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-31T15:48:16Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-31T15:44:24Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

```markdown
## Walkthrough

이 변경 사항은 여러 컴포넌트와 API 모듈에 걸쳐 UI, 상태 관리, 폼 검증, API 요청 헤더, 드래그 앤 드롭 기능, 이벤트 필터링 및 렌더링 로직을 개선합니다. 인증 관련 HTTP 헤더 키가 통일되었고, 이벤트 및 참가자 리스트의 렌더링 방식, 폼 검증, 드래그 앤 드롭 삭제 기능 등이 추가·수정되었습니다.

## Changes

| 파일/그룹                                         | 변경 요약 |
|-------------------------------------------------|-----------|
| src/entities/event/api/event.ts, src/features/join/api/user.ts, src/shared/types/api/http-client.ts | API 요청 헤더를 `{ skipAuth: true }`에서 `{ isPublicApi: true }`로 변경; 인터셉터 로직도 해당 키로 수정 |
| src/features/dashboard/ui/MultiplePieCharts.tsx | Pie 차트 컨테이너에 배경색 추가, import 및 JSX 포맷 개선 |
| src/features/dashboard/ui/ParicipantCard.tsx    | 참가자 카드의 레이아웃, 폰트 사이즈, 버튼 렌더링 방식 개선 |
| src/features/dashboard/ui/PariticipantsList.tsx | useParticipantStore 구조분해 문법 및 헤더 gap 값 수정, order 추출 로직 단순화 |
| src/features/dashboard/ui/ResponsesList.tsx     | 텍스트 응답 렌더링 로직을 요약 케이스 내부로 통합, 코드 간결화 및 스타일 소폭 변경 |
| src/features/event/ui/DatePicker.tsx            | 초기 날짜 상태를 eventState 또는 initial 값으로 설정, useEffect에서 state가 null일 때만 업데이트 |
| src/features/event/ui/EventList.tsx, src/features/home/ui/EventSliderSection.tsx | 이벤트 리스트에서 종료된 이벤트 필터링, 클릭 시 상세 페이지 이동 추가, 슬라이더 레이아웃 보정 |
| src/features/ticket/hooks/useTicketOptionDnD.ts | 드래그 앤 드롭으로 티켓 옵션 삭제 기능 추가, attach/detach 로직 정제 |
| src/features/ticket/hooks/useTicketOptionHook.ts| 삭제 성공 시 쿼리 무효화 추가, 에러 메시지에 따른 알림 분기 처리 |
| src/pages/dashboard/ui/EventInfoPage.tsx        | 저장 후 페이지 이동 대신 React Query 캐시 무효화로 동작 변경 |
| src/pages/event/ui/EventDetailsPage.tsx         | 로그인 상태에 따라 북마크 버튼 표시, 주소 및 웹사이트 링크 렌더링 개선, 검색 아이콘 클릭 시 이동 추가 |
| src/pages/menu/ui/MenuPage.tsx                   | 헤더 내 검색 아이콘에 명시적 width 클래스 추가 |
| src/shared/lib/formValidation.ts                 | 전화번호 검증을 길이 13자(하이픈 포함)로 엄격화, 에러 메시지 명확화 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant EventList
    participant Router

    User->>EventList: 이벤트 카드 클릭
    EventList->>Router: /event-details/{event.id}로 이동
```

```mermaid
sequenceDiagram
    participant User
    participant TicketOptionDnD
    participant useDeleteTicketOptionMutation

    User->>TicketOptionDnD: 옵션을 '삭제' 영역으로 드래그
    TicketOptionDnD->>useDeleteTicketOptionMutation: 삭제 뮤테이션 실행
    useDeleteTicketOptionMutation-->>TicketOptionDnD: 성공/실패 콜백
```

```mermaid
sequenceDiagram
    participant Form
    participant formValidation

    Form->>formValidation: 전화번호 입력값 검증
    formValidation-->>Form: 13자리(하이픈 포함) 아니면 에러 반환
```

## Possibly related PRs

- GoTogether-Inc/frontend#106: event API에 새로운 함수 추가, 기존 함수의 헤더는 수정하지 않음. 동일 파일과 일부 함수에 대한 변경이 있어 관련성 높음.
- GoTogether-Inc/frontend#150: event API의 인증 헤더 키(`skipAuth: true`) 추가, 본 PR과 동일 함수의 헤더 키 변경이 있어 인증 처리 맥락에서 직접적 관련.

## Suggested reviewers

- hyeeuncho
- xaexunxang

## Poem

> 🐇  
> 오늘도 코드를 깡총깡총,  
> 이벤트 끝난 건 쏙 빼고,  
> 티켓 옵션 드래그로 휘리릭~  
> 폼 검증은 더 엄격하게,  
> 북마크는 로그인한 토끼만!  
> 변화의 바람, 하얀 배경에  
> 토끼는 기쁘게 춤춘다.  
> 🌸
```

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 63513a71bcc88f46e752d5838be47c29d0feb4c0 and 7065b44c6a688ce5701e4ddd0c489ac1c1d3398d.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/features/event/ui/EventList.tsx` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/features/event/ui/EventList.tsx

</details>

</details>
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEejqANiS4AzeAA8ugCq7ABy2RAGcuAXccADk5EAMdYClTQD0AIIACgCSkIALo4A37ZCALquADTUGAHLYzAKUXACMAKwA7AYAqojZkACaJELw8BgATDlG+sbgUGT0+DY4BMRkyjT0TKzsXLz8wqLiUjLyTEpUqupaOi0mUHCoqJg9hKTkVIMKIxickFQA7pCIGcxoFPJyCgsqapraumCGraYGiBQMILsdTwaRAqSnIJobjwcHsDS4RAcAwAIjRBgAxBjICEIn0DtQ6Ndbvd5F1GLBMKREGZYCRIAAJaDQMLnEgAR2w0lwkDpaAWkGwZXodXQFgskBIENwYAoJAshPo4SiDDQ4uQF3UCCwuAQyGYimwVkgFzQyAYlIwpHoNgoLEgAAMAN7XADW8G4ITwsC4uAoXMgAF8HZACI6XUgwtgBBZ4AwQjDff76cGNDA9RSqfS0DYbFNkLr6TZ8OL8JqrZAbNgMGJ4PgMEjHVL2AAREi4bQWB0AGkdpFwIXFAFFpYgIhg7Bh1CQe332wBhQlEfAPEfsMcTurT2cO/tr04AIVk0DQRFnmHou/b+9wR8XNGXDwdabg9INtHgdlV4nrKHN9cQJAaFOdAGDtRBkDVCVCzKStq1rAD0AwehaHwaRIAwfAeTVGg+EwMlCz4Cx8CIONe24e40DYXDEF7Fc2UQbgAPpS1aFjK0NCMIwsRxCxcOoOsG1DfBQzpSAlAYBVDkE5ByRIewmIoI56O4aNYwYSVThBaQjFSesSE4tEUWaX5/kBPNqGwOVECCWgzVgAR8HuWggmwWEAFkjXEbgrDCUF50pJTEARRBHFRdEeNxfEBiJG5WFJfhugtLMaQMV8UGYRSeUQDsaDYEDiz4B1zAYV12wAeW4H8MBCBsLkoENTWQOVCruXAjjDOpJOwJR0GuCiGGzGxcNE+lhjuF8xLuUUmFObQDnEyQTSobgYQrQseFBTMgpNM1BW4OyOpErqLB67NHQEIgwAuBAaBDAQ0FKog7WrIYS3oySzWC9N6QAKQAZQADUreiNodPyZ2OJjyBApq2RsR6CDlG07WYPrmC8+AwHY+leHwbhxNEKSBN/MM0GuOoiCsbG6npCTieqk0tXwPBMytSmkPkPHuFkvgskpCQ6woNM9LZ6kTUoN9+XpMMNuI0iGF7Q7ydPZ6SCIEmMDovg5WQygOZsYiLk4rjIr4gYZOE0bCc+6TELkhSV2UvhVJjONNPEcQdLSfTDPRAwIDAIwzKCCzcCssE7MQBynIoFy3KCMJ7jjD1MFwRc45CsKjMxbEov2GL6Diu4HkSsXvfSpOlJTijTgzt7Mv004AHJkAB4Gcv9MQI92yDaCEIUjsgd9P3kRjHsp3tixAwCAC9pF7C9IAVWQWZ5JQO3gCxvv+7hRE/OMoNkXsNo1gmslweqyElKx8sRBQ5q3CsKOrhhU5AlcBQiFskJQ9tOw1HtLqcozR0EXsha2n9KCQG/jwSiz1oSwF7pAUiEI+pWGGsPe4pEMBpgAGL1myvAeeyBCqhnkjyeUJA77IA/IxFeHMX7iDfrXbKuUhSQAABQokACJjgAZjpRL2FEgAP7v4YIyAKJACdQ4AD3HAAULSiAAlBLOUkoFLqUsGPdsVsUQ0HsDKHIAAGYetAOC6P0fUFEIM+CzUAjldgFNZ4c0euBZq0hoaASkJAAQIDXRMTqIiSauMrLJVgodOmSAfJoHkHDFqK42pD3HlgDGfEPTGhxqQ+iIC7JqFjLgWQgSJGAFU16RgANVb8JYgQeACBYF1gsDmcNAKZVjHYIkTw5QGkFhWas5BBoQQSt4x66El44y8T4766UpFyMsWYqx6BVp2ikPQJhNc06QQgRtOogtAIxlxgqQasASwCjIXYCgOUwDDBlnGMqPI8l7w1JLfaYT6CdRrKdXqG1EBUSLIQhxY0FQQSGbJQi1x2EFgod9UWyUrToXqiou47yRIbWGNDdgrdl4kTIjwO0PNtaSmlLyC87EiAm24nnc2dshKyzEvTSi1VZLdHkllIkKk1Lu2BF7VKelyB+2MgHUyAJQ4kEstZWy9lHLOVcrCKuIIWGrIADJASzsiHOkU8QF0OLFEkpdyRQupLSMalpxYnTOugWgH4Kzk2YEgQC60qBbw5uNcmooNpKE7tgbuFAnEQXgEQDAd8y4OiFCQKuzD364H+kjSGYZpqPywDYoC9jECyDmvYApco/SggkGqMuYMoEUBDLae0DplYaGsl5Rqe0WqIxXESQtaNLWYxGclUU5IHSG2oO5aEIYl5trqLQEMqp1QoFOCJcmNqqYkBpuQVRvBpCAV/JqXUa8xS4Q5nLDFDACnzn+v9FB0JIBZtOuhZ1Yk+QCjKAhLAsLsz90HrW1GfZoRgHyMYjgZ8wD1AACwhjDLuJ9ABmV976cgADZZwjxsLINdYlx5vwrBfK+WAqE0OHdbZZsqQKxhyryIVCwRYiV1TCh58KZaIrEkwFR8tMWUDtHwViRLcWzT9CWSsRsSW514vxOlVsXVE1pZbB2TLOgu1ZRpdloJOW+3CrywOwcBVhwjjZKOMcJUJwAEpuOYogBVOUlXSY4/nfoGqi5arJElQ1FcxJVrEJk6h+AZjWzKC/QkjpamUGgBQjTjEtMhjpBYPefAqw1kZkvfx6sNXIHUMgKjGkPxyjEBYeQ/iyP0gdMXUkA7QE5rEg6NzFAdO4ALfBaqaZxx2P5GXOw5KObLrMWAW59J8bcaXnl6D1DrgkGczQRL6BhrQJhAa+498yGYQwPVih/AqoyRPmJcgVx4BNOoewTWKAuPoXs9AqCjpmHXMqtVLz7jpDdogbND81Uj5sj1qcyUWpNvkL0QxQ7XjiKlX4HhTag2lKzKFRaSbjM6g4wKTM6yT2Yz4Fe5hebryzqQWuLGIgsBcA9ewAdRUIKu7hxUUvHKsgGNM11OjGtD836wVOyCes2bWsVnJJgRAsLeZ9RRJhR7WnLFsD6aQApDo+2SHgLQbAapMuwVQLEig8Ta0fSkuoR4rN03zQLDBr56KFZcHUNcA5FxotOXNUQN7koKC0eHnO08Qz1l0hqR0SgyAHTjg/ILfnaoDvMQAGqglhY1LUrHqDASJEGvgea1lDCsrrHkfb5IgsJFzoNTuGwkAjTWkMrq/TusxxzVAjSfIHyJGTCmVo0m0zTOVKQVBxQzbGoQu0EpDZlj/OjoVzAcb0CeDYksfOBIVip7rmLsyZlL1G+Nh7TXLaLu1NbNLpmhckF7O0+zbWOtdaLFvVdFqIGd5BZ19jZsuOWypXTPjFL6UzqdsykTbsxNaQ5c0SAkKLMFhEoy4/wmeCiY9tLm2DMZIAG5rbIqbjyB0MeZQ2mQEx2Io983MkWpwlACMg0bIMagoNYFmtAPKJkQcfw8mQqmOYIzYkICcLYhIfkpUlAemKqecaqRmaO6W2q5mKU+qw6IIaoxCK25IYMOUQ2+Bd0v8TYyEHBkMbBNAB6ycKgVggC+oigWezyx00OvUCM4oD0r2v6W44gaoEa7BhIoBjoSh8AaoQ4PB6hj2Uwkg8oSW3QDoOB4auUkMh6XIkEKimEPIs6ZQpwvYdQ5WnQ3QSgCMmMFYcWUwPWu+jAwe9iYSpWOoOWQaQ4uYUwvm+A+AroZeyu7suo1AHW98aWHYSkvBGhZhehnBcMhGkh/AGAyOqOAhME9I/Bx63QhY8g9wY0QRpwPWGARoFgvYQoTicEwWgk2aKOYSViaMnCDos6EgkAAAvHoFilKJAAAPzTEYQkAXBZFKJkwSD4B878DF4XCeriAVjyRAQczWHSAFIvbZpZqerCHoRkIbRzbiQuaCAiBiCQGATvJiQkBRFiDKL0i6xfIoxFpsGZH6E9odBZFWzpFqE0DlTCDZHAmEiQlCA/oiTDGJqUioTli64fKWGCHnE7LjKvF6JUCXpcGtTe41bdAHJWSB7Dx1B4CXEPrlG3ECH3H5hwHzQIEFG9iVI3Iwbtg7G64sFiTiBsAR40DICDH/G4DmBsDQnISSkzhKKQ78AMAMAUlFE9afg/4rgg71g64MkVGYlwzDF1hChNEtFcGoBZAcxlA8hXFiQnLYaCkGRcQGbkqax37Ww0oH5lwP5KQn7P5n6v6X4BzX4Ea36fFYIIrun76ul66AS+rCrrYMqOw+lP6uzqQBkSaoZpYYHxk2TmGSpBC8GEFlTCyIj2DPg/RQx/6Og3iFlXINQsmuFslIGoZRbDq4QwGOk5x8poEhwKYip5kJw3gFYkERRkHRTGbEjxTUHlypTpSEZ9z0DkxVbL6652IExhjyTdS9TmEK4pECz0iW5N5zAsAvwcw/ZILmFoodC6lcL0Tik3nqnXl9GoBWrepWhKKnjy5cn1EG7BEuZWlWxWq0BTgI6aAVnmHLxASXZ1IVgKn1hqnSHoQ7miR7loCeL2GaRKDIE4hmrk4YBHy9jnl4r2JMbzTQJ2614kaMDqSugXFeK1G4Ur5FHzg0UErIRWB8DJE8j4WCwawik8avH4obwAJwKkCCjjqQAaaIySVryUBoqBplCpBoU+r6EHJxFR6KXKV8WQxqWui14LZCZmlTjKGxjzyLnfmVkww8hWBSAWAFLLn6wr70A8UqWMzd51FtnhZo4j4noHn2D7EVh3AEwbYuynh1Bo4QUlqkqcYWyIQBEenRmCaP566plsoX4SZX434pRWzenOx+lpnsryAJXcbXrhkGQVm/5WXVnSgFYhixn4VYENlCTwRIEoHdlybmSYGKZBAHJsD5k3j/SxgLD/SGH1gjnGSqrjmUGmZlzzl0GVX2IKkOXXYbIYCqT/47ndoG5RI5X2BbkHmjgmgHKwQ5G0AgnmlvFE5IoNHrwuacIj6hgLb0iYRi6MFmWAV84gWI4KKTSoDLVEhYa4C9i9rkp0A3iICzhyiRK9KQKeo4LZpmGjghiihbY3ACBlCcj2Ld5kIbyUBWpTgVg3RxgXmHVhh0KRL5IVmIDqJOXE6EgNWCSJEaQ2kVFDXQJwUBaEiyRYAg24Rg1I3DpuEBqbUFLUJVRjyYns6fJiVk4hbbwiR07qAWjoRhjK2va82Sy0Dg0aBWBWi6ghiHB0icWWhNijg61kBED61pghC4XnbijHxHXXzwVkjToQXp4a5axITDpbLwA7I8B7IkAHIWACiqhxy155bZ4iQxqbxYArzLrxp2I1iU3pSGksy0IkCVK64Ogt5WA60kScI7kKJ1WWH+oxJ2aLIFIuV8WM2cnVJsVsRW4gpbwShBqw0+rhUSiI3rjm162wAFofSyCSSTxHXE1NXIAtVZjIFOlb6xWUopbv78b2yJmGUsr+nibexQBZXQpum5W+mpXn6exFVRl0rf5IonlVn1XxnVXsCDV86UAjWXoaGtkQFNVNmT1tWyboGdU5lBBCBrEYBQgwiuRlAllIj6aTXqrTVTlmYzl0FMgshsiY3YZnqN2iiOQE5gxZKlD1k9q9GEjYP5qdGXr3IqIFGVgPrOhugehei6hJgBjBigkRiIBRhn4JjwB0MpjlmbBiEfgtIK4sS4bQJlTyBBqFE87fgyyzZX38n0jKiMBbZ1qM6IDugExoDejAjfiCSWLtromjoSL704iRDaMKhEDkRYTAgXY5h5i1gVga7WzyFlQQL0RAUN2mgqJhYUCzr3z0lyhIMBIVnWQ8gyOIM2HWUbpcHppWQ6iyB3Kv0T3QpT2mxkrb5xXz3FUCbL3JWr0FXpUb3BkzkCUVE+oNWKZelJlHBBbEOua4YEPZF4M0C1OZm9ldUip/11CAOwh+4hTPj6btVf2Co/07btg9WxGug2RBolS7ZTb1gtgYAtghTKqjk4jkEEhHBUEwNzVpSnqjOOgTN1m4B7aCSzMtgVrIBkCWiDRHkZRZROIQJhYIK8l9Q3EYy5SM0KUkBthWA0CTMVTTMYCeSvP1jdrERWjPGSN+UBW67e6PRIJLzCW/YvOulc4zNUBEC6H9r10cXIIQagiFEz6eI7A0b6Rp366G7ywRN0CvRpxsiY5Xrm5wKnIczfykIiX86eo+Gos4K66KMbRD6IR1Hkw55DPYQgLf4Klq7e5i3eOjrtQwuQJ/PEVYDqkbQvzCgwIti2H0hKWpDW223dH22LzzFXC0CotgAXhgAmv4zyOwRwz8hYVcA3TXwmuqy3N/aM08tiR8tCQCtWzkyWurR0UPWwLNxKBfMkDNwJEUZanIRrr7Nuu/iwKoCqtEgXguHdCHp84nxw2kArXUpUKMyIuMxhj4vgvxtIZaRyg9aKNbbCvfTuR1AfTal4XZpq1jNhn8gDx2I2h3kZofEcgC65LJ2evF5bZMaV6sY14Kk3CrROzp1fOxtEFBN/PICCx+umvmv+uoZ2t4W+uysWi3N/wdj7scTT3JOz1um8a2yJWZPJkpUv7r2pSb0hnZUbnlN733sX5H1Xsn3Wy730C6W7NlA/MHN/PHMG3ULy5v0JOoaX1YEf38rf1YE2TCsjNxHjNAf7OHP1gMijMLPgNjmQNrMzU6q37zWhklte0UJUDkAksxiYB6UjK+XXNOwdaElLx2D2DowNvWIsD+rEntRp4QRcgFLwGVN23DziFfjMFKkqlMfvOfPtgkDAdYf/N4CawxFxFcC/g3BKlzpVgSihvtiCQJEAcKnbt20IUZthL8OOjQvK20DKfLshiY2lyqjK3T6dZ7IdE530jkuVF3yF5IYG4rgmeErQKoDnOYCXNWyuF7wfEbREt8BS0m7f7qlq5tTK02edwcwoieDCKQCACK44AK9NgAOC2QCAAPS4AMjzkAgArGOAAag4AKHjgAE02AAnTRoJYpwmwJgDl+lMK2WwSpBBYFko8CQNfNG1hcJIoi4dlBrrDoxPvF+GKJQKHjG5o+tJSEuyVZ7n7izrHuaJgBhVkITGG9heVIRJqGUNN+rmWLDuqu7AjFvD3GqMt79a4tDSrWJKnRwnd2JsF3hBxTyCPsuol4LXFyVtFYOCk3PZGd+xk0fre9k2lZ7BlUGVveLAEbB6U/ROk0vfD3lQYw+2GSRvBz2QKhRNSKKtHOKnHP1dKOOMWEnJzqWYsxNQRxQUR9A7NaR1s+Xr1CWzZwZSx0vEKCbgGkGkpbxfoYo7lj9jKC9LhBaywBoVDQHZFoD57u8wAIpcgPAsWginAFoUMAACHYDYR7roQQICYgYALnsg5Z44Y+9GceaFkMYnxngtNAFW5IVd7eujv7QlYqscSyovKO2n7qvSiA+nw8an1Ukbigz1NemyjB1n1sUlHx2vlAcwML3yxtFRcXB8YmQl/8W8kAtvkAwjr35cEnvDGZKr+AZyOnEfXigdyl9E1b5wslGEWlK2OebnYkSfQ1msvYsYZU/h2bjlULDJ5MLU7Yv2ht0CyRWA5M5PMs1HgE4PzpUPF7ebsPuPf7d7a9uTj7+T85hTC9npd5xTOZZTK9p+OTyPRxFV59VVDoN49P+AjPkM7SkH8T1oLZ98mPLHBAlyok8OqQQFfrmWlC09WwxfbeJ/3GoGYVmhcSciXA2bc90ogvL7ME3Yh6U4wv4L6FojLr4U2AvxNGA6BwEIlHQOAiILNHLIhAjWzHLAaYSDQ0NYA8eOUKc1NQTcyYunCCD0HNzMIVslRUImCw1KNwrKhrG4tzm0wkRrQ44Yui5lQB1JFk5De0BtDUYE4coV1RFAST0o7cqMWAfgkKAKRlB7gv2PAUqzCICNpYfAOGJFxrCR1m+1FK5BcV7A+9HmARB0EEFMEAg+6HfGkkUWcGlRewD1DtneiLh5IiUsyZUqcnojY5iE5VdKCP3pCcJHIcREuK6CUS11fwqABUrLX1Y9YI6nQYoiYWtg7dUA8sP/nUHIhyhpQHMasBoIEGHwjgfuUhA+jKAkAasfADxojHqSe5lg4FfFHa2si0IIkK8ZBGQ1VpNsLO8gPLKqVKHqCzUIw1REBFohmluKifRCjICwhIIHQwwudNkWEpbwbaJrA4ZWFBDB1/wNYemoqBMGvMNIKIQAK81gAXaHIAgAT7HAANQOeBAAHUuWIcBrHRmGXXe5XM/WyaKiO7DXw05xQ1TPMLrEGgKoMAYzQ3moMEp/kOwhFOVtHDLCusLBJ8SwFPi4JD1SodFYoGpjlSQVERKFJdmQEgJPMFioYFQPjlhbzJ8Y5xAQheishv5vcnqTkkcQh4uluM8VY+nD336I8D62kY/mjxVr3432KZD9ofXP7RlSqVFE9EoM/A2BQB/TCAUEHyjYB8y7kMgNgHgHM98OyzKahz1QFc9aCPPKvjiw+4VEhUvglALNAyii9XCd9a2CgwoBoo4aiOB+MBB5DZCsATwbdhakgDbpd0n0PgQ6AuBgBv0Z/B0MAAWxEA9AIYZDPCEgBhBahRpamg7Q2R3AxKKJDCDKMzxvx1etAAnFGLWF95E+oeM5mohThI5NENyESJqArFIJP0CBKLAUiDH+iyKOsYvkJFbJtiCc5IPIF2PvhLxZ4YAMPJx3JCGJ8M/AYFN3kbanBx21eebJBHeKDBN8Z7T0kKN35CQkqCPO/kjwlG6R9IvTT+iHGjh1EXIsYAQKHDiTO5k+msPDqQTNGEdNUnPEjtaPShWcVs2NUGDlm4AHJyABaC4SKCsGOhWo/0ZWncE4F2jsKWYqUDmMSxXdfGbkZGGhjAl6l2WfvDLrCzZCkBOOFEdqJQCwBkJyYsAGJubjRygT9IGEDIFkDwjrIRI+5LjlOAxhoxdaltJBPOMrr0ju8OUZuppEKgw0PkSuOUCRLgTkSKAwY2XJ50GQ2dnw+NThDkCLoUhRAelUIY6FLRvFaYnCTSdRAOTPINuM6dRE2K0mlRIs1RT7rhPRwcxeJBOCLvYERg9YcggGZKASRoii0/uRuDnERhUT1MU2SpT+JTH8IK18+kGKkThMYnNFMg0CDGNhiO6eTtoiMRuiC11zGodSNE0CTSK4QGRiUQiAxIYmfT1B/0n6MAHkBAwFAAAHIokr6EZHqoFAqRtAAlFt78DYHuCqwckJSWJy8C2gTkIn2iZ0UwCXK9QcLxYkAxhXcTFX3FpNhRe/WUQf3v7njUez7bekT2lhn8ceR4vgIAITJ4932h/B/riTfCSdmhjNZ2r1msYdQxIHUxmt3ndEvEIWIkisG2jiRwS6QCEh+GwVOCaibxgUOgEEAaw2RoQsIRHLgG4DnJYw8IRECz0QHmjvxlo38dvToKYTuQnlAEJ1iRgPwzsjNfIs2VVp0hXsLNSYk4VgLBNpBLDdSGw18yCNuhZvXDNllSzKNqG3oBmTYNKymFZodgYlF6MQDTENAkYUTPTNrzJ4CRG0TGSlJiYEDSEVkYFI0I0YrZW2lJPGINDoCQRIs50hiEEzsn0gPGg0KqPRBiwuFpCc+AwcKQNTaSKWDxRmGQgaHqMtIEjegDLMRAecY049RAu/VPbzToyB4j/MtNv75UzxgZJ9gU0DmL19pFMOMkhxv5ZNTx4ojlNtKUDwdvg6wTCmXA0F7B2eRIcaKMHOBoArg6zeis8GUBLB3gqwL4AYEznjR1AAAfT5yIAG52YhYnQAbn3kWgtctoJABAz/o8gnktAAUByACAlSdUuqTYE/QgYSABQPIPUFoB5A6p/6OqVkE/QFAGA9QAAJy0ADEeYAQJ+gYBGI1gvcgoAYhAx5AD5h8kDGgBAwTzBohQUqSQE/Rmpd5DAT9HVK3mPQcgDAHILQH/T/ot5dU+gN3Lrm8dG5zc1uahPbm0AG515fQEAA== -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=GoTogether-Inc/frontend&utm_content=157):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-31T15:46:22Z
body: 📖storybook: https://677a88d1e04e06676106db14-cdmrjfzqrv.chromatic.com/
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-31T15:54:07Z
body: 📖storybook: https://677a88d1e04e06676106db14-yripgdrvmx.chromatic.com/
author_association: NONE