Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bd69009
refact: 'TOKEN4001' 에러 코드를 반환할 때 자동 로그인되도록 설정
Yejiin21 May 20, 2025
434d94d
fix: 파일 경로 에러 수정
Yejiin21 May 20, 2025
8a7e0be
refact: 유저 정보가 없어도 API 호출 가능하도록 설정
Yejiin21 May 20, 2025
232da5c
feat: 메인페이지 배너 이미지 데이터 연동
Yejiin21 May 20, 2025
ce77052
refac: 최신 이벤트 이벤트 카드 주소대신 온오프라인 타입으로 변경
Yejiin21 May 20, 2025
e9a2c29
feat: 공유하기 모달 이벤트 이미지, 제목 data 연결
Yejiin21 May 20, 2025
545f622
feat: 파일 업로드 공통 커스텀 훅으로 분리 및 적용
Yejiin21 May 21, 2025
4eb7c2a
refact: 실검 삭제 및 검색 초기 상태에 모든 데이터값 보이게 리팩토링
Yejiin21 May 21, 2025
245b031
fix: 해시태그 한글 입력 시 조합 문제 해결
Yejiin21 May 21, 2025
3e84b10
fix: Storybook 옵션 타입 오류 수정 및 객체 배열로 변경
Yejiin21 May 21, 2025
f5cd92d
refact: formatISO 유틸 함수 적용하여 날짜+시간 ISO 포맷 통합 처리
Yejiin21 May 21, 2025
fd152f5
fix: DatePicker 초기값 현재 날짜로 설정 및 드롭다운 UI 제거 처리
Yejiin21 May 21, 2025
6a846d1
fix: 위치 좌표가 0.0이 아닐 때만 KakaoMap 렌더링되도록 조건 추가
Yejiin21 May 21, 2025
a9984f8
feat: 데이터가 없을 때 안내 문구 표시 추가
Yejiin21 May 21, 2025
d582277
fix: Kakao 공유 기능과 UI 표시 불일치 해결
Yejiin21 May 21, 2025
e26744d
refact: 불필요한 이벤트 상세 API 호출 제거
Yejiin21 May 21, 2025
eefd505
fix: 카카오 공유 설명에서 HTML 태그 제거
Yejiin21 May 21, 2025
50d2a27
feat: 호스트 프로필 미등록시 기본 프로필 이미지 설정
Yejiin21 May 22, 2025
1aa4ee3
fix: 날짜 포맷 적용
Yejiin21 May 22, 2025
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
11 changes: 9 additions & 2 deletions design-system/stories/ChoiceChip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export default meta;
export const TwoOptions: Story = {
args: {
label: '',
options: ['선착순', '주최자 선별'],
options: [
{ label: '선착순', value: 'FIRST_COME' },
{ label: '주최자 선별', value: 'SELECTION' },
],
onSelect: (selected: string) => {
console.log(`Selected option: ${selected}`);
},
Expand All @@ -39,7 +42,11 @@ export const TwoOptions: Story = {
export const ThreeOptions: Story = {
args: {
label: '',
options: ['객관식', '주관식', '여러 개 선택'],
options: [
{ label: '객관식', value: 'MULTIPLE_CHOICE' },
{ label: '주관식', value: 'SUBJECTIVE' },
{ label: '여러 개 선택', value: 'MULTI_SELECT' },
],
onSelect: (selected: string) => {
console.log(`Selected option: ${selected}`);
},
Expand Down
6 changes: 3 additions & 3 deletions design-system/ui/ChoiceChip.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';

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

interface ChoiceChipProps {
Expand All @@ -24,7 +24,7 @@ const ChoiceChip = ({
buttonClassName = '',
value,
}: ChoiceChipProps) => {
const [selected, setSelected] = useState(value || options[0]);
const [selected, setSelected] = useState<string>(value || options[0].value || '');

useEffect(() => {
if (value) {
Expand Down
19 changes: 17 additions & 2 deletions design-system/ui/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ interface ProfileProps {
id?: number;
name?: string;
profile?: Profile;
profileImageUrl?: string;
className: string;
onClick?: (id: number) => void;
children?: React.ReactNode;
}

const ProfileCircle = ({ id, name, profile = 'userProfile', className = '', onClick, children }: ProfileProps) => {
const ProfileCircle = ({
id,
name,
profile = 'userProfile',
profileImageUrl,
className = '',
onClick,
children,
}: ProfileProps) => {
const profileClassName = profile === 'userProfile' ? `bg-main ${className}` : `bg-gray4 ${className}`;

// 기존 userProfile 크기: lg:w-10 lg:h-10 md:h-9 md:w-9 sm:h-8 sm:w-8 lg:text-sm md:text-xs sm:text-xs
Expand All @@ -32,7 +41,13 @@ const ProfileCircle = ({ id, name, profile = 'userProfile', className = '', onCl
>
<div className="flex items-center">
<div className={`${profileClassName} flex items-center justify-center rounded-full`}>
{profile === 'userProfile' && name && <span className="font-bold text-white">{name}</span>}
{profileImageUrl ? (
<img src={profileImageUrl} alt="프로필 이미지" className="object-cover w-full h-full rounded-full" />
) : (
<div className={`${profileClassName} flex items-center justify-center rounded-full w-full h-full`}>
{profile === 'userProfile' && name && <span className="font-bold text-white">{name}</span>}
</div>
)}
</div>
{children && <span className="ml-2 text-black">{children}</span>}
</div>
Expand Down
6 changes: 6 additions & 0 deletions design-system/ui/textFields/MultilineTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ interface MultilineTextFieldProps {
value?: string;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onCompositionStart?: (e: React.CompositionEvent<HTMLTextAreaElement>) => void;
onCompositionEnd?: (e: React.CompositionEvent<HTMLTextAreaElement>) => void;
disabled?: boolean;
placeholder?: string;
className?: string;
Expand All @@ -20,6 +22,8 @@ const MultilineTextField = forwardRef<HTMLTextAreaElement, MultilineTextFieldPro
value,
onChange,
onKeyDown,
onCompositionStart,
onCompositionEnd,
disabled = false,
placeholder = '',
className = '',
Expand All @@ -39,6 +43,8 @@ const MultilineTextField = forwardRef<HTMLTextAreaElement, MultilineTextFieldPro
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
disabled={disabled}
placeholder={placeholder}
{...rest}
Expand Down
Binary file removed public/assets/banners/1.png
Binary file not shown.
Binary file removed public/assets/banners/2.png
Binary file not shown.
Binary file removed public/assets/banners/3.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions public/assets/event-manage/creation/BasicProfile.svg

This file was deleted.

2 changes: 1 addition & 1 deletion src/entities/event/hook/useEventHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const useEventDetail = () => {
const { data } = useQuery({
queryKey: ['eventDetail', eventId],
queryFn: () => eventDetail({ eventId, userId: user?.id }),
enabled: !!user?.id,
enabled: !!user?.id && !!eventId,
});

return { data };
Expand Down
2 changes: 1 addition & 1 deletion src/entities/event/hook/useEventListHook.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EventList } from '../../../features/event/event-list/model/eventList';
import { EventList } from '../../../features/event/model/event';
import { useInfiniteScroll } from '../../../shared/hooks/useInfiniteScroll';
import { getAllEventsInfinite } from '../api/eventDetail';

Expand Down
7 changes: 7 additions & 0 deletions src/features/event/lib/stripHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const stripHtml = (html: string): string => {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
};
Comment on lines +1 to +5
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

HTML 태그 제거 함수의 안전성을 개선하세요.

이 함수는 HTML 문자열에서 태그를 제거하는 용도로 잘 구현되었으나, 다음과 같은 개선사항이 있습니다:

  1. 입력 유효성 검사가 없습니다. html 매개변수가 null이나 undefined인 경우 오류가 발생할 수 있습니다.
  2. DOM 조작은 비용이 많이 들 수 있으므로, 간단한 HTML 제거에는 정규식 접근 방식이 더 효율적일 수 있습니다.

다음과 같이 개선할 수 있습니다:

-const stripHtml = (html: string): string => {
+const stripHtml = (html: string | null | undefined): string => {
+  if (html == null) return '';
+  
+  // 간단한 HTML에 대해서는 정규식 사용 (더 효율적)
+  // return html.replace(/<[^>]*>?/g, '');
+  
   const tmp = document.createElement('div');
   tmp.innerHTML = html;
   return tmp.textContent || tmp.innerText || '';
 };

DOM 기반 방식은 HTML 엔티티를 올바르게 디코딩하는 이점이 있으므로, 정규식 접근 방식은 선택적으로 적용하세요.

📝 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 stripHtml = (html: string): string => {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
};
const stripHtml = (html: string | null | undefined): string => {
if (html == null) return '';
// 간단한 HTML에 대해서는 정규식 사용 (더 효율적)
// return html.replace(/<[^>]*>?/g, '');
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
};
🤖 Prompt for AI Agents
In src/features/event/lib/stripHtml.ts lines 1 to 5, improve the stripHtml
function by adding input validation to handle null or undefined html parameters
safely, returning an empty string in such cases. Additionally, consider
providing an alternative implementation using a regular expression to remove
HTML tags for better performance in simple cases, while keeping the DOM-based
approach optional to preserve correct HTML entity decoding.


export default stripHtml;
23 changes: 10 additions & 13 deletions src/features/event/ui/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import DatePicker from 'react-datepicker';
import { ko } from 'date-fns/locale';
import 'react-datepicker/dist/react-datepicker.css';
import { FunnelState } from '../model/FunnelContext';
import { formatISO } from '../../../shared/lib/date';

interface DatePickerProps {
className?: string;
Expand All @@ -25,8 +26,11 @@ const EventDatePicker = ({
onEndDateChange,
isLabel = false,
}: DatePickerProps) => {
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(null);
const [startDate, setStartDate] = useState<Date | null>(
eventState?.startDate ? new Date(eventState.startDate) : new Date()
);
const [endDate, setEndDate] = useState<Date | null>(eventState?.endDate ? new Date(eventState.endDate) : new Date());

const [startTime, setStartTime] = useState<string>('06:00');
const [endTime, setEndTime] = useState<string>('23:00');

Expand Down Expand Up @@ -67,17 +71,8 @@ const EventDatePicker = ({

useEffect(() => {
if (startDate && endDate) {
const [startHour, startMin] = startTime.split(':').map(Number);
const [endHour, endMin] = endTime.split(':').map(Number);

const start = new Date(startDate);
start.setHours(startHour, startMin, 0, 0);

const end = new Date(endDate);
end.setHours(endHour, endMin, 0, 0);

const startISO = new Date(start.getTime() + 9 * 60 * 60 * 1000).toISOString();
const endISO = new Date(end.getTime() + 9 * 60 * 60 * 1000).toISOString();
const startISO = formatISO(startDate, startTime);
const endISO = formatISO(endDate, endTime);

if (setEventState) {
setEventState(prev => ({
Expand Down Expand Up @@ -109,6 +104,7 @@ const EventDatePicker = ({
onChange={(date: Date | null) => setStartDate(date)}
locale={ko}
dateFormat="MM월 dd일"
autoComplete="off"
className="w-20 h-9 md:w-24 md:h-10 border border-placeholderText text-sm md:text-md rounded-[5px] p-2"
renderCustomHeader={({
date,
Expand Down Expand Up @@ -156,6 +152,7 @@ const EventDatePicker = ({
onChange={(date: Date | null) => setEndDate(date)}
locale={ko}
dateFormat="MM월 dd일"
autoComplete="off"
className="w-20 h-9 md:w-24 md:h-10 border border-placeholderText text-sm md:text-md rounded-[5px] p-2"
renderCustomHeader={({
date,
Expand Down
5 changes: 4 additions & 1 deletion src/features/event/ui/EventTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface EventTagProps {

const EventTag = ({ eventState, setEventState }: EventTagProps) => {
const [inputValue, setInputValue] = useState('');
const [isComposing, setIsComposing] = useState(false);
const MAX_TAGS = 5;

const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
Expand All @@ -18,7 +19,7 @@ const EventTag = ({ eventState, setEventState }: EventTagProps) => {
const hashtags = eventState?.hashtags || [];

const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && inputValue.trim()) {
if (e.key === 'Enter' && !isComposing && inputValue.trim()) {
e.preventDefault();

if (hashtags.length >= MAX_TAGS) {
Expand Down Expand Up @@ -57,6 +58,8 @@ const EventTag = ({ eventState, setEventState }: EventTagProps) => {
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onCompositionStart={() => setIsComposing(true)} // ✅ 한글 조합 시작
onCompositionEnd={() => setIsComposing(false)}
placeholder="엔터를 이용해 태그를 입력하세요"
className="w-full h-40"
disabled={hashtags.length >= MAX_TAGS}
Expand Down
78 changes: 17 additions & 61 deletions src/features/event/ui/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import FileUploadImage from '../../../../public/assets/event-manage/creation/FileUpload.svg';
import { useEffect, useRef, useState } from 'react';
import { uploadFile } from '../hooks/usePresignedUrlHook';
import { FunnelState } from '../model/FunnelContext';
import useImageUpload from '../../../shared/hooks/useImageUpload';

interface FileUploadProps {
value?: string;
Expand All @@ -10,62 +9,13 @@ interface FileUploadProps {
}

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

const handleFileUpload = async (file: File) => {
if (file.size > 500 * 1024) {
alert('파일 크기는 500KB를 초과할 수 없습니다.');
return;
}

if (!['image/jpg', 'image/jpeg', 'image/png'].includes(file.type)) {
alert('jpg, jpeg, png 파일만 업로드 가능합니다.');
return;
}

try {
const imageUrl = await uploadFile(file);
setPreviewUrl(imageUrl);
onChange?.(imageUrl);
if (setEventState) {
setEventState(prev => ({ ...prev, bannerImageUrl: imageUrl }));
}
} catch (error) {
console.error('파일 업로드 실패:', error);
}
};

const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};

const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
};

const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file) handleFileUpload(file);
};

const handleClick = () => {
fileInputRef.current?.click();
};

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) handleFileUpload(file);
};

useEffect(() => {
if (value) setPreviewUrl(value);
}, [value]);
const { previewUrl, fileInputRef, handleFileChange, handleDrop, setIsDragging, isDragging } = useImageUpload({
value, // 서버에서 받아온 기본 이미지
onSuccess: url => {
onChange?.(url);
setEventState?.(prev => ({ ...prev, bannerImageUrl: url }));
},
});

return (
<div className="flex flex-col justify-start gap-1">
Expand All @@ -75,10 +25,16 @@ const FileUpload = ({ value, onChange, setEventState }: FileUploadProps) => {
className={`flex flex-col items-center justify-center h-44 border border-dashed ${
isDragging ? 'border-main bg-dropdown' : 'border-placeholderText bg-gray3'
} rounded-[10px] mb-4 cursor-pointer`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDragOver={e => {
e.preventDefault();
setIsDragging(true);
}}
onDragLeave={e => {
e.preventDefault();
setIsDragging(false);
}}
onDrop={handleDrop}
onClick={handleClick}
onClick={() => fileInputRef.current?.click()}
>
<input
type="file"
Expand Down
14 changes: 8 additions & 6 deletions src/features/event/ui/ShareEventModal.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import profile from '../../../../public/assets/banners/1.png';
import link from '../../../../public/assets/event-manage/details/Link.svg';
import kakao from '../../../../public/assets/event-manage/details/KaKao.svg';
import { shareToKakao } from '../../../shared/lib/kakaoShare';
import stripHtml from '../lib/stripHtml';

interface ShareEventModalProps {
closeModal: () => void;
eventName: string;
title: string;
eventDescription?: string;
eventImageUrl?: string;
eventUrl?: string;
}

const ShareEventModal = ({
closeModal,
eventName,
title,
eventDescription = '',
eventImageUrl = '',
eventUrl = window.location.href,
}: ShareEventModalProps) => {
const description = stripHtml(eventDescription);

const handleKakaoShare = async () => {
try {
await shareToKakao(eventName, eventDescription, eventImageUrl, eventUrl);
await shareToKakao(title, description, eventImageUrl, eventUrl);
} catch (error) {
console.error('카카오 공유 실패:', error);
alert('카카오 공유하기에 실패했습니다.');
Expand Down Expand Up @@ -69,8 +71,8 @@ const ShareEventModal = ({
</div>
<h1 className="font-semibold text-xl text-center mb-6">공유하기</h1>
<div className="flex items-center gap-5 mb-8">
<img src={profile} alt="프로필 사진" className="w-20 h-20 rounded-[5px]" />
<span className="font-semibold text-lg">{eventName}</span>
<img src={eventImageUrl} alt="프로필 사진" className="w-20 h-20 rounded-[5px]" />
<span className="font-semibold text-lg">{title}</span>
</div>
<div className="flex flex-col gap-4 py-3">
<div onClick={handleCopyLink} className="flex items-center gap-4 cursor-pointer">
Expand Down
14 changes: 7 additions & 7 deletions src/features/home/ui/EventSliderSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ const EventSliderSection = ({ title, events }: EventSliderSectionProps) => {
const eventsToShow =
events.length > 0
? events
.slice(startIndex, startIndex + maxCardsToShow)
.concat(
startIndex + maxCardsToShow > events.length
? events.slice(0, (startIndex + maxCardsToShow) % events.length)
: []
)
.slice(startIndex, startIndex + maxCardsToShow)
.concat(
startIndex + maxCardsToShow > events.length
? events.slice(0, (startIndex + maxCardsToShow) % events.length)
: []
)
: [];
return (
<div className="relative w-full px-6">
Expand All @@ -55,7 +55,7 @@ const EventSliderSection = ({ title, events }: EventSliderSectionProps) => {
dDay={event.remainDays}
host={event.hostChannelName}
eventDate={event.startDate}
location={event.address}
location={event.onlineType}
hashtags={event.hashtags}
onClick={() => navigate(`/event-details/${event.id}`)}
/>
Expand Down
Loading