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

맛집 추가 및 수정 폼을 개발합니다. #29

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8318877
fix: 메인페이지에 테스트용 AreaInput 들어간 것 제거
jaychang99 Sep 10, 2023
b163880
feat(Organization): 맛집 생성 및 수정 페이지 기본 골격 셋업
jaychang99 Sep 10, 2023
0bb2678
feat(Restaurant): 맛집 작성 폼 골격 생성
jaychang99 Sep 10, 2023
49aee6d
feat(Restaurant): 맛집 등록 페이지에 맛집 이름 넣는 인풋 추가
jaychang99 Sep 10, 2023
c7322b5
feat(Restaurant): 맛집 등록 페이지에 맛집 한줄평 넣는 인풋 추가
jaychang99 Sep 10, 2023
cc38f9a
feat(Restaurant): 맛집 등록 페이지에 맛집 주소 넣는 인풋 추가
jaychang99 Sep 10, 2023
7e67bf1
Merge branch 'feature/checkbox-component' into feature/form-restaurant
jaychang99 Sep 17, 2023
7346e65
feat(Restaurant): 맛집 추가 시 배달 가능 여부 체크박스 추가
jaychang99 Sep 17, 2023
db9833d
feat(Restaurant): 폼 기본 전송 로직 모킹용 console.log 추가
jaychang99 Sep 17, 2023
3dfa43c
feat(Restaurant): tagIds 선택하는 부분 추가
jaychang99 Sep 17, 2023
491ad33
feat(Restaurant): 맛집 최대 수용 인원 항목 추가
jaychang99 Sep 17, 2023
f7ed53e
feat(Restaurant): 맛집 운영 시간 항목 추가
jaychang99 Sep 17, 2023
dcc6c15
feat(Restaurant): 맛집 운영 시 주문팁 추가
jaychang99 Sep 17, 2023
0acd45b
feat(Restaurant): 맛집 운영 시 외부 링크란 추가
jaychang99 Sep 17, 2023
c5a6b71
feat(Restaurant): 맛집 추가 폼에 맛집 분류 (양식, 일식 등) 추가하는 드롭다운 추가
jaychang99 Sep 19, 2023
2e8f2ff
feat(Restaurant): 맛집에 사진 입력기 추가
jaychang99 Sep 19, 2023
eb2a548
fix(TextInput): 최초 value 넣은 이후 그 변경에 반응하지 못하는 문제 수정
jaychang99 Sep 19, 2023
6675322
feat(HashtagInput): 해시태그 입력기 공통 컴포넌트 개발
jaychang99 Sep 19, 2023
64ed0df
feat(Restaurant): 맛집 추가 폼에 추천 메뉴 등록할 수 있도록 HashtagInput 이용
jaychang99 Sep 19, 2023
d5e1ae2
feat(HashtagInput): 해시태그 입력기에 placeholder 추가하게 개발
jaychang99 Sep 19, 2023
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
4 changes: 4 additions & 0 deletions src/components/checkbox/types/TCheckboxItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TCheckboxItem = {
name: string;
value: string;
};
4 changes: 4 additions & 0 deletions src/components/dropdown/types/TDropdownItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TDropdownItem = {
value: string;
label: string;
};
71 changes: 71 additions & 0 deletions src/components/inputs/HashtagInput/HashtagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import styled from "@emotion/styled";
import HashtagItem from "components/inputs/HashtagInput/items/HashtagItem";
import TextInput from "components/inputs/TextInput/TextInput";
import { useCallback, useEffect, useState } from "react";

interface Props {
hashtagList: string[];
setHashTagList: (hashtagList: string[]) => void;
placeholder?: string;
}

type TTextInput = {
value: string;
isValid: boolean;
};

