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

[Feature/BAR-6] WriteInput 작성 #8

Merged
merged 38 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
77a4f48
feat: alias 추가
wonjin-dev Dec 15, 2023
82ab400
feat: 스토리북 초안 작성
wonjin-dev Dec 15, 2023
5809aaa
feat: useInput 초안 작성
wonjin-dev Dec 15, 2023
71718b2
refactor: 편의를 위해 @/ alias 제거
wonjin-dev Dec 15, 2023
523dcf3
feat: 제어 컴포넌트 BaseInput 작성
wonjin-dev Dec 15, 2023
3f1bc53
refactor: 스토리 작성 및 컴포넌트 이름 변경
wonjin-dev Dec 15, 2023
b4d2402
feat: 추상화 설계
wonjin-dev Dec 16, 2023
27d58a3
fix: 스토리북 위치 롤백
wonjin-dev Dec 16, 2023
f8ccb55
fix: 병합 문제 해결
wonjin-dev Dec 16, 2023
3d46fa1
[WIP] 스타일링
wonjin-dev Dec 16, 2023
733efc3
feat: 기본 인풋 스타일링 (value가 없을 때)
wonjin-dev Dec 17, 2023
33fcc35
[WIP] feat: 스타일링
wonjin-dev Dec 21, 2023
4008758
fix: 병합 문제 해결
wonjin-dev Dec 23, 2023
bcaf904
Merge branch 'main' of https://github.com/YAPP-Github/23rd-Web-Team-2…
wonjin-dev Dec 26, 2023
13d957e
fix: 컨플릭 해결
wonjin-dev Dec 27, 2023
b598b1e
[WIP] 스타일링
wonjin-dev Dec 30, 2023
67b580c
fix: 병합 문제 해결
wonjin-dev Dec 30, 2023
cb2aa31
fix: 병합 alias 적용
wonjin-dev Dec 30, 2023
874c2ce
fix: 병합 문제 해결
wonjin-dev Jan 1, 2024
ac62aea
fix: 불필요 주석 제거
wonjin-dev Jan 1, 2024
4cfff1b
fix: alias,lint 롤백
wonjin-dev Jan 1, 2024
5f7f8d9
feat: 스토리 추가
wonjin-dev Jan 1, 2024
bde8c5d
feat: 스토리 설명 추가
wonjin-dev Jan 1, 2024
ff64470
style: 기본 스타일링
wonjin-dev Jan 3, 2024
ca5dddc
style: 인풋 스타일링
wonjin-dev Jan 3, 2024
7d74f95
style: 스타일링 얼추 완성
wonjin-dev Jan 5, 2024
554abd9
refactor: 코드 정리
wonjin-dev Jan 5, 2024
1fb72fa
refactor: readme 제거
wonjin-dev Jan 5, 2024
875bafd
refactor: pr에 연관 없는 변경 내역 제거
wonjin-dev Jan 5, 2024
2e9e503
fix: input height 로직 수정
wonjin-dev Jan 6, 2024
0f8efd3
refactor: 네이밍 변경
wonjin-dev Jan 6, 2024
9e14d6f
fix: 병합 문제 해결
wonjin-dev Jan 6, 2024
a9939e1
refactor: svg 적용
wonjin-dev Jan 6, 2024
2ec3437
fix: 타입문제 해결
wonjin-dev Jan 6, 2024
68852c8
fix: 컨벤션 수정
wonjin-dev Jan 6, 2024
19638d1
Merge branch 'main' of https://github.com/YAPP-Github/23rd-Web-Team-2…
wonjin-dev Jan 10, 2024
3ca0569
refactor: 리뷰반영
wonjin-dev Jan 10, 2024
cf1bb68
fix: ignore 제거
wonjin-dev Jan 10, 2024
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
16 changes: 14 additions & 2 deletions pages/index.tsx
dmswl98 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
const HomePage = () => {
return <>Hello World</>;
import type { NextPage } from 'next';

import WriteInput from '@/src/components/Input/WriteInput';
import { useInput } from '@/src/hooks/useInput';

const HomePage: NextPage = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

NextPage와 같은 타입 명시가 필요한가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

자동으로 추론하지 못하기 때문에 필요합니당 !

const testInputProps = useInput({
id: 'test',
defaultValue: '',
});

return (
<WriteInput inputProps={testInputProps} placeholder="메모를 끄적여보세요" />
);
};

