-
Notifications
You must be signed in to change notification settings - Fork 2
[FEAT] 입력 검증 디바운스 + 붉은 테두리·카운터 현출 #480
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
2236edc
[FEAT] 토론 테이블 입력 검증 규칙 및 디바운스 훅 추가
coli-geonwoo 044c3af
[FEAT] ClearableInput 에러 테두리·글자 수 카운터 지원
coli-geonwoo 9739854
[FEAT] 토론 기본정보 입력 디바운스 검증 및 에러 표시 연결
coli-geonwoo 3fa8aad
[FEAT] 타임박스 발언 유형 입력 디바운스 검증 연결
coli-geonwoo babcbf3
[FIX] 글자 수 카운터 i18n 처리
coli-geonwoo ee150f7
[FIX] 기본정보 제출 시 전체 필드 재검증
coli-geonwoo 0c23ab9
[FIX] 기본정보 제출 검증 실패 시 사유 메시지 노출
coli-geonwoo 014ed9e
[TEST] 기본정보 제출 검증 실패 시 alert 노출 테스트 추가
coli-geonwoo 4cdd24d
[FEAT] 기본정보 제출 검증 alert를 인라인 에러 메시지로 전환
coli-geonwoo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { useEffect, useState } from 'react'; | ||
|
|
||
| /** | ||
| * 값의 변경이 delay(ms) 동안 멈추면(= 입력이 끝났다고 판단되면) 갱신되는 디바운스 값을 반환한다. | ||
| * 입력 중에는 이전 값을 유지하므로, 타이핑 도중이 아니라 입력이 마쳤을 때 검증을 트리거하는 데 사용한다. | ||
| */ | ||
| const useDebounce = <T>(value: T, delay = 400): T => { | ||
| const [debouncedValue, setDebouncedValue] = useState<T>(value); | ||
|
|
||
| useEffect(() => { | ||
| const timer = setTimeout(() => setDebouncedValue(value), delay); | ||
| return () => clearTimeout(timer); | ||
| }, [value, delay]); | ||
|
|
||
| return debouncedValue; | ||
| }; | ||
|
|
||
| export default useDebounce; |
122 changes: 122 additions & 0 deletions
122
src/page/TableComposition/components/TableNameAndType/TableNameAndType.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { describe, it, expect, vi } from 'vitest'; | ||
| import { render, screen } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||
| import { MemoryRouter } from 'react-router-dom'; | ||
| import { GlobalPortal } from '../../../../util/GlobalPortal'; | ||
| import { DebateInfo } from '../../../../type/type'; | ||
| import TableNameAndType from './TableNameAndType'; | ||
|
|
||
| // ------------------ | ||
| // 헬퍼: 유효한 기본 info 를 만들고 필요한 필드만 덮어쓴다. | ||
| // ------------------ | ||
| function makeInfo(overrides: Partial<DebateInfo> = {}): DebateInfo { | ||
| return { | ||
| name: '토론 시간표', | ||
| agenda: '토론 주제', | ||
| prosTeamName: '우리팀', | ||
| consTeamName: '상대팀', | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| function renderComponent(info: DebateInfo, onButtonClick = vi.fn()) { | ||
| const queryClient = new QueryClient(); | ||
| render( | ||
| <QueryClientProvider client={queryClient}> | ||
| <GlobalPortal.Provider> | ||
| <MemoryRouter> | ||
| <TableNameAndType | ||
| info={info} | ||
| isLoading={false} | ||
| onInfoChange={vi.fn()} | ||
| onButtonClick={onButtonClick} | ||
| /> | ||
| </MemoryRouter> | ||
| </GlobalPortal.Provider> | ||
| </QueryClientProvider>, | ||
| ); | ||
| } | ||
|
|
||
| async function clickNext() { | ||
| await userEvent.click(screen.getByRole('button', { name: '다음' })); | ||
| } | ||
|
|
||
| describe('TableNameAndType - 제출 검증 인라인 에러', () => { | ||
| // useDebounce 는 초기 useState(value) 라 마운트 시 디바운스값=초기값 → | ||
| // 무효값으로 렌더하면 인라인 사유 메시지가 즉시 노출된다. | ||
| it('시간표 이름 길이 초과 시 사유 메시지를 인라인으로 노출하고 진행을 막는다', async () => { | ||
| const onButtonClick = vi.fn(); | ||
| renderComponent(makeInfo({ name: 'a'.repeat(21) }), onButtonClick); | ||
|
|
||
| expect( | ||
| await screen.findByText( | ||
| '시간표 이름은 최대 20자까지 입력할 수 있습니다.', | ||
| ), | ||
| ).toBeInTheDocument(); | ||
|
|
||
| await clickNext(); | ||
|
|
||
| expect(onButtonClick).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('토론 주제 길이 초과 시 사유 메시지를 인라인으로 노출한다', async () => { | ||
| renderComponent(makeInfo({ agenda: 'a'.repeat(256) })); | ||
|
|
||
| expect( | ||
| await screen.findByText('토론 주제는 최대 255자까지 입력할 수 있습니다.'), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('팀명 길이 초과 시 사유 메시지를 인라인으로 노출한다', async () => { | ||
| renderComponent(makeInfo({ prosTeamName: 'a'.repeat(16) })); | ||
|
|
||
| expect( | ||
| await screen.findByText('팀명은 최대 15자까지 입력할 수 있습니다.'), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('팀명에 사용할 수 없는 문자가 있으면 형식 오류 메시지를 인라인으로 노출한다', async () => { | ||
| // 제어문자(U+0001)는 NAME_REGEX 를 통과하지 못한다(FORM 오류). | ||
| renderComponent( | ||
| makeInfo({ consTeamName: `team${String.fromCharCode(1)}` }), | ||
| ); | ||
|
|
||
| expect( | ||
| await screen.findByText( | ||
| '팀명에 사용할 수 없는 문자가 포함되어 있습니다.', | ||
| ), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('여러 필드가 잘못되면 각 입력창 아래 사유 메시지를 동시에 노출한다', async () => { | ||
| const onButtonClick = vi.fn(); | ||
| renderComponent( | ||
| makeInfo({ name: 'a'.repeat(21), prosTeamName: 'b'.repeat(16) }), | ||
| onButtonClick, | ||
| ); | ||
|
|
||
| expect( | ||
| await screen.findByText( | ||
| '시간표 이름은 최대 20자까지 입력할 수 있습니다.', | ||
| ), | ||
| ).toBeInTheDocument(); | ||
| expect( | ||
| screen.getByText('팀명은 최대 15자까지 입력할 수 있습니다.'), | ||
| ).toBeInTheDocument(); | ||
|
|
||
| await clickNext(); | ||
|
|
||
| expect(onButtonClick).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('모든 필드가 유효하면 에러 메시지 없이 onButtonClick 을 호출한다', async () => { | ||
| const onButtonClick = vi.fn(); | ||
| renderComponent(makeInfo(), onButtonClick); | ||
|
|
||
| await clickNext(); | ||
|
|
||
| expect(screen.queryByRole('alert')).not.toBeInTheDocument(); | ||
| expect(onButtonClick).toHaveBeenCalledOnce(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
초과 글자 이상으로는 입력되지 않도록 하는 방법이랑 비교했을 때 이 방식을 선택하신 이유도 궁금해요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안그래도 처음 작업 같은 경우는 Hard Limit이 있었는데요.
Hard Limit은 유저에게 제한 요건을 필수적으로 만족하도록 해서 개발자 입장에서는 안정성을 보장받을 수 있지만
잘못된 입력사안에 대한 회복을 인지하고 몇자를 줄여야 하는지 안내하는 가이드가 부족하다고 느꼈던 것 같아요.
Hard Limit을 쓰면서 느꼈던 불편함
그래서 Hard Limit에서 일단 유저의 입력을 받고(붙여넣기 등등) 이를 제한에 맞게 조정할 수 있도록 글자수 카운터로 바꾸었습니다.
썬데이는 Hard Limit 넣는게 더 괜찮다고 생각하나요? 만약 그 경우에는 붉은색 테두리 + 디바운싱 관련된 부분이 필요치 않게 될 수 있을 것 같아요.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
알림창 적용이 된 것과 콜리의 의견을 확인하고 생각해봤어요. 지정 글자수 이상으로 입력이 안되게 하기 + 글자수 카운터 두고 잘못된 입력 사안에 대한 가이드를 인라인으로 바로 보여주는 방법은 어떨까요? ? ? ?
요렇게 바로 밑에 보이는 방식

이유는 아래와 같아요.
현재는 확인을 누른 후 알림창으로 잘못된 입력 사안에 대한 피드백이 나와서 카운트는 즉각적으로 보일 수 있도록 만들었는데 메세지는 알림창으로 나오니까 (메세지 인지까지 다음 버튼 + 안내창 확인 버튼 두 번의 유저 행동이 필요함) 즉각적인 느낌은 덜 드는 것 같아요.
아래 사진 처럼 여러개가 한번에 나와서 잘못된 입력을 많이 한 경우에 모든 메세지를 바로 받아들이기 힘들수도 있다고 생각합니다
!!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 아이디어 감사합니다 썬데이! 저도 썬데이가 말한 방향이 합리적이라고 생각해서 인라인 메시지를 박스 내에 현출하는 것으로 바꾸었습니다.
다만 Hard Limit을 걸지는 않았어요. 그 이유는 Hard Limit과 카운터가 공존할 수 없기 때문입니다. 이미 우리가 원한 limit 까지 밖에 입력을 못하도록 강제하는 순간 카운터가 큰 의미가 없어지는 상황이 생겼어요.
그래서 인라인 박스 현출 + 만약 조건을 만족시키지 못하는 박스가 있다면 다음으로 이동하는 버튼이 blocked 되도록 수정했습니다!