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

[단체 생성 및 수정 페이지] 화면구성을 진행합니다. #25

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
47e7f18
feat(Organization): 단체 생성 및 수정 페이지 기본 골격 셋업
jaychang99 Aug 28, 2023
e57aeca
feat(Checkbox): 체크박스 목록 공통 커포넌트 구현
jaychang99 Sep 10, 2023
5647565
feat(Organization): 단체 생성 폼에 단체 종류 묻는 질문 추가
jaychang99 Sep 10, 2023
4d800a6
sync: Merge branch 'develop' into feature/form-organization
jaychang99 Sep 10, 2023
f5cd984
sync: Merge branch 'develop' into feature/form-organization
jaychang99 Sep 10, 2023
ca0699e
feat(Organization): 단체 생성 폼에 단체별 닉네임, 단체이름, 단체 활동 지역 묻는 질문 추가 , 및 제출 …
jaychang99 Sep 10, 2023
43eeffd
feat(Organization): 단체 생성 폼에 공개, 비공개 단체 및 비공개 단체일 시 비밀번호 생성 입력기 추가
jaychang99 Sep 10, 2023
78443e2
feat(Organization): 단체 생성 시 textinput 하는 요소에 validation 규칙 추가
jaychang99 Sep 10, 2023
6ef738d
feat(Organization): 목업으로 초기값 받기 시뮬레이션
jaychang99 Sep 10, 2023
9a6fef7
feat(TextInput): 비동기적 value 변경에도 반응할 수 있도록 처리
jaychang99 Sep 16, 2023
aabc44e
fix(AreaInput): 기본값 주어질 때 무한 리렌더링 되는 문제 해결
jaychang99 Sep 17, 2023
90545f6
fix(AreaInput): 시도, 시군구, 동 중 어느 하나라도 없으면 value 세팅 안하게 조치
jaychang99 Sep 17, 2023
7e7bb30
feat(FormOranization): 단체에 이미지 추가하는 컴포넌트 추가
jaychang99 Sep 17, 2023
840a315
refactor(Organization): 단체 최대 없로드 가능 사진 수를 상수로 관리
jaychang99 Sep 17, 2023
2ebe424
feat(Organization): 문항 순서 조정
jaychang99 Sep 17, 2023
1656c36
fix(Organization): 단체 생성 폼에서 비밀번호 input type password 로 변경
jaychang99 Sep 17, 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
Binary file added public/images/profile-image-default-org.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/components/checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import CheckboxGroup from "components/checkbox/items/CheckboxGroup";
import CheckboxItem from "components/checkbox/items/CheckboxItem";

const Checkbox = () => {};

Checkbox.Group = CheckboxGroup;
Checkbox.Item = CheckboxItem;

export default Checkbox;
39 changes: 39 additions & 0 deletions src/components/checkbox/items/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import styled from "@emotion/styled";
import { Children, HTMLAttributes, ReactNode, cloneElement, isValidElement } from "react";

export interface Props extends HTMLAttributes<HTMLUListElement> {
children: ReactNode;
checkedList: string[];
setCheckedItem: (checkedList: string) => void;
}

export interface CheckboxItemProps {
checked: boolean;
value: string;
label: string;
children: ReactNode;
setChecked: (value: string) => void;
}

const CheckboxGroup = ({ children, checkedList, setCheckedItem, ...props }: Props) => {
const childrenWithProps = Children.map(children, (child) => {
if (isValidElement<CheckboxItemProps>(child)) {
const checked = checkedList.includes(child.props.value);
const value = child.props.value;

const setChecked = () => setCheckedItem(value);

return cloneElement(child, { checked, setChecked, value });
}
return child;
});

return <EmotionWrapper {...props}>{childrenWithProps}</EmotionWrapper>;
};

export default CheckboxGroup;

const EmotionWrapper = styled.ul`
display: flex;
flex-direction: column;
`;
104 changes: 104 additions & 0 deletions src/components/checkbox/items/CheckboxItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import styled from "@emotion/styled";
import { ReactNode } from "react";

export interface Props {
children: ReactNode;
checked?: boolean;
setChecked?: (checked: boolean | string) => void;
value: string;
}