const HashtagInput: React.FC<Props> = ({ hashtagList, setHashTagList, placeholder }) => {
const [currentInput, setCurrentInput] = useState<TTextInput>({
value: "",
isValid: false,
});

useEffect(() => {
if (currentInput.value.includes(",") && currentInput.isValid) {
const newHashtagList = [...hashtagList, currentInput.value.replace(",", "")];
setHashTagList(newHashtagList);
setCurrentInput({ value: "", isValid: false });
}
}, [currentInput, hashtagList, setHashTagList]);

const handleChangeCurrentInput = useCallback((value: string, isValid: boolean) => {
setCurrentInput({ value, isValid });
}, []);

const handleRemoveHashtag = useCallback(
(hashtag: string) => {
const newHashtagList = hashtagList.filter((item) => item !== hashtag);
setHashTagList(newHashtagList);
},
[hashtagList, setHashTagList]
);

return (
<EmotionWrapper>
<TextInput
placeholder={placeholder}
conditionList={["쉼표로 구분하여 메뉴를 추가하세요!"]}
value={currentInput.value}
onTextChange={handleChangeCurrentInput}
/>
<ul className="hashtag-container">
{hashtagList.map((item) => (
<HashtagItem key={item} onClick={handleRemoveHashtag}>
{item}
</HashtagItem>
))}
</ul>
</EmotionWrapper>
);
};

export default HashtagInput;

const EmotionWrapper = styled.div`
ul.hashtag-container {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
row-gap: 8px;
}
`;
33 changes: 33 additions & 0 deletions src/components/inputs/HashtagInput/items/HashtagItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import styled from "@emotion/styled";
import { LiHTMLAttributes, ReactNode, useCallback } from "react";

interface Props extends Omit<LiHTMLAttributes<HTMLLIElement>, "onClick"> {
children: ReactNode;
onClick: (hashtag: string) => void;
}

const HashtagItem = ({ children, onClick, ...props }: Props) => {
const handleClickHashtag = useCallback(() => {
onClick(typeof children === "string" ? children : "");
}, [children, onClick]);

return (
<EmotionWrapper onClick={handleClickHashtag} {...props}>
{children}
</EmotionWrapper>
);
};

export default HashtagItem;

const EmotionWrapper = styled.li`
position: relative;
display: inline-block;
padding: 4px 8px;

border-radius: 4px;
margin-right: 8px;
font-size: 12px;
background-color: ${({ theme }) => theme.color.primary200};
color: ${({ theme }) => theme.color.primary700};
`;
12 changes: 10 additions & 2 deletions src/components/inputs/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, InputHTMLAttributes } from "react";
import styled from "@emotion/styled";
import { css, Theme } from "@emotion/react";
import CheckIcon from "components/inputs/TextInput/CheckIcon";
import { TConditionCheck } from "./types/TConditionCheck";

