Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: 커피챗 오픈/수정 페이지 변경사항 적용 #1702

Merged
merged 8 commits into from
Dec 17, 2024
2 changes: 1 addition & 1 deletion src/components/coffeechat/page/CoffeechatUploadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default function CoffeechatUploadPage({ uploadType, form, onSubmit }: Cof
aside={
<ProgressBox
uploadType={uploadType}
myInfoInprogress={!!(watch('memberInfo.career') && watch('memberInfo.introduction'))}
myInfoInprogress={!!watch('memberInfo.career')}
coffeechatInfoInprogress={
!!(
watch('coffeeChatInfo.sections') &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import styled from '@emotion/styled';
import { colors } from '@sopt-makers/colors';
import { fonts } from '@sopt-makers/fonts';
import { IconCheck, IconChevronDown } from '@sopt-makers/icons';
import { Button } from '@sopt-makers/ui';
import { useEffect, useState } from 'react';

import { zIndex } from '@/styles/zIndex';

interface Option {
label: string;
value: string;
}

interface BottomSheetSelectProps {
options: Option[];
value: string | string[] | null | undefined;
placeholder: string;
onChange: (value: string) => void;
}
const BottomSheetSelect = ({ options, value, placeholder, onChange }: BottomSheetSelectProps) => {
const [open, setOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(value);
const [temporaryValue, setTemporaryValue] = useState(value);

const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);

const handleOptionSelect = (value: string) => {
setTemporaryValue(value);
};

const handleConfirm = () => {
setSelectedValue(temporaryValue);
if (temporaryValue !== '') onChange(temporaryValue as string);

handleClose();
};

useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}

return () => {
document.body.style.overflow = '';
};
}, [open]);
Comment on lines +40 to +50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q
요기 돔을 직접 조작하는 코드같은데, 이렇게 사용한 이유가 있는지 궁금해요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a
요기는 BottomSheet가 열리면 뒤 배경이 스크롤되지 않도록 추가해줬습니다!


return (
<Container>
<InputField onClick={handleOpen}>
{selectedValue !== null ? <p>{selectedValue}</p> : <p style={{ color: '#808087' }}>{placeholder}</p>}
<IconChevronDown
style={{
width: 20,
height: 20,
transform: open ? 'rotate(-180deg)' : '',
transition: 'all 0.5s',
}}
/>
</InputField>

{open && (
<>
<Overlay onClick={handleClose} />
<BottomSheet>
<OptionList>
{options.map((option) => (
<OptionItem key={option.value} onClick={() => handleOptionSelect(option.value)}>
{option.label}
{temporaryValue === option.value && <CheckedIcon />}
</OptionItem>
))}
</OptionList>
<Button size='lg' style={{ width: '100%' }} onClick={handleConfirm}>
확인
</Button>
</BottomSheet>
</>
)}
</Container>
);
};
export default BottomSheetSelect;

const Container = styled.div`
position: relative;
width: 100%;
`;

const InputField = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 10px;
background-color: ${colors.gray800};
cursor: pointer;
padding: 11px 16px;
${fonts.BODY_16_M};
`;

const Overlay = styled.div`
position: fixed;
top: 0;
left: 0;
z-index: ${zIndex.헤더};
background-color: rgb(15 15 18 / 80%);
width: 100%;
height: 100%;
`;

const BottomSheet = styled.section`
position: fixed;
bottom: 0;
z-index: ${zIndex.헤더};
margin-bottom: 12px;
border-radius: 16px;
background-color: ${colors.gray800};
padding: 16px;
width: calc(100% - 40px);
`;

const OptionList = styled.ul`
margin: 0 0 16px;
padding: 0;
max-height: 300px;
overflow-y: auto;
list-style: none;
`;

const OptionItem = styled.li`
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 4px;
cursor: pointer;
padding: 10px;
height: 44px;
${fonts.BODY_14_M}
`;

const CheckedIcon = styled(IconCheck)`
width: 24px;
height: 24px;
color: ${colors.success};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function ChipField({ field, errorMessage, chipList, isSingleSelec
</Chip>
</Responsive>
<Responsive only='mobile'>
<Chip size='md' active={isActive(field, chip)}>
<Chip size='sm' active={isActive(field, chip)}>
{chip}
</Chip>
</Responsive>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SelectV2, TextArea } from '@sopt-makers/ui';
import { Controller, useFormContext } from 'react-hook-form';

import { COFFEECHAT_MOBILE_MEDIA_QUERY } from '@/components/coffeechat/mediaQuery';
import BottomSheetSelect from '@/components/coffeechat/upload/CoffeechatForm/BottomSheetSelect';
import ChipField from '@/components/coffeechat/upload/CoffeechatForm/ChipField';
import {
COFFECHAT_SECTION,
Expand Down Expand Up @@ -83,12 +84,7 @@ export default function CoffeechatInfoForm() {
value={field.value ?? ''}
maxLength={1000}
fixedHeight={189}
lineBreakPlaceholder={[
'• PM과 서비스기획자로 일하는 방법',
'• 포트폴리오 준비 및 작성 노하우',
'• 직무 전환 시 준비할 것들',
'• 당근, 토스, 넥슨, 하나은행, LG전자 면접 후기',
]}
lineBreakPlaceholder={['• PM과 서비스 기획자로 일하는 방법', '• 앱잼 전 미리 준비하면 좋은 것']}
isError={!!errors.coffeeChatInfo?.topic}
errorMessage={errors.coffeeChatInfo?.topic?.message}
onChange={(e) => field.onChange(e.target.value)}
Expand All @@ -100,12 +96,7 @@ export default function CoffeechatInfoForm() {
value={field.value ?? ''}
maxLength={1000}
fixedHeight={176}
lineBreakPlaceholder={[
'• PM과 서비스기획자로 일하는 방법',
'• 포트폴리오 준비 및 작성 노하우',
'• 직무 전환 시 준비할 것들',
'• 당근, 토스, 넥슨, 하나은행, LG전자 면접 후기',
]}
lineBreakPlaceholder={['• PM과 서비스 기획자로 일하는 방법', '• 앱잼 전 미리 준비하면 좋은 것']}
isError={!!errors.coffeeChatInfo?.topic}
errorMessage={errors.coffeeChatInfo?.topic?.message}
onChange={(e) => field.onChange(e.target.value)}
Expand All @@ -124,23 +115,33 @@ export default function CoffeechatInfoForm() {
name='coffeeChatInfo.meetingType'
control={control}
render={({ field }) => (
<div {...field}>
<SelectV2.Root
type='text'
visibleOptions={3}
defaultValue={MEETING_TYPE_OPTIONS.find((option) => option.value === field.value)}
onChange={(value) => field.onChange(value)}
>
<SelectV2.Trigger>
<SelectV2.TriggerContent placeholder={'진행 방식 선택'} />
</SelectV2.Trigger>
<SelectV2.Menu>
{MEETING_TYPE_OPTIONS.map((option) => (
<SelectV2.MenuItem key={option.value} option={option} />
))}
</SelectV2.Menu>
</SelectV2.Root>
</div>
<>
<Responsive only='desktop' {...field}>
<SelectV2.Root
type='text'
visibleOptions={3}
defaultValue={MEETING_TYPE_OPTIONS.find((option) => option.value === field.value)}
onChange={(value) => field.onChange(value)}
>
<SelectV2.Trigger>
<SelectV2.TriggerContent placeholder={'진행 방식 선택'} />
</SelectV2.Trigger>
<SelectV2.Menu>
{MEETING_TYPE_OPTIONS.map((option) => (
<SelectV2.MenuItem key={option.value} option={option} />
))}
</SelectV2.Menu>
</SelectV2.Root>
</Responsive>
<Responsive only='mobile' {...field}>
<BottomSheetSelect
options={[...MEETING_TYPE_OPTIONS]}
value={field.value}
placeholder='진행 방식 선택'
onChange={(value) => field.onChange(value)}
/>
</Responsive>
</>
)}
/>
</FormItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import styled from '@emotion/styled';
import { SelectV2 } from '@sopt-makers/ui';
import { Controller, useFormContext } from 'react-hook-form';

import { COFFEECHAT_MOBILE_MEDIA_QUERY } from '@/components/coffeechat/mediaQuery';
import ChipField from '@/components/coffeechat/upload/CoffeechatForm/ChipField';
import { CAREER_LEVEL } from '@/components/coffeechat/upload/CoffeechatForm/constants';
import BottomSheetSelect from '@/components/coffeechat/upload/CoffeechatForm/BottomSheetSelect';
import { CAREER_LEVEL_OPTIONS } from '@/components/coffeechat/upload/CoffeechatForm/constants';
import { CoffeechatFormContent } from '@/components/coffeechat/upload/CoffeechatForm/types';
import FormItem from '@/components/common/form/FormItem';
import FormTitle from '@/components/common/form/FormTitle';
import TextFieldLineBreak from '@/components/common/form/TextFieldLineBreak';
import Responsive from '@/components/common/Responsive';
import { MOBILE_MEDIA_QUERY } from '@/styles/mediaQuery';

export default function MyInfoForm() {
const {
Expand All @@ -25,17 +28,49 @@ export default function MyInfoForm() {
>
경력
</FormTitle>
<ChipField
field='memberInfo.career'
errorMessage={errors.memberInfo?.career?.message ?? ''}
chipList={CAREER_LEVEL}
isSingleSelect
/>

<FormItem errorMessage={errors.memberInfo?.career?.message ?? ''}>
<Controller
name='memberInfo.career'
control={control}
render={({ field }) => (
<>
<CareerOptionContainer>
<Responsive only='desktop' {...field}>
<SelectV2.Root
type='text'
className='option-container'
visibleOptions={6}
defaultValue={CAREER_LEVEL_OPTIONS.find((option) => option.value === field.value)}
onChange={(value) => field.onChange(value)}
>
<SelectV2.Trigger>
<SelectV2.TriggerContent placeholder={'경력 선택'} />
</SelectV2.Trigger>
<SelectV2.Menu>
{CAREER_LEVEL_OPTIONS.map((option) => (
<SelectV2.MenuItem key={option.value} option={option} />
))}
</SelectV2.Menu>
</SelectV2.Root>
</Responsive>

<Responsive only='mobile' {...field}>
<BottomSheetSelect
options={[...CAREER_LEVEL_OPTIONS]}
value={field.value}
placeholder='경력 선택'
onChange={(value) => field.onChange(value)}
/>
</Responsive>
</CareerOptionContainer>
</>
)}
/>
</FormItem>
</CareerWrapper>
<article>
<FormTitle essential breakPoint={COFFEECHAT_MOBILE_MEDIA_QUERY}>
자기소개
</FormTitle>
<FormTitle breakPoint={COFFEECHAT_MOBILE_MEDIA_QUERY}>자기소개</FormTitle>
<Controller
name='memberInfo.introduction'
control={control}
Expand Down Expand Up @@ -82,3 +117,36 @@ const CareerWrapper = styled.article`
flex-direction: column;
gap: 12px;
`;

const CareerOptionContainer = styled.div`
.option-container {
width: 312px;

button {
width: 312px;

div {
width: 312px;
}
}
}

@media ${MOBILE_MEDIA_QUERY} {
.option-container {
width: 100%;

ul {
margin-bottom: 24px;
max-height: 400px !important;
}

button {
width: 100%;

div {
width: 100%;
}
}
}
}
`;
15 changes: 8 additions & 7 deletions src/components/coffeechat/upload/CoffeechatForm/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
export const CAREER_LEVEL = [
'아직 없어요',
'인턴 경험만 있어요',
'주니어 (0-3년)',
'미들 (4-8년)',
'시니어 (9년 이상)',
'창업 중',
export const CAREER_LEVEL_OPTIONS = [
{ label: '아직 없어요', value: '아직 없어요' },
{ label: '인턴 경험만 있어요', value: '인턴 경험만 있어요' },
{ label: '주니어 (0-3년)', value: '주니어 (0-3년)' },
{ label: '미들 (4-8년)', value: '미들 (4-8년)' },
{ label: '시니어 (9년 이상)', value: '시니어 (9년 이상)' },
{ label: '창업 중', value: '창업 중' },
] as const;
export type CareerLevelOptions = typeof CAREER_LEVEL_OPTIONS;

export const COFFECHAT_SECTION = ['SOPT 활동', '기획', '디자인', '프론트', '백엔드', '앱 개발', '기타'] as const;

Expand Down
1 change: 0 additions & 1 deletion src/components/coffeechat/upload/CoffeechatForm/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const myInfoSchema = yup.object().shape({
(value) => typeof value === 'string' || (Array.isArray(value) && value.length > 0),
)
.required('경력을 선택해주세요'),
introduction: yup.string().required('자기소개를 입력해주세요'),
});

const coffeeChatInfoSchema = yup.object().shape({
Expand Down
Loading