Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions design-system/ui/ChoiceChip.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';

interface ChoiceChipOption {
label?: string; // UI에 보여질 한국어
value?: string; // 서버에서 오는 값
}

interface ChoiceChipProps {
label?: string;
options: string[];
value?: string;
options: ChoiceChipOption[];
onSelect: (selected: string) => void;
className?: string;
labelClassName?: string;
buttonClassName?: string;
}

const ChoiceChip = ({ label, options, onSelect, className, labelClassName = '', buttonClassName = '' }: ChoiceChipProps) => {
const [selected, setSelected] = useState(options[0]);
const ChoiceChip = ({
label,
options,
onSelect,
className,
labelClassName = '',
buttonClassName = '',
value,
}: ChoiceChipProps) => {
const [selected, setSelected] = useState(value || options[0]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

빈 options 배열에 대한 방어 코드가 필요합니다.

현재 코드는 options[0]의 값을 초기 선택값으로 사용하고 있는데, options 배열이 비어있을 경우 undefined 참조 오류가 발생할 수 있습니다. 아래와 같이 방어 코드를 추가하는 것이 좋습니다.

- const [selected, setSelected] = useState(value || options[0]);
+ const [selected, setSelected] = useState(value || (options.length > 0 ? options[0].value : ''));
📝 Committable suggestion

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

Suggested change
const [selected, setSelected] = useState(value || options[0]);
const [selected, setSelected] = useState(
value || (options.length > 0 ? options[0].value : '')
);


useEffect(() => {
if (value) {
setSelected(value);
}
}, [value]);

const handleClick = (option: string) => {
setSelected(option);
onSelect(option);
const handleClick = (optionValue: string) => {
setSelected(optionValue);
onSelect(optionValue);
};

return (
Expand All @@ -26,12 +46,12 @@ const ChoiceChip = ({ label, options, onSelect, className, labelClassName = '',
key={index}
className={`
flex justify-center items-center sm:text-xs md:text-sm lg:text-base px-2 rounded-full
${selected === option ? 'bg-white text-black' : 'text-black bg-transparent'}
${selected === option.value ? 'bg-white text-black' : 'text-black bg-transparent'}
${buttonClassName}
`}
onClick={() => handleClick(option)}
onClick={() => handleClick(option.value || '')}
>
{option}
{option.label}
</button>
))}
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/entities/event/api/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { axiosClient } from '../../../shared/types/api/http-client';
import { EventDetailRequest } from '../model/event';

export const eventDetail = async (dto: EventDetailRequest) => {
const response = await axiosClient.get(`/events/${dto.eventId}`);
return response.data;
};
17 changes: 17 additions & 0 deletions src/entities/event/hook/useEventHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { eventDetail } from '../api/event';
import { useParams } from 'react-router-dom';

const useEventDetail = () => {
const { id } = useParams();

const eventId = Number(id);

const { data } = useQuery({
queryKey: ['eventDetail', eventId],
queryFn: () => eventDetail({ eventId }),
});

return { data };
};
Comment on lines +1 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이벤트 상세 정보를 가져오는 훅이 구현되었습니다. 몇 가지 개선 사항을 제안합니다.

이벤트 상세 정보를 가져오는 중앙화된 훅의 구현은 잘 되었으나, 다음과 같은 개선 사항을 고려해 보세요:

  1. 에러 처리와 로딩 상태 노출이 필요합니다
  2. 타입 안전성 향상을 위한 반환 타입 명시가 필요합니다
  3. id 파라미터 유효성 검사가 필요합니다

아래는 개선된 구현을 제안합니다:

import { useQuery } from '@tanstack/react-query';
import { eventDetail } from '../api/event';
import { useParams } from 'react-router-dom';
+ import { EventDetailResponse } from '../model/event';

- const useEventDetail = () => {
+ const useEventDetail = () => {
  const { id } = useParams();

+  // id가 없거나 유효한 숫자가 아닌 경우 처리
+  if (!id || isNaN(Number(id))) {
+    console.error('Invalid event ID');
+    return { data: undefined, isLoading: false, error: new Error('Invalid event ID') };
+  }

  const eventId = Number(id);

-  const { data } = useQuery({
+  const { data, isLoading, error } = useQuery<EventDetailResponse, Error>({
    queryKey: ['eventDetail', eventId],
    queryFn: () => eventDetail({ eventId }),
  });

-  return { data };
+  return { data, isLoading, error };
};
export default useEventDetail;
📝 Committable suggestion

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

Suggested change
import { useQuery } from '@tanstack/react-query';
import { eventDetail } from '../api/event';
import { useParams } from 'react-router-dom';
const useEventDetail = () => {
const { id } = useParams();
const eventId = Number(id);
const { data } = useQuery({
queryKey: ['eventDetail', eventId],
queryFn: () => eventDetail({ eventId }),
});
return { data };
};
import { useQuery } from '@tanstack/react-query';
import { eventDetail } from '../api/event';
import { useParams } from 'react-router-dom';
import { EventDetailResponse } from '../model/event';
const useEventDetail = () => {
const { id } = useParams();
// id가 없거나 유효한 숫자가 아닌 경우 처리
if (!id || isNaN(Number(id))) {
console.error('Invalid event ID');
return { data: undefined, isLoading: false, error: new Error('Invalid event ID') };
}
const eventId = Number(id);
const { data, isLoading, error } = useQuery<EventDetailResponse, Error>({
queryKey: ['eventDetail', eventId],
queryFn: () => eventDetail({ eventId }),
});
return { data, isLoading, error };
};
export default useEventDetail;

export default useEventDetail;
15 changes: 15 additions & 0 deletions src/entities/event/model/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BaseEvent } from '../../../shared/types/baseEventType';

export interface EventDetailRequest {
eventId: number;
}

export interface EventDetailResponse {
result: BaseEvent & {
id: number;
participantCount: number;
hostChannelName: string;
hostChannelDescription: string;
status: string;
};
}
8 changes: 7 additions & 1 deletion src/features/dashboard/api/event.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { axiosClient } from '../../../shared/types/api/http-client';
import { UpdateEventRequest } from '../model/event';

export const getEventInfo = async (eventId: string) => {
export const getHostDashboard = async (eventId: number) => {
const response = await axiosClient.get('/host-channels/dashboard', {
params: {
eventId: eventId,
},
});
return response.data.result;
};

export const updateEventInfo = async (eventId: number, dto: Partial<UpdateEventRequest>) => {
const response = await axiosClient.put(`/events/${eventId}`, dto);
return response.data;
};
29 changes: 29 additions & 0 deletions src/features/dashboard/hook/useEventHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { useParams } from 'react-router-dom';
import { getHostDashboard, updateEventInfo } from '../api/event';
import { dashboardData } from '../../../shared/types/dashboardType';
import { UpdateEventRequest } from '../model/event';

export const useGetEventHook = () => {
const { id } = useParams();

const eventId = Number(id);

const { data: eventInfo } = useQuery<dashboardData>({
queryKey: ['eventInfo', eventId],
queryFn: () => getHostDashboard(eventId),
});

return { eventInfo };
};
Comment on lines +7 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

캐시 설정과 오류 처리 개선 필요

useGetEventHook이 React Query를 사용하여 잘 구현되었지만, 캐시 무효화 정책과 오류 처리가 누락되어 있습니다. 이벤트 데이터가 변경될 때 적절히 갱신되도록 추가 설정이 필요합니다.

export const useGetEventHook = () => {
  const { id } = useParams();

  const eventId = Number(id);

- const { data: eventInfo } = useQuery<dashboardData>({
+ const { data: eventInfo, error, isLoading } = useQuery<dashboardData>({
    queryKey: ['eventInfo', eventId],
    queryFn: () => getHostDashboard(eventId),
+   staleTime: 5 * 60 * 1000, // 5분 동안 데이터를 '신선'하게 유지
+   retry: 1, // 실패 시 1번만 재시도
+   refetchOnWindowFocus: true, // 창이 포커스를 받을 때 데이터 새로고침
  });

- return { eventInfo };
+ return { eventInfo, error, isLoading };
};
📝 Committable suggestion

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

Suggested change
export const useGetEventHook = () => {
const { id } = useParams();
const eventId = Number(id);
const { data: eventInfo } = useQuery<dashboardData>({
queryKey: ['eventInfo', eventId],
queryFn: () => getHostDashboard(eventId),
});
return { eventInfo };
};
export const useGetEventHook = () => {
const { id } = useParams();
const eventId = Number(id);
const { data: eventInfo, error, isLoading } = useQuery<dashboardData>({
queryKey: ['eventInfo', eventId],
queryFn: () => getHostDashboard(eventId),
staleTime: 5 * 60 * 1000, // 5분 동안 데이터를 '신선'하게 유지
retry: 1, // 실패 시 1번만 재시도
refetchOnWindowFocus: true, // 창이 포커스를 받을 때 데이터 새로고침
});
return { eventInfo, error, isLoading };
};


export const useUpdateEventHook = () => {
const { id } = useParams();
const eventId = Number(id);

const mutation = useMutation({
mutationFn: (dto: Partial<UpdateEventRequest>) => updateEventInfo(eventId, dto),
});

return mutation;
};
15 changes: 0 additions & 15 deletions src/features/dashboard/hook/useGetEventHook.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/features/dashboard/model/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { BaseEvent } from '../../../shared/types/baseEventType';

export interface UpdateEventRequest extends BaseEvent {
hostChannelId: number;
}
5 changes: 0 additions & 5 deletions src/features/event-manage/event-create/api/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,3 @@ export const createEvent = async (data: CreateEventRequest) => {
const response = await axiosClient.post('/events', data);
return response.data;
};

export const readEventDetail = async (eventId: number) => {
const response = await axiosClient.get(`/events/${eventId}`);
return response.data;
};
19 changes: 3 additions & 16 deletions src/features/event-manage/event-create/model/eventCreation.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
export interface CreateEventRequest {
import { BaseEvent } from '../../../../shared/types/baseEventType';

export interface CreateEventRequest extends BaseEvent {
hostChannelId: number;
title: string;
startDate: string;
endDate: string;
startTime: string;
endTime: string;
bannerImageUrl: string;
description: string;
referenceLinks: { address: string; detailAddress: string; title: string; url: string }[];
onlineType: 'ONLINE' | 'OFFLINE';
address: string;
location: { lat: number; lng: number };
category: string;
hashtags: string[];
organizerEmail: string;
organizerPhoneNumber: string;
}
22 changes: 17 additions & 5 deletions src/features/event-manage/event-create/ui/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import FileUploadImage from '../../../../../public/assets/event-manage/creation/FileUpload.svg';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { uploadFile } from '../hooks/usePresignedUrlHook';
import { useFunnelState } from '../model/FunnelContext';
import { FunnelState } from '../model/FunnelContext';

const FileUpload = () => {
interface FileUploadProps {
value?: string;
onChange?: (url: string) => void;
setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
}

const FileUpload = ({ value, onChange, setEventState }: FileUploadProps) => {
const [isDragging, setIsDragging] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const { setEventState } = useFunnelState();

const handleFileUpload = async (file: File) => {
if (file.size > 500 * 1024) {
Expand All @@ -23,7 +28,10 @@ const FileUpload = () => {
try {
const imageUrl = await uploadFile(file);
setPreviewUrl(imageUrl);
setEventState(prev => ({ ...prev, bannerImageUrl: imageUrl }));
onChange?.(imageUrl);
if (setEventState) {
setEventState(prev => ({ ...prev, bannerImageUrl: imageUrl }));
}
} catch (error) {
console.error('파일 업로드 실패:', error);
}
Expand Down Expand Up @@ -55,6 +63,10 @@ const FileUpload = () => {
if (file) handleFileUpload(file);
};

useEffect(() => {
if (value) setPreviewUrl(value);
}, [value]);

return (
<div className="flex flex-col justify-start gap-1">
<h1 className="font-bold text-black text-lg">배너 사진 첨부</h1>
Expand Down
70 changes: 34 additions & 36 deletions src/features/event-manage/event-create/ui/LinkInput.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,62 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { FunnelState } from '../model/FunnelContext';
import AddButton from '../../../../../public/assets/event-manage/creation/AddBtn.svg';
import CloseButton from '../../../../../public/assets/event-manage/creation/CloseBtn.svg';
import Link from '../../../../../public/assets/event-manage/creation/Link.svg';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

'Link' 명칭 충돌 해결 필요

Link 인터페이스와 가져온 SVG 이미지 Link가 이름 충돌을 일으키고 있습니다. 둘 중 하나의 이름을 변경하여 충돌을 해결해야 합니다.

- import Link from '../../../../../public/assets/event-manage/creation/Link.svg';
+ import LinkIcon from '../../../../../public/assets/event-manage/creation/Link.svg';

export interface Link {
  title: string;
  url: string;
  address: string;
  detailAddress: string;
}

그리고 JSX에서도 아이콘 사용 부분을 변경해야 합니다:

- <img src={Link} alt="링크 이미지" className="p-2" />
+ <img src={LinkIcon} alt="링크 이미지" className="p-2" />

Also applies to: 13-18

import { FunnelState } from '../model/FunnelContext';

interface LinkInputProps {
eventState?: FunnelState['eventState'];
value?: Link[];
onChange?: (links: Link[]) => void;
setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
}

const LinkInput = ({ eventState, setEventState }: LinkInputProps) => {
const [links, setLinks] = useState<{ title: string; url: string; address: string; detailAddress: string }[]>(
eventState?.referenceLinks || []
);
export interface Link {
title: string;
url: string;
address: string;
detailAddress: string;
}

const LinkInput = ({ value, onChange, setEventState }: LinkInputProps) => {
const [links, setLinks] = useState<Link[]>([]);
const [activeInput, setActiveInput] = useState<{ field: 'title' | 'url' | null }>({
field: null,
});
const [hoveredInput, setHoveredInput] = useState<{ field: 'title' | 'url' | null }>({
field: null,
});

const updateAll = (newLinks: Link[]) => {
setLinks(newLinks);
onChange?.(newLinks);
setEventState?.(prev => ({ ...prev, referenceLinks: newLinks }));
};

const addNewLink = () => {
const newLink = {
title: '',
url: '',
address: '',
detailAddress: '',
};
setLinks([...links, newLink]);
setActiveInput({ field: null });
if (setEventState) {
setEventState(prev => ({
...prev,
referenceLinks: [...prev.referenceLinks, newLink],
}));
}
updateAll([...links, newLink]);
};

const removeLink = (title: string) => {
const updatedLinks = links.filter(link => link.title !== title);
setLinks(updatedLinks);
if (setEventState) {
setEventState(prev => ({
...prev,
referenceLinks: updatedLinks,
}));
}
const removeLink = (index: number) => {
const newLinks = links.filter((_, i) => i !== index);
updateAll(newLinks);
};

const updateLink = (title: string, field: 'url' | 'title', value: string) => {
const updatedLinks = links.map(link => (link.title === title ? { ...link, [field]: value } : link));
setLinks(updatedLinks);
if (setEventState) {
setEventState(prev => ({
...prev,
referenceLinks: updatedLinks,
}));
}
const updateLink = (index: number, field: keyof Link, value: string) => {
const newLinks = [...links];
newLinks[index] = { ...newLinks[index], [field]: value };
updateAll(newLinks);
};

useEffect(() => {
setLinks(value ?? []);
}, [value]);

return (
<div className="flex flex-col gap-1">
<h1 className="font-bold text-black text-lg">관련 링크</h1>
Expand All @@ -77,7 +75,7 @@ const LinkInput = ({ eventState, setEventState }: LinkInputProps) => {
<input
type="text"
value={link.title}
onChange={e => updateLink(link.title, 'title', e.target.value)}
onChange={e => updateLink(index, 'title', e.target.value)}
className="w-full min-w-[3rem] md:min-w-[6rem] h-8 text-placeholderText ml-1 outline-none bg-transparent text-sm md:text-base"
placeholder="참조링크"
autoFocus={activeInput.field === link.title && activeInput.field === 'title'}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

autoFocus 로직 오류 수정 필요

현재 autoFocus 조건 (activeInput.field === link.title && activeInput.field === 'title')에 문제가 있습니다. activeInput.field는 'title', 'url', 또는 null 중 하나인데, 이를 link.title(문자열)과 비교하면 항상 false가 반환됩니다.

- autoFocus={activeInput.field === link.title && activeInput.field === 'title'}
+ autoFocus={activeInput.field === 'title'}
📝 Committable suggestion

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

Suggested change
autoFocus={activeInput.field === link.title && activeInput.field === 'title'}
autoFocus={activeInput.field === 'title'}

Expand All @@ -94,7 +92,7 @@ const LinkInput = ({ eventState, setEventState }: LinkInputProps) => {
<input
type="text"
value={link.url}
onChange={e => updateLink(link.title, 'url', e.target.value)}
onChange={e => updateLink(index, 'url', e.target.value)}
className="w-full min-w-[10rem] md:min-w-[15rem] h-8 text-placeholderText ml-2 outline-none bg-transparent text-sm md:text-base"
placeholder="URL을 입력하세요"
autoFocus={activeInput.field === 'url'}
Expand All @@ -104,7 +102,7 @@ const LinkInput = ({ eventState, setEventState }: LinkInputProps) => {
<button
onClick={e => {
e.stopPropagation();
removeLink(link.title);
removeLink(index);
}}
className="absolute top-1/2 right-2 transform -translate-y-1/2"
>
Expand Down
Loading