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
6,831 changes: 0 additions & 6,831 deletions package-lock.json

This file was deleted.

11 changes: 10 additions & 1 deletion src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,19 @@ import LogoutPage from '../../pages/join/LogoutPage';
import BookmarkPage from '../../pages/bookmark/ui/BookmarkPage';
import CategoryPage from '../../pages/event/ui/CategoryPage';
import CheckInPage from '../../pages/dashboard/ui/ticket/CheckInPage';
import { FunnelProvider } from '../../features/event/model/FunnelContext';

const mainRoutes = [
{ path: MAIN_ROUTES.main, element: <MainPage />, requiresAuth: false },
{ path: MAIN_ROUTES.eventCreation, element: <FunnelPage />, requiresAuth: false },
{
path: MAIN_ROUTES.eventCreation,
element: (
<FunnelProvider>
<FunnelPage />
</FunnelProvider>
),
requiresAuth: false,
},
{ path: MAIN_ROUTES.allEvents, element: <AllEventsPage />, requiresAuth: false },
{ path: MAIN_ROUTES.eventDatail, element: <EventDetailsPage />, requiresAuth: false },
{ path: MAIN_ROUTES.search, element: <SearchPage />, requiresAuth: false },
Expand Down
2 changes: 1 addition & 1 deletion src/features/event/model/FunnelContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const FunnelProvider = ({ children }: { children: ReactNode }) => {
address: '',
detailAddress: '',
locationLat: 37.5665,
locationLng: 126.9780,
locationLng: 126.978,
category: 'DEVELOPMENT_STUDY',
hashtags: [],
organizerEmail: '',
Expand Down
12 changes: 6 additions & 6 deletions src/features/event/ui/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ const EventDatePicker = ({
}, [startDate, endDate, startTime, endTime, setEventState, onStartDateChange, onEndDateChange]);

return (
<div className={`flex flex-col w-full ${className}`}>
<div className="flex flex-wrap lg:flex-nowrap items-center justify-between gap-2">
<div className={`w-full ${className}`}>
<div className="flex flex-wrap justify-between">
<div className="flex flex-col w-full sm:w-auto gap-2">
{!isLabel && <span className="text-sm font-medium">시작 날짜</span>}
<div className="flex gap-1 ">
<div className="flex">
<DatePicker
id="startDate"
selected={startDate}
Expand All @@ -113,7 +113,7 @@ const EventDatePicker = ({
text-sm md:text-md
rounded-[5px]
p-2 cursor-pointer
caret-transparent
caret-transparent mr-1
"
renderCustomHeader={({
date,
Expand Down Expand Up @@ -154,15 +154,15 @@ const EventDatePicker = ({

<div className="flex flex-col w-full sm:w-auto gap-2">
{!isLabel && <span className="text-sm font-medium">종료 날짜</span>}
<div className="flex gap-1">
<div className="flex">
<DatePicker
id="endDate"
selected={endDate}
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 cursor-pointer caret-transparent"
className="w-20 h-9 md:w-24 md:h-10 border border-placeholderText text-sm md:text-md rounded-[5px] p-2 cursor-pointer caret-transparent mr-1"
renderCustomHeader={({
date,
decreaseMonth,
Expand Down
2 changes: 1 addition & 1 deletion src/features/event/ui/EventFunnel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const EventFunnel = ({ onNext, onPrev, Funnel, Step, currentStep }: EventFunnelI
setHostState(initialHostState);
handleNext(String(currentStep - 1));
},
onError: (error: any) => {
onError: error => {
const message = error?.message || '호스트 생성에 실패했습니다. 다시 시도해주세요.';
alert(message);
},
Expand Down
12 changes: 3 additions & 9 deletions src/features/event/ui/EventType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ import OfflineIcon from '../../../../public/assets/event-manage/creation/Offline
import { useFunnelState } from '../model/FunnelContext';
import KakaoMap from '../../../shared/ui/KakaoMap';
import { AddressSearch } from '../../../shared/ui/AddressSearch';
import { useState } from 'react';

interface EventTypeProps {
className?: string;
}

const EventType = ({ className }: EventTypeProps) => {
const { eventState, setEventState } = useFunnelState();
const [address, setAddress] = useState('');
const [detailAddress, setDetailAddress] = useState('');

const handleTypeClick = (type: 'ONLINE' | 'OFFLINE') => {
setEventState(prev => ({
Expand All @@ -22,19 +19,15 @@ const EventType = ({ className }: EventTypeProps) => {
};

const handleAddressChange = (address: string) => {
setAddress(address);
setEventState(prev => ({
...prev,
address,
detailAddress,
}));
};

const handleDetailAddressChange = (detailAddress: string) => {
setDetailAddress(detailAddress);
setEventState(prev => ({
...prev,
address,
detailAddress,
}));
};
Expand All @@ -46,7 +39,7 @@ const EventType = ({ className }: EventTypeProps) => {
locationLng,
}));
};
console.log(eventState.locationLat, eventState.locationLng);

return (
<div className={`flex flex-col justify-center w-full ${className}`}>
<div className="flex flex-col justify-start">
Expand Down Expand Up @@ -78,8 +71,9 @@ const EventType = ({ className }: EventTypeProps) => {
<div className="mt-6 space-y-2">
<h1 className="font-bold text-black text-lg">이벤트는 어디서 진행되나요?</h1>
<AddressSearch
address={address}
address={eventState.address}
setAddress={handleAddressChange}
detailAddress={eventState.detailAddress}
onLocationChange={handleLocationChange}
onDetailAddressChange={handleDetailAddressChange}
/>
Expand Down
13 changes: 6 additions & 7 deletions src/features/event/ui/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import FileUploadImage from '../../../../public/assets/event-manage/creation/FileUpload.svg';
import { FunnelState } from '../model/FunnelContext';
import { FunnelState, useFunnelState } from '../model/FunnelContext';
import useImageUpload from '../../../shared/hooks/useImageUpload';
import { useEffect } from 'react';

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

const FileUpload = ({ value, onChange, setEventState, useDefaultImage, onValidationChange }: FileUploadProps) => {
const FileUpload = ({ onChange, setEventState, useDefaultImage, onValidationChange }: FileUploadProps) => {
const { eventState } = useFunnelState();

const { previewUrl, fileInputRef, handleFileChange, handleDrop, setIsDragging, isDragging } = useImageUpload({
value, // 서버에서 받아온 기본 이미지
value: eventState.bannerImageUrl,
onSuccess: url => {
onChange?.(url);
setEventState?.(prev => ({ ...prev, bannerImageUrl: url }));
Expand All @@ -22,9 +23,7 @@ const FileUpload = ({ value, onChange, setEventState, useDefaultImage, onValidat
});

useEffect(() => {
if (onValidationChange) {
onValidationChange(!!previewUrl);
}
onValidationChange?.(!!previewUrl);
}, [previewUrl, onValidationChange]);

return (
Expand Down
28 changes: 7 additions & 21 deletions src/features/event/ui/LinkInput.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { useEffect, useState } from 'react';
import { FunnelState } from '../model/FunnelContext';
import { useState } from 'react';
import { useFunnelState } 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';

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

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

const LinkInput = ({ value, onChange, setEventState }: LinkInputProps) => {
const [links, setLinks] = useState<Link[]>([]);
const LinkInput = () => {
const { eventState, setEventState } = useFunnelState();
const links = eventState.referenceLinks;

const [activeInput, setActiveInput] = useState<{ field: 'title' | 'url' | null }>({
field: null,
});
Expand All @@ -25,17 +21,11 @@ const LinkInput = ({ value, onChange, setEventState }: LinkInputProps) => {
});

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

const addNewLink = () => {
const newLink = {
title: '',
url: '',
};
updateAll([...links, newLink]);
updateAll([...(links || []), { title: '', url: '' }]);
};

const removeLink = (index: number) => {
Expand All @@ -49,10 +39,6 @@ const LinkInput = ({ value, onChange, setEventState }: LinkInputProps) => {
updateAll(newLinks);
};

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

return (
<div className="flex flex-col gap-1">
<h1 className="font-bold text-black text-lg">관련 링크</h1>
Expand Down
96 changes: 49 additions & 47 deletions src/features/event/ui/TextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,37 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import { uploadFile } from '../hooks/usePresignedUrlHook';
import { FunnelState } from '../model/FunnelContext';
import { useFunnelState } from '../model/FunnelContext';

interface TextEditorProps {
value?: string;
onChange?: (value: string) => void;
setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
onValidationChange?: (isValid: boolean) => void;
}

const MAX_LENGTH = 2000;
const IMAGE_WEIGHT = 200;

const formats = [
'font', 'header', 'bold', 'italic', 'underline', 'strike', 'blockquote',
'list', 'bullet', 'indent', 'link', 'image', 'align', 'color', 'background',
'size', 'h1',
'font',
'header',
'bold',
'italic',
'underline',
'strike',
'blockquote',
'list',
'bullet',
'indent',
'link',
'image',
'align',
'color',
'background',
'size',
'h1',
];

const TextEditor = ({ value = '', onChange, setEventState, onValidationChange }: TextEditorProps) => {
const [content, setContent] = useState(value);
const TextEditor = ({ onValidationChange }: TextEditorProps) => {
const { eventState, setEventState } = useFunnelState();
const quillRef = useRef<ReactQuill | null>(null);
const [isOverLimit, setIsOverLimit] = useState(false);

Expand Down Expand Up @@ -49,16 +60,40 @@ const TextEditor = ({ value = '', onChange, setEventState, onValidationChange }:
}
};
};

const getImageCount = (htmlContent: string): number => {
const matches = htmlContent.match(/<img [^>]*src="[^"]*"[^>]*>/g);
return matches ? matches.length : 0;
};
const getPlainText = (htmlContent: string): string => {
return htmlContent.replace(/<[^>]*>/g, '').trim();
};
const getTotalContentLength = (htmlContent: string): number => {
const textLength = getPlainText(htmlContent).length;
const imageCount = getImageCount(htmlContent);
return textLength + imageCount * IMAGE_WEIGHT;
};

const handleChange = (value: string) => {
const totalLength = getTotalContentLength(value);

if (totalLength <= MAX_LENGTH) {
setEventState?.(prev => ({ ...prev, description: value }));
onValidationChange?.(getPlainText(value).length > 0);
setIsOverLimit(false);
} else {
const editorInstance = quillRef.current?.getEditor();
if (editorInstance) {
editorInstance.setContents(editorInstance.clipboard.convert(eventState.description));
}
setIsOverLimit(true);
}
};

useEffect(() => {
onValidationChange?.(getPlainText(eventState.description).length > 0);
}, [eventState.description, onValidationChange]);

const modules = useMemo(
() => ({
toolbar: {
Expand All @@ -79,44 +114,16 @@ const TextEditor = ({ value = '', onChange, setEventState, onValidationChange }:
}),
[]
);
const getPlainText = (htmlContent: string): string => {
return htmlContent.replace(/<[^>]*>/g, '').trim();
};

const handleChange = (value: string) => {
const totalLength = getTotalContentLength(value);

if (totalLength <= MAX_LENGTH) {
setContent(value);
onChange?.(value);
setEventState?.(prev => ({ ...prev, description: value }));
onValidationChange?.(getPlainText(value).length > 0);
setIsOverLimit(false);
} else {
const editorInstance = quillRef.current?.getEditor();
if (editorInstance) {
editorInstance.setContents(editorInstance.clipboard.convert(content));
}
setIsOverLimit(true);
}
};

useEffect(() => {
setContent(value); // 외부 value가 바뀌면 내부에 반영
const plainText = getPlainText(value);
onValidationChange?.(plainText.length > 0);
}, [value]);

const totalLength = getTotalContentLength(content);
const imageCount = getImageCount(content);

const totalLength = getTotalContentLength(eventState.description);
const imageCount = getImageCount(eventState.description);

return (
<div className="flex flex-col justify-start gap-2 mb-4">
<h1 className="font-bold text-black text-lg">이벤트에 대한 상세 설명</h1>
<ReactQuill
theme="snow"
value={content}
value={eventState.description}
ref={quillRef}
modules={modules}
formats={formats}
Expand All @@ -125,14 +132,9 @@ const TextEditor = ({ value = '', onChange, setEventState, onValidationChange }:
/>
<div className="flex justify-between items-center mt-1">
<p className={`text-sm ${isOverLimit ? 'text-red-500' : 'text-gray-500'}`}>
{totalLength} / {MAX_LENGTH}자
{imageCount > 0 && ` (이미지 ${imageCount}개 포함)`}
{totalLength} / {MAX_LENGTH}자{imageCount > 0 && ` (이미지 ${imageCount}개 포함)`}
</p>
{isOverLimit && (
<p className="text-sm text-red-500 font-medium">
{MAX_LENGTH}자를 초과할 수 없습니다.
</p>
)}
{isOverLimit && <p className="text-sm text-red-500 font-medium">{MAX_LENGTH}자를 초과할 수 없습니다.</p>}
</div>
</div>
);
Expand Down
Loading