export default HomePage;
1 change: 0 additions & 1 deletion plopfile.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable import/no-anonymous-default-export */
export default function (plop) {
const getComponentName = {
type: 'input',
Expand Down
4 changes: 4 additions & 0 deletions src/assets/icons/submit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 115 additions & 0 deletions src/components/Input/WriteInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { ChangeEvent, HTMLAttributes, KeyboardEvent } from 'react';
import { useMemo, useRef, useState } from 'react';
import { assignInlineVars } from '@vanilla-extract/dynamic';

import { MAIN_INPUT_MAX_LENGTH } from '@/src/constants/config';
import type { UseInputReturn } from '@/src/hooks/useInput';
import { COLORS } from '@/src/styles/tokens';

import Icon from '../../SvgIcon';
import * as style from './style.css';

interface WriteInputProps extends HTMLAttributes<HTMLTextAreaElement> {
inputProps: UseInputReturn;
placeholder?: string;
maxLength?: number;
}

const WriteInput = ({
inputProps,
placeholder,
maxLength = MAIN_INPUT_MAX_LENGTH,
}: WriteInputProps) => {
const { id, value } = inputProps;
const inputRef = useRef<HTMLTextAreaElement | null>(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

| null은 제거되어도 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

초기 값을 위해 필요한 부분입니다.

const [textareaHeight, setTextareaHeight] = useState({
row: 1,
lineBreak: {},
});

const handleResize = (e: ChangeEvent<HTMLTextAreaElement>) => {
const { scrollHeight, clientHeight, value } = e.target;

if (value.length === 0) {
setTextareaHeight((prev) => ({
row: 1,
lineBreak: { ...prev.lineBreak, [e.target.value.length]: false },
}));
}

if (scrollHeight > clientHeight) {
setTextareaHeight((prev) => ({
row: prev.row + 1,
lineBreak: { ...prev.lineBreak, [value.length - 1]: true },
}));
}

if (textareaHeight.lineBreak[value.length]) {
setTextareaHeight((prev) => ({
row: prev.row - 1,
lineBreak: { ...prev.lineBreak, [value.length]: false },
}));
}
};

const handleKeydownEnter = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.code === 'Enter') {
setTextareaHeight((prev) => ({
row: prev.row + 1,
lineBreak: { ...prev.lineBreak, [value.length]: true },
}));
}
};

const isValid = useMemo(() => value.length > 0, [value.length]);

return (
<div className={style.conatiner}>
<div
className={style.contentWrapper}
style={assignInlineVars({
[style.inputHeight]: `${textareaHeight.row * 27}px`,
})}
>
<label htmlFor={id} className={style.label}>
<textarea
{...inputProps}
ref={inputRef}
autoComplete="off"
rows={textareaHeight.row}
className={style.input}
placeholder={placeholder}
maxLength={maxLength}
onInput={handleResize}
onKeyDown={handleKeydownEnter}
/>
</label>

<div
className={style.submitWrapper({
multirow: textareaHeight.row > 1,
})}
>
<div className={style.submit}>
{value.length > 0 && (
<span className={style.textCount}>
<span className={style.currentTextCount}>{value.length}</span>
&nbsp;/&nbsp;500자
</span>
)}
<button disabled={!isValid}>
<Icon
icon="submit"
width={48}
height={48}
color={isValid ? COLORS['Blue/Default'] : undefined}
/>
</button>
</div>
</div>
</div>
</div>
);
};

export default WriteInput;
90 changes: 90 additions & 0 deletions src/components/Input/WriteInput/style.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createVar, style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

import { COLORS } from '@/src/styles/tokens';

export const conatiner = style({
display: 'flex',
justifyContent: 'space-between',
borderRadius: '16px',
width: '100%',
padding: '22px 12px 22px 24px',
border: `2px solid ${COLORS['Blue/Gradient']}`,
});

export const inputHeight = createVar();
export const contentWrapper = style({
display: 'flex',
alignItems: 'center',
width: '100%',
height: inputHeight,
minHeight: '27px',
maxHeight: '260px',
});

export const label = style({
width: '100%',
});

export const input = style({
padding: '0',
width: '100%',
maxHeight: '216px',
resize: 'none',
color: COLORS['Grey/900'],
fontSize: '17px',
lineHeight: '27px',
overflowWrap: 'break-word',
'::placeholder': {
color: COLORS['Grey/250'],
},
});

export const submitWrapper = recipe({
base: {
display: 'flex',
alignItems: 'flex-end',
paddingLeft: '20px',
},
variants: {
multirow: {
true: {
height: '100%',
},
},
},
});

export const submit = style({
display: 'flex',
alignItems: 'center',
gap: '16px',
height: '48px',
});

export const textCount = style({
color: COLORS['Grey/400'],
fontSize: '14px',
fontWeight: '400',
whiteSpace: 'nowrap',
});

export const currentTextCount = style({
color: COLORS['Blue/Default'],
fontSize: '14px',
fontWeight: '700',
});

export const alert = style({
display: 'flex',
alignItems: 'center',
marginTop: '12px',
});

export const alertMsg = style({
marginLeft: '6px',
color: COLORS['Grey/600'],
fontSize: '13px',
fontWeight: '600',
lineHeight: '17px',
});
19 changes: 6 additions & 13 deletions src/components/SvgIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { Icon as icon } from '../constants/icon';
import { iconFactory, type Icons } from '../constants/icon';

interface IconProps {
icon: keyof typeof icon;
fill?: string;
stroke?: string;
icon: Icons;
color?: string;
width?: number;
height?: number;
}

const Icon = ({
icon: iconKey,
fill,
stroke,
width = 24,
height = 24,
}: IconProps) => {
const SvgIcon = icon[iconKey];
const Icon = ({ icon, color, width = 24, height = 24 }: IconProps) => {
const SvgIcon = iconFactory[icon];

return <SvgIcon fill={fill} stroke={stroke} width={width} height={height} />;
return <SvgIcon color={color} width={width} height={height} />;
};

export default Icon;
4 changes: 4 additions & 0 deletions src/constants/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* "끄적끄적" 최대 입력 글자 수
*/
export const MAIN_INPUT_MAX_LENGTH = 500;
12 changes: 5 additions & 7 deletions src/constants/icon.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { FC, SVGProps } from 'react';

import Profle from '@/src/assets/icons/profile.svg';
import Submit from '@/src/assets/icons/submit.svg';

export type IconFactory = {
[key: string]: FC<SVGProps<SVGSVGElement>>;
};

export const Icon: IconFactory = {
export const iconFactory = {
Copy link
Member

Choose a reason for hiding this comment

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

상수는 모두 UPPER_CASE로 작성해주시면 감사하겠습니당

Copy link
Member Author

Choose a reason for hiding this comment

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

요건 하드 코딩이 필요한 상수가 아니여서 변수로 사용하는 부분이어서 이렇게 처리하였습니다 !

profile: Profle,
submit: Submit,
};

export type Icons = keyof typeof iconFactory;
19 changes: 19 additions & 0 deletions src/hooks/useInput.ts
dmswl98 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ChangeEvent } from 'react';
import { useState } from 'react';
Comment on lines +1 to +2
Copy link
Member

Choose a reason for hiding this comment

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

@dongkyun-dev
이후에 같은 경로에서 import될 때 type을 inline으로 작성할 지 아니면 위처럼 분리해 작성할지
한 가지 방식으로 강제하는 rule을 추가하면 좋을 것 같습니다!

현재는 두가지 방식 다 작성 가능하도록 되어있더라고요.

Suggested change
import type { ChangeEvent } from 'react';
import { useState } from 'react';
import { useState, type ChangeEvent } from 'react';

Copy link
Contributor

Choose a reason for hiding this comment

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

@wonjin-dev @dmswl98
좋아요 혹시 선호하시는 방식 있으신가요?

Copy link
Member

Choose a reason for hiding this comment

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

@dongkyun-dev 저는 현재 코드에서 많이 사용된 방식으로 하면 좋을 것 같아요.

Copy link
Contributor

Choose a reason for hiding this comment

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

@dmswl98
저는 개인적으로 분리하지 않고 하나의 라인에서 작성하는게 좋긴 한데....! 관련해서 린트룰을 찾아볼까요?

Copy link
Member

@dmswl98 dmswl98 Jan 9, 2024

Choose a reason for hiding this comment

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

@dongkyun-dev 그럼 이렇게 작성하시면 됩니다!!

'@typescript-eslint/consistent-type-imports': [
   'error',
   { fixStyle: 'inline-type-imports' },
],

Copy link
Contributor

Choose a reason for hiding this comment

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

@dmswl98
오오 혹시 반영해서 PR 작성해주실 수 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

린트 룰 추가하고 반영할때 일괄 fix 가시죵


interface UseInputArgs {
id: string;
defaultValue?: string;
}

export const useInput = ({ id, defaultValue = '' }: UseInputArgs) => {
const [value, setValue] = useState(defaultValue);

const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setValue(e.currentTarget.value);
};

return { id, value, onChange };
};

export type UseInputReturn = ReturnType<typeof useInput>;
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"strict": true,
"strict": false,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
Expand Down