ํน๋ณํ ๊ฑด ์๋ ๋จ์ ๊ตฌํ์ด์ง๋ง ๋ง์ด ๊ณ ๋ฏผํ๊ณ ๋
ธ๋ ฅํ๊ฒ ์๊น์์ ์ ๋ ํฌ์คํ
. ์ฑ์ ๊ฐ์ฅ ํต์ฌ์ธ ๋๊ธธ์ ์์ฑํ๋ ๊ณผ์ ์ '๋๊ธธ ๊ณ์ฝํ๊ธฐ'๋ผ๋ ์๋ฉ์ ํตํด ์์ด๋ค๋ ์ฌ๋ฏธ์๊ฒ ๋๋ ์ ์๋๋ก ํ๋ค. ์ด ๋ค์ฏ๊ฐ์ ๋จ๊ณ๋ฅผ ๋ฐ์ ์ ๋ณด๋ฅผ ์
๋ ฅํ๊ณ , ๋ง์ง๋ง์ ์ฌ์ธ์ ํ๊ณ ์ ์ถํ๋ฉด ๊ณ์ฝ ์์์ฆ์ด ๋ณด์ฌ์ง๋ ํ์์ด๋ค. ๊ธฐํ๊ณผ ๋์์ธํ์ ๋
ธ๊ณ ๊ฐ ๋๊ปด์ง๋ค. ๊ทธ๋ฆฌ๊ณ ์ ๊ฑธ ๊ตฌํํ ๋๋... ๊ธฐ๋ฅ ํ๋ํ๋์ ๋ง์ ๊ณต์ ๋ค์์ด์ ๊ทธ๋ฐ์ง ํนํ ์ ์ ์ด ์๋ ๋ทฐ๋ค์ด๋ค.
ํ์๊ณผ ๊ธฐ์ ๋ธ๋ก๊ทธ์ ๋ํ ์ด์ผ๊ธฐ์ ๋๋ ์ ์ด ์๋ค. ํ๋ก์ ํธ ๊ฒฝํ์ ๊ธฐ๋กํ ๋ ์ด๋ ต๊ฒ ๊ณต๋ถํ๋ฉฐ ์ฌ์ฉํ๋ ๊ธฐ์ ์ ์ ๋ฆฌํ ์ ๋ ์๊ณ , ์ด๋ ค์ด ํ๋ฉด์ ๊ตฌํํ๋ฉฐ ๋จธ๋ฆฌ ์ธ๋งธ๋ ๊ณ ๋ฏผ์ ๊ธฐ๋กํ ์ ๋ ์๋ค. ๋ ๋ธ๋ก๊ทธ ํฌ์คํ
์ ์๊ฐ์ ๊ฝค ๋ง์ด ์ฐ๋ ํธ์ด๋ผ, ์ด ๋ชจ๋ ๊ฒ๋ค์ ๊ธฐ๋ก์ผ๋ก ๋จ๊ธฐ๋๊ฒ ๊ณผ์ฐ ํจ์จ์ ์ธ ๊ณต๋ถ๋ฐฉ๋ฒ์ธ์ง์ ํ์ ์ด ์์๋ค. ํ์์๊ฒ ์ด๋ป๊ฒ ์๊ฐํ๋๊ณ ๋ฌผ์๋๋ - ์ด๋ ค์ด ๊ธฐ์ ์ ๋์ค์ ๋ค์ ์์ ๋ณผ ์ ์์ด์ ์ข์ ๋ฐ๋ฉด, ๋จ์ ๊ตฌํ์ ๋ํ ๊ณ ๋ฏผ์ ๊ทธ ๊ณผ์ ์์ ๋์ ํผ์ง์ปฌ์ด ์ฌ๋ผ๊ฐ์ผ๋ ๊ทธ๊ฑฐ๋ง์ผ๋ก๋ ์ด๋์ด๋ค - ๋ผ๊ณ ์๊ฐํ๋๋ค. ๊ทธ๋๋ ๊ทธ๋ฅ ๋์ด๊ฐ๊ธฐ๋ ๋ญ๊ฐ ์์ฝ์์. ์์ผ๋ก ๊ธฐ์ ๋ธ๋ก๊ทธ ์ด์ํ๋ ๋ฐฉ๋ฒ์ ๋ ๋ง์ด ๊ณ ๋ฏผํด๋ด์ผ๊ฒ ๋ค. ๊ทธ๋๋ ๋ฑ
ํค์ฆํ๋ฉด์ ๊ฐ๋ฐ ํผ์ง์ปฌ์ด ํ ์ข์์ง๊ฑด ํ๋ฆผ์๋ค.
1. ๋ฐํ ์ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ - ๋ฐํ ์ํธ ๋ฐ๊นฅ ๋ถ๋ถ ํฐ์น ์ฒ๋ฆฌ ์ปค์คํ ํ
๋ฐํ ์ํธ์ ๋ฐ๊นฅ๋ถ๋ถ์ ๋๋ ์๋๋ ์ํธ๊ฐ ๋ซํ๋ค. ๊ทผ๋ฐ ํด๋น ์ธํ๋ถ๋ถ์ ๋๋ ์๋ ๊ทธ๋๋ก ์ธํ์ ํฌ์ปค์ค๊ฐ ์ ์ง๋๋ฉด์ ์ํธ๋ ๊ณ์ ์ฌ๋ผ์ ์์ด์ผ ํ๋ค. ๊ทธ ๋ก์ง์ ์ปค์คํ ํ ์ผ๋ก ๋ฐ๋ก ๋นผ ์ปดํฌ๋ํธ์ ๋ถ๋ฆฌ๋ฅผ ํด์ฃผ์๋ค.
import { useEffect, useRef } from 'react';
function useBottomSheetOutSideRef(handler: () => void) {
const sheetDivRef = useRef<HTMLDivElement>(null);
const inputDivRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (e: MouseEvent): void => {
if (
sheetDivRef.current &&
inputDivRef.current &&
!inputDivRef.current.contains(e.target as Node) &&
!sheetDivRef.current.contains(e.target as Node)
) {
handler();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [inputDivRef]);
return [sheetDivRef, inputDivRef];
}
export default useBottomSheetOutSideRef;
๋ฐํ ์ํธ์ ์ธํ ์์์ ์ ๊ทผํ๊ธฐ ์ํ ref๋ฅผ ๋๊ฐ ๋๋ค. ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๊ณณ์ด ํด๋น ์กฐ๊ฑด์ ๋ง์กฑํ ๋์๋ง ์ ๋ฌ๋ ํธ๋ค๋ฌ ํจ์๋ฅผ ์คํ๋๋๋ก ํ๋ค. ๊ทธ๋ฆฌ๊ณ ํ ์์ ๊ทธ ref๋ฅผ ๋ฐํํ๋ค.
function Step3({ currentStep }: { currentStep: number }) {
const [open, onOpen, onDismiss] = useBottomSheet(false);
const [sheetDivRef, inputDivRef] = useBottomSheetOutSideRef(onDismiss);
return (
<Wrapper>
<InputSection validate={validateAmount}>
<div onClick={onOpen} ref={inputDivRef}>
<InputForm
sheetOpen={open}
/>
</div>
<p>{validateAmount.message}</p>
</InputSection>
<ContractSheet
open={open}
onDismiss={onDismiss}
sheetRef={sheetDivRef}
>
<div>{/* ๋ฐํ
์ํธ */}</div>
</ContractSheet>
</Wrapper>
);
}
export default Step3;
ํ์ด์ง ์ปดํฌ๋ํธ์์ ์ด๋ฐ์์ผ๋ก ์ฌ์ฉํ ์ ์๋ค. ๊ด๋ จ์ด ์๋ ๋ถ๋ถ๋ง ๋จ๊ธฐ๊ณ ์ง์์ ๊ฐ์ ธ์๋๋, ์์๋ณด๊ธฐ ์กฐ๊ธ ํ๋ค์๋. ์ค์ํ ๋ถ๋ถ์ ์ปค์คํ
ํ
์์ ๋ฐํ๋ฐ์ inputRef์ sheetDivRef๋ฅผ ๊ฐ๊ฐ ์ปดํฌ๋ํธ์ ๋๊ฒจ์ฃผ๋ ๊ฒ!! ๋ฐํ
์ํธ ๋ซ๋ ํจ์๋ฅผ ์ธ์๋ก ๋๊ฒจ์ ํธ๋ค๋ฌ ํจ์๋ก ์ฌ์ฉํ๋ค.
2. ๊ณ์ฐ๊ธฐ๋ฅผ ํ์ํํ ๊ธ์ก ์ ๋ ฅ ์ปค์คํ ํค๋ณด๋
๋ชฉํ ๊ธ์ก์ ์
๋ ฅํ ์ ์๋ ์ปค์คํ
ํค๋ณด๋์ด๋ค. ์์ด๋ค์ด ์ฌ๋ฏธ์๊ฒ ๋๊ธธ์ ๊ณ์ฝํ ์ ์๋๋ก ๊ณ์ฐ๊ธฐ์ ํ์ํ ํ ๋์์ธ์ด๋ค. ๊ธฐํ์์ ์๊ตฌํ ์ ์ด ๊ฝค ํน์ดํ๋ค. ๊ฐ ๊ธ์ก์ ํด๋นํ๋ ์งํ ๋ชจ์์ ๋ฒํผ์ ๋๋ฅด๋ฉด ๊ทธ๋งํผ์ ๋์ด ์ถ๊ฐ๋๋ค. ์ค๋ฅธ์ชฝ ์๋ ๋ ๋ฒํผ ์ค, ์ผ์ชฝ์ ๋๋ฅด๋ฉด ๊ฐ์ฅ ์ต๊ทผ์ ์ถ๊ฐํ ๊ธ์ก๋งํผ ์ง์์ง๋ค. ์๋ฅผ ๋ค์ด 500์, 5000์, ๋ง์ ์์๋๋ก ๋๋ ๋ค๊ฐ ์ทจ์ํ๋ฉด : 15500 → 5500 → 500 → x ์์ผ๋ก ๋์๊ฐ๊ฒ ๋๋ ๊ฒ.
์ด๋ฅผ ๊ตฌํํ๊ธฐ ์ํด ์คํ์ ์ฌ์ฉํ๋ค.
function useStackAmount() {
const [amountStack, setAmountStack] = useState<number[]>([]);
const pushAmount = (amount: number) => {
setAmountStack((prev) => [...prev, amount]);
};
const popAmount = () => {
setAmountStack((prev) => prev.filter((v, i) => i !== prev.length - 1));
};
const resetAmount = () => {
setAmountStack([]);
};
return [amountStack, pushAmount, popAmount, resetAmount] as const;
}
export default useStackAmount;
์๋ฐ์คํฌ๋ฆฝํธ์ ์คํ์ด ์๋?? ๋์ค์ ์ฐพ์๋ด์ผ์ง. ์ผ๋จ ๊ทธ๋ฅ ๋น์ทํ๊ฒ ๊ตฌํํ๋ค. ๋ฒํผ์ ํด๋ฆญํ ๋ ๋ง๋ค ์ซ์ ๋ฐฐ์ด์ ๋ค์ ๊ฐ์ ์ถ๊ฐํ๋ค. ์ญ์ (๋์๊ฐ๊ธฐ) ๋ฒํผ์ ๋๋ฅผ ๋ ๋ฐฐ์ด์ ๋ง์ง๋ง ๊ฐ์ ์ญ์ ํด์ค.
// stack์ ์๋ ์ซ์๋ค ๋ํด์ form state์ ์ ์ฅ
useEffect(() => {
const amount = amountStack.reduce((acc, cur) => {
return (acc += cur);
}, 0);
setForm({ ...form, contractAmount: amount });
}, [amountStack]);
๋ฐํํ๋๊ฑด ์ ํํ ์งํ๋ค์ ํฉ์ฐ์ด ์๋ ๊ทธ๋ฅ ๋ฐฐ์ด์ด๊ธฐ ๋๋ฌธ์, ์ ์ฒด๊ธ์ก์ผ๋ก ๋ค ๋ํด์ฃผ๋ ๊ณผ์ ์ด ํ์ํ๋ค.
3. ๋ฐธ๋ฆฌ๋ฐ์ด์ ๊ฒ์ฌ ์ปค์คํ ํ
์ ๊ธ์ก ์ ๋ ฅ ๊ด๋ จ ์์งค์์ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฌธ๊ตฌ๊ฐ ์ ๋์ ๋ ์ ๋จ๋๊ฑธ ํ์ธํ ์ ์๋ค. ๋ชฉํ ๊ธ์ก์ 1500์ ์ด์ 30๋ง์ ์ดํ๋ง ์ ๋ ฅํ ์ ์๋ค. ๊ฐ๊ฐ ๋จ๊ณ์์ ๋ค์ด๊ฐ๋ ๊ฐ๋ค์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๋ ๋ก์ง์ ์ปดํฌ๋ํธ ๋ด์์ ์ฒ๋ฆฌํ๋ ค๊ณ ํ์ง๋ง, ๋๋ฌด๋๋ฌด ๊ธธ์ด์ง๋ ๋ฐ๋์. ๊ทธ ๋ก์ง์ ์ปดํฌ๋ํธ์ ๋ถ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค. ์ปค์คํ ํ ์ด๋ค. (๋?)
type TFormType = 'contractName' | 'contractAmount' | 'comment';
const validateResultContent = {
// ...์๋ต,
contractAmount: {
default: { error: false, message: '์ต์ 1500์์์ ์ต๋ 30๋ง์๊น์ง ์ค์ ํ ์ ์์ด์!' },
under: { error: true, message: '1,500์ ์ด์์ผ๋ก ๋ถํํด์!' },
over: { error: true, message: '30๋ง์ ์ดํ๋ก ๋ถํํด์!' },
pass: { error: false, message: '์ ์ ํ ๊ธ์ก์ด์์!' },
},
};
function useValidation() {
const [validateResult, setValidateResult] =
useState<TValidationResult>(initialState);
const validateContractName = (
value: string,
existChallengeNames: string[],
) => {
//... ์๋ต
};
const validateContractAmount = (value: number) => {
if (!value) setValidateResult(validateResultContent.contractAmount.default);
else if (value < 1500)
setValidateResult(validateResultContent.contractAmount.under);
else if (value > 300000)
setValidateResult(validateResultContent.contractAmount.over);
else setValidateResult(validateResultContent.contractAmount.pass);
};
const validateComment = (value: string) => {
//... ์๋ต
};
const checkValidate = (
formType: TFormType,
value: string | number,
existChallengeNames?: string[],
) => {
if (formType === 'contractName' && typeof value === 'string') {
validateContractName(value, existChallengeNames!);
}
if (formType === 'contractAmount' && typeof value === 'number') {
validateContractAmount(value);
}
if (formType === 'comment' && typeof value === 'string') {
validateComment(value);
}
};
return [validateResult, checkValidate] as const;
}
export default useValidation;
์ด๋ฐ์์ผ๋ก!! validateํ๋ ํจ์์ validate ๊ฒฐ๊ณผ๋ฅผ ๋ฆฌํดํด์ค๋ค. form๋ง๋ค ๋ค๋ฅธ ๊ฒ์ฌ๋ฅผ ์ํํ ๋ค์, error ์ฌ๋ถ์ message๋ฅผ ๋ด์ ๊ฐ์ฒด๋ฅผ ๊ฒฐ๊ณผ๋ก ๋ณด๋ธ๋ค.
function Step3({ currentStep }: { currentStep: number }) {
const [disabledNext, setDisabledNext] = useState<boolean>(true);
const [validateName, checkValidateName] = useValidation();
const [validateAmount, checkValidateAmount] = useValidation();
//form ๊ฐ์ด ๋ฐ๋๋๋ง๋ค ์ ํจ์ฑ๊ฒ์ฌ ์คํ
useEffect(() => {
checkValidateName('contractName', form.contractName, existingDongilName);
checkValidateAmount('contractAmount', form.contractAmount);
}, [form]);
// ๋ค์์ผ๋ก ๋ฒํผ ํ์ฑํ,๋นํ์ฑํ ์ฒ๋ฆฌ
useEffect(() => {
validateName.message === '์์ ์ข์ ์ด๋ฆ์ธ๋ฐ์!' &&
validateAmount.message === '์ ์ ํ ๊ธ์ก์ด์์!'
? setDisabledNext(false)
: setDisabledNext(true);
}, [validateAmount, validateName]);
return (
<Wrapper>
{/* ...์๋ต */}
<InputSection validate={validateAmount}>
<InputForm
placeholder="๋ถ๋ชจ๋๊ณผ ํจ๊ป ๋ชจ์ ๊ธ์ก"
value={
form.contractAmount === 0
? ''
: getCommaThreeDigits(form.contractAmount)
}
error={validateAmount.error}
onBlur={() => {
checkValidateAmount('contractAmount', form.contractAmount);
}}
/>
<p>{validateAmount.message}</p>
</InputSection>
</Wrapper>
);
}
ํ ํ์ด์ง์ ํผ์ด ๋๊ฐ ์ด์ ๋ค์ด๊ฐ๋ ๊ฒฝ์ฐ์๋, ํ
์ ํ์ํ ๋งํผ ๊ฐ์ ธ์์ ์ฐ๋ฉด ๋๋ค. ๋ฐฐ์ด๋ก ๋ฆฌํดํ๊ธฐ ๋๋ฌธ์ ์ด๋ฆ์ ์ง์ ํด์ ๋ฐ์์ฌ ์ ์์ด ํธ๋ฆฌํ๋ค. ํผ ๋ด์ฉ์ด ๋ฐ๋ ๋, ์ธํ์ด blur ๋ ๋ ๊ฒ์ฌ๋ฅผ ๋งค๋ฒ ์คํํ๋ค. ๋ฐํ๋ฐ์ ๊ฒฐ๊ณผ๊ฐ์์ error๊ฐ ์๋ ๋์๋ง ๋ค์์ผ๋ก ๋ฒํผ์ ํ์ฑํ ์ํจ๋ค.
๊ฐ์ฒด์์ error๊ฐ์ผ๋ก ์กฐ๊ฑด์ ๋๋ฉด ์๋๋?? - error ๋ถ์ธ๋ก ์ธํ ํ
๋๋ฆฌ์ ์๊น์ ๊ฒฐ์ ํ๋๋ฐ, ์
๋ ฅํ ๊ฐ์ด ์์ ๋ ๊ธฐ๋ณธ๊ฐ์ผ๋ก error๊ฐ false์ด๋ค. ๊ฒ์ฌ์ ํต๊ณผํ์ ๋ ๋์ค๋ ๋ฉ์์ง๋ฅผ ์กฐ๊ฑด์ผ๋ก ๋์๋ค. ์ฝ๊ฐ ์์ฝ๊ธด ํ๋ฐ, ๋ญ... ๊ทธ๋๋ ์ง๊ด์ ์ด์ผ.
4. ์ค์์ดํ ์ ๊ธ์ก ์ ๋ ฅ
์ ์์ ํ์ด์ง ํ๋์์ ๊ณ ๋น๊ฐ ์ธ ๊ตฐ๋ฐ๋ ์์๋ค.
- ๋ชฉํ ์ ๊ธ์ก์ ๋ฐ๋ผ์ ๋งค์ฃผ ์ ๊ธ์ก์ ์ํ, ํํ์ด ์ ํด์ง๋ค. ๊ทผ๋ฐ ์ด์๋ถ์คํฐ๊ฐ ๊ปด์์ด์ ๊ทธ๊ฑธ ๊ณ์ฐํ๋ ๋ฐฉ๋ฒ์ ๋๊ฒ ๋ง์ด ๊ณ ๋ฏผํด์ผ ํ์.
- ์ค์์ดํํ๋ฉด์ ์ ๋ ฅ๋ ๋งค์ฃผ ์ ๊ธ์ก์ด ๊ณ์ ๋ฐ๋๋๋ฐ, ์ด์๋ถ์คํฐ์ ๋ฐ๋ฅธ ์ถ๊ฐ ์ ๊ธ์ก๊ณผ ๋๋๋ ์ฃผ๋ฅผ ๊ณ์ฐ์ ํด์ผ ํ๋ค.
- ๊ทธ๋ฆฌ๊ณ ๋ฌด์๋ณด๋ค ์ ์ค์์ดํ ๋ฐ css๊ฐ ์ ์ผ ๋ฌธ์ . ํ์ง๋ง ๋์์ธ์ด ์ ์ผ ์ค์ํ๋๊น!!
์ฒซ๋ฒ์งธ.
์๋ ๊ธฐํ์ ๋์ ๋ชจ์ผ๋ ๊ธฐ๊ฐ๊ณผ ๋งค์ฃผ ์ ๊ธ์ก์ ๋ฐ๋ผ ์ด์๋ฅผ ๋ฐ๋๋ก ํ์๋ค. ๋งค์ฃผ 1000์์ ๋ชจ์ผ๊ณ 5์ฃผ๋ฅผ ๋ชจ์ผ๋ฉด 5000์์ ๋ฐ๋ ํ์. ํ์ง๋ง ๊ธฐ๊ฐ๊ณผ ๋งค์ฃผ ์ ๊ธ์ก์ ์ ํ๊ธฐ ์ ์ ์ํ๊ณผ ํํ๊ฐ์ ๋จผ์ ๋ณด์ฌ์ค์ผ ํ๋๋ฐ, ์ํ๊ณผ ํํ๊ฐ์ ์ ์ฒด ๊ธ์ก์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ค. ๊ทผ๋ฐ ๋ ์ ์ฒด ๊ธ์ก์ ์ด์์ก์ ๋ฐ๋ผ ๋ฌ๋ผ์ง. ํ๋ฆ์ด ๋ค์์ผ์ ์ ์ด์ ๋ถ๊ฐ๋ฅํ ๋ฐฉ๋ฒ์ด์๋ค.
๊ทธ๋์ ๊ฒฐ๊ตญ ๊ธฐํ์ ๋ฐ๊ฟ๋ฒ๋ฆผ. ์ ์ฒด ์ ๊ธ์ก * ์ด์์จ๋ก ๋จผ์ ๊ณ์ฐ์ ํด๋๊ณ ์ฌ์ฉํ๋ค. ๊ทธ๋ผ ํ๋ฆ์ด ์์๋๋ก ๊ฐ์ ๊ณ์ฐ๋ ๊ฐ์ ์ธ ์ ์์.
const getChallengeStep4Prices = (
totalPrice: number,
interestRate: 10 | 20 | 30 | null,
) => {
// 500์ ๋จ์๋ก ์ฌ๋ฆผ
const getRoundUpBy500 = (price: number) =>
price % 500 === 0 ? price : price - (price % 500) + 500;
const maxPrice = interestRate
? getRoundUpBy500(((1 - 0.01 * interestRate) * totalPrice) / 3)
: getRoundUpBy500((0.8 * totalPrice) / 3);
const minPrice = interestRate
? getRoundUpBy500(((1 - 0.01 * interestRate) * totalPrice) / 15)
: getRoundUpBy500((0.8 * totalPrice) / 15);
// 20ํผ์ผํธ์ผ๋ ๊ฐ์ ํ ์ค๊ฐ๊ธ์ก
const middlePrice =
(minPrice + maxPrice) / 2 - (((minPrice + maxPrice) / 2) % 500);
return { minPrice, maxPrice, middlePrice };
}
export default getChallengeStep4Prices;
๋ฑ
ํค์ฆ์์ ๊ณ์ฝํ ์ ์๋ ๊ธฐ๊ฐ์ 3์ฃผ๋ถํฐ 15์ฃผ๊น์ง์ด๋ค. ์ด์ ๋ง์ถ๋ ค๊ณ (์ด์๋ถ์คํฐ๋ฅผ ์ ์ธํ) ํผ์ ๋ชจ์ผ๋ ๊ธ์ก์ 3 ๋๋ 15๋ฅผ ๋๋ ๊ธ์ก์ผ๋ก ์ํ ํํ์ ์ค์ ํ๋ค. ๊ธฐ๋ณธ๊ฐ์ ์ด์์จ์ด 20%์ธ ๊ฒฝ์ฐ๋ฅผ ๋ณด์ฌ์ค.
๋๋ฒ์งธ.
ํ์ ์ฃผ์์, ๋๋๋ ์ฃผ์ ์ฃผ (n์ n์ฃผ์ฐจ)๋ฅผ ๊ณ์ฐํด์ ๋ณด์ฌ์ค๋ค. ์ฃผ์ฐจ ๊ณ์ฐ ๋ก์ง์ ์ฌ๊ธฐ์ ๊ฐ์ ธ์ด. JS๋ก ๊ตฌํ๋ ํจ์๋ฅผ ํ์
์ผ๋ก ๋ฐ๊พธ๊ธฐ๋ง ํ๋ค. ๋๋ถ์ ์ ์ผ ๊ฑฑ์ ํ๋ ๋ถ๋ถ์ ๋น ๋ฅด๊ฒ ํด๊ฒฐํ ์ ์์๋ค. ํด! ์ฝ๋๋ฅผ ๋ถ์ฌ๋ฃ์ผ๋ ค๊ณ ํ๋ค๊ฐ ๊ทธ๋ฅ ๋จ์๊ณ์ฐ์ธ๋ฐ ๊ตณ์ด ํ์ํ ๊น ์ถ์ด์.. ์คํต.
์ธ๋ฒ์งธ.
์ง์ง ์ ์ผ ๊ณ ์ํ๋ ๋ถ๋ถ. rc-slider
๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ค. ๊ธฐ๋ณธ input ํ๊ทธ๋ฅผ ์ด์ฉํ ์ ์์์ง๋ง, ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์ ํฐ์น๋ก ์กฐ์ํ ๋ ๋ถ์์ฐ์ค๋ฌ์ด ๋๋์ด ๋ง์์ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋์
ํ๋ค. ๋ด๋ถ์ ์ผ๋ก touch event๊น์ง ๋ฐ์์ ์ฌ์ฉํ๋๋ผ.
<RangeInputForm>
<StyledSlider
min={min}
max={max}
value={value}
onChange={(v) => setValue(v as number)}
step={step}
railStyle={RcSliderRailStyle}
trackStyle={RcSliderTrackStyle}
handleStyle={RcSliderHandleStyle}
/>
<Selector percent={percent}>
<WalkingBanki />
</Selector>
<ProgressBar percent={percent} />
<Track />
</RangeInputForm>;
ํ. ์ฝ์ง์ ๋ง์ดํ๋ค. ๊ฒฐ๊ตญ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ๊ฐ์ ธ์จ ์ฌ๋ผ์ด๋์ ์คํ์ผ์ ๋ชจ๋ ๊ฐ๋ฆฌ๊ณ , ๋์ ๋ณด์ด๋ ์ปดํฌ๋ํธ๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด ๊ฐ์ด ์์ง์ด๋๋ก ํ๋ค. Selector๋ ๋ฑ ํค ๋ฒํผ, ProgressBar๋ ๋ ธ๋์ ์งํ ๋ฐ, Track์ ํ์ ๋ฐ.
const percent = ((value - min) * 100) / (max - min);
const Selector = styled.div<{ percent: number }>`
position: absolute;
top: -16px;
height: 40px;
width: 44px;
z-index: 3;
${({ percent }) => {
return percent > 0
? css`
left: calc(${percent}% - (0.44 * ${percent}px));
`
: css`
left: 0px;
`;
}};
`;
position์ absolute๋ก ๋๊ณ , left ์์ฑ์ ์ง์ ์ฃผ๊ณ ์ด๋์์ผฐ๋ค. ๊ทธ๋ฅ ํผ์ผํธ๋ก ํ๋ฉด ์๋๋๊ฒ, ์ ์์์ ์ผ์ชฝ ๋์ ๊ธฐ์ค์ผ๋ก ์ด๋ํ๊ธฐ ๋๋ฌธ์ 100%์ผ๋ ์์ ธ๋์ค๋ ๊ฒฝ์ฐ๊ฐ ์์์. ๊ทธ๋์ ํผ์ผํธ์ ๋๋น์ ๋น๋กํด์ ์กฐ๊ธ ๋นผ์ค์ผํ๋ค.
๊ทผ๋ฐ ๋ ProgressBar๋ Selector์ ๋๊ฐ์ด ํ๋ฉด ๋ฑ
ํค ์ผ์ชฝ ์๊ตฌ๋ฆฌ๊ฐ ๋น์ด์ 22px(๋ฑ
ํค ์ ๋ฐ)๋งํผ ๋ํด์ค์ผํ์. ์๋ผ์ด.
5. ์ฌ์ธ ์ ์กํ๊ธฐ (Presigned Url)
์ฌ์ธ์ ํ๊ณ ์๋ฒ์ ์ ์กํ๋ค. react-signature-canvas
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ค.
function Signature({ setDisabledNext, setSign }: SignatureProps) {
const canvasRef = useRef<any>(null);
const onEndSign = () => {
setDisabledNext(false);
if (canvasRef.current) {
const signImage = canvasRef.current
.getTrimmedCanvas()
.toDataURL('image/png');
setSign(signImage);
}
};
return (
<Wrapper>
<CanvasContainer>
<SignatureCanvas
penColor={theme.palette.greyScale.black}
canvasProps={{ className: 'sigCanvas' }}
ref={canvasRef}
onEnd={onEndSign}
minWidth={1.5}
maxWidth={3.5}
/>
</CanvasContainer>
<p>์ด๊ณณ์ ์ฌ์ธ์ ํ๋ฉด ๊ณ์ฝ์ด ์งํ๋ผ์</p>
</Wrapper>
);
}
export default Signature;
onEnd
props์์ ์ฌ์ธ์ด ๋๋ฌ์ ๋ (ํด๋ฆญ/ํฐ์น๊ฐ ๋๋ฌ์๋) ์คํํ ํจ์๋ฅผ ์ง์ ํด์ค ์ ์๋ค. png ์ด๋ฏธ์ง๋ฅผ dataUrl ํํ๋ก ๋ฐ๊ฟ state์ ์ ์ฅํด๋๋ค.
๊ธฐ์กด์ s3 ์
๋ก๋๋ฅผ ๊ตฌํํ์ ๋ ํด๋ผ์ด์ธํธ์์ ์๋ฒ๋ก ์ด๋ฏธ์ง๋ฅผ ๋ณด๋ด๊ณ , ์๋ฒ์์ s3๋ก ์
๋ก๋ ํ ๋ค์์ ๋ฐํ๋ฐ์ ๋งํฌ๋ฅผ ๋ค์ ํด๋ผ์ด์ธํธ๋ก ๋ณด๋ด์ฃผ๋ ํ์์ด์๋ค. ๋ฆฌ์์ค ๋ญ๋น๊ฐ ์ฌํ๊ณ ์๋ฒ์ ๋ถ๋ด์ด ์ฌํ๋ค.
presigned URL์ ๋ง ๊ทธ๋๋ก ์ด๋ฏธ ์๋ช
๋ ์ฃผ์๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. ์๋ฒ์์ s3 ๋ฒํท์ ์
๋ก๋ํ ์ ์๋ ์ฃผ์๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ๊ธ๋ฐ๊ณ ํด๋ผ์ด์ธํธ๋ก ๋ณด๋ด๋ฉด, ํด๋ผ์ด์ธํธ์์๋ ๊ทธ ์ฃผ์๋ก ์
๋ก๋ ์์ฒญ์ ๋ณด๋ธ๋ค.
// ๋ ๋๋งํ์๋ง์ presignedUrl ๊ฐ์ ธ์ค๊ธฐ
useEffect(() => {
const getPresignedUrl = async () => {
try {
const response = await axiosPrivate.get('/s3/url');
dispatch(setFileName(response.data.imageName));
setPreSignedUrl(response.data);
} catch (err) {
console.error(err);
}
};
getPresignedUrl();
}, []);
step5 ํ์ด์ง๊ฐ ๋ ๋๋ง ๋๋ฉด ๋ฐ๋ก ์๋ฒ๋ก url์ ๋ฌ๋ผ๊ณ ์์ฒญํ๋ค.
// ๋ค์์ผ๋ก ๋ฒํผ ํด๋ฆญ
const onClickNextButton = () => {
// s3 ์
๋ก๋ ๋ก์ง
const uploadS3 = async (sign: any) => {
const file = convertDataURLtoFile(sign, preSignedUrl.imageName);
let formData = new FormData();
formData.append('file', file);
const response = await axios.put(preSignedUrl.preSignedUrl, file, {
headers: { 'Content-Type': 'image/png' },
});
};
uploadS3(sign);
mutatePostChallenge(createChallengePayload);
};
๋ค์์ผ๋ก ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋ฐ๊ธ๋ฐ์ preSignedUrl๋ก ์ฌ์ธ ์ด๋ฏธ์ง๋ฅผ ์ ์กํ๊ณ , ๋ฆฌ๋์ค ์คํ ์ด์ ์์๋ ๋๊ธธ ๊ณ์ฝ ๊ด๋ จ ์
๋ ฅ ์ ๋ณด๋ค์ ์๋ฒ๋ก ๋ณด๋ด ๊ณ์ฝ์ ์๋ฃํ๋ค. ์ด๋ฏธ์ง๋ฅผ ํ์ผ๋ก ๋ณํํ๊ณ ํผ๋ฐ์ดํฐ ํ์์ผ๋ก ๋ ๋ฐ๊ฟ์ฃผ๋ ๊ณผ์ ์ด ํ์ํ๋ค. ์ด ๊ณผ์ ์์ ์ฝ์ง์ ์ด๋ง๋ฌด์ํ๊ฒ ํ๋ค.
์ฒ์์ ๊ณ์ 4xx ์๋ฌ๊ฐ ๋ฌ๋ค. aws ๋ฒํท ์ค์ ์์ cors ๊ด๋ จ ์ค์ ์ ํ์ด์คฌ์ด์ผ ํ๋๋ฐ, put ๋ฉ์๋๊ฐ ์ค์ ์ ๋ฑ๋ก์ด ๋์ด ์์ง ์์์ ์๊ธด ๋ฌธ์ ์์. ์ถ๊ฐํด์ฃผ๋ ์ ๋๋ก ์
๋ก๋๋์๋ค.
๊ต์ฅํ ์ค๋๊ฑธ๋ ธ๋ ๊ณผ์ ์ธ๋ฐ ๊ธ๋ก ์ฐ๊ณ ๋ณด๋ ์ด๋ ๊ฒ ์งง๋ค.. ํํ์ค๋ค.