# 핀노트 붙이기 내 사이트에 스크립트 한 줄을 붙이면, 방문자가 화면 요소에 핀을 꽂아 의견을 남긴다. 의견은 핀노트 관리 화면에 모인다. ## 복사해서 바로 쓰는 한 줄 ```html ``` - `pk_여기에_사이트_키` 자리에 내 **사이트 키**를 넣는다(아래 1번에서 받는다). - 모든 화면에 들어가는 공통 틀(`` 끝이나 `` 앞)에 **한 번만** 붙인다. - 붙이면 화면 오른쪽 아래에 **핀 버튼**이 생긴다. 버튼을 누르면 의견 모드가 켜지고, 요소마다 핀이 붙는다. - 주소에 `?pinnote=1` 을 더하면 처음부터 켜진 채로 열린다(`?pinnote=0` 은 끄기). 켜고 끈 상태는 그 브라우저에 기억된다. 로그인한 사용자의 이름을 이름 칸에 미리 채우려면: ```html ``` ## 1. 사이트 키 받는 법 사이트 키는 관리 화면에서 사이트를 만들면 나온다. **키를 만드는 CLI 명령은 없다**(CLI는 이미 있는 키를 보여 주기만 한다). 1. [https://pinnote.itemview.co.kr/admin/](https://pinnote.itemview.co.kr/admin/) 을 연다. 2. **관리자 비밀번호**를 넣고, 이름을 고르거나 **또는 새 이름**에 적은 뒤 [들어가기]. 3. 위쪽 메뉴에서 **사이트 설정**으로 간다. 4. 아래 **새 사이트** 상자를 채우고 [만들기]. 칸은 넷뿐이다(아래 표). 5. 만들면 그 사이트의 설정 화면으로 넘어간다. 맨 위 **설치 한 줄** 칸의 [복사]를 눌러 그대로 붙인다. 6. 같은 화면에서 경로 규칙, 요소 캡처, 사라짐 기준, 시간당 한도를 나중에 바꿀 수 있다. **사이트 키 새로 만들기**를 누르면 이전 키는 즉시 멈춘다. | 새 사이트 칸 | 무엇을 적나 | |---|---| | 프로젝트 | 사이트를 묶는 이름. 비워 두면 `기본` | | 사이트 이름 | 관리 화면에 보일 이름 | | 환경 | `demo` · `staging` · `prod` 중 하나 | | 허용 도메인 | 위젯을 쓸 도메인. 쉼표로 여러 개, `*.example.com` 도 된다. **여기 없는 도메인에서는 위젯이 동작하지 않는다** | 서버에서 이미 있는 사이트와 키를 보려면: ```bash 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` 이 우선). - 값이 빈 문자열이면 속성이 없는 것으로 본다. ### 이름 기본값과 개인정보 이름 칸에 들어가는 값의 **우선순위**는 이렇다. 1. `Pinnote.setName()` 으로 넣은 값 2. 그 브라우저에 저장된 이름(전에 의견을 남길 때 적은 이름) 3. `data-name` 값 즉 `data-name` 은 **저장된 이름이 없을 때만** 쓰이는 기본값이고, `setName()` 은 언제나 이긴다. 단 `data-name-lock` 이 붙어 있으면 `data-name` 이 위 순서보다 먼저이고 사용자가 고칠 수 없다. > 여기 넣은 이름은 의견의 작성자로 저장되고, **같은 요소에 달린 의견 목록에서 다른 방문자에게도 보인다.** - 실명·이메일 주소·전화번호·사번을 넣지 않는다. - 화면에 보여도 괜찮은 **표시용 이름(닉네임)만** 넣는다. - 사용자를 우리 쪽 번호로 알아봐야 한다면 이름이 아니라 `setContext` 로 번호만 따로 붙인다(아래 5번). ## 3. 자바스크립트 API 설치 한 줄에 `defer` 가 붙어 있어서, **사이트 코드는 `DOMContentLoaded` 안에서** 부르면 안전하다. 그 뒤에 부르는 것은 위젯 본체가 아직 안 왔어도 쌓아 두었다가 실행한다. ```js 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`(모달 안으로 들어가고 나옴). ```js Pinnote.on('submitted', function (d) { console.log('의견 번호', d.id, '화면', d.page_key); }); document.addEventListener('pinnote:submitted', function (e) { console.log(e.detail); }); ``` ## 4. 로그인한 사용자 이름 채우기 ```js // 로그인 상태가 바뀔 때마다 부른다 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` 에 그대로 보인다. 어느 사용자가 어느 화면에서 남긴 의견인지 우리 시스템 번호로 맞춰 볼 때 쓴다. ```js 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 ``` ```html ``` - 위젯은 자기 스타일을 Shadow DOM 안에 넣기 때문에 **`style-src` 는 풀지 않아도 된다.** `default-src 'self'` 만 있는 엄격한 CSP에서 핀·의견 창이 제대로 그려지는 것을 확인했다(콘솔에 인라인 스타일 경고가 한 줄 남을 수 있지만 동작에는 문제가 없다). - 요소 캡처에 쓰는 `html2canvas` 도 핀노트 주소에서 받으므로 같은 `script-src` 로 함께 허용된다. ## 7. 모달 · 핀에서 빼기 · 요소에 이름표 달기 - **모달(``) 안에서도 된다.** `showModal()` 로 연 모달이 있으면 위젯이 스스로 모달 안으로 들어가고, 닫히면 원래 자리로 돌아온다. 모달이 열려 있는 동안에는 **모달 안 요소만** 의견 대상이 된다. 모달을 연 뒤에 켜고 꺼도 모달은 그대로 열려 있다. - **핀을 달지 않을 곳**에는 `data-pinnote-ignore` 를 단다. 그 안의 요소는 대상에서 빠진다. ```html
... 광고·알림 띠처럼 의견을 받을 필요가 없는 곳 ...
``` - **가릴 곳**에는 `data-pinnote-mask` 나 `class="pn-mask"` 를 단다. 캡처와 기록에서 가려진다(입력칸 값·비밀번호는 기본으로 가려진다). - **의견 대상을 잘 잡히게 하려면** 중요한 요소에 안정적인 이름표를 달아 둔다. 핀노트는 `data-ui` → `id` → 짧은 선택자 → DOM 경로 순으로 요소를 다시 찾는다. 클래스 이름이 배포마다 바뀌는 사이트일수록 효과가 크다. ```html ``` 이름표가 없으면 화면을 고칠 때 요소를 못 찾아 **사라진 요소**로 넘어가고, 사람이 [다시 연결]해 줘야 한다. ## 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 오류로만 보여서 구분이 안 되기 때문이다. ```bash 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}}` → 관리 화면에서 위젯을 닫아 둔 상태다 ## 더 읽을 것 - [의견을 코드에서 읽고 처리하기(REST API · CLI)](?f=API) - [어떻게 동작하는지(설계)](?f=DESIGN)