11'use client' ;
22
3- import { useEffect , useState } from 'react' ;
3+ import { CompositionEvent , useEffect , useState } from 'react' ;
44import Image from 'next/image' ;
5- import ProblemTable from './ui/Table' ;
6- import { useProblemListQuery } from '@/entities/problems/model/query' ;
5+ import { useAutoCompleteKeywordQuery , useProblemListQuery } from '@/entities/problems/model/query' ;
76import { Select } from '@/shared/ui/select/Select' ;
87import { Button } from '@/shared/ui/button/Button' ;
98import { LevelUtil } from '@/shared/util/levelUtil' ;
10-
11- const categoryCodeOptions = [
12- { label : '출력' , value : 'OUTPUT' } ,
13- { label : '사칙연산' , value : 'ARITHMETIC' } ,
14- { label : '배열' , value : 'ARRAY' } ,
15- { label : '조건문' , value : 'CONDITIONAL' } ,
16- { label : '정렬' , value : 'SORTING' } ,
17- { label : '수학' , value : 'MATH' } ,
18- { label : '시뮬레이션' , value : 'SIMULATION' } ,
19- { label : '자료구조' , value : 'DATA_STRUCTURE' } ,
20- { label : '입문자용' , value : 'FOR_BEGINNER' } ,
21- { label : '구현' , value : 'IMPLEMENTATION' } ,
22- { label : '그리디' , value : 'GREEDY' } ,
23- { label : '문자열 처리' , value : 'STRING_PROCESSING' } ,
24- { label : '해시맵' , value : 'HASH_MAP' } ,
25- { label : '선형 탐색' , value : 'LINEAR_SEARCH' } ,
26- { label : '동적 계획법' , value : 'DP' } ,
27- { label : '순환 탐지' , value : 'CYCLE_DETECTION' } ,
28- { label : '비트 연산' , value : 'BIT_OPERATION' } ,
29- { label : '반복 제어' , value : 'LOOP_CONTROL' } ,
30- { label : '카운팅' , value : 'COUNTING' } ,
31- { label : '너비 우선 탐색' , value : 'BFS' } ,
32- { label : '깊이 우선 탐색' , value : 'DFS' } ,
33- { label : '비트마스킹' , value : 'BITMASK' } ,
34- { label : '해시' , value : 'HASH' } ,
35- { label : '맵' , value : 'MAP' } ,
36- { label : '반복문' , value : 'LOOPS' } ,
37- { label : '분할 정복' , value : 'DIVIDE_AND_CONQUER' } ,
38- { label : '문자열' , value : 'STRING' } ,
39- { label : '집합론' , value : 'SET_THEORY' } ,
40- { label : '누적 합' , value : 'PREFIX_SUM' } ,
41- { label : '기하학' , value : 'GEOMETRY' } ,
42- { label : '이분 탐색' , value : 'BINARY_SEARCH' } ,
43- { label : '투포인터' , value : 'TWO_POINTERS' } ,
44- { label : '그래프 이론' , value : 'GRAPH_THEORY' } ,
45- { label : '탐색' , value : 'SEARCH' } ,
46- { label : '우선순위 큐' , value : 'PRIORITY_QUEUE' } ,
47- { label : '백트래킹' , value : 'BACKTRACKING' } ,
48- { label : '알고리즘' , value : 'ALGORITHM' } ,
49- { label : '트리' , value : 'TREE' } ,
50- { label : '상태 압축' , value : 'STATE_COMPRESSION' } ,
51- { label : '재귀' , value : 'RECURSION' } ,
52- { label : '큐' , value : 'QUEUE' } ,
53- { label : '최대 유량' , value : 'MAX_FLOW' } ,
54- { label : '최소 컷' , value : 'MIN_CUT' } ,
55- { label : '최소 스패닝 트리' , value : 'MINIMUM_SPANNING_TREE' } ,
56- { label : '완전 탐색' , value : 'BRUTE_FORCE' } ,
57- { label : '조합론' , value : 'COMBINATORICS' } ,
58- { label : '세그먼트 트리' , value : 'SEGMENT_TREE' } ,
59- { label : 'Deque' , value : 'DEQUE' } ,
60- { label : '해밍 거리' , value : 'HAMMING_DISTANCE' } ,
61- { label : '2차원 배열' , value : 'TWO_DIMENSIONAL_ARRAY' } ,
62- { label : '누적 선택 최적화' , value : 'CUMULATIVE_SELECTION_OPTIMIZATION' } ,
63- { label : '좌표' , value : 'COORDINATE' } ,
64- { label : '최대공약수(GCD)' , value : 'GCD' } ,
65- { label : '수열' , value : 'SEQUENCE' } ,
66- { label : '집합 처리' , value : 'SET_PROCESSING' } ,
67- { label : '그래프 탐색' , value : 'GRAPH_SEARCH' } ,
68- { label : '분리 집합' , value : 'DISJOINT_SET' } ,
69- { label : '조합' , value : 'COMBINATION' } ,
70- ] ;
9+ import ProblemTable from '@/entities/problems/ui/table/ProblemTable' ;
10+ import { CATEGORY_OPTIONS , DIFFICULTY_OPTIONS } from '@/entities/problems/model/filter-options' ;
7111
7212const ProblemsList = ( ) => {
7313 const [ currentPage , setCurrentPage ] = useState ( 0 ) ;
74-
75- const difficultyOptions = [
76- { label : '입문 (1단계)' , value : 'LV1' } ,
77- { label : '초급 (2단계)' , value : 'LV2' } ,
78- { label : '중급 (3단계)' , value : 'LV3' } ,
79- { label : '중상급 (4단계)' , value : 'LV4' } ,
80- { label : '고급 (5단계)' , value : 'LV5' } ,
81- { label : '전문가 (6단계)' , value : 'LV6' } ,
82- { label : '마스터 (7단계)' , value : 'LV7' } ,
83- ] ;
8414 const [ categoryCode , setCategoryCode ] = useState ( '' ) ;
8515 const [ difficulty , setDifficulty ] = useState ( '' ) ;
8616 const [ keyword , setKeyword ] = useState ( '' ) ;
8717 const [ search , setSearch ] = useState ( '' ) ;
18+ const [ autoCompleteKeyword , setAutoCompleteKeyword ] = useState ( '' ) ;
8819
8920 const { data, isLoading } = useProblemListQuery (
9021 currentPage ,
@@ -95,11 +26,27 @@ const ProblemsList = () => {
9526 search
9627 ) ;
9728
29+ const { data : autoComplete } = useAutoCompleteKeywordQuery ( autoCompleteKeyword ) ;
9830 const totalPages = data ?. totalPages ?? 0 ;
9931
10032 useEffect ( ( ) => {
10133 setCurrentPage ( 0 ) ;
10234 } , [ categoryCode , difficulty ] ) ;
35+
36+ const handleCompositionEnd = ( e : CompositionEvent < HTMLInputElement > ) => {
37+ const value = e . currentTarget . value ;
38+ if ( value . length >= 2 && value . length <= 25 ) {
39+ setAutoCompleteKeyword ( value ) ;
40+ }
41+ } ;
42+
43+ const handleChangeKeyword = ( value : string ) => {
44+ if ( ! value ) {
45+ setAutoCompleteKeyword ( '' ) ;
46+ }
47+ setKeyword ( value ) ;
48+ } ;
49+
10350 return (
10451 < div className = "flex flex-col px-10 py-18 w-full gap-4 justify-center items-center" >
10552 < div className = "flex flex-col max-w-[1600px] w-full gap-6" >
@@ -108,6 +55,7 @@ const ProblemsList = () => {
10855 < p className = "text-gray-400" > 코딩테스트 문제를 난이도별로 확인하고 도전해보세요</ p >
10956 </ section >
11057
58+ { /* 필터영역 */ }
11159 < section className = "flex flex-col gap-10" >
11260 < section className = "mb-6 p-6 bg-gray-900/50 rounded-[10px] border border-gray-800" >
11361 < div className = "flex flex-row gap-4 items-center w-full" >
@@ -119,7 +67,7 @@ const ProblemsList = () => {
11967 id = "category"
12068 className = "w-full"
12169 title = "카테고리"
122- option = { categoryCodeOptions }
70+ option = { CATEGORY_OPTIONS }
12371 setValue = { ( value ) => setCategoryCode ( value ) }
12472 value = { categoryCode }
12573 />
@@ -133,7 +81,7 @@ const ProblemsList = () => {
13381 id = "difficulty"
13482 className = "w-full "
13583 title = "난이도"
136- option = { difficultyOptions }
84+ option = { DIFFICULTY_OPTIONS }
13785 setValue = { ( value ) => setDifficulty ( value ) }
13886 value = { difficulty }
13987 />
@@ -142,18 +90,39 @@ const ProblemsList = () => {
14290 < div className = "flex flex-col gap-1 w-1/3" >
14391 < label className = "text-base text-secondary" > 검색</ label >
14492 < div className = "flex flex-row w-full gap-5" >
145- < input
146- placeholder = "문제 제목 또는 번호 검색"
147- className = "text-base border px-2 border-gray-700 rounded h-12 w-full bg-gray-800"
148- onChange = { ( e ) => setKeyword ( e . target . value ) }
149- onKeyDown = { ( e ) => {
150- if ( e . key === 'Enter' ) {
151- setSearch ( keyword ) ;
152- setCurrentPage ( 0 ) ; // 검색하면 0페이지(첫페이지)로
153- }
154- } }
155- />
156-
93+ < div className = "relative flex-grow" >
94+ < input
95+ value = { keyword }
96+ placeholder = "2~25글자 사이로 검색해주세요"
97+ className = "text-base border px-2 border-gray-700 rounded h-12 w-full bg-gray-800"
98+ onChange = { ( e ) => handleChangeKeyword ( e . target . value ) }
99+ onCompositionEnd = { handleCompositionEnd }
100+ onKeyDown = { ( e ) => {
101+ if ( e . key === 'Enter' ) {
102+ setSearch ( keyword ) ;
103+ setCurrentPage ( 0 ) ; // 검색하면 0페이지(첫페이지)로
104+ }
105+ } }
106+ />
107+ { autoComplete && autoComplete . length > 0 && (
108+ < div className = "absolute top-14 border px-2 border-gray-700 rounded bg-gray-800 w-full" >
109+ { autoComplete . map ( ( item ) => (
110+ < p
111+ key = { item }
112+ className = "py-1 hover:bg-gray-700 cursor-pointer"
113+ onClick = { ( ) => {
114+ setKeyword ( item ) ;
115+ setSearch ( item ) ;
116+ setAutoCompleteKeyword ( '' ) ;
117+ setCurrentPage ( 0 ) ; // 검색하면 0페이지로
118+ } }
119+ >
120+ { item }
121+ </ p >
122+ ) ) }
123+ </ div >
124+ ) }
125+ </ div >
157126 < Button
158127 aria-label = "검색"
159128 onClick = { ( ) => {
@@ -189,9 +158,7 @@ const ProblemsList = () => {
189158 >
190159 { categoryCode !== '전체' && categoryCode && (
191160 < div className = "flex flex-row gap-2 items-center bg-[#00d084]/20 border border-[#00d084]/30 text-[#00d084] px-3 py-1.5 rounded-full text-sm font-medium" >
192- < span >
193- { categoryCodeOptions . find ( ( item ) => item . value === categoryCode ) ?. label }
194- </ span >
161+ < span > { CATEGORY_OPTIONS . find ( ( item ) => item . value === categoryCode ) ?. label } </ span >
195162 < Image
196163 src = "/icons/close/closeWithBorder.svg"
197164 className = "cursor-pointer hover:opacity-70 transition-opacity"
@@ -206,7 +173,7 @@ const ProblemsList = () => {
206173 < div
207174 className = { `flex flex-row gap-2 items-center px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200 border ${ LevelUtil . getLevelBg ( difficulty ) } ${ LevelUtil . getLevelColorClass ( difficulty ) } ` }
208175 >
209- < span > { difficultyOptions . find ( ( item ) => item . value === difficulty ) ?. label } </ span >
176+ < span > { DIFFICULTY_OPTIONS . find ( ( item ) => item . value === difficulty ) ?. label } </ span >
210177 < Image
211178 src = "/icons/close/closeWithBorder.svg"
212179 className = "cursor-pointer hover:opacity-70 transition-opacity"
0 commit comments