핀노트 붙이기
내 사이트에 스크립트 한 줄을 붙이면, 방문자가 화면 요소에 핀을 꽂아 의견을 남긴다. 의견은 핀노트 관리 화면에 모인다.
복사해서 바로 쓰는 한 줄
<script src="https://pinnote.itemview.co.kr/w.js" data-site="pk_여기에_사이트_키" defer></script>
pk_여기에_사이트_키자리에 내 사이트 키를 넣는다(아래 1번에서 받는다).- 모든 화면에 들어가는 공통 틀(
<head>끝이나</body>앞)에 한 번만 붙인다. - 붙이면 화면 오른쪽 아래에 핀 버튼이 생긴다. 버튼을 누르면 의견 모드가 켜지고, 요소마다 핀이 붙는다.
- 주소에
?pinnote=1을 더하면 처음부터 켜진 채로 열린다(?pinnote=0은 끄기). 켜고 끈 상태는 그 브라우저에 기억된다.
로그인한 사용자의 이름을 이름 칸에 미리 채우려면:
<script src="https://pinnote.itemview.co.kr/w.js" data-site="pk_여기에_사이트_키" data-name="홍길동" defer></script>
1. 사이트 키 받는 법
사이트 키는 관리 화면에서 사이트를 만들면 나온다. 키를 만드는 CLI 명령은 없다(CLI는 이미 있는 키를 보여 주기만 한다).
- https://pinnote.itemview.co.kr/admin/ 을 연다.
- 관리자 비밀번호를 넣고, 이름을 고르거나 또는 새 이름에 적은 뒤 [들어가기].
- 위쪽 메뉴에서 사이트 설정으로 간다.
- 아래 새 사이트 상자를 채우고 [만들기]. 칸은 넷뿐이다(아래 표).
- 만들면 그 사이트의 설정 화면으로 넘어간다. 맨 위 설치 한 줄 칸의 [복사]를 눌러 그대로 붙인다.
- 같은 화면에서 경로 규칙, 요소 캡처, 사라짐 기준, 시간당 한도를 나중에 바꿀 수 있다. 사이트 키 새로 만들기를 누르면 이전 키는 즉시 멈춘다.
| 새 사이트 칸 | 무엇을 적나 |
|---|---|
| 프로젝트 | 사이트를 묶는 이름. 비워 두면 기본 |
| 사이트 이름 | 관리 화면에 보일 이름 |
| 환경 | demo · staging · prod 중 하나 |
| 허용 도메인 | 위젯을 쓸 도메인. 쉼표로 여러 개, *.example.com 도 된다. 여기 없는 도메인에서는 위젯이 동작하지 않는다 |
서버에서 이미 있는 사이트와 키를 보려면:
php bin/pinnote.php sites
2. 설치 한 줄의 옵션
| 속성 | 기본값 | 뜻 · 언제 쓰는지 |
|---|---|---|
data-site | (필수) | 사이트 키 pk_…. 이 값으로 어느 사이트인지 정한다 |
data-env | 없음 | 의견에 함께 저장할 환경 문자열. staging·prod 를 섞어 쓸 때 |
data-version | 없음 | 의견에 함께 저장할 앱 버전. 배포마다 값을 바꾸면 "언제 것인지" 알 수 있다 |
data-mode | toggle | toggle 핀 버튼으로 켜고 끈다 · always 항상 켜 둔다(시연·테스트용) · off 아무것도 그리지 않는다 |
data-lang | ko | 화면 문구 언어. 지금은 한국어만 있다. 다른 값을 넣어도 한국어로 나온다 |
data-capture | auto | 의견 창을 열 때 요소 주변을 캡처한다. off 면 끈다. 사이트 설정에서 꺼 두면 그쪽이 우선 |
data-errors | on | 최근 JS 오류 5개를 의견에 함께 저장한다. off 면 안 모은다 |
data-position | right | 핀 버튼을 놓을 쪽. left 면 왼쪽 |
data-offset-bottom | 0 | 핀 버튼을 바닥에서 이만큼(px) 올린다. 하단 고정 버튼·탭바가 가릴 때 |
data-offset-x | 16 | 핀 버튼과 화면 옆 가장자리 사이(px) |
data-name | 없음 | 이름 칸 기본값. 로그인한 사용자의 표시용 이름을 넣는다 |
data-name-lock | 없음 | 속성이 있으면 data-name 으로 이름을 고정한다. 이름 칸은 읽기 전용이 되고 저장된 이름·setName() 보다 우선한다. 로그인한 사용자 이름으로만 남기게 할 때 |
data-offset-bottom대신 사이트 CSS의:root에--pinnote-offset-bottom: 72px를 두어도 된다(속성과setPosition이 우선).- 값이 빈 문자열이면 속성이 없는 것으로 본다.
이름 기본값과 개인정보
이름 칸에 들어가는 값의 우선순위는 이렇다.
Pinnote.setName()으로 넣은 값- 그 브라우저에 저장된 이름(전에 의견을 남길 때 적은 이름)
data-name값
즉 data-name 은 저장된 이름이 없을 때만 쓰이는 기본값이고, setName() 은 언제나 이긴다.
단 data-name-lock 이 붙어 있으면 data-name 이 위 순서보다 먼저이고 사용자가 고칠 수 없다.
여기 넣은 이름은 의견의 작성자로 저장되고, 같은 요소에 달린 의견 목록에서 다른 방문자에게도 보인다.
- 실명·이메일 주소·전화번호·사번을 넣지 않는다.
- 화면에 보여도 괜찮은 표시용 이름(닉네임)만 넣는다.
- 사용자를 우리 쪽 번호로 알아봐야 한다면 이름이 아니라
setContext로 번호만 따로 붙인다(아래 5번).
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' // 실험 묶음
});
- 키는 최대 30개, 키 이름은 40자까지, 값은 문자열 기준 300자까지 저장된다.
- 개인정보는 넣지 않는다. 이름·이메일·전화번호·주소·카드번호 대신 우리 쪽 번호만 넣는다. (전화번호처럼 보이는 문자열은 서버가 한 번 더 가리지만, 애초에 넣지 않는 것이 맞다.)
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>
- 위젯은 자기 스타일을 Shadow DOM 안에 넣기 때문에
style-src는 풀지 않아도 된다.default-src 'self'만 있는 엄격한 CSP에서 핀·의견 창이 제대로 그려지는 것을 확인했다(콘솔에 인라인 스타일 경고가 한 줄 남을 수 있지만 동작에는 문제가 없다). - 요소 캡처에 쓰는
html2canvas도 핀노트 주소에서 받으므로 같은script-src로 함께 허용된다.
7. 모달 · 핀에서 빼기 · 요소에 이름표 달기
- 모달(
<dialog>) 안에서도 된다.showModal()로 연 모달이 있으면 위젯이 스스로 모달 안으로 들어가고, 닫히면 원래 자리로 돌아온다. 모달이 열려 있는 동안에는 모달 안 요소만 의견 대상이 된다. 모달을 연 뒤에 켜고 꺼도 모달은 그대로 열려 있다. - 핀을 달지 않을 곳에는
data-pinnote-ignore를 단다. 그 안의 요소는 대상에서 빠진다.
<div data-pinnote-ignore> ... 광고·알림 띠처럼 의견을 받을 필요가 없는 곳 ... </div>
- 가릴 곳에는
data-pinnote-mask나class="pn-mask"를 단다. 캡처와 기록에서 가려진다(입력칸 값·비밀번호는 기본으로 가려진다). - 의견 대상을 잘 잡히게 하려면 중요한 요소에 안정적인 이름표를 달아 둔다. 핀노트는
data-ui→id→ 짧은 선택자 → DOM 경로 순으로 요소를 다시 찾는다. 클래스 이름이 배포마다 바뀌는 사이트일수록 효과가 크다.
<button data-ui="order.submit">주문하기</button>
이름표가 없으면 화면을 고칠 때 요소를 못 찾아 사라진 요소로 넘어가고, 사람이 [다시 연결]해 줘야 한다.
8. 위젯 열고 닫기
시연이 끝났거나 잠시 받지 않고 싶을 때는 지우지 말고 닫으면 된다.
- 관리 화면 받은 의견 첫 화면의 위젯 줄, 또는 사이트 설정 목록에서 [위젯 닫기]를 누른다. 다시 받을 때는 [위젯 열기].
- 닫으면 그 사이트 방문자에게 핀 버튼·핀·알림이 하나도 보이지 않는다. 콘솔 오류도 남기지 않는다. 설치 한 줄은 그대로 두어도 된다.
- 의견·댓글·요소 정보·히스토리는 그대로 남는다. 다시 열면 예전 의견이 그 자리에 다시 보인다.
- 닫혀 있는 동안 방문자 브라우저에 보관되어 있던(전송 실패한) 의견은 지워지지 않고, 다시 열렸을 때 보내진다.
9. 잘 붙었는지 확인하기
붙인 화면을 ?pinnote=1 로 열고 이렇게 본다.
- 오른쪽 아래에 핀 버튼이 보인다.
- 버튼을 누르면 화면 요소마다 핀이 생긴다.
- 핀을 눌러 의견을 보내고, 관리 화면 받은 의견에 들어왔는지 본다.
- 브라우저 콘솔에서
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/"}'
- 잘 붙은 경우:
{"ok":true,"data":{...}} {"ok":false,"error":{"code":"SITE_NOT_FOUND"}}→ 사이트 키가 틀렸다{"ok":false,"error":{"code":"ORIGIN_DENIED"}}→ 그 도메인이 허용 도메인에 없다. 사이트 설정에서 더한다{"ok":false,"error":{"code":"URL_DENIED"}}→ 보낸 주소의 도메인이 허용 도메인에 없다{"ok":true,"data":{"closed":true}}→ 관리 화면에서 위젯을 닫아 둔 상태다