const CheckboxItem = ({ children, checked, setChecked, value }: Props) => {
const handleClickCheckbox = () => {
setChecked?.(!checked);
};

return (
<EmotionWrapper data-checked={checked} onClick={handleClickCheckbox}>
<div data-role="checkbox" data-checked={checked} />
<label data-checked={checked}>{children}</label>
<input hidden type="checkbox" checked={checked} value={value} />
</EmotionWrapper>
);
};

export default CheckboxItem;

const EmotionWrapper = styled.button`
display: flex;
align-items: center;
padding: 4px 8px;
border: 1px solid ${({ theme }) => theme.color.gray200};
border-radius: 4px;
margin: 4px 0;
cursor: pointer;
color: ${({ theme }) => theme.color.gray400};

&[data-checked="true"] {
border: 1px solid ${({ theme }) => theme.color.primary500};
color: ${({ theme }) => theme.color.primary600};
font-weight: 500;
}

div {
width: 20px;
height: 20px;
border: 1px solid ${({ theme }) => theme.color.gray200};
border-radius: 4px;
margin-right: 4px;
cursor: pointer;

&[data-checked="true"] {
background: ${({ theme }) => theme.color.primary500};
border: 1px solid ${({ theme }) => theme.color.primary600};

&::after {
content: "";
display: block;
width: 6px;
height: 10px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
position: relative;
top: 2px;
left: 5px;

&:hover {
border: solid #fff;
border-width: 0 2px 2px 0;

transform: rotate(45deg);
position: relative;
top: 2px;
left: 5px;
}
}
}
}
label {
padding: 10px 10px;
white-space: wrap;
word-break: break-all;

/* &:hover {
border: 1px solid ${({ theme }) => theme.color.primary500};
color: ${({ theme }) => theme.color.primary500};
font-weight: 500;
}

&[data-disabled="true"] {
border: 1px solid ${({ theme }) => theme.color.gray200};
color: ${({ theme }) => theme.color.gray200};
background-color: ${({ theme }) => theme.color.gray100};
cursor: not-allowed;
}

&[data-checked="true"] {
color: #fff;
background: ${({ theme }) => theme.color.primary500};
border: 1px solid ${({ theme }) => theme.color.primary600};
font-weight: 500;
} */
}
`;
36 changes: 25 additions & 11 deletions src/components/inputs/AreaInput/AreaInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ import { useState, useEffect } from "react";
import AreaInputSido from "components/inputs/AreaInput/items/AreaInputSido";
import AreaInputSigungu from "components/inputs/AreaInput/items/AreaInputSigungu";
import AreaInputDong from "components/inputs/AreaInput/items/AreaInputDong";
import usePrevious from "hooks/usePrevious";

interface Props {
label?: string;
value?: number; // 기존에 선택된 지역 id
onSelectValueChange?: (value: number) => void; // 지역 id 콜백 함수
className?: string;
}

