diff --git a/design-system/ui/modals/QrModal.tsx b/design-system/ui/modals/QrModal.tsx index 37d3e763..9de81ddc 100644 --- a/design-system/ui/modals/QrModal.tsx +++ b/design-system/ui/modals/QrModal.tsx @@ -5,7 +5,8 @@ import { flexColumnCenter, flexColumn, flexRowSpaceBetweenCenter } from '../../s import qr_calendar from '../../icons/QrCalendar.svg'; import qr_location from '../../icons/QrLocation.svg'; import qr_ticket from '../../icons/QrTicket.svg'; -import qr_check from '../../icons/QrCheck.svg'; +import qr_check from '../../../public/assets/menu/completed.svg'; +import qr_pending from '../../../public/assets/menu/pending.svg'; interface QrModalProps { isChecked: boolean; // QR 상태 @@ -57,7 +58,17 @@ const QrModal = ({
{iconPath1 &&
{iconPath1}
}
- QR Code + {ticketQrCode ? ( + QR Code + ) : ( +
+ 주최자의 승인이 완료되면
QR이 발급됩니다. +
+ )}
@@ -68,19 +79,19 @@ const QrModal = ({
} + iconPath={qr_calendar} children={formattedDate} className="text-11" > } + iconPath={qr_location} children={location} className="text-11" > } + iconPath={qr_ticket} children={ticketName} className="text-11" > @@ -88,13 +99,13 @@ const QrModal = ({
} + iconPath={qr_check} children={orderStatus === 'COMPLETED' ? '승인됨' : '대기 중'} className="text-11" > } + iconPath={qr_check} children={isCheckIn ? '체크인 완료' : '체크인 미완료'} className="text-11" > diff --git a/design-system/ui/textFields/UnderlineTextField.tsx b/design-system/ui/textFields/UnderlineTextField.tsx index c7014adc..46503e9d 100644 --- a/design-system/ui/textFields/UnderlineTextField.tsx +++ b/design-system/ui/textFields/UnderlineTextField.tsx @@ -1,6 +1,6 @@ -import { ChangeEvent, forwardRef } from 'react'; +import { ChangeEvent, forwardRef, InputHTMLAttributes } from 'react'; -interface UnderlineTextFieldProps { +interface UnderlineTextFieldProps extends InputHTMLAttributes { label: string; type?: string; value?: string; diff --git a/design-system/ui/texts/Countdown.tsx b/design-system/ui/texts/Countdown.tsx index 4d947d9f..5fb3b84b 100644 --- a/design-system/ui/texts/Countdown.tsx +++ b/design-system/ui/texts/Countdown.tsx @@ -21,7 +21,10 @@ const Countdown = ({ children, isChecked }: CountdownProps) => { border-[0.1px] font-medium ${flexCenter} `; - return ; + const isEnded = children === 'false'; + const displayText = isEnded ? '종료' : children; + + return }; export default Countdown; diff --git a/src/features/dashboard/ui/ResponsesList.tsx b/src/features/dashboard/ui/ResponsesList.tsx index efb651b2..df0e8f9a 100644 --- a/src/features/dashboard/ui/ResponsesList.tsx +++ b/src/features/dashboard/ui/ResponsesList.tsx @@ -63,6 +63,9 @@ const ResponsesList = ({ listType, ticketOptionResponses, ticketId }: ResponsesL if (isLoading) return

로딩 중...

; if (error || !data?.result) return

데이터를 불러오지 못했습니다.

; const allOrders = data.result.flatMap(user => user.orders); + if (allOrders.length === 0) { + return

응답이 없습니다.

; + } return ( ); diff --git a/src/features/event/ui/EventFunnel.tsx b/src/features/event/ui/EventFunnel.tsx index 5ffdf7d9..bfb62bd2 100644 --- a/src/features/event/ui/EventFunnel.tsx +++ b/src/features/event/ui/EventFunnel.tsx @@ -12,10 +12,11 @@ import { EventFunnelInterface, StepNames } from '../../../shared/types/funnelTyp import { useFunnelState } from '../model/FunnelContext'; import { useEventCreation } from '../hooks/useEventHook'; import { useHostCreation } from '../../host/hook/useHostHook'; +import { HostCreationRequest } from '../../host/model/host'; const EventFunnel = ({ onNext, onPrev, Funnel, Step, currentStep }: EventFunnelInterface) => { const navigate = useNavigate(); - const { eventState, hostState } = useFunnelState(); + const { eventState, hostState, setHostState } = useFunnelState(); const { mutate: createEvent } = useEventCreation(); const { mutate: createHost } = useHostCreation(); @@ -33,10 +34,17 @@ const EventFunnel = ({ onNext, onPrev, Funnel, Step, currentStep }: EventFunnelI onNext(nextStep); } }; + const initialHostState: HostCreationRequest = { + profileImageUrl: '', + hostChannelName: '', + hostEmail: '', + channelDescription: '', + }; const handleHostCreation = () => { createHost(hostState, { onSuccess: () => { + setHostState(initialHostState); handleNext(String(currentStep - 1)); }, }); diff --git a/src/features/event/ui/TextEditor.tsx b/src/features/event/ui/TextEditor.tsx index 632ba87a..6cc0f228 100644 --- a/src/features/event/ui/TextEditor.tsx +++ b/src/features/event/ui/TextEditor.tsx @@ -8,31 +8,22 @@ interface TextEditorProps { value?: string; onChange?: (value: string) => void; setEventState?: React.Dispatch>; + 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 }: TextEditorProps) => { - const [content, setContent] = useState(''); +const TextEditor = ({ value = '', onChange, setEventState, onValidationChange }: TextEditorProps) => { + const [content, setContent] = useState(value); const quillRef = useRef(null); + const [isOverLimit, setIsOverLimit] = useState(false); const imageHandler = async () => { if (!quillRef.current) return; @@ -58,6 +49,15 @@ const TextEditor = ({ value, onChange, setEventState }: TextEditorProps) => { } }; }; + const getImageCount = (htmlContent: string): number => { + const matches = htmlContent.match(/]*src="[^"]*"[^>]*>/g); + return matches ? matches.length : 0; + }; + const getTotalContentLength = (htmlContent: string): number => { + const textLength = getPlainText(htmlContent).length; + const imageCount = getImageCount(htmlContent); + return textLength + imageCount * IMAGE_WEIGHT; + }; const modules = useMemo( () => ({ @@ -79,19 +79,38 @@ const TextEditor = ({ value, onChange, setEventState }: TextEditorProps) => { }), [] ); + const getPlainText = (htmlContent: string): string => { + return htmlContent.replace(/<[^>]*>/g, '').trim(); + }; const handleChange = (value: string) => { - setContent(value); - onChange?.(value); - if (setEventState) { - setEventState(prev => ({ ...prev, description: value })); + 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 ?? ''); + setContent(value); // 외부 value가 바뀌면 내부에 반영 + const plainText = getPlainText(value); + onValidationChange?.(plainText.length > 0); }, [value]); + const totalLength = getTotalContentLength(content); + const imageCount = getImageCount(content); + + return (

이벤트에 대한 상세 설명

@@ -104,7 +123,19 @@ const TextEditor = ({ value, onChange, setEventState }: TextEditorProps) => { onChange={handleChange} className="custom-quill-editor" /> +
+

+ {totalLength} / {MAX_LENGTH}자 + {imageCount > 0 && ` (이미지 ${imageCount}개 포함)`} +

+ {isOverLimit && ( +

+ {MAX_LENGTH}자를 초과할 수 없습니다. +

+ )} +
); }; + export default TextEditor; diff --git a/src/features/event/ui/TimePicker.tsx b/src/features/event/ui/TimePicker.tsx index ec6dae29..0ea080c7 100644 --- a/src/features/event/ui/TimePicker.tsx +++ b/src/features/event/ui/TimePicker.tsx @@ -3,13 +3,34 @@ import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; interface TimePickerProps { + value?: string; onChange: (datetime: string) => void; } -const TimePicker = ({ onChange }: TimePickerProps) => { - const [selectedDate, setSelectedDate] = useState(new Date()); - const [selectedHour, setSelectedHour] = useState('00'); - const [selectedMinute, setSelectedMinute] = useState('00'); +const parseUtcToKst = (utcString: string): Date => { + const utcDate = new Date(utcString); + const kstTimestamp = utcDate.getTime() + 9 * 60 * 60 * 1000; + return new Date(kstTimestamp); +}; + +const TimePicker = ({ value, onChange }: TimePickerProps) => { + const initialKstDate = value ? parseUtcToKst(value) : new Date(); + const [selectedDate, setSelectedDate] = useState(initialKstDate); + const [selectedHour, setSelectedHour] = useState( + initialKstDate.getHours().toString().padStart(2, '0') + ); + const [selectedMinute, setSelectedMinute] = useState( + initialKstDate.getMinutes().toString().padStart(2, '0') + ); + + useEffect(() => { + if (value) { + const date = new Date(value); + setSelectedDate(date); + setSelectedHour(date.getHours().toString().padStart(2, '0')); + setSelectedMinute(date.getMinutes().toString().padStart(2, '0')); + } + }, [value]); // 날짜, 시간 바뀔 때마다 업데이트 useEffect(() => { diff --git a/src/features/join/api/user.ts b/src/features/join/api/user.ts index ba992d3e..b4a589a4 100644 --- a/src/features/join/api/user.ts +++ b/src/features/join/api/user.ts @@ -1,5 +1,5 @@ import { axiosClient } from '../../../shared/types/api/http-client'; -import { UserInfoRequest, UserInfoResponse } from '../model/userInformation'; +import { TermsAgreementRequest, UserInfoRequest, UserInfoResponse } from '../model/userInformation'; export const readUser = async (): Promise => { const response = await axiosClient.get<{ result: UserInfoResponse }>('/users', { @@ -14,3 +14,11 @@ export const updateUser = async (data: UserInfoRequest): Promise { + const response = await axiosClient.post('/terms', data, { + headers: { isPublicApi: true }, + }); + return response.data; +}; diff --git a/src/features/join/hooks/useUserHook.ts b/src/features/join/hooks/useUserHook.ts index 1b717a8a..aa3af4f2 100644 --- a/src/features/join/hooks/useUserHook.ts +++ b/src/features/join/hooks/useUserHook.ts @@ -1,5 +1,5 @@ import { useMutation, useQuery } from '@tanstack/react-query'; -import { readUser, updateUser } from '../api/user'; +import { agreeTerms, readUser, updateUser } from '../api/user'; import { UserInfoRequest, UserInfoResponse } from '../model/userInformation'; export const useUserInfo = (enabled: boolean = true) => { @@ -15,3 +15,16 @@ export const useUserUpdate = () => { mutationFn: updateUser, }); }; + +export const useAgreeTerms = () => { + return useMutation({ + mutationFn: agreeTerms, + onSuccess: () => { + alert('이용약관 동의 완료'); + }, + onError: (error) => { + alert('동의 처리 실패'); + console.error('이용약관 동의 실패', error); + } + }); +}; \ No newline at end of file diff --git a/src/features/join/model/agreementStore.ts b/src/features/join/model/agreementStore.ts index d576dac7..4232f32a 100644 --- a/src/features/join/model/agreementStore.ts +++ b/src/features/join/model/agreementStore.ts @@ -13,6 +13,12 @@ interface AgreementState { agreements: Agreements; toggleAgreement: (key: keyof Agreements) => void; // 특정 항목 상태 토글 isAllRequiredAgreed: () => boolean; // 필수 항목 체크 여부 + getAgreementStates: () => { + serviceAgreed: boolean; + privacyPolicyAgree: boolean; + personalInfoUsageAgreed: boolean; + marketingAgreed: boolean; + }; } export const useAgreementStore = create((set, get) => ({ @@ -56,4 +62,13 @@ export const useAgreementStore = create((set, get) => ({ const { agreements } = get(); return agreements.terms && agreements.privacy && agreements.dataUsage; }, + getAgreementStates: () => { + const { agreements } = get(); + return { + serviceAgreed: agreements.terms, + privacyPolicyAgree: agreements.privacy, + personalInfoUsageAgreed: agreements.dataUsage, + marketingAgreed: agreements.marketing, + }; + }, })); diff --git a/src/features/join/model/userInformation.ts b/src/features/join/model/userInformation.ts index 14553792..6fdb0ced 100644 --- a/src/features/join/model/userInformation.ts +++ b/src/features/join/model/userInformation.ts @@ -9,4 +9,11 @@ export interface UserInfoRequest { name: string; phoneNumber: string; email: string; +} + +export interface TermsAgreementRequest { + serviceAgreed: boolean; + privacyPolicyAgree: boolean; + personalInfoUsageAgreed: boolean; + marketingAgreed: boolean; } \ No newline at end of file diff --git a/src/features/join/ui/AgreementList.tsx b/src/features/join/ui/AgreementList.tsx index 059128cd..be517c85 100644 --- a/src/features/join/ui/AgreementList.tsx +++ b/src/features/join/ui/AgreementList.tsx @@ -3,6 +3,7 @@ import { useAgreementStore } from '../model/agreementStore'; const AgreementList = () => { const { agreements, toggleAgreement } = useAgreementStore(); + const NOTION_TERMS_LINK = 'https://namu00.notion.site/1a5eaffb9b0e8196b408f986b13aa15d?source=copy_link'; return (
@@ -11,24 +12,28 @@ const AgreementList = () => { required={true} checked={agreements.terms} onChange={() => toggleAgreement('terms')} + link={NOTION_TERMS_LINK} /> toggleAgreement('privacy')} + link={NOTION_TERMS_LINK} /> toggleAgreement('dataUsage')} + link={NOTION_TERMS_LINK} /> toggleAgreement('marketing')} + link={NOTION_TERMS_LINK} />
); diff --git a/src/features/ticket/api/order.ts b/src/features/ticket/api/order.ts index 5b43bfc4..339331ef 100644 --- a/src/features/ticket/api/order.ts +++ b/src/features/ticket/api/order.ts @@ -23,7 +23,9 @@ export const orderTickets = async (data: OrderTicketRequest) => { }; // 티켓 취소 -export const cancelTickets = async (orderId: number) => { - const response = await axiosClient.post(`/orders/${orderId}/cancel`); +export const cancelTickets = async (orderIds: number[]) => { + const response = await axiosClient.post('/orders/cancel', { + orderIds, + }); return response.data; }; diff --git a/src/features/ticket/hooks/useOrderHook.ts b/src/features/ticket/hooks/useOrderHook.ts index 96d367dc..26878710 100644 --- a/src/features/ticket/hooks/useOrderHook.ts +++ b/src/features/ticket/hooks/useOrderHook.ts @@ -22,7 +22,7 @@ export const useTicketOrderDetail = (orderId: number) => { // 주문 취소 export const useCancelTicket = () => { return useMutation({ - mutationFn: (orderId: number) => cancelTickets(orderId), + mutationFn: (orderIds: number[]) => cancelTickets(orderIds), onSuccess: () => { alert('티켓이 성공적으로 취소되었습니다.'); }, diff --git a/src/pages/dashboard/ui/ResponseManagementPage.tsx b/src/pages/dashboard/ui/ResponseManagementPage.tsx index b282b709..3e632960 100644 --- a/src/pages/dashboard/ui/ResponseManagementPage.tsx +++ b/src/pages/dashboard/ui/ResponseManagementPage.tsx @@ -9,18 +9,29 @@ import { usePurchaserAnswers } from '../../../features/ticket/hooks/useTicketOpt const ResponseManagementPage = () => { const [listType, setListType] = useState<'summary' | 'individual'>('summary'); const { isModalOpen, closeModal, selectedTicketId } = useResponseStore(); - const { data } = usePurchaserAnswers(selectedTicketId); + const { data, isLoading, isError } = usePurchaserAnswers(selectedTicketId); + const orderCount = data?.result?.orderCount ?? 0; return ( {isModalOpen && ( )}
-

응답 {data?.result.orderCount}개

-
- -
- +

+ {isLoading ? '응답 불러오는 중...' : isError ? '응답 0개' : `응답 ${orderCount}개`} +

+ {isError ? ( +
+ 응답이 존재하지 않습니다 +
+ ) : ( + <> +
+ +
+ + + )}
); diff --git a/src/pages/dashboard/ui/mail/EmailEditPage.tsx b/src/pages/dashboard/ui/mail/EmailEditPage.tsx index 24b15fb4..a6ba02f8 100644 --- a/src/pages/dashboard/ui/mail/EmailEditPage.tsx +++ b/src/pages/dashboard/ui/mail/EmailEditPage.tsx @@ -46,6 +46,7 @@ const EmailEditPage = () => { /> {/*시간 선택 컴포넌트*/} { setReservationDate(isoString); }} diff --git a/src/pages/dashboard/ui/ticket/TIcketConfirmPage.tsx b/src/pages/dashboard/ui/ticket/TIcketConfirmPage.tsx index 27335567..aee1ce04 100644 --- a/src/pages/dashboard/ui/ticket/TIcketConfirmPage.tsx +++ b/src/pages/dashboard/ui/ticket/TIcketConfirmPage.tsx @@ -23,9 +23,11 @@ const TicketConfirmPage = () => { navigate(-1); }; const cancleOrderTicket = async (orderIds: number[]) => { - for (const orderId of orderIds) { - cancelTicket(orderId); - } + cancelTicket(orderIds, { + onSuccess: () => { + navigate('/menu/myticket'); + }, + }); }; return ( <> @@ -72,11 +74,7 @@ const TicketConfirmPage = () => { approveButtonText="티켓 취소" rejectButtonText="뒤로가기" onClose={() => setIsModalOpen(false)} - onClick={() => { - cancleOrderTicket(orderIds).then(() => { - navigate('/menu/myticket'); - }); - }} + onClick={() => cancleOrderTicket(orderIds)} /> )} diff --git a/src/pages/event/ui/EventDetailsPage.tsx b/src/pages/event/ui/EventDetailsPage.tsx index cafe5920..16e500cd 100644 --- a/src/pages/event/ui/EventDetailsPage.tsx +++ b/src/pages/event/ui/EventDetailsPage.tsx @@ -138,12 +138,17 @@ const EventDetailsPage = () => {

관련 링크

-
+
{event.result.referenceLinks.map((link: { title: string; url: string }, index: number) => ( -
+
링크 이모지 {link.title} - + {link.url}
diff --git a/src/pages/event/ui/create-event/EventInfoPage.tsx b/src/pages/event/ui/create-event/EventInfoPage.tsx index 665d2164..15de8d62 100644 --- a/src/pages/event/ui/create-event/EventInfoPage.tsx +++ b/src/pages/event/ui/create-event/EventInfoPage.tsx @@ -2,6 +2,7 @@ import FileUpload from '../../../../features/event/ui/FileUpload'; import TextEditor from '../../../../features/event/ui/TextEditor'; import LinkInput from '../../../../features/event/ui/LinkInput'; import { useFunnelState } from '../../../../features/event/model/FunnelContext'; +import { useEffect, useState } from 'react'; interface EventInfoPageProps { onValidationChange?: (isValid: boolean) => void; @@ -9,10 +10,25 @@ interface EventInfoPageProps { const EventInfoPage = ({ onValidationChange }: EventInfoPageProps) => { const { setEventState } = useFunnelState(); + const [isFileValid, setIsFileValid] = useState(false); + const [isTextValid, setIsTextValid] = useState(false); + + const handleFileValidation = (valid: boolean) => { + setIsFileValid(valid); + }; + + const handleTextValidation = (valid: boolean) => { + setIsTextValid(valid); + }; + useEffect(() => { + const allValid = isFileValid && isTextValid; + onValidationChange?.(allValid); + }, [isFileValid, isTextValid, onValidationChange]); + return (
- - + +
); diff --git a/src/pages/join/AgreementPage.tsx b/src/pages/join/AgreementPage.tsx index ff6a31ad..89cf3663 100644 --- a/src/pages/join/AgreementPage.tsx +++ b/src/pages/join/AgreementPage.tsx @@ -4,10 +4,29 @@ import Button from '../../../design-system/ui/Button'; import AgreementList from '../../features/join/ui/AgreementList'; import { useAgreementStore } from '../../features/join/model/agreementStore'; import { useNavigate } from 'react-router-dom'; +import { useAgreeTerms } from '../../features/join/hooks/useUserHook'; +import { TermsAgreementRequest } from '../../features/join/model/userInformation'; const AgreementPage: React.FC = () => { - const { isAllRequiredAgreed } = useAgreementStore(); + const { isAllRequiredAgreed, getAgreementStates } = useAgreementStore(); const navigate = useNavigate(); + const { mutate:agreeTerms } = useAgreeTerms(); + + const handleAgree = () => { + const states = getAgreementStates(); + const agreementData: TermsAgreementRequest = { + serviceAgreed: states.serviceAgreed, + privacyPolicyAgree: states.privacyPolicyAgree, + personalInfoUsageAgreed: states.personalInfoUsageAgreed, + marketingAgreed: states.marketingAgreed, + }; + + agreeTerms(agreementData, { + onSuccess: () => { + navigate('/join/info-input'); + }, + }); + }; return (
@@ -15,7 +34,7 @@ const AgreementPage: React.FC = () => { centerContent="이용약관" leftButtonLabel="<" leftButtonClassName="text-2xl z-30 font-semibold" - leftButtonClick={() => navigate(-1)} + leftButtonClick={() => navigate('/')} color="black" />
@@ -32,7 +51,7 @@ const AgreementPage: React.FC = () => {
diff --git a/src/pages/join/InfoInputPage.tsx b/src/pages/join/InfoInputPage.tsx index 1418fa0f..9c591d6e 100644 --- a/src/pages/join/InfoInputPage.tsx +++ b/src/pages/join/InfoInputPage.tsx @@ -95,6 +95,7 @@ const InfoInputPage = () => { type="email" errorMessage={errors.email?.message} className="text-xl" + readOnly {...register('email')} /> diff --git a/src/pages/menu/ui/MyTicketPage.tsx b/src/pages/menu/ui/MyTicketPage.tsx index 3397f7a8..2e267b41 100644 --- a/src/pages/menu/ui/MyTicketPage.tsx +++ b/src/pages/menu/ui/MyTicketPage.tsx @@ -67,7 +67,7 @@ const MyTicketPage = () => { return ( - {!isModalOpen && tickets.length > 0 && ( + {tickets.length > 0 && (
{ location={ticket.event.address} hashtags={ticket.event.hashtags} onClick={() => handleEventCardClick(ticket)} - className={`transition-transform duration-200 ${ - isCancelMode && selectedIds.includes(ticket.id) ? 'scale-95 border-2 border-pink-400' : '' - }`} + className={`transition-transform duration-200 ${isCancelMode && selectedIds.includes(ticket.id) ? 'scale-95 border-2 border-pink-400' : '' + }`} >
티켓 @@ -123,13 +122,15 @@ const MyTicketPage = () => { )) ) : ( -

구매하신 티켓 정보가 없습니다.

+
+

구매하신 티켓 정보가 없습니다.

+
+ )}
{isModalOpen && selectedTicket && ( -
-
+
} @@ -147,7 +148,6 @@ const MyTicketPage = () => { onClick={() => setIsModalOpen(false)} />
-
)} {isDeleteModalOpen && ( @@ -157,11 +157,16 @@ const MyTicketPage = () => { rejectButtonText="뒤로가기" onClose={() => setIsDeleteModalOpen(false)} onClick={() => { - Promise.all(selectedIds.map(id => cancelTicket(id))).then(() => { - setTickets(prev => prev.filter(ticket => !selectedIds.includes(ticket.id))); - setIsDeleteModalOpen(false); - setIsCancelMode(false); - setSelectedIds([]); + cancelTicket(selectedIds, { + onSuccess: () => { + setTickets(prev => prev.filter(ticket => !selectedIds.includes(ticket.id))); + setIsDeleteModalOpen(false); + setIsCancelMode(false); + setSelectedIds([]); + }, + onError: () => { + alert('티켓 취소에 실패했습니다.'); + }, }); }} /> diff --git a/src/pages/menu/ui/myHost/HostDetailPage.tsx b/src/pages/menu/ui/myHost/HostDetailPage.tsx index 52f6d3aa..aafd2937 100644 --- a/src/pages/menu/ui/myHost/HostDetailPage.tsx +++ b/src/pages/menu/ui/myHost/HostDetailPage.tsx @@ -10,6 +10,7 @@ const HostDetailPage = () => { const hostChannelId = Number(id); const { data } = useHostDetail(hostChannelId); + const events = data?.result.events ?? []; return ( { } >
- {data?.result.events?.map(event => ( - - ))} + {events.length === 0 ? ( +

+ 등록된 이벤트가 없습니다. +

+ ) : ( + events.map(event => ( + + )) + )}
); diff --git a/src/shared/hooks/useImageUpload.ts b/src/shared/hooks/useImageUpload.ts index 7b4a9fad..6e4d0a42 100644 --- a/src/shared/hooks/useImageUpload.ts +++ b/src/shared/hooks/useImageUpload.ts @@ -1,5 +1,6 @@ import { useRef, useState, useCallback, useEffect } from 'react'; import { uploadFile } from '../../features/event/hooks/usePresignedUrlHook'; +import basicProfile from '../../../public/assets/event-manage/creation/BasicProfile.png'; const useImageUpload = ({ value, @@ -10,7 +11,6 @@ const useImageUpload = ({ onSuccess?: (url: string) => void; useDefaultImage?: boolean; }) => { - const DEFAULT_BASIC_PROFILE = 'https://gotogetherbucket.s3.ap-northeast-2.amazonaws.com/default.png'; const [previewUrl, setPreviewUrl] = useState(null); const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); @@ -18,9 +18,9 @@ const useImageUpload = ({ useEffect(() => { if (value) { setPreviewUrl(value); - } else if (useDefaultImage && previewUrl !== DEFAULT_BASIC_PROFILE) { - setPreviewUrl(DEFAULT_BASIC_PROFILE); - onSuccess?.(DEFAULT_BASIC_PROFILE); + } else if (useDefaultImage && previewUrl !== basicProfile) { + setPreviewUrl(basicProfile); + onSuccess?.(basicProfile); } }, [value, onSuccess, useDefaultImage, previewUrl]); diff --git a/src/shared/lib/date.ts b/src/shared/lib/date.ts index 2815eca3..b54f1bd2 100644 --- a/src/shared/lib/date.ts +++ b/src/shared/lib/date.ts @@ -21,3 +21,18 @@ export const formatISO = (date: Date, time: string): string => { const kstDate = new Date(newDate.getTime() + 9 * 60 * 60 * 1000); // UTC+9 return kstDate.toISOString(); }; +export const formatUtcToKst = (utcString: string): string => { + const utcDate = new Date(utcString); + + const kstTimestamp = utcDate.getTime() + 9 * 60 * 60 * 1000; + const kstDate = new Date(kstTimestamp); + + const year = kstDate.getFullYear(); + const month = (kstDate.getMonth() + 1).toString().padStart(2, '0'); + const day = kstDate.getDate().toString().padStart(2, '0'); + const hours = kstDate.getHours().toString().padStart(2, '0'); + const minutes = kstDate.getMinutes().toString().padStart(2, '0'); + + return `${year}년 ${month}월 ${day}일 ${hours}:${minutes}`; +}; + diff --git a/src/shared/ui/AgreementCard.tsx b/src/shared/ui/AgreementCard.tsx index e9638f25..fb8be0b7 100644 --- a/src/shared/ui/AgreementCard.tsx +++ b/src/shared/ui/AgreementCard.tsx @@ -6,9 +6,10 @@ interface AgreementCardProps { required: boolean; checked: boolean; onChange: () => void; + link?: string; } -const AgreementCard: React.FC = ({ title, required, checked, onChange }) => ( +const AgreementCard: React.FC = ({ title, required, checked, onChange, link }) => (
@@ -17,7 +18,16 @@ const AgreementCard: React.FC = ({ title, required, checked, {title}
- + {link && ( + + > + + )}
); diff --git a/src/shared/ui/backgrounds/EventRegisterLayout.tsx b/src/shared/ui/backgrounds/EventRegisterLayout.tsx index 03dbbf7b..0bc7a885 100644 --- a/src/shared/ui/backgrounds/EventRegisterLayout.tsx +++ b/src/shared/ui/backgrounds/EventRegisterLayout.tsx @@ -1,6 +1,9 @@ import { ReactNode, useState, Children, isValidElement, cloneElement } from 'react'; import Button from '../../../../design-system/ui/Button'; import Header from '../../../../design-system/ui/Header'; +import IconButton from '../../../../design-system/ui/buttons/IconButton'; +import { useNavigate } from 'react-router-dom'; +import HomeButton from '../../../../public/assets/menu/HomeButton.svg'; interface EventRegisterLayoutProps { children: ReactNode; @@ -35,6 +38,7 @@ const EventRegisterLayout = ({ } return child; }); + const navigate = useNavigate(); return (
@@ -46,6 +50,13 @@ const EventRegisterLayout = ({ leftButtonClick={onPrev} color="white" leftButtonClassName="text-xl z-30" + rightContent={ + } + onClick={() => navigate('/')} + iconClassName="cursor-pointer z-30 ml-auto" + /> + } />
diff --git a/src/widgets/dashboard/ui/email/SentMailCard.tsx b/src/widgets/dashboard/ui/email/SentMailCard.tsx index 2628482c..0872c19c 100644 --- a/src/widgets/dashboard/ui/email/SentMailCard.tsx +++ b/src/widgets/dashboard/ui/email/SentMailCard.tsx @@ -1,7 +1,7 @@ import arrow from '../../../../../public/assets/dashboard/mail/Arrow.svg'; import { useState } from 'react'; import IconButton from '../../../../../design-system/ui/buttons/IconButton'; -import { formatDate, formatTime } from '../../../../shared/lib/date'; +import { formatUtcToKst } from '../../../../shared/lib/date'; import TertiaryButton from '../../../../../design-system/ui/buttons/TertiaryButton'; import { useNavigate, useParams } from 'react-router-dom'; import { ReadEmailResponse } from '../../../../features/dashboard/model/email'; @@ -38,7 +38,7 @@ const SentMailCard = ({ mail, isPending = false, onClickDelete }: SentMailCardPr

{mail.title}

- {formatDate(mail.reservationDate)} {formatTime(mail.reservationDate)} + {formatUtcToKst(mail.reservationDate)}