Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
129ebca
refact: 주문 취소 엔드포인트 수정
hyeeuncho Jun 4, 2025
fabcaa6
refact: 예약 메일 시간 kst로 수정
hyeeuncho Jun 4, 2025
4ee85c1
refact: 메일 수정 시 시간 초기화 해결
hyeeuncho Jun 4, 2025
e693564
refact: d-day false 값 종료 처리
hyeeuncho Jun 4, 2025
a4a5e31
refact: 체크 UI 수정
hyeeuncho Jun 4, 2025
f5054d8
refact: 빈 QR 처리
hyeeuncho Jun 4, 2025
4a088a5
refact: 티켓 없을 때 문구 위치 조정
hyeeuncho Jun 4, 2025
28da70c
feat: 이용 약관 동의 api 연동
hyeeuncho Jun 4, 2025
48d1126
feat: 이용 약관 상세 연결
hyeeuncho Jun 4, 2025
7c196fd
refact: 이벤트 등록 페이지 홈 버튼 추가
hyeeuncho Jun 4, 2025
c341cb4
refact: 이벤트 없을 시 문구 추가
hyeeuncho Jun 4, 2025
838be65
fix: qr모달 열릴 때 이벤트 카드 위로 올라가는 문제 해결
hyeeuncho Jun 5, 2025
82f3e93
refact: 회원가입 시 이메일 수정 불가 처리
hyeeuncho Jun 5, 2025
9f0943b
refact: 관련 링크 여러줄 처리
hyeeuncho Jun 5, 2025
e2264c2
refact: 이벤트 설명 미입력 시 다음 버튼 비활성화
hyeeuncho Jun 5, 2025
04ccd21
refact: 이벤트 상세 설명 글자수 제한
hyeeuncho Jun 6, 2025
f89f5a6
refact: 기본 프로필 오류 수정 및 호스트 생성 후 재생성 시 이전 데이터 남아있는 문제 해결
hyeeuncho Jun 6, 2025
679b4db
refact: 타입 오류 수정
hyeeuncho Jun 6, 2025
08528d3
refact: 사용자 응답 없을 시 처리
hyeeuncho Jun 6, 2025
1eb5f56
refact: 이벤트 상세 설명 글자 수 이미지 길이 포함
hyeeuncho Jun 6, 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
25 changes: 18 additions & 7 deletions design-system/ui/modals/QrModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 상태
Expand Down Expand Up @@ -57,7 +58,17 @@ const QrModal = ({
<div>
{iconPath1 && <div className="w-full">{iconPath1}</div>}
<div className={`ml-[5%] -mt-[180%] ${isChecked ? '' : 'opacity-50'}`}>
<img src={`data:image/png;base64,${ticketQrCode}`} alt="QR Code" className="w-60 h-60" />
{ticketQrCode ? (
<img
src={`data:image/png;base64,${ticketQrCode}`}
alt="QR Code"
className="w-60 h-60"
/>
) : (
<div className="w-60 h-60 flex items-center justify-center bg-deDayBgLight rounded-md border border-deDayTextDark text-deDayTextDark text-sm text-center px-4">
주최자의 승인이 완료되면<br /> QR이 발급됩니다.
</div>
)}
</div>
<div className={`${flexColumn} justify-start px-6 ${isChecked ? '' : 'opacity-50'}`}>
<div className={`${flexRowSpaceBetweenCenter} w-full mt-[22%]`}>
Expand All @@ -68,33 +79,33 @@ const QrModal = ({
<div className="space-y-1 text-deDayTextDark">
<IconText
size="xSmall"
iconPath={<img src={qr_calendar} alt="qr_calendar" className='mr-1'/>}
iconPath={<img src={qr_calendar} alt="qr_calendar" className='mr-1' />}
children={formattedDate}
className="text-11"
></IconText>
<IconText
size="xSmall"
iconPath={<img src={qr_location} alt="qr_location" className='mr-1'/>}
iconPath={<img src={qr_location} alt="qr_location" className='mr-1' />}
children={location}
className="text-11"
></IconText>
<IconText
size="xSmall"
iconPath={<img src={qr_ticket} alt="qr_ticket" className='mr-1'/>}
iconPath={<img src={qr_ticket} alt="qr_ticket" className='mr-1' />}
children={ticketName}
className="text-11"
></IconText>
<span className="text-sm font-bold">{formattedPrice}원</span>
<hr />
<IconText
size="xSmall"
iconPath={<img src={qr_check} alt="qr_check" className='mr-1'/>}
iconPath={<img src={orderStatus === 'COMPLETED' ? qr_check : qr_pending} alt="qr_check" className='mr-1' />}
children={orderStatus === 'COMPLETED' ? '승인됨' : '대기 중'}
className="text-11"
></IconText>
<IconText
size="xSmall"
iconPath={<img src={qr_check} alt="qr_check" className='mr-1'/>}
iconPath={<img src={isCheckIn ? qr_check : qr_pending} alt="qr_check" className='mr-1' />}
children={isCheckIn ? '체크인 완료' : '체크인 미완료'}
className="text-11"
></IconText>
Expand Down
4 changes: 2 additions & 2 deletions design-system/ui/textFields/UnderlineTextField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChangeEvent, forwardRef } from 'react';
import { ChangeEvent, forwardRef, InputHTMLAttributes } from 'react';

interface UnderlineTextFieldProps {
interface UnderlineTextFieldProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
type?: string;
value?: string;
Expand Down
5 changes: 4 additions & 1 deletion design-system/ui/texts/Countdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ const Countdown = ({ children, isChecked }: CountdownProps) => {
border-[0.1px] font-medium ${flexCenter}
`;

return <button className={`${baseStyles} ${isChecked ? activeStyles : inactiveStyles}`}>{children}</button>;
const isEnded = children === 'false';
const displayText = isEnded ? '종료' : children;

return <button className={`${baseStyles} ${isChecked && !isEnded ? activeStyles : inactiveStyles}`}>{displayText}</button>
};

export default Countdown;
3 changes: 3 additions & 0 deletions src/features/dashboard/ui/ResponsesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ const ResponsesList = ({ listType, ticketOptionResponses, ticketId }: ResponsesL
if (isLoading) return <p>로딩 중...</p>;
if (error || !data?.result) return <p>데이터를 불러오지 못했습니다.</p>;
const allOrders = data.result.flatMap(user => user.orders);
if (allOrders.length === 0) {
return <p className="text-center text-gray-500">응답이 없습니다.</p>;
}
return (
<IndividualResponseViewer orders={allOrders} currentIndex={currentIndex} setCurrentIndex={setCurrentIndex} />
);
Expand Down
10 changes: 9 additions & 1 deletion src/features/event/ui/EventFunnel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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));
},
});
Expand Down
79 changes: 55 additions & 24 deletions src/features/event/ui/TextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,22 @@ 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 }: TextEditorProps) => {
const [content, setContent] = useState('');
const TextEditor = ({ value = '', onChange, setEventState, onValidationChange }: TextEditorProps) => {
const [content, setContent] = useState(value);
const quillRef = useRef<ReactQuill | null>(null);
const [isOverLimit, setIsOverLimit] = useState(false);

const imageHandler = async () => {
if (!quillRef.current) return;
Expand All @@ -58,6 +49,15 @@ const TextEditor = ({ value, onChange, setEventState }: TextEditorProps) => {
}
};
};
const getImageCount = (htmlContent: string): number => {
const matches = htmlContent.match(/<img [^>]*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(
() => ({
Expand All @@ -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 (
<div className="flex flex-col justify-start gap-2 mb-4">
<h1 className="font-bold text-black text-lg">이벤트에 대한 상세 설명</h1>
Expand All @@ -104,7 +123,19 @@ const TextEditor = ({ value, onChange, setEventState }: TextEditorProps) => {
onChange={handleChange}
className="custom-quill-editor"
/>
<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}개 포함)`}
</p>
{isOverLimit && (
<p className="text-sm text-red-500 font-medium">
{MAX_LENGTH}자를 초과할 수 없습니다.
</p>
)}
</div>
</div>
);
};

export default TextEditor;
29 changes: 25 additions & 4 deletions src/features/event/ui/TimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Date | null>(new Date());
const [selectedHour, setSelectedHour] = useState<string>('00');
const [selectedMinute, setSelectedMinute] = useState<string>('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<Date | null>(initialKstDate);
const [selectedHour, setSelectedHour] = useState<string>(
initialKstDate.getHours().toString().padStart(2, '0')
);
const [selectedMinute, setSelectedMinute] = useState<string>(
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(() => {
Expand Down
10 changes: 9 additions & 1 deletion src/features/join/api/user.ts
Original file line number Diff line number Diff line change
@@ -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<UserInfoResponse> => {
const response = await axiosClient.get<{ result: UserInfoResponse }>('/users', {
Expand All @@ -14,3 +14,11 @@ export const updateUser = async (data: UserInfoRequest): Promise<UserInfoRespons
});
return response.data;
};

// 이용 약관
export const agreeTerms = async (data: TermsAgreementRequest) => {
const response = await axiosClient.post('/terms', data, {
headers: { isPublicApi: true },
});
return response.data;
};
15 changes: 14 additions & 1 deletion src/features/join/hooks/useUserHook.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -15,3 +15,16 @@ export const useUserUpdate = () => {
mutationFn: updateUser,
});
};

export const useAgreeTerms = () => {
return useMutation({
mutationFn: agreeTerms,
onSuccess: () => {
alert('이용약관 동의 완료');
},
onError: (error) => {
alert('동의 처리 실패');
console.error('이용약관 동의 실패', error);
}
});
};
15 changes: 15 additions & 0 deletions src/features/join/model/agreementStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgreementState>((set, get) => ({
Expand Down Expand Up @@ -56,4 +62,13 @@ export const useAgreementStore = create<AgreementState>((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,
};
},
}));
7 changes: 7 additions & 0 deletions src/features/join/model/userInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,11 @@ export interface UserInfoRequest {
name: string;
phoneNumber: string;
email: string;
}

export interface TermsAgreementRequest {
serviceAgreed: boolean;
privacyPolicyAgree: boolean;
personalInfoUsageAgreed: boolean;
marketingAgreed: boolean;
}
Loading