interface Props {
interface Props extends InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> {
name?: string;
label?: string;
value?: string;
Expand All @@ -24,6 +24,7 @@ const TextInput: React.FC<Props> = ({
conditionCheckList,
multiline = false,
onTextChange,
...props
}) => {
const [status, setStatus] = useState(value === "" ? "default" : "success"); // default / success / invalid / focus
const [enteredValue, setEnteredValue] = useState(value);
Expand Down Expand Up @@ -75,6 +76,11 @@ const TextInput: React.FC<Props> = ({
}
}, [enteredValue, status, onTextChange]);

useEffect(() => {
// 비동기 API 호출로 인한 value 변경 시에만 실행
setEnteredValue(value);
}, [value]);

return (
<EmotionWrapper>
{label && <span className="label">{label}</span>}
Expand All @@ -97,6 +103,7 @@ const TextInput: React.FC<Props> = ({
onFocus={handleFocus}
onBlur={handleBlur}
data-status={status}
{...props}
/>
) : (
<input
Expand All @@ -107,6 +114,7 @@ const TextInput: React.FC<Props> = ({
onFocus={handleFocus}
onBlur={handleBlur}
data-status={status}
{...props}
/>
)}
{status == "success" && (
Expand Down
14 changes: 11 additions & 3 deletions src/components/radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ interface Props {
children: ReactNode;
disabled?: boolean;
initialValue?: boolean; // 첫 렌더링 시 체크 여부, 이후의 state 변화는 감지하지 않음
className?: string;
}

const Radio = ({ name, value, children, disabled = false, initialValue = false }: Props) => {
const Radio = ({
name,
value,
children,
disabled = false,
initialValue = false,
className,
}: Props) => {
const [checked, setChecked] = useState(initialValue);

const onChangeRadio = (event: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -19,7 +27,7 @@ const Radio = ({ name, value, children, disabled = false, initialValue = false }

return (
<EmotionWrapper>
<label data-checked={checked} data-disabled={disabled}>
<label data-checked={checked} data-disabled={disabled} className={className}>
<input
hidden
type="checkbox"
Expand All @@ -41,7 +49,7 @@ export default Radio;

const EmotionWrapper = styled.div`
label {
width: 80px;
min-width: 80px;
padding: 3px 15px;
height: 100%;

Expand Down
1 change: 1 addition & 0 deletions src/components/radio/items/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const EmotionWrapper = styled.div`

.radio-item-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
`;
5 changes: 5 additions & 0 deletions src/components/radio/types/TRadioItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type TRadioItem = {
name: string;
value: string;
label: string;
};
11 changes: 11 additions & 0 deletions src/constant/limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 맛집 생성 관련 제약
export const RESTAURANT_NAME_MAX_LENGTH = 10; // 맛집 이름 최대 길이
export const RESTAURANT_COMMENT_MAX_LENGTH = 30; // 맛집 한줄평 최대 길이
export const RESTAURANT_ADDRESS_MAX_LENGTH = 30; // 맛집 주소 최대 길이
export const RESTAURANT_OPENING_HOUR_MAX = 30; // 맛집 운영 시간 최대 길이
export const RESTAURANT_RECOMMENDED_MENU_MAX_COUNT = 10; // 맛집 추천 메뉴 최대 개수
export const RESTAURANT_ORDER_TIP_MAX_LENGTH = 30; // 맛집 주문 팁 최대 길이
export const RESTAURANT_LINK_MAX_LENGTH = 200; // 맛집 링크 최대 길이
export const RESTAURANT_IMAGES_MAX_COUNT = 10; // 맛집 이미지 최대 개수
export const RESTAURANT_CAPACITY_MIN_VALUE = 1; // 맛집 수용인원 최소 수
export const RESTAURANT_CAPACITY_MAX_VALUE = 1000; // 맛집 수용인원 최대 수
102 changes: 102 additions & 0 deletions src/feature/restaurants/components/RestaurantForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import styled from "@emotion/styled";
import Button from "components/button/Button";
import FormItemRestaurantAddress from "feature/restaurants/components/formItems/FormItemRestaurantAddress";
import FormItemRestaurantCapacity from "feature/restaurants/components/formItems/FormItemRestaurantCapacity";
import FormItemRestaurantCategoryId from "feature/restaurants/components/formItems/FormItemRestaurantCategoryId";
import FormItemRestaurantComment from "feature/restaurants/components/formItems/FormItemRestaurantComment";
import FormItemRestaurantDelivery from "feature/restaurants/components/formItems/FormItemRestaurantDelivery";
import FormItemRestaurantImages from "feature/restaurants/components/formItems/FormItemRestaurantImages";
import FormItemRestaurantLink from "feature/restaurants/components/formItems/FormItemRestaurantLink";
import FormItemRestaurantName from "feature/restaurants/components/formItems/FormItemRestaurantName";
import FormItemRestaurantOpeningHour from "feature/restaurants/components/formItems/FormItemRestaurantOpeningHour";
import FormItemRestaurantOrderTip from "feature/restaurants/components/formItems/FormItemRestaurantOrderTip";
import FormItemRestaurantRecommendedMenu from "feature/restaurants/components/formItems/FormItemRestaurantRecommendedMenu";
import FormItemTagIds from "feature/restaurants/components/formItems/FormItemTagIds";
import FormTitle from "feature/restaurants/components/formItems/FormTitle";
import { RESTAURANT_FORM_INITIAL_VALUES } from "feature/restaurants/constants/RestaurantFormInitialValues";
import { TTagIdName } from "feature/restaurants/constants/restaurantTagIdsItemList";
import { TRestaurantFormValues } from "feature/restaurants/types/TRestaurantFormValues";
import { useState } from "react";

type TTagId = {
// eslint 가 key 값을 unused var 로 인식. TS 에서 이런 일이 일어나면 안되지만 임시조치
// eslint-disable-next-line no-unused-vars
[key in TTagIdName]: {
checked: boolean;
};
};

type TImageInput = {
imageInput: { files: FileList };
imageUrlInput: { value: string };
};

interface Props {
isEditMode?: boolean;
}

const RestaurantForm = ({ isEditMode = false }: Props) => {
const [formValues, setFormValues] = useState<TRestaurantFormValues>(
RESTAURANT_FORM_INITIAL_VALUES
);

const commonProps = {
isEditMode,
formValues,
setFormValues,
};

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(formValues);

// TODO: 더 나은 방식 생각해내기
const target = event.target as typeof event.target & TTagId & TImageInput;

const mood = target.mood.checked;
const value = target.value.checked;
const kind = target.kind.checked;
const liquor = target.liquor.checked;

console.log("mood", mood);
console.log("value", value);
console.log("kind", kind);
console.log("liquor", liquor);

// images
console.log("newly added images: ", target.imageInput.files);
console.log("exising Images: ", target.imageUrlInput.value);
};

return (
<EmotionWrapper onSubmit={handleSubmit}>
<FormTitle />
<FormItemRestaurantName {...commonProps} />
<FormItemRestaurantComment {...commonProps} />
<FormItemRestaurantAddress {...commonProps} />
<FormItemRestaurantDelivery {...commonProps} />
<FormItemTagIds {...commonProps} />
<FormItemRestaurantCapacity {...commonProps} />
<FormItemRestaurantOpeningHour {...commonProps} />
<FormItemRestaurantRecommendedMenu {...commonProps} />
<FormItemRestaurantCategoryId {...commonProps} />
<FormItemRestaurantOrderTip {...commonProps} />
<FormItemRestaurantLink {...commonProps} />
<FormItemRestaurantImages />
<Button className="submit">저장하기</Button>
</EmotionWrapper>
);
};

export default RestaurantForm;

const EmotionWrapper = styled.form`
display: flex;
flex-direction: column;
row-gap: 36px;

button.submit {
margin-top: 24px;
float: right;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import styled from "@emotion/styled";
import TextInput from "components/inputs/TextInput/TextInput";
import { validateRestaurantAddressLength } from "feature/restaurants/functions/validateRestaurantForm";
import { TRestaurantFormCommonProps } from "feature/restaurants/types/TRestaurantFormCommonProps";
import { useCallback } from "react";

interface Props extends TRestaurantFormCommonProps {}

const FormItemRestaurantAddress: React.FC<Props> = ({ setFormValues }) => {
const handleChangeName = useCallback(
(value: string, isValid: boolean) => {
setFormValues((prev) => ({
...prev,
address: {
value,
isValid,
},
}));
},
[setFormValues]
);

return (
<EmotionWrapper>
<TextInput
label="맛집 주소"
placeholder="간단한 주소를 입력해주세요. ex) 도평건물 1층"
conditionCheckList={[validateRestaurantAddressLength]}
onTextChange={handleChangeName}
/>
</EmotionWrapper>
);
};

export default FormItemRestaurantAddress;

const EmotionWrapper = styled.div``;
Loading