📌 핀노트 (Pinnote)
어떤 웹사이트에든 스크립트 한 줄을 붙이면 됩니다. 사람들이 화면 요소에 핀을 꽂아 의견을 남기고, 여러 사이트의 의견이 한곳에 모입니다.
개발자(사람 또는 AI)는 어느 사이트·화면·요소에 대한 의견인지 정확히 보고 고칩니다. 고친 뒤에는 건마다 완료 응답을 남기고, 의견을 남긴 사람이 확인하거나 다시 엽니다.
- 관리 화면: https://pinnote.itemview.co.kr/admin/
- 데모: https://pinnote.itemview.co.kr/demo/?pinnote=1 (이름만 바뀐 버전
?v=2, 요소가 사라진 버전?v=3) - 문서: https://pinnote.itemview.co.kr/docs/ (붙이기 · API · 설계). 다른 사이트에 붙일 때는 [docs/INSTALL.md](docs/INSTALL.md) 한 장만 보면 된다.
설치 한 줄
<script src="https://pinnote.itemview.co.kr/w.js" data-site="pk_사이트키" defer></script>
선택 속성
| 속성 | 뜻 |
|---|---|
data-version="1.4.2" | 앱 버전(의견에 함께 저장) |
data-env="staging" | 환경 |
data-mode="toggle" | toggle(기본, 📌 버튼으로 켜기) · always(항상 켜짐) · off |
data-capture="off" | 요소 주변 캡처 끄기 |
data-errors="off" | 최근 JS 오류 수집 끄기 |
data-lang="ko" | 언어(지금은 한국어만) |
data-position="left" | 📌 버튼을 왼쪽에 둔다(기본 right) |
data-offset-bottom="72" | 📌 버튼을 바닥에서 이만큼(px) 올린다. 하단 고정 버튼·탭바가 있는 사이트용 |
data-offset-x="16" | 📌 버튼과 화면 옆 가장자리 사이(px) |
data-name="홍길동" | 의견 창 이름 칸의 기본값(저장된 이름이 있으면 그것이 우선). 표시용 이름만 넣는다 |
- 사이트 CSS의
:root에--pinnote-offset-bottom: 72px를 두어도 된다(속성·setPosition이 우선).
- 주소에
?pinnote=1을 붙이면 의견 모드가 켜집니다(?pinnote=0은 끄기). - 사이트에 Content-Security-Policy가 있으면 다음 항목에
https://pinnote.itemview.co.kr을 더해야 합니다. script-src,connect-src: 필수img-src: 다시 연결 썸네일용- 사이트 요소에
data-ui="order.submit"같은 안정 이름을 붙이면 요소를 가장 정확히 다시 찾습니다. - 가릴 영역에는
data-pinnote-mask또는.pn-mask를, 핀을 달지 않을 영역에는data-pinnote-ignore를 붙입니다.
사이트 코드에서 쓰기
Pinnote.setContext({ screen: 'checkout', journey_id: 'J123' }); // 의견에 함께 저장
Pinnote.setName('홍길동'); // 이름 칸 기본값 (''이면 지우기, 창이 열려 있으면 바로 반영)
Pinnote.setPosition({ bottom: 72, side: 'right' }); // 하단 바 높이가 바뀔 때마다 알려 주기
Pinnote.open({ page: true }); // 화면 전체 의견 창
Pinnote.open({ selector: '#pay-button' }); // 특정 요소 의견 창
Pinnote.open(); // 요소 고르기 모드
Pinnote.on('submitted', (d) => console.log('의견', d.id));
document.addEventListener('pinnote:submitted', (e) => console.log(e.detail));
설치 한 줄(defer)이 실행된 뒤의 호출은 위젯 본체가 아직 안 왔어도 로더가 쌓아 두었다가 넘깁니다. 그래서 사이트 코드는 DOMContentLoaded 안에서 부르면 안전합니다.
기능
- Shadow DOM 위젯
- 호스트 페이지의 레이아웃·CSS에 영향을 주지 않습니다. 켜기 전후에 요소 좌표가 같은지 확인했습니다.
- 인라인 style을 막는 엄격한 CSP에서도 동작합니다.
- 핀
[data-ui]요소를 먼저, 나머지는 휴리스틱으로 고릅니다. 겹치지 않게 놓고, 모바일에서는 28px로 그립니다.- 기존 의견 개수와 상태를 배지로 보여 줍니다.
- 요소 직접 고르기와 화면 전체 의견도 됩니다.
- 의견 창
- 입력: 종류(아이디어·불편·오류·문구·디자인, 기본은 아이디어), 내용, 이름(
data-name·Pinnote.setName()으로 미리 채울 수 있음) - 이 요소의 기존 의견(상태·공감·댓글·완료 응답)
- 여러 겹의 구분자
- 사이트: 도메인, URL, 화면 키(경로 규칙
/t/:token), 제목, 버전·환경 - 요소 찾는 방법 9가지: data-ui, id, 짧은 선택자, DOM 경로, 태그·역할·접근성 이름, 텍스트, 기준 제목, 좌표 비율, 속성
- 요소 원래 모습: HTML·스타일·크기, 요소 주변 캡처
- 환경·사용자 문맥
- 요소가 바뀌거나 사라질 때
- 방문마다 다시 찾아 ATTACHED / RELOCATED(선택자 자동 갱신 + 이력) / MISSING(방문 N번·N일)으로 나눕니다.
- 사라진 요소 목록에서 원래 모습을 보고 [수정으로 해결됨] / [다시 연결](페이지에서 새 요소 클릭) / [보관]으로 정리합니다.
- 상태 흐름
NEW → TRIAGED → IN_PROGRESS → RESOLVED(완료 응답 필수) → VERIFIED / REOPENED- 그 밖에
WONT_FIX(사유),DUPLICATE(원본),NEEDS_INFO가 있습니다.VERIFIED·REOPENED는 관리 화면·REST·CLI에서만 바꿉니다. - 모든 변경은 히스토리에 남습니다: 누가·언제·이전→이후·메모.
- 보고자에게 처리 시각·처리 알림을 보이지 않음: 위젯에는 "N분 전"·"처리됐어요" 알림·[확인했어요]가 없습니다(작업 시간 추적 방지). 확인 완료·다시 열기는 관리 화면·REST·CLI에서 합니다.
- 전송 실패 보관 후 재전송, 포커스 모드(
?pinnote_focus=번호: 스크롤 + 깜빡임) - 개발자·AI
- REST API v1(토큰)
- 마크다운/JSON 내보내기(사이트 → 화면 → 요소 → 의견)
- CLI
bin/pinnote.php - 관리 화면: 받은 의견함(필터·일괄 처리), 사이트별 보기, 의견 상세(스냅샷·문맥·댓글·히스토리·상태 버튼), 사라진 요소, 사이트 설정, API 토큰, 통계
- 보안
- 위젯 API: 공개 키 + Origin 허용 목록, CORS, IP 해시 기준 시간당 제한, 크기 제한
- 관리 화면: CSRF, 서명 쿠키
- 출력 이스케이프, 민감 값 가림
- 설정·저장소는 웹 루트 밖
관리 화면
- [위젯 닫기] / [위젯 열기]: 첫 화면 위젯 줄과 사이트 목록에서 한 번 누르면 된다. 닫으면 그 사이트에서 핀노트가 흔적 없이 사라지고(버튼·오류·재시도 없음), 의견과 기록은 그대로 남는다.
/admin/에서 관리자 비밀번호와 이름(목록에서 고르거나 새로 입력)으로 들어갑니다.- 사이트 설정 → 새 사이트: 도메인을 넣으면 사이트 키와 설치 한 줄이 나옵니다. 여기서 경로 규칙, 사라짐 기준, 캡처 켜기·끄기를 정합니다.
- 받은 의견에서 정리하고, 의견 상세에서 이 요소를 실제 페이지에서 보기로 확인합니다.
- 해결할 때는 완료 응답(무엇을 어떻게 고쳤는지)을 적습니다.
- API 토큰을 만들어 에이전트나 CI에 넘깁니다. 사용법은 [docs/API.md](docs/API.md)에 있습니다.
API 한눈에
curl -H "Authorization: Bearer $PN_TOKEN" "https://pinnote.itemview.co.kr/api/v1/items?site=1&status=open"
curl -H "Authorization: Bearer $PN_TOKEN" "https://pinnote.itemview.co.kr/api/v1/export?site=1&status=open" # 마크다운
curl -X POST -H "Authorization: Bearer $PN_TOKEN" -H 'Content-Type: application/json' \
-d '{"status":"RESOLVED","note":"무엇을 어떻게 고쳤는지","version":"1.4.2"}' https://pinnote.itemview.co.kr/api/v1/items/12/status
php bin/pinnote.php resolve 12 "무엇을 어떻게 고쳤는지" --version=1.4.2
서버 설치·운영
# PHP 8.2 + MariaDB + Apache(AllowOverride All), 웹 루트 = public/
cp config.local.php.example config.local.php # db, base_url, admin_password, app_secret (웹 루트 밖, git 제외)
php bin/migrate.php # db/migrations/NNN_*.sql 순서대로 적용 (여러 번 실행해도 안전)
php bin/seed.php # 기본 워크스페이스·뉴팅 데모·핀노트 데모 사이트와 사이트 키 출력 (중복 없음)
config.local.php는 apache가 읽을 수 있어야 합니다(chgrp apache, 640).storage/(logs, shots)는 apache가 쓸 수 있어야 합니다.
폴더
public/ 웹 루트
w.php /w.js 로더(캐시 안 함) → widget/pinnote.js?v=버전(길게 캐시)
widget/ 위젯 본체
vendor/ html2canvas 1.4.1 (MIT, 라이선스 파일 포함)
api/w/ 위젯 API
api/v1/ REST API v1
admin/ 관리 화면
demo/ 데모 사이트(샘플 페이지 2개)
docs/ 문서 보기
shot.php 캡처 보기(서명 URL)
lib/ 공통 코드(웹 루트 밖)
db/migrations/ 스키마
bin/ migrate, seed, pinnote(CLI)
docs/ INSTALL.md, API.md, DESIGN.md
storage/ logs, shots (git 제외)