const AreaInput: React.FC<Props> = ({ label, value, onSelectValueChange }) => {
const AreaInput: React.FC<Props> = ({ label, value, onSelectValueChange, className }) => {
const [sido, setSido] = useState<string>();
const [sigungu, setSigungu] = useState<string>();
const [dong, setDong] = useState<string>();

const previousSido = usePrevious(sido);
const previousSigungu = usePrevious(sigungu);
const previousDong = usePrevious(dong);

const handleSidoChange = (value: string | undefined) => {
setSido(value);
};
Expand All @@ -28,21 +34,29 @@ const AreaInput: React.FC<Props> = ({ label, value, onSelectValueChange }) => {
};

useEffect(() => {
if (value && !sido && !sigungu && !dong) {
// 초기값이 존재하고, sido가 선택되지 않은 경우
const areaId = `${sido ?? "NN"}${sigungu ?? "NNN"}${dong ?? "NNN"}`;
if (!sido || !sigungu || !dong) return;
onSelectValueChange?.(parseInt(areaId));
}, [sido, sigungu, dong, onSelectValueChange]);

useEffect(() => {
if (value) {
const area = value.toString();
setSido(area.slice(1, 3));
setSigungu(area.slice(3, 6));
setDong(area.slice(6));
}

if (onSelectValueChange && dong) {
onSelectValueChange(parseInt(dong));
const nextSido = area.slice(0, 2);
const nextSigungu = area.slice(2, 5);
const nextDong = area.slice(5);

if (nextSido === previousSido && nextSigungu === previousSigungu && nextDong === previousDong)
return;
setSido(nextSido);
setSigungu(nextSigungu);
setDong(nextDong);
}
}, [value, sido, sigungu, dong, onSelectValueChange]);
}, [value, sido, sigungu, dong, previousSido, previousSigungu, previousDong]);

return (
<EmotionWrapper>
<EmotionWrapper className={className}>
{label && <span className="label">{label}</span>}
<div className="dropdowns">
<AreaInputSido value={sido} onSelectSidoChange={handleSidoChange} />
Expand Down
10 changes: 8 additions & 2 deletions src/components/inputs/ImageInput/ImageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ interface Props {
maxImageCount?: number; // 최대 입력할 수 있는 이미지 개수
existingImageUrlList?: string[]; // 이미 서버에 존재하던 이미지 URL 리스트
label?: string; // 이미지 인풋 레이블
className?: string;
}

const ImageInput: React.FC<Props> = ({ maxImageCount = 1, existingImageUrlList = [], label }) => {
const ImageInput: React.FC<Props> = ({
maxImageCount = 1,
existingImageUrlList = [],
label,
className,
}) => {
const imageUrlRef = useRef<HTMLInputElement>(null); // 이미 서버에 존재하던 이미지 URL 리스트
const fileInputRef = useRef<HTMLInputElement>(null); // 로컬에서 업로드한 이미지 리스트
const fileInputUniqueIndexCount = useRef(0); // 로컬 이미지 리스트의 uniqueIndex를 위한 카운트
Expand Down Expand Up @@ -89,7 +95,7 @@ const ImageInput: React.FC<Props> = ({ maxImageCount = 1, existingImageUrlList =
}, [imageUrlListState]);

return (
<EmotionWrapper>
<EmotionWrapper className={className}>
<label className="label">{label}</label>
<p className="description">최대 {maxImageCount}장 선택</p>
<div className="image-input-item-wrapper">
Expand Down
16 changes: 13 additions & 3 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 @@ -13,6 +13,7 @@ interface Props {
conditionCheckList?: TConditionCheck[];
multiline?: boolean;
onTextChange?: (value: string, isValid: boolean) => void;
className?: string;
}

const TextInput: React.FC<Props> = ({
Expand All @@ -24,6 +25,8 @@ const TextInput: React.FC<Props> = ({
conditionCheckList,
multiline = false,
onTextChange,
className,
...props // input 태그의 나머지 속성들
}) => {
const [status, setStatus] = useState(value === "" ? "default" : "success"); // default / success / invalid / focus
const [enteredValue, setEnteredValue] = useState(value);
Expand Down Expand Up @@ -75,8 +78,13 @@ const TextInput: React.FC<Props> = ({
}
}, [enteredValue, status, onTextChange]);

// 비동기적인 value 변경에 대한 처리 (API 호출 시)
useEffect(() => {
setEnteredValue(value);
}, [value]);

return (
<EmotionWrapper>
<EmotionWrapper className={className}>
{label && <span className="label">{label}</span>}
{conditionList && (
<div className="spanList">
Expand All @@ -97,6 +105,7 @@ const TextInput: React.FC<Props> = ({
onFocus={handleFocus}
onBlur={handleBlur}
data-status={status}
{...props}
/>
) : (
<input
Expand All @@ -107,6 +116,7 @@ const TextInput: React.FC<Props> = ({
onFocus={handleFocus}
onBlur={handleBlur}
data-status={status}
{...props}
/>
)}
{status == "success" && (
Expand Down
7 changes: 7 additions & 0 deletions src/constant/limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// 닉네임 최대 글자수 제한, 파일 업로드 최대 용량 제한 등 각종 제한 사항을 정의하는 파일입니다.
export const NICKNAME_MAX_LENGTH = 10;
export const ORGANIZATION_NAME_MAX_LENGTH = 15;
export const ORGANIZATION_PASSWORD_MIN_LENGTH = 4;
export const ORGANIZATION_PASSWORD_MAX_LENGTH = 15;

export const ORGANIZATION_IMAGE_MAX_COUNT = 1;
Loading