From 16e78ef982815b10e9e9b62a8319b952e61794b3 Mon Sep 17 00:00:00 2001 From: Lee jin <83453646+j-nary@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:48:35 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Chip,=20dialog=20=EB=B0=B0=ED=8F=AC=20(#938?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chip 파트 한 개 이슈 대응 (#932) * test: 임시로 Chip 구현 (MDS 문의 필요) * feat: chip 구현 및 반응형 작업 * feat: 전체파트 클릭 로직 + MDS Chip 연결 * docs: 불필요한 파일 삭제 * docs: 불필요한 코드 삭제 * fix: 개별 옵션 해제 시 전체파트 해제 로직 추가 * refactor: 코드리뷰 반영 * refactor: input 높이 반응형 제거 * feat: 모임 안내 모집 대상 chip 변경 * style: 색상변경 * style: QA 반영 * fix: 이슈대응 * fix: Chip 한개 선택 이슈 대응 * fix: Chip 5개 선택 시 이슈 해결 (#934) * feat: 모임 삭제 dialog mds 마이그레이션 (#937) --- package.json | 2 +- pages/edit/index.tsx | 5 +- .../Presentation/JoinablePartsField/index.tsx | 59 +++++++++++++++++++ src/components/form/Presentation/index.tsx | 56 +++++++++--------- src/components/form/TableOfContents/index.tsx | 4 +- .../Information/InformationPanel.tsx | 10 +++- .../Modal/Confirm/HostConfirmModal.tsx | 22 ------- .../meetingDetail/MeetingController/index.tsx | 45 +++++++------- src/constants/option.ts | 4 +- src/data/options.ts | 3 +- src/types/form.ts | 2 +- yarn.lock | 10 ++-- 12 files changed, 133 insertions(+), 89 deletions(-) create mode 100644 src/components/form/Presentation/JoinablePartsField/index.tsx delete mode 100644 src/components/page/meetingDetail/MeetingController/Modal/Confirm/HostConfirmModal.tsx diff --git a/package.json b/package.json index fe44b089..dd03c085 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@sopt-makers/fonts": "^2.0.1", "@sopt-makers/icons": "^1.0.5", "@sopt-makers/playground-common": "^1.5.2", - "@sopt-makers/ui": "^2.4.2", + "@sopt-makers/ui": "^2.4.4", "@stitches/react": "^1.2.8", "@tanstack/react-query": "^4.10.3", "@types/autosize": "^4.0.3", diff --git a/pages/edit/index.tsx b/pages/edit/index.tsx index b1c4eb50..37f11882 100644 --- a/pages/edit/index.tsx +++ b/pages/edit/index.tsx @@ -73,10 +73,10 @@ const EditPage = () => { const joinableParts = // NOTE: null(디폴트), all(전체) 옵션을 제외한 나머지 옵션 개수와 서버에서 내려온 개수가 같으면 '전체' 옵션이 선택된 것 처럼 여겨져야 한다. // NOTE: 그게 아니라면, 서버에서 저장된 옵션에 더해 null(디폴트) 옵션을 추가해준다. - parts.length - 2 === formData?.joinableParts.length + parts.length - 1 === formData?.joinableParts.length ? parts : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - [parts[0], ...formData!.joinableParts.map(partString => parts.find(part => part.value === partString))]; + [...formData!.joinableParts.map(partString => parts.find(part => part.value === partString))]; formMethods.reset({ ...formData, @@ -94,7 +94,6 @@ const EditPage = () => { isMentorNeeded: formData?.isMentorNeeded, joinableParts, canJoinOnlyActiveGeneration: formData?.canJoinOnlyActiveGeneration, - //targetDesc: formData?.targetDesc, note: formData?.note ?? '', }, }); diff --git a/src/components/form/Presentation/JoinablePartsField/index.tsx b/src/components/form/Presentation/JoinablePartsField/index.tsx new file mode 100644 index 00000000..24cedf52 --- /dev/null +++ b/src/components/form/Presentation/JoinablePartsField/index.tsx @@ -0,0 +1,59 @@ +import { Option } from '@components/form/Select/OptionItem'; +import { parts } from '@data/options'; +import { Chip } from '@sopt-makers/ui'; + +interface JoinablePartsFieldProps { + value: Option[]; + onChange: (newSelectedParts: Option[]) => void; +} + +const JoinablePartsField = ({ value, onChange }: JoinablePartsFieldProps) => { + const handleClick = (selectedOption: Option) => { + const isValidValue = Array.isArray(value); + let updatedParts = isValidValue ? [...value] : []; + + // 'all' 옵션을 클릭했을 때 처리 + if (selectedOption.value === 'all') { + // 전체 옵션이 이미 선택되어 있으면 해제, 아니면 전체 선택 + updatedParts = isValidValue && value.some(part => part.value === 'all') ? [] : parts; + } else { + // 개별 옵션을 선택할 때 + if (isValidValue && value.some(part => part.value === selectedOption.value)) { + // 이미 선택된 항목이면 해제 + updatedParts = updatedParts.filter(part => part.value !== selectedOption.value); + } else { + // 선택되지 않은 항목이면 추가 + updatedParts.push(selectedOption); + } + + // 개별 옵션 해제 시 전체 옵션도 해제 + if (updatedParts.some(part => part.value === 'all') && updatedParts.length < parts.length) { + updatedParts = updatedParts.filter(part => part.value !== 'all'); + } + + // 모든 개별 파트가 선택되었으면 'all' 옵션도 활성화 + if (updatedParts.length === parts.length - 1) { + updatedParts.push(parts[0]); // 'all'을 활성화 + } + } + + onChange(updatedParts); + }; + + return ( + <> + {parts.map(part => ( + selected.value === part.value)} + onClick={() => handleClick(part)} + key={part.value} + style={{ width: '80px' }} + > + {part.label} + + ))} + > + ); +}; + +export default JoinablePartsField; diff --git a/src/components/form/Presentation/index.tsx b/src/components/form/Presentation/index.tsx index 360582cc..f90eab35 100644 --- a/src/components/form/Presentation/index.tsx +++ b/src/components/form/Presentation/index.tsx @@ -13,7 +13,6 @@ import TextInput from '../TextInput'; import ImagePreview from './ImagePreview'; import { MAX_FILE_SIZE } from '@type/form'; import NeedMentor from '../CheckBox/NeedMentor'; -import { parts } from '@data/options'; import { useRouter } from 'next/router'; import { getPresignedUrl, uploadImage } from '@api/API_LEGACY/meeting'; import { imageS3Bucket } from '@constants/url'; @@ -26,6 +25,7 @@ import { IconAlertCircle } from '@sopt-makers/icons'; import { useDialog } from '@sopt-makers/ui'; import sopt_schedule_tooltip from 'public/assets/images/sopt_schedule_tooltip.png'; import BubblePointIcon from 'public/assets/svg/bubble_point.svg'; +import JoinablePartsField from '@components/form/Presentation/JoinablePartsField'; interface PresentationProps { submitButtonLabel: React.ReactNode; @@ -431,23 +431,24 @@ function Presentation({ }; return ( - ( - - )} - > - + + ( + + )} + > + {/* 모집 인원 */} - - + + ( 명} required {...field} @@ -457,7 +458,7 @@ function Presentation({ /> )} > - + )} > - + ); }} @@ -599,19 +600,18 @@ const SNeedMentorFieldWrapper = styled('div', { }); const STargetFieldWrapper = styled('div', { display: 'flex', - alignItems: 'center', - gap: '10px', + flexDirection: 'column', + gap: '$16', marginBottom: '16px', - height: '52px', - '@tablet': { - height: '48px', - }, +}); - '@media(max-width: 525px)': { - flexDirection: 'column', - alignItems: 'flex-start', +const STargetChipContainer = styled('div', { + display: 'flex', + gap: '$10', + flexWrap: 'wrap', - marginBottom: '52px', + '@media(max-width: 430px)': { + maxWidth: '320px', }, }); @@ -682,15 +682,17 @@ const SSectionCountBox = styled('div', { }); const SMemberCountWrapper = styled('div', { - width: '94px', - height: '52px', + display: 'flex', + alignItems: 'center', + gap: '16px', + width: '227px', + height: '48px', }); const SFormCheckBox = styled('div', { ...fontsObject.BODY_3_14_R, display: 'flex', alignItems: 'center', - marginLeft: '$16', color: '$gray300', variants: { active: { diff --git a/src/components/form/TableOfContents/index.tsx b/src/components/form/TableOfContents/index.tsx index 9113c510..69d8ba50 100644 --- a/src/components/form/TableOfContents/index.tsx +++ b/src/components/form/TableOfContents/index.tsx @@ -25,10 +25,10 @@ function TableOfContents({ label }: TableOfContentsProps) { const isTargetValid = form.detail && form.detail.joinableParts && - form.detail.joinableParts.length > 1 && + form.detail.joinableParts.length > 0 && form.capacity && !errors.capacity && - !errors.detail; // default 옵션이 선택되어 있기 때문 최소 2개 이상 선택되어야 통과 + !errors.detail; const isActivationDateValid = form.detail && form.detail.mStartDate && form.detail.mEndDate; const isProcessDesc = form.detail?.processDesc; return ( diff --git a/src/components/page/meetingDetail/Information/InformationPanel.tsx b/src/components/page/meetingDetail/Information/InformationPanel.tsx index 82565eab..cc2f5aec 100644 --- a/src/components/page/meetingDetail/Information/InformationPanel.tsx +++ b/src/components/page/meetingDetail/Information/InformationPanel.tsx @@ -8,6 +8,7 @@ dayjs.locale('ko'); import { PART_NAME } from '@constants/option'; import { useCallback, useRef, useState } from 'react'; import { GetMeetingResponse } from '@api/API_LEGACY/meeting'; +import { Chip } from '@sopt-makers/ui'; interface InformationPanelProps { detailData: GetMeetingResponse; @@ -88,9 +89,12 @@ const InformationPanel = ({ detailData }: InformationPanelProps) => { {title} {title === '모집 대상' && ( - 대상 기수 : {generation} - - 대상 파트 : {partList?.join(', ')} + {partList?.map(part => ( + + {part} + + ))} + {generation} )} {handleContent(content)} diff --git a/src/components/page/meetingDetail/MeetingController/Modal/Confirm/HostConfirmModal.tsx b/src/components/page/meetingDetail/MeetingController/Modal/Confirm/HostConfirmModal.tsx deleted file mode 100644 index f7cea061..00000000 --- a/src/components/page/meetingDetail/MeetingController/Modal/Confirm/HostConfirmModal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import ConfirmModal from '@components/modal/ConfirmModal'; - -interface HostConfirmModalProps { - isModalOpened: boolean; - handleModalClose: () => void; - handleConfirm: () => void; -} - -const HostConfirmModal = ({ isModalOpened, handleModalClose, handleConfirm }: HostConfirmModalProps) => { - return ( - - ); -}; - -export default HostConfirmModal; diff --git a/src/components/page/meetingDetail/MeetingController/index.tsx b/src/components/page/meetingDetail/MeetingController/index.tsx index 9a990550..c6e6e63f 100644 --- a/src/components/page/meetingDetail/MeetingController/index.tsx +++ b/src/components/page/meetingDetail/MeetingController/index.tsx @@ -8,7 +8,6 @@ import dayjs from 'dayjs'; import { playgroundLink } from '@sopt-makers/playground-common'; import useModal from '@hooks/useModal'; import DefaultModal from '@components/modal/DefaultModal'; -import HostConfirmModal from './Modal/Confirm/HostConfirmModal'; import ProfileConfirmModal from './Modal/Confirm/ProfileConfirmModal'; import GuestConfirmModal from './Modal/Confirm/GuestConfirmModal'; import ApplicationModalContent from './Modal/Content/ApplicationModalContent'; @@ -93,11 +92,6 @@ const MeetingController = ({ const meetingId = router.query.id; const isRecruiting = status === ERecruitmentStatus.RECRUITING; - const { - isModalOpened: isHostModalOpened, - handleModalOpen: handleHostModalOpen, - handleModalClose: handleHostModalClose, - } = useModal(); const { isModalOpened: isGuestModalOpened, handleModalOpen: handleGuestModalOpen, @@ -122,6 +116,30 @@ const MeetingController = ({ setModalTitle(`모집 현황 (${approvedApplyCount}/${capacity}명)`); }; + const handleHostModalOpen = () => { + const dialogOption: DialogOptionType = { + title: '모임을 삭제하시겠습니까?', + description: '삭제한 모임은 되돌릴 수 없어요', + type: 'default', + typeOptions: { + cancelButtonText: '취소', + approveButtonText: '삭제하기', + buttonFunction: handleDeleteMeeting, + }, + }; + dialogOpen(dialogOption); + }; + + const handleDeleteMeeting = () => { + queryClient.invalidateQueries({ queryKey: ['fetchMeetingList'] }); + mutateMeetingDeletion(Number(meetingId), { + onSuccess: () => { + dialogClose(); + router.push('/'); + }, + }); + }; + const handleApplicationModal = () => { if (!me?.hasActivities) { handleProfileModalOpen(); @@ -209,16 +227,6 @@ const MeetingController = ({ }); }; - const handleDeleteMeeting = () => { - queryClient.invalidateQueries({ queryKey: ['fetchMeetingList'] }); - mutateMeetingDeletion(Number(meetingId), { - onSuccess: () => { - router.push('/'); - }, - }); - handleHostModalClose(); - }; - return ( <> @@ -272,11 +280,6 @@ const MeetingController = ({ )} - = { PM: '기획', DESIGN: '디자인', IOS: 'iOS', - ANDROID: '안드로이드', + ANDROID: 'Android', SERVER: '서버', WEB: '웹', }; @@ -22,7 +22,7 @@ export const APPROVAL_STATUS_KOREAN_TO_ENGLISH: StringKeyObject = { }; export const APPLICATION_TYPE = ['신청', '초대']; export const CATEGORY_OPTIONS = ['스터디', '행사']; -export const PART_OPTIONS = ['기획', '디자인', '안드로이드', 'iOS', '웹', '서버']; +export const PART_OPTIONS = ['기획', '디자인', 'Android', 'iOS', '웹', '서버']; export const PART_VALUES = ['PM', 'DESIGN', 'ANDROID', 'IOS', 'WEB', 'SERVER']; export const ACTION_STATUS = ['모집 전', '모집 중', '모집 마감', '활동 중', '활동 종료']; diff --git a/src/data/options.ts b/src/data/options.ts index 552110b3..791a381f 100644 --- a/src/data/options.ts +++ b/src/data/options.ts @@ -18,12 +18,11 @@ export const generationOptions = [ ]; export const parts = [ - { label: '대상 파트', value: null }, { label: '전체', value: 'all', order: 1 }, { label: '기획', value: 'PM', order: 2 }, { label: '디자인', value: 'DESIGN', order: 3 }, { label: '웹', value: 'WEB', order: 4 }, - { label: '안드로이드', value: 'ANDROID', order: 5 }, + { label: 'Android', value: 'ANDROID', order: 5 }, { label: 'iOS', value: 'IOS', order: 6 }, { label: '서버', value: 'SERVER', order: 7 }, ]; diff --git a/src/types/form.ts b/src/types/form.ts index 9697e209..bc7f164d 100644 --- a/src/types/form.ts +++ b/src/types/form.ts @@ -75,7 +75,7 @@ export const schema = z.object({ value: z.string().nullable(), }) ) - .min(2, { message: '대상 파트를 선택해주세요.' }), // NOTE: default 옵션에 더해 최소 1개는 선택해야 한다(총 2개) + .min(1, { message: '대상 파트를 선택해주세요.' }), note: z.string().max(1000, { message: '1000자 까지 입력 가능합니다.' }).optional().nullable(), }), }); diff --git a/yarn.lock b/yarn.lock index 0f188f58..df10032c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6781,9 +6781,9 @@ __metadata: languageName: node linkType: hard -"@sopt-makers/ui@npm:^2.4.2": - version: 2.4.2 - resolution: "@sopt-makers/ui@npm:2.4.2" +"@sopt-makers/ui@npm:^2.4.4": + version: 2.4.4 + resolution: "@sopt-makers/ui@npm:2.4.4" dependencies: "@radix-ui/react-dialog": "npm:^1.0.5" "@radix-ui/react-switch": "npm:^1.0.3" @@ -6796,7 +6796,7 @@ __metadata: peerDependencies: react: ^18.2.0 react-dom: ^18.2.0 - checksum: 10c0/05921a1b83e3ee5828627724e42b9eb98c6f07ce65e9be436cd2cbf0e60990a19495dd249b6c4b75a02d888916d3e55ac50a16b2c28361d1a71fbb8d5b09910a + checksum: 10c0/bb5fa03a300a674b564459393cdef9e5a9bd07bd99521f710a2cbcabe870794bfb9f9348d060edf2304a118e75248d3b5ec3294898778028c56e25ce47fe3907 languageName: node linkType: hard @@ -20703,7 +20703,7 @@ __metadata: "@sopt-makers/fonts": "npm:^2.0.1" "@sopt-makers/icons": "npm:^1.0.5" "@sopt-makers/playground-common": "npm:^1.5.2" - "@sopt-makers/ui": "npm:^2.4.2" + "@sopt-makers/ui": "npm:^2.4.4" "@stitches/react": "npm:^1.2.8" "@storybook/addon-essentials": "npm:^8.1.11" "@storybook/addon-interactions": "npm:^8.1.11" From d02b113938e242188dd50068ff7155c570bd6126 Mon Sep 17 00:00:00 2001 From: Hyeonsu Kim <86764406+borimong@users.noreply.github.com> Date: Sun, 3 Nov 2024 17:38:14 +0900 Subject: [PATCH 2/3] Update FeedFormPresentation.tsx --- src/components/feed/Modal/FeedFormPresentation.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/feed/Modal/FeedFormPresentation.tsx b/src/components/feed/Modal/FeedFormPresentation.tsx index 187dcaa9..c563d665 100644 --- a/src/components/feed/Modal/FeedFormPresentation.tsx +++ b/src/components/feed/Modal/FeedFormPresentation.tsx @@ -59,13 +59,7 @@ function FeedFormPresentation({ const textAreaRef = useRef(null); const [remainingHeight, setRemainingHeight] = useState(100); const [selectedMeeting, setSelectedMeeting] = useState(undefined); - const [isMobileDevice, setIsMobileDevice] = useState(false); - const userAgent = navigator.userAgent; - - if (/android/i.test(userAgent) || /iPad|iPhone|iPod/.test(userAgent)) { - setIsMobileDevice(true); - } - + const handleWindowResize = () => { setTextareaHeightChangeFlag(flag => !flag); }; @@ -91,7 +85,7 @@ function FeedFormPresentation({ ); const availableHeight = window.innerHeight - allComponentHeights - BasicPadding; - setRemainingHeight(isMobileDevice ? availableHeight - 44 : availableHeight); + setRemainingHeight(availableHeight); } }, [textareaHeightChangeFlag]); From 4b6edeb5bf65f181f00675975f290059a10a3ff0 Mon Sep 17 00:00:00 2001 From: Hyeonsu Kim <86764406+borimong@users.noreply.github.com> Date: Mon, 4 Nov 2024 00:17:27 +0900 Subject: [PATCH 3/3] Update FeedFormPresentation.tsx --- src/components/feed/Modal/FeedFormPresentation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/feed/Modal/FeedFormPresentation.tsx b/src/components/feed/Modal/FeedFormPresentation.tsx index c563d665..ada46cc9 100644 --- a/src/components/feed/Modal/FeedFormPresentation.tsx +++ b/src/components/feed/Modal/FeedFormPresentation.tsx @@ -59,7 +59,6 @@ function FeedFormPresentation({ const textAreaRef = useRef(null); const [remainingHeight, setRemainingHeight] = useState(100); const [selectedMeeting, setSelectedMeeting] = useState(undefined); - const handleWindowResize = () => { setTextareaHeightChangeFlag(flag => !flag); };