릴리즈 이력 5
nexibase/nexibase
릴리즈 노트
v0.30.2 후속입니다. TLS를 끊는 리버스 프록시 뒤에서 쿠키 동의 저장이 항상 실패하던 문제를 고쳤습니다.
증상
배너나 쿠키 설정에서 선택을 저장하면 이 문구만 떴습니다.
저장하지 못했습니다. 잠시 후 다시 시도해 주세요.
동의는 저장되지 않고 배너가 계속 남습니다.
원인
POST /api/consent 의 같은 사이트 검사가 스킴까지 포함한 전체 출처를 비교했습니다.
Apache·Nginx 등이 HTTPS를 받아 앱에는 평문 HTTP로 넘기면서 X-Forwarded-Proto 를 붙이지 않으면:
| 값 | |
|---|---|
| 앱이 계산한 기대 출처 | http://example.com |
브라우저가 보낸 Origin |
https://example.com |
호스트는 같은데 스킴이 어긋나 모든 저장이 403 cross_origin 으로 거부됐습니다. 클라이언트는 실패 종류와 무관하게 같은 문구를 띄우기 때문에, 출처 거부가 저장 오류처럼 보였습니다.
로컬 개발 환경은 브라우저가 http:// 로 접속해 스킴이 우연히 맞아떨어지므로 재현되지 않습니다. TLS 종료 프록시가 앞에 설 때만 깨집니다.
수정
전체 출처 대신 호스트(와 포트) 를 비교합니다. X-Forwarded-Proto 설정 여부와 무관하게 동작하므로 vhost 를 고칠 필요는 없습니다.
CSRF 방어는 그대로입니다. 막아야 할 것은 다른 호스트에서 온 쓰기이고, 공격자는 운영자 호스트가 내려 주는 문서를 만들 수 없습니다. 같은 호스트의 평문 HTTP 를 가로챌 수 있는 공격자라면 이미 연결 전체를 장악한 상태라 이 검사로 막을 수 있는 범위 밖입니다.
판정 로직은 직접 테스트할 수 있도록 src/lib/consent/origin.ts 로 분리했습니다.
영향 범위
0.30.0 ~ 0.30.2 를 HTTPS 리버스 프록시 뒤에서 운영 중이라면 쿠키 동의 저장이 전혀 되지 않는 상태입니다. 배너는 정상적으로 보이기 때문에 저장 버튼을 눌러 보기 전에는 드러나지 않습니다.
쿠키 동의 배너를 켜 두었다면 업데이트를 권합니다.
확인 방법
수정 전후를 이렇게 구분할 수 있습니다.
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://example.com/api/consent \
-H "Origin: https://example.com" \
-H "Content-Type: application/json" \
-d '{"action":"reject_all","analytics":false}'
403 이면 영향을 받는 상태, 200 이면 정상입니다.
검증
- 출처 판정 테스트 9개 추가 — 수정 전 프록시 시나리오 2개가 실패하는 것을 먼저 확인한 뒤 수정
- 전체 테스트 49개 통과
- 프로덕션과 동일한 헤더 모양(
Host는 공개 도메인,Origin은https://, 앱 수신은 평문 HTTP)으로 실제 요청 →200 OK와nb_consent쿠키 발급 확인 - 다른 호스트에서 온 쓰기는 여전히
403 cross_origin
릴리즈 노트
v0.30.1 후속입니다. 푸터 저작권 옆에 있던 "쿠키 설정" 링크를 정식 쿠키 정책 페이지로 대체했습니다.
쿠키 정책 페이지
/policies/cookie 에 추가되고, 약관·개인정보처리방침과 같은 푸터 메뉴 그룹에서 링크됩니다.
정책은 사이트가 실제로 심는 쿠키만 기재합니다. 일반론으로 쓰지 않고 실제 Set-Cookie 헤더로 확인했습니다.
| 분류 | 쿠키 | 보존 |
|---|---|---|
| 필수 | nb_consent |
180일 |
| 필수 | NEXT_LOCALE |
세션 |
| 필수 | next-auth.session-token / csrf-token / callback-url |
24시간·세션 |
| 분석 | _ga, _ga_*, _gid (동의 후에만) |
24시간~2년 |
이 밖에 카테고리별 법적 근거, 변경·철회, 브라우저 차원의 관리, 동의 기록에 저장되는 항목(그리고 IP·User-Agent는 저장하지 않는다는 점), 제3국 이전, 정보주체의 권리를 다룹니다.
배너의 링크도 개인정보처리방침에서 이 페이지로 바뀌었습니다.
철회 수단의 위치
정책 페이지는 ePrivacy 5(3)과 GDPR 13조의 정보 제공 의무를 이행합니다. GDPR 7(3)의 철회는 별개 의무이고 읽을 거리로는 충족되지 않으므로, 설정 변경 버튼을 그 페이지에 함께 두었습니다. 정보와 조작 수단을 한자리에 모으는 유럽식 구성입니다.
철회 경로가 푸터의 쿠키 정책 링크를 지나가므로, 이 메뉴 항목을 지우면 방문자가 동의를 철회할 수 없게 됩니다.
함께 고친 것
- 크래시 — 동의 기능이 꺼져 있으면 Provider가 마운트되지 않는데, 정책 페이지의 설정 버튼이 그 상태에서 throw하는 훅을 호출했습니다.
- 동의 기능이 꺼져 있으면 쿠키 정책을 푸터에서 감춥니다. 그 모드에서는 정책 문구가 사이트가 운영하지 않는 절차를 설명하게 되기 때문입니다.
- i18n 오류 — 관리자 설정 페이지가
INVALID_MESSAGE: UNCLOSED_TAG로 실패했습니다. 검색엔진 소유 확인 설명 문구의<head>를 next-intl이 리치 텍스트 태그로 해석한 것으로, v0.30.0 이전부터 있던 문제입니다. - 배너·다이얼로그의 모두 거부·모두 동의를 채움 버튼으로 바꿔 결정 쌍이 도드라지게 했습니다. 둘은 서로 동일하게 유지됩니다 — 한쪽만 강조하면 EDPB Guidelines 03/2022이 다루는 기만적 설계가 됩니다.
업그레이드
마이그레이션이 기존 사이트에 쿠키 정책 본문과 푸터 메뉴 항목을 넣고, 배너 링크를 쿠키 정책으로 옮깁니다. 운영자가 consent_policy_slug를 따로 지정해 둔 경우는 건드리지 않습니다.
정책 본문은 임시 문구가 아니라 실제 내용으로 들어갑니다 — 방문자에게 보이는 법적 고지이기 때문입니다. 관리자 화면에서 언제든 수정하실 수 있습니다.
v0.29.x에서 올라오시는 경우 v0.30.0 릴리스 노트를 함께 확인하세요. 기존 사이트는 쿠키 동의 기능이 꺼진 상태로 업그레이드되며 관리자 설정에서 직접 켜야 합니다.
릴리즈 노트
v0.30.0 후속입니다. 전부 UI 조정이며 동의 로직·저장·게이팅·API는 건드리지 않았습니다. 동의 전 Google Analytics가 로드되지 않는 동작은 그대로입니다.
배너
흰 배경 페이지 위의 흰 카드가 테두리로만 구분되어, 방문자가 동의를 요구받고 있다는 사실 자체를 인지하지 못하는 문제가 있었습니다.
- 가장자리에서 띄운 카드에 그림자를 줘 페이지 위에 얹힌 층으로 보이게 했습니다
- 강조색 쿠키 아이콘과 16px 제목으로 색과 무게를 더했습니다
- 아래에서 올라오는 등장 모션을 넣었습니다 (
prefers-reduced-motion은 존중) - "설정" 버튼에 테두리를 넣어 클릭 대상으로 보이게 했습니다
위치는 하단 그대로입니다. 중앙에 두면 더 눈에 띄지만 본문을 덮는 것은 압박에 해당해, GDPR 4(11)이 동의를 자유롭게 주어질 것으로 요구하며 배제하는 지점입니다.
철회 진입점
모든 페이지 좌하단에 고정돼 따라다니던 버튼을 푸터 저작권 옆으로 옮겼습니다. 테마가 ConsentSettingsLink로 배치하며, 테마가 이를 빠뜨리면 Provider가 페이지 하단에 예비 줄을 대신 내보냅니다 — 철회 경로가 테마 선택에 좌우되면 GDPR 7(3)을 충족할 수 없기 때문입니다.
설정 다이얼로그
- 모두 거부 / 모두 동의 버튼을 추가했습니다. 수락이 배너에서 1클릭인데 철회가 3단계인 것은 7(3)의 "철회는 동의만큼 쉬울 것"에 미치지 못했습니다
- 현재 선택 상태를 상단에 표시합니다
토스트
저장하면 무엇이 저장됐는지 알려주고 5초 뒤 사라집니다. 코너 버튼이 주던 즉각적인 피드백을 대신하며, role="status"와 aria-live를 달아 배너가 사라질 때 스크린리더에 아무 안내가 없던 공백도 함께 메웠습니다.
업그레이드
v0.30.0에서 올라오는 경우 설정 변경 없이 그대로 적용됩니다. v0.29.x에서 올라오는 경우는 v0.30.0 릴리스 노트를 함께 확인하세요 — 기존 사이트는 기능이 꺼진 상태로 업그레이드되며 관리자 설정에서 직접 켜야 합니다.
릴리즈 노트
방문자가 동의하기 전에는 Google Analytics가 로드되지 않습니다. 동의 전 googletagmanager.com으로 나가는 요청이 0건이고, gtag 스크립트 태그 자체가 DOM에 존재하지 않습니다.
추가된 것
- 동의 배너 — 비modal. 콘텐츠를 가리거나 스크롤을 막지 않습니다. 거부 버튼이 수락과 같은 크기·같은 위치에 놓입니다.
- 설정 다이얼로그 — 카테고리별 선택과 철회를 겸합니다. 배너와 동일한 모두 거부/모두 동의를 두어 되돌리기가 1클릭입니다.
- 동의 기록 —
consent_logs테이블. 익명 UUID 기준 append-only이며 IP와 User-Agent를 저장하지 않습니다. 철회도 기존 행 수정이 아니라 새 행으로 남습니다. POST /api/consent— 교차 출처 요청과 JSON 아닌 본문을 거부합니다.- 관리자 토글 — 설정 > Google Analytics 섹션에서 켜고 끕니다.
- 거부 후 180일간 재요청 안 함.
기준
현행 ePrivacy 지침 5(3), GDPR 4(11)·7, EDPB Guidelines 2/2023(2024-10-16 채택)을 기준으로 구현했습니다. Digital Omnibus는 2026년 8월 현재 삼자협의 중이라 반영하지 않았고, 통과가 유력한 88a(4)(c)의 "거부 후 6개월 재요청 금지"만 180일로 선반영했습니다. 브라우저 신호(88b)는 Council 입장에서 삭제되어 제외했습니다.
업그레이드
기존 사이트는 기능이 꺼진 상태로 올라갑니다. GA는 지금까지와 똑같이 동작하고 배너도 뜨지 않습니다. 달라지는 것이 없습니다.
켜려면 관리자 설정에서 토글을 켜십시오. 켠 순간부터 방문자가 동의하기 전에는 GA가 수집되지 않으므로 통계가 그만큼 줄어드는 것이 정상입니다. 배너가 개인정보처리방침으로 링크하므로 policies 플러그인을 켜고 privacy 정책을 발행해 두어야 합니다.
신규 설치는 켜진 상태로 시작합니다.
함께 정리한 것
charmgen 전용 작업(AI 초안 기능, CharmgenHero 위젯, about 페이지)이 이 저장소에 올라와 있던 것을 되돌렸습니다. ecosystem.config.js가 pm2를 charmgen으로 가리키고 있어 이 저장소에서 배포하면 엉뚱한 애플리케이션이 뜨는 상태였고, ai-draft 플러그인에 withdrawal-policy.ts가 없어 npm run build가 실패하던 것도 함께 해소됐습니다. 해당 작업은 github.com/kagla/charmgen 로 갑니다.
릴리즈 노트
🐛 버그 수정
플러그인 사이드바 메뉴가 사라지는 문제 해결
src/plugins/<slug>/admin/menus.ts 파일의 첫 줄이 // src/plugins/... 같은 경로 주석으로 시작하면, scan-plugins.js 파서가 빈 메뉴 배열을 반환해 관리자 사이드바에서 해당 플러그인 메뉴가 통째로 사라지던 문제를 수정했습니다.
원인: new Function('return ' + content)로 평가할 때 JavaScript의 자동 세미콜론 삽입(ASI)이 return // 주석… 라인을 return;으로 종료시켜 뒤따르는 배열 리터럴이 도달 불가능해졌습니다.
수정 방식: withdrawal-policy.ts에서 이미 사용 중인 안전한 패턴(export default 이후만 잘라내기)을 동일하게 적용했습니다.
영향: AI 코딩 도구로 생성한 플러그인이나 IDE가 자동 삽입하는 경로 주석을 포함한 모든 admin/menus.ts가 이제 정상 동작합니다.
✨ 관리자 UX 개선
플러그인 관리 페이지 — URL 경로 클릭 가능
/admin/plugins 페이지에서 각 플러그인의 "URL 경로" 항목이 클릭 가능한 링크로 바뀌었습니다.
- 공개 라우트가 있는 플러그인(
hasRoutes: true): 새 탭으로 해당 페이지가 열리는 앵커 + ExternalLink 아이콘 + 호버 강조 - 공개 라우트가 없는 플러그인(관리자 전용 등): 기존처럼 일반 코드 텍스트
📦 변경 파일
- `scripts/scan-plugins.js` — admin/menus.ts 파서 강화
- `src/app/[locale]/admin/plugins/page.tsx` — URL 경로 링크화
- `package.json` / `package-lock.json` — v0.29.1 → v0.29.2
🔧 업그레이드 방법
기존 사이트에서: ```bash git fetch upstream git merge upstream/main npm install npm run build pm2 restart ```
스키마 변경이나 마이그레이션 없음, 의존성 추가/제거 없음 — 그대로 머지 후 재빌드만 하면 됩니다.
전체 변경: https://github.com/nexibase/nexibase/compare/v0.29.1...v0.29.2