Skip to content

Commit

Permalink
[FEAT] "간편 입력" 에서 문제 언어를 정할 수 있는 기능을 추가 (#133)
Browse files Browse the repository at this point in the history
* ✨ feat: AlgorithmPool의 검색 창의 placeholder를 더 명확한 의미를 가지는 문구로 변경

* ✨ feat: 쿼리의 최대 글자 수를 300자에서 512자로 변경

* ✨ feat: 최초 설치 시 생성되는 추첨에서 한국어 문제만 결과로 나오도록 설정

* ✨ feat: 문제 언어에 대응되는 타입을 추가

* ✨ feat: 문제 언어를 고를 수 있도록 validator / 커스텀 훅에 기능을 추가

* ✨ feat: "간편 입력" 모드에서 문제 언어를 고를 수 있는 UI를 구현하고, 커스텀 훅 및 Select 컴포넌트를 연결

* ✨ feat: 쿼리 생성을 담당하는 도메인에서 문제 언어를 반영할 수 있도록 개선

* ✨ feat: 추첨 수정 모달에서도 솔브드 고급검색 페이지 참고 문구를 보여주도록 개선

- 기존에는 직접 입력을 누른 경우에만 이 문구가 보이기에, 직접 입력 메뉴를 사용하지 않는 유저는 이 팁을 발견하지 못할 수 있음

* ♻️ refactor: 한 곳에서만 쓰이는 상수를 카멜 케이스로 변경하고, SearchOperator 타입을 직접 명시적으로 사용하도록 변경

* 🧪 test: 변경된 쿼리의 최대 글자 수에 맞춰 쿼리 검증 로직의 테스트 코드를 수정

* 🧪 test: 언어 설정을 반영한 쿼리 생성 도메인 로직 테스트를 반영

* 🧪 test: 변경된 쿼리의 최대 글자 수에 맞춰 쿼리 생성 시 폼의 데이터를 검증하는 로직의 테스트 코드를 수정
  • Loading branch information
wzrabbit authored Oct 25, 2024
1 parent eb303ec commit 2045677
Show file tree
Hide file tree
Showing 18 changed files with 167 additions and 45 deletions.
2 changes: 1 addition & 1 deletion components/AlgorithmPool/AlgorithmPool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const AlgorithmPool = () => {
<SearchIcon />
</S.SearchIconWrapper>
<S.SearchInput
placeholder="검색어를 입력해 주세요..."
placeholder="알고리즘 분류를 입력해 주세요..."
value={keyword}
onChange={handleChangeKeyword}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ export const Form = styled.form`
width: 600px;
max-width: 100%;
height: 270px;
height: 320px;
`;

export const Label = styled.label`
display: flex;
flex-direction: column;
row-gap: 6px;
`;

export const InformationTextContainer = styled.div`
display: flex;
flex-direction: column;
row-gap: 5px;
`;
19 changes: 17 additions & 2 deletions components/QuickSlotMenu/SlotEditModal/SlotEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { theme } from '@/styles/theme';
import Modal, { ModalActionButtonsContainer } from '@/components/common/Modal';
import IconButton from '@/components/common/IconButton';
import Text from '@/components/common/Text';
import TextLink from '@/components/common/TextLink';
import Textarea from '@/components/common/Textarea';
import Input from '@/components/common/Input';
import ErrorText from '@/components/common/ErrorText';
import useSlotEditModal from '@/hooks/randomDefense/useSlotEditModal';
import { CloseCircleIcon, CheckCircleIcon } from '@/assets/svg';
import * as S from './SlotEditModal.styled';
import { MAX_CUSTOM_QUERY_LENGTH } from '@/constants/randomDefense';

interface SlotEditModalProps {
title: string;
Expand Down Expand Up @@ -75,15 +77,28 @@ const SlotEditModal = (props: SlotEditModalProps) => {
name="query"
value={query}
ref={queryRef}
maxLength={300}
placeholder="1 ~ 300자"
maxLength={MAX_CUSTOM_QUERY_LENGTH}
placeholder={`1 ~ ${MAX_CUSTOM_QUERY_LENGTH}자`}
hasError={isQueryElementHasErrors}
ariaLabel="새로운 쿼리를 입력해주세요"
onChange={(event) => {
setQuery(event.target.value);
}}
/>
</S.Label>
<S.InformationTextContainer>
<Text type="normal" fontSize="14px">
solved.ac 검색 쿼리 작성법을 모르신다면,{' '}
<TextLink href="https://solved.ac/search" fontSize="14px">
solved.ac 문제 고급 검색
</TextLink>{' '}
페이지를 확인해 보세요!
</Text>
<Text type="normal" fontSize="14px">
추첨은 비로그인 상태에서 진행되므로, 서포터 전용 쿼리는 사용할 수
없음에 유의해 주세요.
</Text>
</S.InformationTextContainer>
<ErrorText fontSize="14px" errorMessage={errorMessage} />
</S.Form>
<ModalActionButtonsContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const Form = styled.form`
width: 100%;
height: 100%;
padding-top: 6px;
padding-top: 22px;
z-index: 1;
`;
Expand Down
43 changes: 39 additions & 4 deletions components/RandomDefenseCreateMenu/RandomDefenseCreateMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,34 @@ import AlgorithmSearchInput from './AlgorithmSearchInput';
import useRandomDefenseCreateMenu from '@/hooks/randomDefense/useRandomDefenseCreateMenu';
import * as S from './RandomDefenseCreateMenu.styled';
import type { SlotNo } from '@/types/randomDefense';
import Select from '@/components/common/Select';
import { MAX_CUSTOM_QUERY_LENGTH } from '@/constants/randomDefense';

interface RandomDefenseCreateMenuProps {
selectedSlotNo: SlotNo;
isLoaded: boolean;
onSubmit: (title: string, query: string) => void;
}

const languageOptions = [
{
label: '한국어',
value: 'ko',
},
{
label: '영어',
value: 'en',
},
{
label: '한국어 및 영어',
value: 'ko/en',
},
{
label: '모든 언어',
value: 'all',
},
];

const RandomDefenseCreateMenu = (props: RandomDefenseCreateMenuProps) => {
const { selectedSlotNo, isLoaded, onSubmit } = props;
const {
Expand All @@ -27,6 +48,7 @@ const RandomDefenseCreateMenu = (props: RandomDefenseCreateMenuProps) => {
handle,
solvedMin,
solvedMax,
language,
startTier,
endTier,
searchOperator,
Expand All @@ -36,6 +58,7 @@ const RandomDefenseCreateMenu = (props: RandomDefenseCreateMenuProps) => {
errorElementName,
setMode,
setRandomDefenseInputValue,
setLanguage,
setTierRange,
setSearchOperator,
setAlgorithmIds,
Expand All @@ -48,7 +71,7 @@ const RandomDefenseCreateMenu = (props: RandomDefenseCreateMenuProps) => {
} = useRandomDefenseCreateMenu({ selectedSlotNo, onSubmit });

return (
<NamedFrame width="650px" height="357px" padding="10px" title="추첨 만들기">
<NamedFrame width="650px" height="373px" padding="10px" title="추첨 만들기">
<S.Form>
<S.ErrorTextWrapper>
<ErrorText errorMessage={errorMessage} fontSize="14px" />
Expand Down Expand Up @@ -76,7 +99,7 @@ const RandomDefenseCreateMenu = (props: RandomDefenseCreateMenuProps) => {
</S.Label>
{mode === 'easy' ? (
<>
<S.Row $columnGap="30px">
<S.Row $columnGap="50px">
<S.Label $width="190px">
<Text type="primary" fontSize="16px">
검색에서 제외할 닉네임
Expand Down Expand Up @@ -130,6 +153,18 @@ const RandomDefenseCreateMenu = (props: RandomDefenseCreateMenuProps) => {
/>
</S.SolvedRangeInputsContainer>
</S.PanelContainer>
<S.PanelContainer $width="190px">
<Text type="primary" fontSize="16px">
문제 언어
</Text>
<Select
options={languageOptions}
selectedValue={language}
width="130px"
ariaLabel="문제 언어 고르기"
onChange={setLanguage}
/>
</S.PanelContainer>
</S.Row>
<S.PanelContainer $width="100%">
<Text type="primary" fontSize="16px">
Expand Down Expand Up @@ -173,9 +208,9 @@ const RandomDefenseCreateMenu = (props: RandomDefenseCreateMenuProps) => {
height="160px"
name="customQuery"
value={customQuery}
placeholder="1 ~ 300자"
placeholder={`1 ~ ${MAX_CUSTOM_QUERY_LENGTH}자`}
minLength={1}
maxLength={300}
maxLength={MAX_CUSTOM_QUERY_LENGTH}
hasError={errorElementName === 'customQuery'}
ariaLabel="쿼리"
onChange={setRandomDefenseInputValue}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { CheckIcon } from '@/assets/svg';
import type { SearchOperator } from '@/types/randomDefense';
import * as S from './SearchOperatorSelect.styled';

interface SearchOperatorSelectProps {
searchOperator: 'OR' | 'AND' | 'NOR';
onClick: (searchOperator: 'OR' | 'AND' | 'NOR') => void;
searchOperator: SearchOperator;
onClick: (searchOperator: SearchOperator) => void;
}

const OPERATORS = ['OR', 'AND', 'NOR'] as const;
const operators: readonly SearchOperator[] = ['OR', 'AND', 'NOR'];

const SearchOperatorSelect = (props: SearchOperatorSelectProps) => {
const { searchOperator, onClick } = props;

return (
<S.Container>
{OPERATORS.map((operator) => (
{operators.map((operator) => (
<S.ButtonContainer key={operator}>
{operator === searchOperator && (
<S.CheckIconWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const Container = styled.div`

export const HistoryListContainer = styled.div`
width: 348px;
height: 414px;
height: 429px;
padding: 8px;
border: 1px solid ${({ theme }) => theme.color.LIGHT_BROWN};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const RandomDefenseHistoryMenu = () => {
useModal<'confirmClearHistory'>();

return (
<NamedFrame width="370px" height="537px" padding="10px" title="추첨 기록">
<NamedFrame width="370px" height="553px" padding="10px" title="추첨 기록">
<S.Container>
{isLoaded && (
<>
Expand Down
2 changes: 1 addition & 1 deletion constants/randomDefense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ export const MAX_SOLVED_COUNT = 100_000_000;

export const NUMBER_REGEX = /^([1-9][0-9]*|0)$/;

export const MAX_CUSTOM_QUERY_LENGTH = 300;
export const MAX_CUSTOM_QUERY_LENGTH = 512;
6 changes: 3 additions & 3 deletions domains/dataHandlers/dataInitializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ const INITIAL_QUICK_SLOTS: QuickSlots = {
1: {
isEmpty: false,
title: '골드 랜덤 디펜스',
query: 'o? -w? *g',
query: 'o? -w? *g lang:ko',
},
2: {
isEmpty: false,
title: '실버 랜덤 디펜스',
query: 'o? -w? *s',
query: 'o? -w? *s lang:ko',
},
3: { isEmpty: false, title: '올 랜덤', query: '(*0&!s?|!*0) o? -w?' },
3: { isEmpty: false, title: '올 랜덤', query: '(*0&!s?|!*0) o? -w? lang:ko' },
4: { isEmpty: true },
5: { isEmpty: true },
6: { isEmpty: true },
Expand Down
10 changes: 10 additions & 0 deletions domains/randomDefense/randomDefenseFormDataValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const emptyValidFormData: RandomDefenseFormData = {
handle: '',
solvedMin: '',
solvedMax: '',
language: 'ko',
startTier: 1,
endTier: 30,
searchOperator: 'OR',
Expand All @@ -29,6 +30,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: 'testuser1234',
solvedMin: '1',
solvedMax: '2',
language: 'ko',
startTier: 1,
endTier: 30,
searchOperator: 'OR',
Expand All @@ -41,6 +43,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: '1_ABc__DeFgHIJ0_KlM9',
solvedMin: '0',
solvedMax: '100000',
language: 'en',
startTier: 0,
endTier: 30,
searchOperator: 'AND',
Expand All @@ -53,6 +56,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: 'abc',
solvedMin: '300',
solvedMax: '',
language: 'ko/en',
startTier: 5,
endTier: 10,
searchOperator: 'NOR',
Expand All @@ -65,6 +69,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: '',
solvedMin: '',
solvedMax: '10000',
language: 'all',
startTier: 3,
endTier: 3,
searchOperator: 'OR',
Expand All @@ -77,6 +82,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: '',
solvedMin: '100000000',
solvedMax: '100000000',
language: 'ko',
startTier: 19,
endTier: 19,
searchOperator: 'OR',
Expand All @@ -89,6 +95,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: ' ',
solvedMin: ' ',
solvedMax: ' ',
language: 'en',
startTier: 8,
endTier: 29,
searchOperator: 'OR',
Expand All @@ -101,6 +108,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: '',
solvedMin: '',
solvedMax: '0',
language: 'ko/en',
startTier: 10,
endTier: 15,
searchOperator: 'NOR',
Expand All @@ -113,6 +121,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: 'Invalid Handle',
solvedMin: '-1029',
solvedMax: '-92823',
language: 'all',
startTier: 0,
endTier: 30,
searchOperator: 'OR',
Expand All @@ -125,6 +134,7 @@ describe('# Test 1 - 정상 추첨 판정', () => {
handle: '',
solvedMin: '',
solvedMax: '',
language: 'all',
startTier: 20,
endTier: 20,
searchOperator: 'NOR',
Expand Down
9 changes: 9 additions & 0 deletions domains/randomDefense/randomDefenseFormDataValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@ import {
isTierWithoutNotRatable,
} from '@/types/typeGuards';
import type {
Language,
RandomDefenseFormData,
RandomDefenseFormDataVerdict,
} from '@/types/randomDefense';

export const isLanguage = (data: unknown): data is Language => {
return (
typeof data === 'string' && ['ko', 'en', 'ko/en', 'all'].includes(data)
);
};

export const isRandomDefenseFormData = (
data: unknown,
): data is RandomDefenseFormData => {
Expand All @@ -27,6 +34,7 @@ export const isRandomDefenseFormData = (
'handle' in data &&
'solvedMin' in data &&
'solvedMax' in data &&
'language' in data &&
'startTier' in data &&
'endTier' in data &&
'searchOperator' in data &&
Expand All @@ -36,6 +44,7 @@ export const isRandomDefenseFormData = (
['easy', 'manual'].includes(data.mode) &&
typeof data.title === 'string' &&
typeof data.handle === 'string' &&
typeof data.language === 'string' &&
typeof data.solvedMin === 'string' &&
typeof data.solvedMax === 'string' &&
isNumericArray(data.algorithmIds) &&
Expand Down
Loading

0 comments on commit 2045677

Please sign in to comment.