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

[FE] assignment : Daystar #4

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
78 changes: 78 additions & 0 deletions 2024-summer-FE-seminar/packages/web/src/app/Daystar/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client";

import React, { useState } from "react";
import styled from "styled-components";

import Card from "@sparcs-clubs/web/common/components/Card";
import DaystarItemNumberInput from "@sparcs-clubs/web/common/components/Daystar/DaystarItemNumberInput";
import DaystarTextInput from "@sparcs-clubs/web/common/components/Daystar/DaystarTextInput";
import ItemNumberInput from "@sparcs-clubs/web/common/components/Forms/ItemNumberInput";
import TextInput from "@sparcs-clubs/web/common/components/Forms/TextInput";
import Select from "@sparcs-clubs/web/common/components/Select";
import Typography from "@sparcs-clubs/web/common/components/Typography";

const DaystarWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;
Comment on lines +15 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

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

FlexWrapper를 사용합시당


const Daystar = () => {
const [firstText, setFirstText] = useState("");
const [secondText, setSecondText] = useState("");
const [firstItemNumber, setFirstItemNumber] = useState("0");

const [secondHasError, setSecondHasError] = useState(true);

const handleSecondTextChange = (value: string) => {
setSecondText(value);
setSecondHasError(value.length < 5); // 5글자보다 짧으면 에러 뜨는 것으로 테스트
};
Comment on lines +29 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

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

(optional) 여기서 이 input을 한 번도 클릭하지 않았을 때는 에러가 안 뜨게 하려면 어떻게 해야할까요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

secondHasError state의 초깃값을 false로 하면 되는 것 같습니다!


const handleFirstItemNumberChange = (value: string) => {
setFirstItemNumber(value);
};

return (
<DaystarWrapper>
<TextInput placeholder="" />
<ItemNumberInput placeholder="" />
<Select items={[]} />
<Card outline gap={16}>
<Typography type="h3">부모 컴포넌트가 받는 값들</Typography>
<Typography>
입력 값들
<br />
{`1번째 InputText : ${firstText}`}
<br />
{`2번째 InputText : ${secondText}`}
<br />
{`1번째 ItemNumberInput : ${firstItemNumber}`}
</Typography>
</Card>
<DaystarTextInput placeholder="Placeholder" handleChange={setFirstText} />
<DaystarTextInput
placeholder="5글자 미만이면 Error"
errorMessage={secondHasError ? "5글자 이상 입력해주세요" : ""}
handleChange={handleSecondTextChange}
/>
<DaystarTextInput placeholder="Disabled" disabled />
<DaystarItemNumberInput
value={firstItemNumber}
itemLimit={50}
unit="개"
placeholder="0개"
handleChange={handleFirstItemNumberChange}
/>
<DaystarItemNumberInput
itemLimit={50}
disabled
unit="개"
placeholder="0개"
handleChange={handleFirstItemNumberChange}
/>
</DaystarWrapper>
);
};

export default Daystar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import React, {
ChangeEvent,
InputHTMLAttributes,
useEffect,
useRef,
useState,
} from "react";

import isPropValid from "@emotion/is-prop-valid";
import styled, { css } from "styled-components";

import FormError from "@sparcs-clubs/web/common/components/FormError";

// import FormError from "../FormError";
// import Typography from "../Typography";

export interface DaystarItemNumberInputProps
extends InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> {
placeholder?: string;
// hasError?: boolean; <- 이건 errorMessage의 존재 여부로 판단합니다
disabled?: boolean;
unit?: string; // 단위
value?: string; // 입력 값
itemLimit?: number; // 최대 수량
handleChange?: (value: string) => void; // 값이 변할 때 상위 컴포넌트의 함수를 호출할 수 있도록
setErrorStatus?: (hasError: boolean) => void; // 에러 상태를 상위 컴포넌트에 전달
}

const errorStyle = css`
border: 1px solid ${({ theme }) => theme.colors.RED[600]};
&:focus {
border: 1px solid ${({ theme }) => theme.colors.RED[600]};
}
`;

const disabledStyle = css`
background: ${({ theme }) => theme.colors.GRAY[100]};
color: ${({ theme }) => theme.colors.GRAY[300]};
`;

