티스토리 뷰

문제

직전의 글(교보문고, 예스24, 알라딘 상품페이지에서 ISBN만 복사하기)에서, 아래와 같은 두 가지 문제가 있었습니다.

  • 미리 작업해두지 않은 상세페이지에서는 ISBN 값을 가져올 수 없다는 점
  • 미리 작업한 상세페이지의 디자인이 바뀌는 경우 작동하지 않는다는 점

 

원인

상세페이지(교보문고, 예스24, 알라딘 등)마다 웹페이지에서 ISBN이 있는 위치를 직접 찾아보고,

해당 요소를 class 등으로 찾아서 ISBN 값을 가져오는 무식한 방법을 사용했기 때문입니다.

 

 

해결

다 만들어서 올리고 나니까 그제야 생각이 났습니다.

그냥 현재 페이지에서 ISBN의 정규식에 일치하는 값을 모두 찾아서 검사하고, 첫번째 값을 출력하도록 만들었다면 되는 문제였습니다.

 

 

사용방법

javascript:(function()%7Bclass%20Notification%20%7B%0A%20%20%20%20static%20defaultOpacity%20%3D%20'0.95'%3B%0A%0A%20%20%20%20constructor(message%2C%20isSuccess)%20%7B%0A%20%20%20%20%20%20%20%20this.message%20%3D%20message%3B%0A%20%20%20%20%20%20%20%20this.isSuccess%20%3D%20isSuccess%3B%0A%20%20%20%20%20%20%20%20this.create()%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20create()%20%7B%0A%20%20%20%20%20%20%20%20this.notificationBox%20%3D%20document.createElement('div')%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.textContent%20%3D%20this.message%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.position%20%3D%20'fixed'%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.top%20%3D%20'20px'%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.left%20%3D%20'20px'%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.padding%20%3D%20'10px'%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.borderRadius%20%3D%20'5px'%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.fontSize%20%3D%20'12px'%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.color%20%3D%20'%23fff'%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.backgroundColor%20%3D%20this.isSuccess%20%3F%20'%2328a745'%20%3A%20'%23dc3545'%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.opacity%20%3D%20Notification.defaultOpacity%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.zIndex%20%3D%20'999999'%3B%0A%20%20%20%20%20%20%20%20document.body.appendChild(this.notificationBox)%3B%0A%0A%20%20%20%20%20%20%20%20this.fadeOut()%3B%0A%0A%20%20%20%20%20%20%20%20this.notificationBox.addEventListener('mouseenter'%2C%20()%20%3D%3E%20this.cancelFadeOut())%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.addEventListener('mouseleave'%2C%20()%20%3D%3E%20this.fadeOut())%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20fadeOut()%20%7B%0A%20%20%20%20%20%20%20%20this.fadeOutTimeoutId%20%3D%20setTimeout(()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20this.notificationBox.style.transition%20%3D%20'opacity%201s'%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20this.notificationBox.style.opacity%20%3D%20'0'%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20this.removeTimeoutId%20%3D%20setTimeout(()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20this.remove()%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%201000)%3B%0A%20%20%20%20%20%20%20%20%7D%2C%202000)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20remove()%20%7B%0A%20%20%20%20%20%20%20%20if%20(this.notificationBox%20%26%26%20this.notificationBox.parentNode)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20this.notificationBox.parentNode.removeChild(this.notificationBox)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20cancelFadeOut()%20%7B%0A%20%20%20%20%20%20%20%20clearTimeout(this.fadeOutTimeoutId)%3B%0A%20%20%20%20%20%20%20%20clearTimeout(this.removeTimeoutId)%3B%0A%0A%20%20%20%20%20%20%20%20this.notificationBox.style.transition%20%3D%20''%3B%0A%20%20%20%20%20%20%20%20this.notificationBox.style.opacity%20%3D%20Notification.defaultOpacity%3B%0A%20%20%20%20%7D%0A%7D%0A%0Avar%20allElements%20%3D%20document.querySelectorAll('*')%3B%0A%0Aconst%20isbn13CandidateRegex%20%3D%20new%20RegExp(%22(%3F%3D97%5B89%5D)%5B0-9%5D%7B13%7D%7C(%3F%3D97%5B89%5D)%5B0-9-%20%5D%7B16%7D%5B0-9%5D%22%2C%20%22gm%22)%3B%0Aconst%20isbn10CandidateRegex%20%3D%20new%20RegExp(%22(%3F!-)%5B0-9%5D%7B9%7D%5B0-9X%5D%7C(%3F!-)%5B0-9-%20%5D%7B12%7D%5B0-9X%5D%22%2C%20%22gm%22)%3B%0A%0Avar%20isbn13Candidates%20%3D%20%5B%5D%3B%0AallElements.forEach(function%20(element)%20%7B%0A%20%20%20%20var%20elementText%20%3D%20element.textContent%20%7C%7C%20element.innerText%3B%0A%20%20%20%20if%20(elementText)%20%7B%0A%20%20%20%20%20%20%20%20var%20match%20%3D%20elementText.match(isbn13CandidateRegex)%3B%0A%20%20%20%20%20%20%20%20if%20(match)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20isbn13Candidates.push(...match)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%7D)%3B%0A%0Avar%20isbn10Candidates%20%3D%20%5B%5D%3B%0AallElements.forEach(function%20(element)%20%7B%0A%20%20%20%20var%20elementText%20%3D%20element.textContent%20%7C%7C%20element.innerText%3B%0A%20%20%20%20if%20(elementText)%20%7B%0A%20%20%20%20%20%20%20%20var%20match%20%3D%20elementText.match(isbn10CandidateRegex)%3B%0A%20%20%20%20%20%20%20%20if%20(match)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20isbn10Candidates.push(...match)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%7D)%3B%0A%0Afunction%20isbnTest(value)%20%7B%0A%20%20%20%20var%20chars%20%3D%20value.replace(%2F%5B-%20%5D%7C%5EISBN(%3F%3A-1%5B03%5D)%3F%3A%3F%2Fg%2C%20%22%22).split(%22%22)%3B%0A%20%20%20%20var%20last%20%3D%20chars.pop()%3B%0A%0A%20%20%20%20var%20sum%20%3D%200%3B%0A%20%20%20%20var%20check%2C%20i%3B%0A%0A%20%20%20%20switch%20(chars.length%20%2B%201)%20%7B%0A%20%20%20%20%20%20%20%20case%2010%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20chars.reverse()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(i%20%3D%200%3B%20i%20%3C%20chars.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sum%20%2B%3D%20(i%20%2B%202)%20*%20parseInt(chars%5Bi%5D%2C%2010)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20check%20%3D%2011%20-%20(sum%20%25%2011)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(check%20%3D%3D%2010)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20check%20%3D%20%22X%22%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(check%20%3D%3D%2011)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20check%20%3D%20%220%22%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%20break%3B%0A%20%20%20%20%20%20%20%20case%2013%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(i%20%3D%200%3B%20i%20%3C%20chars.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sum%20%2B%3D%20(i%20%25%202%20*%202%20%2B%201)%20*%20parseInt(chars%5Bi%5D%2C%2010)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20check%20%3D%2010%20-%20(sum%20%25%2010)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(check%20%3D%3D%2010)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20check%20%3D%20%220%22%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20%7D%20break%3B%0A%20%20%20%20%20%20%20%20default%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20return%20check%20%3D%3D%20last%3B%0A%7D%0A%0Aconst%20isbn13Matches%20%3D%20uniquifyArray(isbn13Candidates.filter(isbnTest))%3B%0Aconst%20isbn10Matches%20%3D%20uniquifyArray(isbn10Candidates.filter(isbnTest))%3B%0A%0Afunction%20uniquifyArray(array)%20%7B%0A%20%20%20%20const%20uniqueSet%20%3D%20new%20Set(array)%3B%0A%20%20%20%20const%20uniqueArray%20%3D%20%5B...uniqueSet%5D%3B%0A%20%20%20%20return%20uniqueArray%3B%0A%7D%0A%0Aconst%20isbn13Count%20%3D%20isbn13Matches.length%3B%0Aconst%20isbn10Count%20%3D%20isbn10Matches.length%3B%0Aconst%20isbnCount%20%3D%20isbn13Count%20%2B%20isbn10Count%3B%0A%0Aif%20(isbnCount%20%3C%201)%20%7B%0A%20%20%20%20new%20Notification(%22ISBN%20%EA%B0%92%EC%9D%84%20%EC%B0%BE%EC%9D%84%20%EC%88%98%20%EC%97%86%EC%8A%B5%EB%8B%88%EB%8B%A4.%22%2C%20false)%3B%0A%7D%20else%20%7B%0A%20%20%20%20let%20isbnValue%20%3D%20isbn13Count%20%3E%200%20%3F%20isbn13Matches%5B0%5D%20%3A%20isbn10Matches%5B0%5D%3B%0A%0A%20%20%20%20let%20resultMessage%20%3D%20%60ISBN%20%EA%B0%92%EC%9D%B4%20%ED%81%B4%EB%A6%BD%EB%B3%B4%EB%93%9C%EC%97%90%20%EB%B3%B5%EC%82%AC%EB%90%98%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4.%20(%24%7BisbnValue%7D)%60%3B%0A%20%20%20%20resultMessage%20%2B%3D%20%22%5Cn%22%3B%0A%0A%20%20%20%20navigator.clipboard.writeText(isbnValue)%0A%20%20%20%20%20%20%20%20.then(()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20new%20Notification(resultMessage%2C%20true)%3B%0A%20%20%20%20%20%20%20%20%7D)%0A%20%20%20%20%20%20%20%20.catch((error)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20new%20Notification(%22%ED%81%B4%EB%A6%BD%EB%B3%B4%EB%93%9C%EC%97%90%20%EA%B0%92%EC%9D%84%20%EB%B3%B5%EC%82%AC%ED%95%98%EB%8A%94%20%EC%A4%91%20%EC%98%A4%EB%A5%98%EA%B0%80%20%EB%B0%9C%EC%83%9D%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4%3A%20%22%20%2B%20error%2C%20false)%3B%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%7D%7D)()%3B

 

위의 코드로 북마클릿(bookmarklet)을 만들면, 실행할 때마다 현재 보고 있는 페이지에서 ISBN 값을 찾아 클립보드에 복사해줍니다.

  • 북마클릿을 만드는 방법은 이전의 글을 참고하세요.

인터파크 도서 페이지에서도,

구글 도서검색 구버전에서도,

 

구글 도서검색 최신 버전에서도 작동합니다.

물론 알라딘, 교보문고, 예스24에서도 작동합니다.

사족

  • ISBN의 정규식 및 검증에 관한 내용은 이 글을 참고했으나, 설명된 정규식을 그대로 사용하니 계속 에러가 발생했습니다.
  • 그래서 우회적인 방법을 사용했습니다. 이게 똑똑한 방법인지는 잘 모르겠습니다.
    • ISBN-13의 경우 13자리의 숫자 부분, 또는 공백(" ")이나 대쉬("-") 4개와 13자리의 숫자로 나열된 부분을 검색하고, 이후 공백과 대쉬를 제거한 후에 ISBN의 유효성을 검증하는 과정을 추가했습니다.
    • ISBN-10의 경우도 9자리의 숫자와 0에서 9, 그리고 X 중 한 글자가 마지막으로 더해진 숫자 부분, 또는 공백이나 대쉬 3개와 10자리의 숫자로 나열된 부분을 간단하게 검색하고, 이후 ISBN의 유효성을 검증하는 과정을 거쳤습니다.
    • ISBN의 마지막 한 자리의 숫자는 도서와 관련된 정보가 아니라, 앞선 12개의 숫자를 조합하여 만든 확인용 숫자라고 합니다.

 

마치며

지금의 코드도 깔끔하지는 않은데, 잘 작동은 합니다. 당분간 손을 못댈 것 같아서 우선 공유합니다.

삽질을 했으나 배운 건 있어서 다행입니다. 정규식은 너무 어려운데 재미있는 것 같습니다.

공지사항
링크
최근에 올라온 글
Total
Today
Yesterday