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-HW1] malloc #5

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/ar-seminar.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions 2024-summer-FE-seminar/packages/web/src/app/malloc/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client";

import React, {useState} from "react";

import TextInput from "@sparcs-clubs/web/common/components/malloc/TextInput";
import ItemNumberInput from "@sparcs-clubs/web/common/components/malloc/ItemNumberInput";

const Malloc = () => {
const [text, setText] = useState("");
const [item, setItem] = useState("");
return (<div>
<TextInput placeholder={"텍스트를 입력해주세요"} value={text} handleChange={setText}></TextInput>
<ItemNumberInput placeholder={"개수를 입력해주세요"} value={item} handleChange={setItem}></ItemNumberInput>
</div>);
};

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

import styled from "styled-components";

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

interface ItemNumberInputProps extends InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> {
label?: string;
placeholder: string;
errorMessage?: string;
disabled?: boolean;
limit?: number;
unit?: string;
value?: string;
handleChange?: (value: string) => void;
setErrorStatus?: (hasError: boolean) => void;
}

const Input = styled.input<ItemNumberInputProps & { hasError: boolean }>`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
padding: 8px 12px;
gap: 8px;
border-radius: 4px;
border: 1px solid ${({theme}) => theme.colors.GRAY[200]};
background: ${({theme}) => theme.colors.WHITE};
font-family: ${({theme}) => theme.fonts.FAMILY.PRETENDARD};
font-size: 16px;
font-style: normal;
font-weight: ${({theme}) => theme.fonts.WEIGHT.REGULAR};
line-height: 20px;
padding-right: ${({limit}) => (limit ? "48px" : "12px")};

&:hover:not(:focus) {
border: 1px solid ${({theme, disabled, hasError}) =>
disabled
? !hasError && theme.colors.GRAY[200]
: (!hasError && theme.colors.GRAY[300]) ||
(hasError && theme.colors.RED[600])};
}

&::placeholder {
color: ${({theme}) => theme.colors.GRAY[200]};
}

&:focus {
outline: none;
border: 1px solid ${({theme, hasError, disabled}) =>
(!hasError && !disabled && theme.colors.PRIMARY) ||
(hasError && !disabled && theme.colors.RED[600])};
}

${({disabled, theme}) =>
`background-color: ${disabled ? theme.colors.GRAY[100] : theme.colors.WHITE};
color: ${disabled ? theme.colors.GRAY[300] : theme.colors.BLACK};`}
${({hasError, theme}) =>
hasError ? `border: 1px solid ${theme.colors.RED[600]};` : ``}
`;

const InputWrapper = styled.div`
display: flex;
width: 100%;
flex-direction: column;
gap: 4px;
`;

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

const UnitWrapper = styled.div<{ hasError: boolean }>`
display: flex;
position: absolute;
right: 10px;
align-items: center;
color: ${({theme}) => theme.colors.GRAY[300]};
font-family: ${({theme}) => theme.fonts.FAMILY.PRETENDARD};
font-size: 16px;
font-style: normal;
font-weight: ${({theme}) => theme.fonts.WEIGHT.REGULAR};
line-height: 20px;
color: ${({theme, hasError}) =>
hasError ? theme.colors.RED[600] : theme.colors.GRAY[300]};
`;

const ItemNumberInput: React.FC<ItemNumberInputProps> = ({
label = "",
placeholder,
disabled = false,
limit = 99,
unit = "개",
value = "",
handleChange = () => {
},
setErrorStatus = () => {
},
...props
}) => {
const [error, setError] = useState("");

useEffect(() => {
const isValidFormat = /^\d+$/g.test(value);
const intValue = parseInt(value);
if (value === "") {
setError("");
setErrorStatus(false);
} else if (!isValidFormat) {
setError("숫자만 입력 가능합니다");
setErrorStatus(true);
} else if (intValue > limit) {
setError("신청 가능한 개수를 초과했습니다");
setErrorStatus(true);
} else {
setError("");
setErrorStatus(false);
}
}, [value, limit, setErrorStatus]);

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const input = e.target.value.replace(/[^0-9]/g, "");

if (input.length <= 2) {
handleChange(input);
}
};

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

return (
<InputWrapper>
{label && <Label>{label}</Label>}
<InputWrapper2>
<Input
ref={inputRef}
onChange={handleInputChange}
value={displayValue}
placeholder={placeholder}
disabled={disabled}
hasError={!!error}
{...props}
/>
{limit !== undefined && (
<UnitWrapper hasError={!!error}>
/ {limit}
{unit}
</UnitWrapper>
)}
</InputWrapper2>
{error && <FormError>{error}</FormError>}
</InputWrapper>
);
};

export default ItemNumberInput;
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, {ChangeEvent, InputHTMLAttributes} from "react";
import styled from "styled-components";

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

