핀노트 붙이기

내 사이트에 스크립트 한 줄을 붙이면, 방문자가 화면 요소에 핀을 꽂아 의견을 남긴다. 의견은 핀노트 관리 화면에 모인다.

복사해서 바로 쓰는 한 줄

<script src="https://pinnote.itemview.co.kr/w.js" data-site="pk_여기에_사이트_키" defer></script>

로그인한 사용자의 이름을 이름 칸에 미리 채우려면:

<script src="https://pinnote.itemview.co.kr/w.js" data-site="pk_여기에_사이트_키" data-name="홍길동" defer></script>

1. 사이트 키 받는 법

사이트 키는 관리 화면에서 사이트를 만들면 나온다. 키를 만드는 CLI 명령은 없다(CLI는 이미 있는 키를 보여 주기만 한다).

  1. https://pinnote.itemview.co.kr/admin/ 을 연다.
  2. 관리자 비밀번호를 넣고, 이름을 고르거나 또는 새 이름에 적은 뒤 [들어가기].
  3. 위쪽 메뉴에서 사이트 설정으로 간다.
  4. 아래 새 사이트 상자를 채우고 [만들기]. 칸은 넷뿐이다(아래 표).
  5. 만들면 그 사이트의 설정 화면으로 넘어간다. 맨 위 설치 한 줄 칸의 [복사]를 눌러 그대로 붙인다.
  6. 같은 화면에서 경로 규칙, 요소 캡처, 사라짐 기준, 시간당 한도를 나중에 바꿀 수 있다. 사이트 키 새로 만들기를 누르면 이전 키는 즉시 멈춘다.
새 사이트 칸무엇을 적나
프로젝트사이트를 묶는 이름. 비워 두면 기본
사이트 이름관리 화면에 보일 이름
환경demo · staging · prod 중 하나
허용 도메인위젯을 쓸 도메인. 쉼표로 여러 개, *.example.com 도 된다. 여기 없는 도메인에서는 위젯이 동작하지 않는다

서버에서 이미 있는 사이트와 키를 보려면:

php bin/pinnote.php sites

2. 설치 한 줄의 옵션

속성기본값뜻 · 언제 쓰는지
data-site(필수)사이트 키 pk_…. 이 값으로 어느 사이트인지 정한다
data-env없음의견에 함께 저장할 환경 문자열. staging·prod 를 섞어 쓸 때
data-version없음의견에 함께 저장할 앱 버전. 배포마다 값을 바꾸면 "언제 것인지" 알 수 있다
data-modetoggletoggle 핀 버튼으로 켜고 끈다 · always 항상 켜 둔다(시연·테스트용) · off 아무것도 그리지 않는다
data-langko화면 문구 언어. 지금은 한국어만 있다. 다른 값을 넣어도 한국어로 나온다
data-captureauto의견 창을 열 때 요소 주변을 캡처한다. off 면 끈다. 사이트 설정에서 꺼 두면 그쪽이 우선
data-errorson최근 JS 오류 5개를 의견에 함께 저장한다. off 면 안 모은다
data-positionright핀 버튼을 놓을 쪽. left 면 왼쪽
data-offset-bottom0핀 버튼을 바닥에서 이만큼(px) 올린다. 하단 고정 버튼·탭바가 가릴 때
data-offset-x16핀 버튼과 화면 옆 가장자리 사이(px)
data-name없음이름 칸 기본값. 로그인한 사용자의 표시용 이름을 넣는다
data-name-lock없음속성이 있으면 data-name 으로 이름을 고정한다. 이름 칸은 읽기 전용이 되고 저장된 이름·setName() 보다 우선한다. 로그인한 사용자 이름으로만 남기게 할 때

이름 기본값과 개인정보

이름 칸에 들어가는 값의 우선순위는 이렇다.

  1. Pinnote.setName() 으로 넣은 값
  2. 그 브라우저에 저장된 이름(전에 의견을 남길 때 적은 이름)
  3. data-name 값

