웹 개발 Spring Day13 PATCH, JSON을 이용해 미디어 정보 가져오기, 라우팅을 이용한 페이지 변환

김지원·2022년 8월 31일
0

WebDevelop2

목록 보기
34/34
문제 발생
  ↓
500인가?    → 예외 종류 파악
  ↓
400인가?    → HTML INPUT NAME 속성 확인 
	       → 맵핑 메서드 매개변수 확인
  ↓
405인가?    → 맵핑 메서드 Method 확인

현재, 페이지와 주소는 변하지만 실제로 새로 로딩되는 것이 아니다.
라우팅이라는 것을 이용해야한다.


관리자계정으로 들어간다면 콘텐츠를 등록할 수 있게 하자.
얘 또한 페이지 로딩없이 이동이 된다. 전부 Ajax로 처리해야한다.

-> 필요한 정보

제목
썸네일 이미지
티져 영상
연도 
등급
종류(시리즈, 영화)
설명
출연(동명 이인 구분 필요하다.)
장르
시리즈 특징
언어

순수 미디어의 영역(해당하는 미디어의 종속되는가)

썸네일 이미지                            
티져 영상                              
연도                                 
설명                                                                  

그 외

등급
종류
출연
장르
시리즈 특징
언어

이렇게 나눌 수 있겠다. 테이블을 짜자.

-> 위 내용에 의한 DB

RequestMethod.PATCH

-> MediaController 생성

  • 서버에서 정보를 불러올때는 PATCH를 사용한다.
public String patchInfo(@RequestParam(value = "mid", required = true) int mediaIndex)

우영우의 mediaIndex를 가지고 그것과 관련된 정보를 가져올것이고 그것을 JSON 으로 넘겨주자.
mediaIndex에 1(우영우)이 들어가야 할 것이다.

-> MediaEntity / ActorEntity / FeatureEntity / GenreEntity / RatingEntity / TypeEntity 추가 + getter setter + equals() hashCode()

-> MediaService getMedia

-> IMediaMapper selectMediaByIndex

-> MediaController

필요한 정보를 엄청 많이 가져와야한다.
배우 / 장르 / 시리즈 / 특징을 DB에 있는 값 그대로 들고 올 수 없으니 처리해줘야한다.

-> MediaService getActors

  • 출연진들을 돌려주는 메서드

-> IMediaMapper selectActorsByMediaIndex

-> MediaController

  • 아래에서 코드가 줄여진다.

ActorEntity를 JSON(문자열)형태로 변형시켜서 JSONArray타입의 actorsJson에 집어 넣을 것이다.
-> MediaController

ObjectMapper objectMapper = new ObjectMapper();
  • writeValueAsStringJsonProcessingException 던질 수 있음으로 throws 해준다.
  • objectMapper은 뒤에 전달해주는 객체를 JSON 문자로 포맷해준다.
  • Rating과 Type은 media와 1:1로 매칭되는 것이다. 즉, 우영우라는 드라마에 시청등급이 '15세 이상' 이라는 것 하나와 시리즈'라는 타입이 하나가 있다는 의미이다. 그렇기 때문에 배열을 통해 가져오지 않는다.

-> MediaService

-> IMediaMapper

-> MediaMapper