interface TextInputProps
extends InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> {
label?: string;
placeholder: string;
errorMessage?: string;
area?: boolean;
disabled?: boolean;
value?: string;
handleChange?: (value: string) => void;
setErrorStatus?: (hasError: boolean) => void;
}

const Input = styled.input<TextInputProps & {hasError: boolean}>`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
padding: 8px 12px;
gap: 8px;
border-radius: 4px;
border: 1px solid ${({theme}) => theme.colors.GRAY[200]};
background: ${({theme}) => theme.colors.WHITE};
font-family: ${({theme}) => theme.fonts.FAMILY.PRETENDARD};
font-size: 16px;
font-style: normal;
font-weight: ${({theme}) => theme.fonts.WEIGHT.REGULAR};
line-height: 20px;

&:hover:not(:focus) {
border: 1px solid ${({theme, disabled, hasError}) =>
disabled
? !hasError && theme.colors.GRAY[200]
: (!hasError && theme.colors.GRAY[300]) ||
(hasError && theme.colors.RED[600])};
}

&::placeholder {
color: ${({theme}) => theme.colors.GRAY[200]};
}

&:focus {
outline: none;
border: 1px solid ${({theme, hasError, disabled}) =>
(!hasError && !disabled && theme.colors.PRIMARY) ||
(hasError && !disabled && theme.colors.RED[600])};
}

${({disabled, theme}) =>
`background-color: ${disabled ? theme.colors.GRAY[100] : theme.colors.WHITE};
color: ${disabled ? theme.colors.GRAY[300] : theme.colors.BLACK};`}
${({hasError, theme}) =>
hasError ? `border: 1px solid ${theme.colors.RED[600]};` : ``}
`;

const InputWrapper = styled.div`
display: flex;
width: 100%;
flex-direction: column;
gap: 4px;
`;

const TextInput : React.FC<TextInputProps> =({
label = "",
placeholder,
errorMessage = "",
area = false,
disabled = false,
value = "",
handleChange = () => {},
setErrorStatus = () => {},
...props

}) => {
const handleInputChange=(e:ChangeEvent<HTMLInputElement>)=>{
const input=e.target.value;
handleChange(input)
}

return (
<InputWrapper>
{label && <Label>{label}</Label>}
<InputWrapper>
<Input
placeholder={placeholder}
hasError={!!errorMessage}
area={area}
disabled={disabled}
value={value}
onChange={handleInputChange}
{...props}
/>
{errorMessage && <FormError>{errorMessage}</FormError>}
</InputWrapper>
</InputWrapper>
);

}

export default TextInput;
33 changes: 20 additions & 13 deletions 2024-summer-zod-semina/interface/db.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import { z } from "zod";

export const dbElement = z.object({
// TODO 2: mockDB에 들어갈 데이터의 타입을 정의해 주세요
// { value: 0 이샹의 정수, updatedAt: ts date 타입 }
// { value: 0 이샹의 정수, updatedAt: ts date 타입 }
value: z.coerce.number().int().nonnegative(),
updatedAt: z.coerce.date(),
});

export const twoPositiveIntegerParser = z.object({
// TODO 1: 아래와 같이 2개의 양의 정수를 포함한 객체를 parsing하는 zod object를 구현하세요
// { a: 양의 정수, b: 양의 정수 }
// { a: 양의 정수, b: 양의 정수 }
a: z.coerce.number().int().min(1),
b: z.coerce.number().int().min(1),
});

export const getDbIndexParser = (dbLength: number) =>
z.object({
// db의 크기를 받아 db 범위 내의 인덱스만을 값으로 받는 parser를 만들어 주세요
// { index: int, 0 <= index <= db_max_index}
});
z.object({
// db의 크기를 받아 db 범위 내의 인덱스만을 값으로 받는 parser를 만들어 주세요
// { index: int, 0 <= index <= db_max_index}
index: z.coerce.number().int().min(0).max(dbLength - 1),
});

export const dbPaginationQueryParser = z.object({
// 페이지네이션을 위한 parameter들을 정의해 주세요
// { startDate, endDate: db의 updatedAt을 통해 filter하기 위한 옵션들입니다. optional합니다.
// pageOffset: int, >= 1
// itemCount: int, >= 1
// },
// 페이지네이션을 위한 parameter들을 정의해 주세요
// { startDate, endDate: db의 updatedAt을 통해 filter하기 위한 옵션들입니다. optional합니다.
// pageOffset: int, >= 1
// itemCount: int, >= 1
// },
startDate: z.optional(z.coerce.date()),
endDate: z.optional(z.coerce.date()),
pageOffset: z.coerce.number().int().min(1),
itemCount: z.coerce.number().int().min(1),
});
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.