이번 서울시 유치원 정보 사이트 프로젝트를 설계할 때 서울시 구 이름 버튼으로 유치원을 찾을 수 있으면서도 홈 화면에서 직접 검색 기능을 넣으면 ux가 더 좋을 것 같다고 판단되었다.
홈 화면 와이어프레임
검색 인풋에 유치원 명을 넣으면 리스트 페이지에서 검색된 유치원 목록 나오게 구현되도록 계획하였다.
검색 시 리스트페이지 와이어프레임
인풋에 밸류를 입력하고 검색버튼을 누르면 searchedKeyword 변수에 검색어를 localStorage로 저장 후 리스트 페이지로 이동된다.
// 검색창 인풋값으로 검색해주는 버튼
document . querySelector ( ".search-btn" )
. addEventListener ( "click" , function () {
// 인풋 값 가져오기
searchedKeyword = searchInput . value . trim (); // 공백 제거
// 인풋이 비어있지 않은 경우에만 검색 수행
if ( searchedKeyword !== "" ) { // 부정연산자
localStorage . setItem ( 'searchedKeyword' , searchedKeyword );
location . href = 'list.html' ;
} else {
// 인풋이 비어있을 때 처리 (선택 사항: 경고 메시지 등을 표시 가능)
console . log ( "검색어를 입력해주세요." );
}
});
리스트페이지에서는 저장되었던 searchedKeyword를 localStorage를 통해 가져온다.
document . addEventListener ( "DOMContentLoaded" , function () {
let clickedGuName = localStorage . getItem ( 'clickedGuName' );
let searchedKeyword = localStorage . getItem ( 'searchedKeyword' );
리스트페이지에는 이전에 작업한 것 처럼 서울시 구 이름 버튼을 클릭해서 리스트가 나올 수도 있고 검색을 통해서도 리스트가 나올 수 있게 하고 싶었다. 그리고 이걸 구현 할 때 왠만해서는 리스트 페이지를 분리하지 않고 한 파일에서 처리하고 싶었다.
중간중간 시행착오도 있었지만 gpt에게 물어보며 그렇게 두가지를 할 수 있는 리스트 페이지로 구축할 수 있었다.
document . addEventListener ( "DOMContentLoaded" , function () {
let clickedGuName = localStorage . getItem ( 'clickedGuName' );
let searchedKeyword = localStorage . getItem ( 'searchedKeyword' );
// 검색 키워드가 있을 경우 우선 처리
if ( searchedKeyword ) {
directSearchList ( searchedKeyword );
listTitle = `
<h1>검색된 유치원 정보</h1>
` ;
document . querySelector ( ".list-title" ). insertAdjacentHTML ( "beforeend" , listTitle );
}
// 검색 키워드가 없을 때 clickedGuName 처리
else if ( clickedGuName ) {
searchList ( clickedGuName );
listTitle = `
<h1> ${ clickedGuName } 유치원 정보</h1>
` ;
document . querySelector ( ".list-title" ). insertAdjacentHTML ( "beforeend" , listTitle );
}
else {
console . log ( "No guName or searchedKeyword!" );
}
});
검색 키워드(searchedKeyword)가 있는 경우 우선적으로 처리하고, 검색 키워드가 없을 때에는 clickedGuName으로 처리.
두 케이스에 대해 페이지 타이틀명도 다르게 나올 수 있게 구성하였다.
리스트 생성에 있어서 위에서 설명한 두 케이스는 받아오는 데이터가 달라서 다른 function을 사용했다.
구 이름 버튼으로 리스트를 뽑을 때에는 기존에 만든 searchList라는 기능을 사용하고,
function searchList ( clickedGuName ) {
fetch ( "./json/kinderGeneral.json" )
. then (( response ) => response . json ())
. then (( data ) => {
검색 인풋에 내용입력으로 리스트를 생성할 때에는 directSearchList라는 기능을 새로 진행했다.
function directSearchList ( searchedKeyword ) {
fetch ( "./json/kinderGeneral.json" )
. then (( response ) => response . json ())
. then (( data ) => {
searchedKeyword로 검색한 내용을 포함한 유치원 명을 모두 포문으로 돌려서 해당하는 유치원명 데이터들을 가져왔다.
//인풋에 입력한 searchedKeyword 포함한 모든 유치원명을 searchedArray라는 배열로 넣어줌
for ( let i = 0 ; i < data . DATA . length ; i ++ ) {
if ( data . DATA [ i ]. kindername . includes ( searchedKeyword )) {
searchedArray . push ( data . DATA [ i ]. kindername );
}
}
그렇게 얻은 유치원명은 단수일 수도 있고 복수일 수도 있어서 배열 형태로 변수에 저장했다. searchedArray라는 변수를 배열형태로 하나 만들어주고 방금 서치해준 유치원명 데이터들을 모두 배열형태로 푸시해서 넣어주었다.
//검색창에서 검색 했을 때 그 키워드를 포함하는 유치원명을 모두 전역변수 배열로 저장
let searchedArray = [];
그러면 유치원 명들이 배열 형태로 searchedArray에 저장되는데 push로 들어가다보니 계속 배열안에 데이터가 쌓여나가는 현상이 일어났고 그걸 해결하기 위해 fetch 안쪽에 searchedArray가 쌓이지 않도록 초기화해주는 구문을 넣었다.
function directSearchList ( searchedKeyword ) {
fetch ( "./json/kinderGeneral.json" )
. then (( response ) => response . json ())
. then (( data ) => {
//searchedArray가 push되면서 배열이 계속 쌓이는 현상을 초기화 해줌
searchedArray = [];
//인풋에 입력한 searchedKeyword 포함한 모든 유치원명을 searchedArray라는 배열로 넣어줌
for ( let i = 0 ; i < data . DATA . length ; i ++ ) {
if ( data . DATA [ i ]. kindername . includes ( searchedKeyword )) {
searchedArray . push ( data . DATA [ i ]. kindername );
}
}
그 후 전체 데이터 유치원 명들과 searchedArray에 담긴 유치원 명들을 filter를 통해 해당 유치원들 전체 데이터 배열을 뽑아냈다.
//searchedArray에 있는 유치원명들을 kindername으로 가지고 있는 데이터들을 배열로 가져옴
let directKinderInfos = data . DATA . filter (( item ) =>
searchedArray . includes ( item . kindername . trim ())
);
for ( let i = 0 ; i < directKinderInfos . length ; i ++ ) {
if ( directKinderInfos ) {
let listCard = `html 내용`
그리고는 기존 searchList때 했던 것과 같은 html 정보를 넣어주었고
<div class="list-card" id="list ${ [ i ] } ">
<div class="list-contentsGrp">
<div class="list-contents">
<div class="kinder-title">
<div class="kinder-tag" id="list-establish ${ [ i ] } ">
${ directKinderInfos [ i ]. establish }
</div>
<div class="kinder-name" id="list-name ${ [ i ] } ">
${ directKinderInfos [ i ]. kindername }
</div>
</div>
<div class="list-infoGrp">
<div class="list-info" id="list-num ${ [ i ] } ">
${ directKinderInfos [ i ]. telno }
</div>
<div class="list-info" id="list-addr ${ [ i ] } ">
${ directKinderInfos [ i ]. addr }
</div>
</div>
.......
이렇게 검색결과를 리스트페이지로 넣는것도 성공
검색 인풋 입력
검색된 정보를 list.html에서 나타냄
이제 그 리스트를 클릭했을 때 해당 유치원에 대한 상세정보를 디테일 페이지에서 보여주는 작업이다.
이건 생각보다 에러가 많이났고 처음엔 구이름 버튼으로 진행한 케이스와 검색으로 진행한 케이스에 대해 디테일 페이지를 나눠서 어찌저찌 해결 했었다. 하지만 디테일 페이지가 두 개로 나눠졌다는 게 계속 찝찝했었고 향후 유지보수도 힘들거라는 판단에 gpt와 함께 하나의 디테일 페이지로 진행할 수 있는 방법을 최대한 찾아보았다.
gpt가 중간에 좋은 힌트를 주었었는데 유치원 json 데이터에 아마 각 데이터들 '고유 아이디'가 있을 거라는 것이었다. 찾아보니 kindercode라는 고유 코드가 존재했다. (역시 갓 gpt!)
json에 적혀있는 kindercode
클릭한 리스트의 kindercode를 저장해주는 변수 clickedkinderCode를 만들어주고 그걸 이용해서 이렇게 디테일 페이지로가는 이벤트 function을 만들었고 이를 통해 두 케이스 모두 공통된 기능으로 디테일페이지로 진행될 수 있게 만들 수 있었다.
//리스트 목록을 클릭 했을 때 clickedKinderName과 clickedkinderCode를 저장하고 디테일페이지로 이동
document . getElementById ( `list ${ [ i ] } ` )
. addEventListener ( "click" , function () {
clickedKinderName = document . getElementById ( `list-name ${ [ i ] } ` ). textContent ;
localStorage . setItem ( 'clickedKinderName' , clickedKinderName );
clickedkinderCode = directKinderInfos [ i ]. kindercode ;
localStorage . setItem ( 'clickedkinderCode' , clickedkinderCode );
location . href = 'detail.html' ;
});
디테일 페이지에서는 리스트 페이지로부터 clickedKinderName과 clickedkinderCode 값을 가져오고
// list.js에서 저장된 전역변수들을 localStorage로 저장했다가 가져오기
document . addEventListener ( "DOMContentLoaded" , function () {
let clickedkinderCode = localStorage . getItem ( 'clickedkinderCode' );
let clickedKinderName = localStorage . getItem ( 'clickedKinderName' );
그 데이터들로 정확한 디테일 정보를 가져오는 작업을 진행했다.
if ( clickedkinderCode , clickedKinderName ) {
searchDetail ( clickedkinderCode , clickedKinderName );
} else {
console . error ( "No Name found in localStorage." );
}
});
searchDetail이라는 function에서 먼저 clickedKinderName을 이용해서 필터기능으로 일치하는 유치원 명을 가진 데이터 배열을 가져왔고
function searchDetail ( clickedkinderCode , clickedKinderName ){
fetch ( "./json/kinderGeneral.json" )
. then (( response ) => response . json ())
. then (( data ) => {
// filter: data.DATA 속 kindername이 특정 명칭(clickedKinderName)과 일치하는지 여부를 판단하고 일치하는 것을 배열로 반환
// .trim().toLowerCase()를 쓰면 띄어쓰기나 대소문자로 인한 불일치를 해소
let kinderInfos = data . DATA . filter (( item ) =>
item . kindername . trim (). toLowerCase () === clickedKinderName . trim (). toLowerCase ()
);
이 데이터 배열은 복수일 수 도 있기 때문에 그걸 clickedkinderCode를 통해서 일치하는 kindercode를 찾고 그 데이터를 kinderInfoPick라는 변수로 넣어주었다.
// kinderInfos는 복수의 데이터가 들어올 수 있으므로 clickedkinderCode를 통해 클릭한 리스트의 데이터와 일치하는 데이터만 찾아냄
let kinderInfoPick = kinderInfos . filter (( item ) =>
item . kindercode === clickedkinderCode
)
그러고나서는 kinderInfoPick을 통해 html에 모든 데이터를 넣어주었다.
const detailTitle = `
<h1> ${ clickedKinderName } 정보</h1>
`
const detailBasic = `
<div class="detail-basicGrp">
<div class="detail-basic">
<div class="kinder-title">
<div class="kinder-tag">
${ kinderInfoPick [ 0 ]. establish }
</div>
<div class="kinder-name">
${ kinderInfoPick [ 0 ]. kindername }
</div>
</div>
<div class="detail-basicInfo">
<div class="key-value">
<div class="key">
전화번호
........
이렇게 검색을 통해서도 디테일 페이지까지 잘 도달할 수 있도록 큰 기능들 구현을 모두 성공하였다 👏👏
디테일페이지 구현
참고로 백버튼을 눌렀는데 검색 결과가 인풋에 남이있는 현상이 있었는데 index.js에 이 구문을 넣어줌으로써 인풋을 초기화 할 수 있었다.
//백버튼시 검색 초기화
window . addEventListener ( "pageshow" , function () {
if ( searchInput ) {
searchInput . value = "" ;
}
});
그리고 인풋에 아무것도 입력되지 않았을 때에는 페이지 이동을 시키지 않기 위해 이런 처리를 했다. 여기서 ! 는 부정 연산자이다.
// 검색창 인풋값으로 검색해주는 버튼
document . querySelector ( ".search-btn" )
. addEventListener ( "click" , function () {
// 인풋 값 가져오기
searchedKeyword = searchInput . value . trim (); // 공백 제거
// 인풋이 비어있지 않은 경우에만 검색 수행
if ( searchedKeyword !== "" ) { // 부정연산자
localStorage . setItem ( 'searchedKeyword' , searchedKeyword );
location . href = 'list.html' ;
} else {
// 인풋이 비어있을 때 처리 (선택 사항: 경고 메시지 등을 표시 가능)
console . log ( "검색어를 입력해주세요." );
}
이 외에도 검색 버튼을 키보드 엔터로 실행할 수 있게하는 기능과
//검색버튼을 키보드 엔터 눌렀을 때도 동작하게
searchInput . addEventListener ( "keyup" , function ( event ){
if ( event . keyCode === 13 ){
document . querySelector ( ".search-btn" ). click ();
}
});
일치하는 검색결과가 없을 때 리스트페이지에서 검색결과 없음을 표시해주는 부분도 진행하였다.
// 일치하는 검색결과가 없을 때
if ( searchedArray . length == 0 ) {
let listEmpty = `
<div class="empty-contents">
<div class="empty-msg">
<div class="empty-img">이미지</div>
<div class="empty-text">일치하는 유치원명이 없습니다.</div>
</div>
<a class="empty-back-btn" href="index.html">돌아가기</a>
</div>
`
document . querySelector ( ".list-container" )
. insertAdjacentHTML ( "beforeend" , listEmpty );
}
이렇게 큰 기능들은 잘 진행된 것 같다. 이제 그래프 기능과 페이지네이션이 남았다.