즉 data-name 은 저장된 이름이 없을 때만 쓰이는 기본값이고, setName() 은 언제나 이긴다.

단 data-name-lock 이 붙어 있으면 data-name 이 위 순서보다 먼저이고 사용자가 고칠 수 없다.

여기 넣은 이름은 의견의 작성자로 저장되고, 같은 요소에 달린 의견 목록에서 다른 방문자에게도 보인다.

3. 자바스크립트 API

설치 한 줄에 defer 가 붙어 있어서, 사이트 코드는 DOMContentLoaded 안에서 부르면 안전하다. 그 뒤에 부르는 것은 위젯 본체가 아직 안 왔어도 쌓아 두었다가 실행한다.

document.addEventListener('DOMContentLoaded', function () {
  Pinnote.setName('홍길동');                       // 이름 칸 기본값 (빈 문자열이면 지운다)
  Pinnote.setContext({ screen: 'checkout' });       // 의견에 함께 저장할 값
});
함수하는 일
Pinnote.setName(이름)이름 칸 기본값을 넣는다. 의견 창이 열려 있으면 바로 바뀐다. '' 이면 지운다. 앞뒤 공백을 다듬고 40자까지
Pinnote.setContext({키:값})의견과 함께 저장할 값을 더한다. 여러 번 불러도 쌓인다
Pinnote.open({page:true})화면 전체에 대한 의견 창을 연다
Pinnote.open({selector:'#pay'})그 요소의 의견 창을 연다
Pinnote.open()요소 직접 고르기 모드로 들어간다
Pinnote.enable() / disable() / toggle()의견 모드를 켜고 끈다(핀 버튼을 누른 것과 같다)
Pinnote.setPosition({side:'left', bottom:72, x:16})핀 버튼 위치를 바꾼다. 하단 바 높이가 바뀔 때마다 알려 주면 된다
Pinnote.on(이름, 함수) / off(이름, 함수)이벤트를 받는다

이벤트 이름: ready(위젯 준비됨) · submitted(의견 보냄) · modechange(켜기·끄기) · anchors(요소 다시 찾은 결과) · relinked(요소 다시 연결함) · focus(포커스 모드) · modal(모달 안으로 들어가고 나옴).

Pinnote.on('submitted', function (d) { console.log('의견 번호', d.id, '화면', d.page_key); });
document.addEventListener('pinnote:submitted', function (e) { console.log(e.detail); });

4. 로그인한 사용자 이름 채우기

// 로그인 상태가 바뀔 때마다 부른다
function pinnoteUser(user) {
  if (user) {
    Pinnote.setName(user.displayName);            // 표시용 이름만
    Pinnote.setContext({ user_no: String(user.id) });  // 우리 쪽 번호는 문맥으로
  } else {
    Pinnote.setName('');                          // 로그아웃하면 지운다
    Pinnote.setContext({ user_no: '' });
  }
}

5. 의견에 우리 쪽 번호를 같이 남기기

setContext 로 넣은 값은 의견과 함께 저장되어, 관리 화면의 문맥, REST context.custom, php bin/pinnote.php show 에 그대로 보인다. 어느 사용자가 어느 화면에서 남긴 의견인지 우리 시스템 번호로 맞춰 볼 때 쓴다.

Pinnote.setContext({
  user_no: '10293',          // 우리 DB의 사용자 번호
  screen: 'checkout.step2',  // 우리가 부르는 화면 이름
  order_no: 'A-20260923-17', // 주문 번호
  ab: 'B'                    // 실험 묶음
});

6. CSP를 쓰는 사이트

사이트에 Content-Security-Policy 가 있으면 아래를 모두 허용해야 한다. 값은 https://pinnote.itemview.co.kr 이다.