const Input = styled.input.withConfig({
shouldForwardProp: prop => isPropValid(prop),
// 왜 InputText에서는 검사하지 않는지는 잘 모르겠습니다. 우선 코드를 가져왔습니다.
// 여기서는 textarea가 될 상황이 없어서 attrs는 사용하지 않습니다.
})<DaystarItemNumberInputProps & { hasError: boolean }>`
width: 100%;
display: flex;
padding: 8px 12px;
justify-content: center;
align-items: center;
gap: 8px;
align-self: stretch;
border-radius: 4px;
color: ${({ theme }) => theme.colors.BLACK};
font-family: Pretendard;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 20px;
jinhyeonkwon marked this conversation as resolved.
Show resolved Hide resolved
border: 1px solid ${({ theme }) => theme.colors.GRAY[200]};
background: ${({ theme }) => theme.colors.WHITE};
&::placeholder {
color: ${({ theme }) => theme.colors.GRAY[200]};
}
&:focus {
border: 1px solid ${({ theme }) => theme.colors.PRIMARY};
outline: none;
}
&:hover:not(:focus) {
border: 1px solid ${({ theme }) => theme.colors.GRAY[300]};
}

${({ hasError }) => hasError && errorStyle};
${({ disabled }) => disabled && disabledStyle};
`;

const MaxUnit = styled.div.withConfig({
shouldForwardProp: prop => isPropValid(prop),
})<{ hasError: boolean }>`
position: absolute;
right: 12px;
left: auto;
top: auto;
bottom: auto;
display: flex;
align-items: center;
color: ${({ theme, hasError }) =>
hasError ? theme.colors.RED[600] : theme.colors.GRAY[300]};
font-family: Pretendard;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 125% */
jinhyeonkwon marked this conversation as resolved.
Show resolved Hide resolved
`;

const InputWrapper = styled.div`
position: relative;
width: 100%;
display: flex;
align-items: center;
`;

const DaystarItemNumberInputWrapper = styled.div`
display: flex;
width: 300px;
flex-direction: column;
align-items: flex-start;
gap: 4px;
`;

const DaystarItemNumberInput: React.FC<DaystarItemNumberInputProps> = ({
itemLimit = 0,
unit = null,
value = "",
handleChange = () => {},
setErrorStatus = () => {},
...props
}) => {
const [errorMessage, setErrorMessage] = useState(""); // 에러 여부를 내부에서 체크

const handleValueChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value.replace(/[^0-9]/g, ""); // 숫자 형식만 떼어냅니다.

if (
inputValue.length <= itemLimit.toString().length &&
inputValue !== "0"
) {
// 이 if문 구조는 너무 비용이 크지만 않다면 기존 컴포넌트에도 제안 드리고 싶습니다
// (itemLimit이 2자리보다 길어도 작동)
jinhyeonkwon marked this conversation as resolved.
Show resolved Hide resolved
handleChange(inputValue); // 이 컴포넌트를 호출한 외부에서 처리하여 적절한 value 값으로
// 이 컴포넌트를 리렌더링해 줄 것입니다. => 아래의 useEffect가 작동하여 새로운 value에 대응합니다.
}
};

useEffect(() => {
const isValidFormat = /^\d+$/g.test(value); // 숫자만 입력되었는지 확인합니다.
const amount = parseInt(value); // 입력을 숫자로 변환
// 기존 코드를 가져왔습니다.
if (value === "") {
//
setErrorMessage("");
setErrorStatus(false);
} else if (!isValidFormat) {
// 숫자 아님
setErrorMessage("숫자만 입력해주세요.");
setErrorStatus(true);
} else if (amount > itemLimit) {
setErrorMessage("신청 가능 개수를 초과했습니다.");
setErrorStatus(true);
} else {
setErrorMessage("");
setErrorStatus(false);
}
}, [value, itemLimit, setErrorStatus]);

const mainInputRef = useRef<HTMLInputElement>(null);
const displayValue = value ? `${value}${unit}` : "";

const handleCursor = () => {
// 매 입력 또는 선택마다 커서를 적절한 위치로 옮기는 함수입니다. 그대로 가져왔습니다.
// 이를 Input의 onSelect로 하여 키보드 입력 또는 커서 선택 시 숫자 범위를 벗어나지 않게 합니다.
mainInputRef.current?.setSelectionRange(
mainInputRef.current.selectionStart! >= displayValue.length
? displayValue.length - 1
: mainInputRef.current.selectionStart,
mainInputRef.current.selectionEnd! >= displayValue.length
? displayValue.length - 1
: mainInputRef.current.selectionEnd,
);
};

return (
<DaystarItemNumberInputWrapper>
<InputWrapper>
<Input
ref={mainInputRef}
hasError={!!errorMessage}
onChange={handleValueChange}
value={displayValue}
onSelect={handleCursor}
{...props}
/>
{unit && itemLimit && (
<MaxUnit hasError={!!errorMessage}>{`/ ${itemLimit}${unit}`}</MaxUnit>
)}
</InputWrapper>
{errorMessage && <FormError>{errorMessage}</FormError>}
</DaystarItemNumberInputWrapper>
);
};
export default DaystarItemNumberInput;
Loading