<select id="selectActorsByMediaIndex"
        resultType="dev.jwkim.studymember.entities.content.ActorEntity">
    SELECT `actor`.`value` AS `value`,
           `actor`.`text`  AS `text`
    FROM `netflix_content`.`media_actors` AS `mediaActor`
             LEFT JOIN `netflix_content`.`actors` AS `actor` ON `mediaActor`.`actor_value` = `ac
    WHERE `mediaActor`.`media_index` = #{mediaIndex}
</select>
<select id="selectFeaturesByMediaIndex"
        resultType="dev.jwkim.studymember.entities.content.FeatureEntity">
    SELECT `feature`.`value` AS `value`,
           `feature`.`text`  AS `text`
    FROM `netflix_content`.`media_features` AS `mediaFeatures`
             LEFT JOIN `netflix_content`.`features` AS `feature`
                       ON `mediaFeatures`.`features_value` = `feature`.`value`
    WHERE `mediaFeatures`.`media_index` = #{mediaIndex}
</select>
<select id="selectGenresByMediaIndex"
        resultType="dev.jwkim.studymember.entities.content.GenreEntity">
    SELECT `genre`.`value` AS `value`,
           `genre`.`text`  AS `text`
    FROM `netflix_content`.`media_genres` AS `mediaGenres`
             LEFT JOIN `netflix_content`.`genres` AS `genre`
                       ON `mediaGenres`.`genres_value` = `genre`.`value`
    WHERE `mediaGenres`.`media_index` = #{mediaIndex}
</select>
<select id="selectMediaByIndex"
        resultType="dev.jwkim.studymember.entities.content.MediaEntity">
    SELECT `index`           AS `index`,
           `title`           AS `title`,
           `description`     AS `description`,
           `published_year`  AS `publishedYear`,
           `fhd_flag`        AS `isFhd`,
           `uhd_flag`        AS `isUhd`,
           `commentary_flag` AS `isCommentary`,
           `thumbnail_image` AS `thumbnailImage`,
           `teaser_video`    AS `teaserVideo`,
           `rating_value`    AS `ratingValue`,
           `type_value`      AS `typeValue`
    FROM `netflix_content`.`media`
    WHERE `index` = #{index}
    LIMIT 1
</select>
<select id="selectRatingByValue"
        resultType="dev.jwkim.studymember.entities.content.RatingEntity">
    SELECT `value` AS `value`,
           `text`  AS `text`
    FROM `netflix_content`.`ratings`
    WHERE `value` = #{value}
    LIMIT 1
</select>
<select id="selectTypeByValue"
        resultType="dev.jwkim.studymember.entities.content.TypeEntity">
    SELECT `value` AS `value`,
           `text`  AS `text`
    FROM `netflix_content`.`types`
    WHERE `value` = #{value}
    LIMIT 1
</select>

-> MediaController patchInfo

@RequestMapping(value = "info", method = RequestMethod.PATCH, produces = "application/json")
  • produces 추가

  • 현재 PostMan을 통해 mid=1 즉 mideaIndex 가 1 인 우영우의 정보를 당겨봤더니 아래와 같은 JSON Array 로 출력이 되는 것을 볼 수 있다.
{
    "result": "success",
    "actors": [
        {
            "text": "강태오",
            "value": "39d91270"
        },
        {
            "text": "강기영",
            "value": "6ce1e425"
        },
        {
            "text": "박은빈",
            "value": "8ab45fa6"
        }
    ],
    "features": [
        {
            "text": "힐링",
            "value": 1
        },
        {
            "text": "감명을 주는",
            "value": 2
        },
        {
            "text": "진심어린",
            "value": 3
        }
    ],
    "genres": [
        {
            "text": "법정",
            "value": 1
        },
        {
            "text": "한국 드라마",
            "value": 2
        },
        {
            "text": "사회 문제를 다룬 드라마",
            "value": 3
        }
    ],
    "rating": {
        "text": "15세 관람가",
        "value": "m"
    },
    "medium": {
        "uhd": false,
        "fhd": true,
        "ratingValue": "m",
        "index": 1,
        "description": "천재적인 두뇌의 소유자 우영우. 대형 로펌의 신입 변호사이자 자폐 스펙트럼 장애를 가진 여성으로서 법정 안팎에서 다양한 난관을 해쳐간다.",
        "typeValue": "s",
        "publishedYear": 1640962800000,
        "title": "이상한 변호사 우영우",
        "commentary": true
    },
    "type": {
        "text": "시리즈",
        "value": "s"
    }
}

상세정보를 눌렀을 때 JSON 정보를 가져와서 화면에 보여주자.

-> index.unsined.html / css

-> media 테이블 logo_image 추가

  • 해당 테이블에 logo 이미지 UPDATE
  • MediaEntity / MediaMapper.xml selectMediaByIndex logo_image추가
media/getLogoImage?mid=1
media/getThumbnailImage?mid=1
media/getTeaserVideo?dmid=1
  • 이렇게 맵핑을 진행 할 것이다.

-> MediaController getLogoImage

  • byte 배열을 실제 응답값으로 바꾸기 위해
    ResponseEntity<byte[]> 사용.
http://localhost:8080/media/logo-image?mid=1

  • 이 주소로 들어가면 logo 사진이 뜨게 된다.

-> MediaController getThumbnailImage / getTeaserVideo


-> index.singed.html

 data-func="media.showDetail" data-mid="1"

  • 각 태그를 js에서 사용할 수 있도록 rel로 이름을 설정해주자.

-> index.js

<script>
const components = {
    mediaContainer: {
        getElement: () => window.document.getElementById('media-container'),
        show: () => components.mediaContainer.getElement().classList.add('visible'),
        hide: () => components.mediaContainer.getElement().classList.remove('visible'),
        setLogoImageSrc: (src) => components.mediaContainer.getElement().querySelector('[rel=logoImage]').setAttribute('src', src),
        setTeaserVideoSrc: (src) => components.mediaContainer.getElement().querySelector('[rel=teaserVideo]').setAttribute('src', src),
        setPublishedYear: (year) => components.mediaContainer.getElement().querySelector('[rel=publishedYear]').innerText = year,
        setFhd: (b) => components.mediaContainer.getElement().querySelector('[rel=fhd]').style.display = b === true ? 'block' : 'none',
        setUhd: (b) => components.mediaContainer.getElement().querySelector('[rel=uhd]').style.display = b === true ? 'block' : 'none',
        setCommentary: (b) => components.mediaContainer.getElement().querySelector('[rel=commentary]').style.display = b === true ? 'block' : 'none',
        setDescription: (desc) => components.mediaContainer.getElement().querySelector('[rel=description]').innerText = desc,
        addActor: (params) => {
            const actors = components.mediaContainer.getElement().querySelector('[rel=actors]');
            const anchorElement = window.document.createElement('a');
            anchorElement.classList.add('link');
            anchorElement.setAttribute('href', '#');
            anchorElement.innerText = params['text'];
            if (actors.children.length > 0) {
                actors.innerHTML += ', ';
            }
            actors.append(anchorElement);
        },
        addGenre: (params) => {
            const genres = components.mediaContainer.getElement().querySelector('[rel=genres]');
            const anchorElement = window.document.createElement('a');
            anchorElement.classList.add('link');
            anchorElement.setAttribute('href', '#');
            anchorElement.innerText = params['text'];
            if (genres.children.length > 0) {
                genres.innerHTML += ', ';
            }
            genres.append(anchorElement);
        },
        addFeatures: (params) => {
            const features = components.mediaContainer.getElement().querySelector('[rel=features]');
            const anchorElement = window.document.createElement('a');
            anchorElement.classList.add('link');
            anchorElement.setAttribute('href', '#');
            anchorElement.innerText = params['text'];
            if (features.children.length > 0) {
                features.innerHTML += ', ';
            }
            features.append(anchorElement);
        }
    }
};

const functions = {
    media: {
        showDetail: (params) => {
            const mediaIndex = parseInt(params.element.dataset.mid);
            // alert(mediaIndex);
            const xhr = new XMLHttpRequest();
            xhr.open('PATCH', `/media/info?mid=${mediaIndex}`);
            xhr.onreadystatechange = () => {
                if (xhr.readyState === XMLHttpRequest.DONE) {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        const responseJson = JSON.parse(xhr.responseText);
                        switch (responseJson['result']) {
                            case 'success':
                                // console.log(xhr.responseText);
                                components.mediaContainer.setTeaserVideoSrc(`/media/teaser-video?mid=${mediaIndex}`);
                                components.mediaContainer.setLogoImageSrc(`/media/logo-image?mid=${mediaIndex}`);
                                components.mediaContainer.setPublishedYear(new Date(responseJson['medium']['publishedYear']).getFullYear());
                                components.mediaContainer.setFhd(responseJson['medium']['fhd']);
                                components.mediaContainer.setUhd(responseJson['medium']['uhd']);
                                components.mediaContainer.setCommentary(responseJson['medium']['commentary']);
                                components.mediaContainer.setDescription(responseJson['medium']['description']);

                                responseJson['actors'].forEach(actor => {
                                    components.mediaContainer.addActor(actor);
                                });
                                responseJson['genres'].forEach(genre => {
                                    components.mediaContainer.addGenre(genre);
                                });
                                responseJson['features'].forEach(feature => {
                                    components.mediaContainer.addFeatures(feature);
                                });

                                components.mediaContainer.show();
                                break;
                            default :
                                alert('미디어 정보를 받아오지 못했습니다.\n\n잠시 후 다시 시도해 주세요')
                        }
                    } else {
                        alert('서버와 통신하지 못하였습니다.\n\n잠시 후 다시 시도해 주세요');
                    }
                }
            };
            xhr.send();
        }
    }
};

window.document.body.querySelectorAll('[data-func]').forEach(x => {
    x.addEventListener('click', e => {
        const dataFunc = x.dataset.func;
        let dataFuncArray = dataFunc.split('.');
        let lastObject = functions; 
        dataFuncArray.forEach(y => { 
            if (typeof (functions[y]) === 'object') {
                lastObject = lastObject[y]; 
            } else if (typeof (lastObject[y]) === 'function') {
                toCall = lastObject[y];
            }
        });
        if (typeof (toCall) === 'function') {
            toCall({
                element: x,
                event: e
            });
        }
    });
}); 
</script>
if (genres.children.length > 0) {
    genres.innerHTML += ', ';
}
  • 어떠한 객체가 가지는 children 속성내의 length 속성을 통해 그 객체가 가지는 자식 객체의 수를 가지고 올 수 있다.
responseJson['actors'].forEach(actor => {
    components.mediaContainer.addActor(actor);
});
  • Array 타입의 JSON 객체는 forEach 함수를 이용하여 각 노드를 반복할 수 있다.
<script>
window.document.body.querySelectorAll('[data-func]').forEach(x => {
    x.addEventListener('click', e => {
        const dataFunc = x.dataset.func;
        let dataFuncArray = dataFunc.split('.'); //'.' 을 기준으로 분리하여 ['media', 'showDetail']  형태로 나오게 된다. 
        let lastObject = functions; // lastObject을 functions으로 초기화된 상태에서 
        let toCall;
        dataFuncArray.forEach(y => { // 첫번째 y = media가 되고 
            if (typeof (functions[y]) === 'object') { // media가 object 인지 체크하고
                lastObject = lastObject[y]; // object라면 그 다음의 lastObject은 media가 된다.
            } else if (typeof (lastObject[y]) === 'function') {
                toCall = lastObject[y];
            }
        });
        if (typeof (toCall) === 'function') {
            toCall({
                element: x,
                event: e
            });
        }
    });
});
</script>  

  • DB에 있는 정보가 잘 가져와지면 된다.

  • 현재 위에서 정보를 불러온 페이지 주소는 이렇다.
    배경을 누르게 되면 상세정보 페이지를 사라지게 하고 주소 또한 localhost:8080으로 돌려놓자.

  • js 수정 따로 뺴준다.

-> js / xhr : success 부분 추가

const url = new URL(window.location.href);
url.searchParams.set('action', 'media');
url.searchParams.set('mid', mediaIndex);
window.history.pushState(null, null, url.toString());
  • pushState로 url 주소를 설정해준다.

-> hideDetail

  • 상세정보 페이지를 닫게 되면 localhost:8080/?action=media&mid=1 의 주소에서 ? 뒤에 것을 다 날리도록 설정해준다.

현재 cover을 누르면 상세정보에서 메인페이졸 이동되지만 새로운 웹에서 localhost:8080/?action=media&mid=1 주소를 치고 들어갔을때도 동일하게 상세정보가 떠야하는데 아래와 같이 뜨지 않는다.

-> index.js 맨 아래에 추가

  • http://localhost:8080/?action=media&mid=1 의 action 이 media 일 때 showDetail 함수 실행하게 한다.
    상세정보 페이지를 보여주기 위해서는

    이와 같은 이벤트가 있어야하지만 클릭했다는 요소가 있어야하는데 없기 때문에

    const mediaIndex = parseInt(params.element.dataset.mid); 이렇게 사용한것을 명시적으로 만들어 준다. mediaIndex는 주소에서 가져오게 되고 요소가 있는 것처럼 속인다.

상세정보 버튼을 누르면 상세정보 페이지가 뜨며 localhost:8080/?action=media&mid=1 주소로 변하게 되고 배경을 클릭하면 localhost:8080 으로 주소가 변하고 창이 닫기게 된다.


라우팅(Routing)

: SPA(Single Page Application=한 페이지에 다 처리하는것)를 구현하기 위한 처리 방법

  • window.history.pushState(...)를 통해 주소를 바꿀 수 있다. 이때 주소가 바뀌어도 실제로 페이지는 변하지 않는다.
  • window의 'popstate' 이벤트는 앞으로가기/뒤로가기 등의 내비게이션이 발생했을 때 발생하는 이벤트이다. 단, 그 탐색 기록이 pushState 에 의한 것이 아닌 일반적인 링크에 의해 발생했을 경우 popstate 이벤트가 발생하지 않거나 무시될 수 있다.
  • 페이지 로드 후, pushState 발생 시, popstate 이벤트 발생 시 이렇게 세 가지의 경우에 현재 주소에 따른 로직을 처리하는 것이 라우팅이다.
  • 보통은 쌩 자바스크립트(바닐라 자바스크립트)로 구현하는 경우는 잘 없음.
localhost:8080/?action=media&mid=1

넷플릭스로 예를 자면 위와 같은 주소를 주소창에 쳐서 들어가는건 페이지 로드 후에 해당한다.
상세정보 눌렀을 때는 pushState에 해당한다.

window.history.pushState(null, null, '/user/login')

으로 console 창에서 테스트 해보니 주소는 변하지만 페이지는 변하지 않는다.

실무에서 라우팅을 사용할 때는 로딩 시간이 길어지게 될 경우를 대비해 처리를 잘해줘야한다. 라우팅을 사용하면 페이지가 변하기 위해 로딩 중이라는 것을 사용자가 알 수 없기 때문에 AJAX를 통해서 로딩페이지를 먼저 띄워줘야한다.

profile
Software Developer : -)

0개의 댓글