지시자왜 필요한가빠지면
script-src로더 /w.js 와 위젯 본체 /widget/pinnote.js위젯이 아예 안 뜬다
connect-src의견·요소 정보를 주고받는 /api/w/*핀 버튼은 보이는데 핀이 안 생기고 의견도 안 보낸다
img-src요소를 [다시 연결]할 때 보여 주는 원래 모습 썸네일썸네일만 안 보인다
Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://pinnote.itemview.co.kr;
  connect-src 'self' https://pinnote.itemview.co.kr;
  img-src 'self' data: https://pinnote.itemview.co.kr

nonce를 쓰는 사이트는 설치 한 줄에 nonce를 달아도 그것만으로는 부족하다. 로더가 위젯 본체를 스크립트로 불러오는데, 그 스크립트에는 nonce가 없기 때문이다. 다음 둘 중 하나를 쓴다.

# 방법 1: nonce 옆에 핀노트 주소를 같이 적는다 (권장)
script-src 'nonce-{매번 새로 만든 값}' https://pinnote.itemview.co.kr;
connect-src 'self' https://pinnote.itemview.co.kr;
img-src 'self' data: https://pinnote.itemview.co.kr

# 방법 2: 'strict-dynamic' 을 쓴다 (nonce 받은 스크립트가 부른 스크립트까지 허용)
script-src 'nonce-{매번 새로 만든 값}' 'strict-dynamic';
connect-src 'self' https://pinnote.itemview.co.kr;
img-src 'self' data: https://pinnote.itemview.co.kr
<script src="https://pinnote.itemview.co.kr/w.js" data-site="pk_여기에_사이트_키" nonce="{매번 새로 만든 값}" defer></script>

7. 모달 · 핀에서 빼기 · 요소에 이름표 달기

<div data-pinnote-ignore> ... 광고·알림 띠처럼 의견을 받을 필요가 없는 곳 ... </div>
<button data-ui="order.submit">주문하기</button>

이름표가 없으면 화면을 고칠 때 요소를 못 찾아 사라진 요소로 넘어가고, 사람이 [다시 연결]해 줘야 한다.

8. 위젯 열고 닫기

시연이 끝났거나 잠시 받지 않고 싶을 때는 지우지 말고 닫으면 된다.

9. 잘 붙었는지 확인하기

붙인 화면을 ?pinnote=1 로 열고 이렇게 본다.

  1. 오른쪽 아래에 핀 버튼이 보인다.
  2. 버튼을 누르면 화면 요소마다 핀이 생긴다.
  3. 핀을 눌러 의견을 보내고, 관리 화면 받은 의견에 들어왔는지 본다.
  4. 브라우저 콘솔에서 window.Pinnote 가 객체이고, document.querySelector('pinnote-root') 가 있는지 본다.

잘 안 될 때 살펴볼 것

증상원인할 일
아무것도 안 보인다스크립트가 안 왔다(광고 차단기·네트워크·주소 오타)개발자 도구 네트워크에서 /w.js 와 /widget/pinnote.js 가 200인지 본다
아무것도 안 보인다CSP script-src 차단콘솔의 Refused to load the script … 줄을 보고 6번대로 고친다
아무것도 안 보인다관리 화면에서 위젯 닫기 상태8번대로 다시 연다
아무것도 안 보인다data-mode="off"속성을 지운다
핀 버튼은 보이는데 핀이 없다사이트 키가 틀렸거나, 그 도메인이 허용 도메인에 없다아래 curl로 확인한다
핀 버튼은 보이는데 핀이 없다CSP connect-src 차단콘솔의 Refused to connect … 줄을 보고 6번대로 고친다
핀은 보이는데 의견이 안 보내진다시간당 한도 초과잠시 뒤 다시 하거나 사이트 설정에서 한도를 올린다

사이트 키와 도메인은 서버에 직접 물어보면 확실하다. 브라우저 콘솔에는 CORS 오류로만 보여서 구분이 안 되기 때문이다.

curl -s -X POST https://pinnote.itemview.co.kr/api/w/init \
  -H 'Origin: https://내사이트.example' -H 'Content-Type: text/plain' \
  --data '{"site":"pk_여기에_사이트_키","url":"https://내사이트.example/"}'

더 읽을 것