🔍 정기점검🗺️ 전체 구도 확인

🔍 점검 방식: 영역별 정밀 감사(코드 실측 + 라이브 프로브 기반, 파일:줄 근거 포함) → 등급 분류 → 최우선 Top 10 도출. 등급: 치명 즉시 조치 높음 이번 주 중간 이번 달 낮음 여유 시 참고 판단용.
ⓘ 각 항목을 저(Claude)에게 지시하시면 코드 작업+검증까지 진행합니다. 완료된 항목은 다음 회차에서 조치완료로 표시해 갱신합니다.
📌 열람 구조: 최신 회차만 펼쳐져 있고 과거 회차는 접혀 있습니다(제목 클릭으로 열람). 각 항목의 K1-03식 번호는 회차 간 참조용 고유 ID입니다(나눔콘의 R1-03과 구분되도록 K 접두).
📊 영역 점수: 각 영역 제목 오른쪽의 현재/기반 두 숫자는 현재=지금 열려 있는 결함을 반영한 실제 상태, 기반=구조·설계 성숙도입니다. 회차 본문 맨 위 점수판에서 한눈에 볼 수 있습니다.
🔗 나눔콘과의 관계: 이 보고서는 KJH Nexus 전용입니다. 나눔콘 정기점검 3차에서 KJH 자산으로 판명된 항목은 이 1차로 이관했고, 나눔콘 쪽에는 나눔콘이 그 자산에 의존하는 부분만 남겼습니다.

제1차 종합검사

2026-08-01 · 10개 영역 정밀감사(코드 실측 + 라이브 프로브)

영역별 수준 — 종합 현재 86점 / 기반 74점(100점 만점 · 본문 맨 위 점수판 참조). 점수는 하드코딩이 아니라 아래 항목의 조치완료 배지를 읽어 자동 계산되므로, 항목을 처리하고 배지를 달면 점수가 즉시 오릅니다. ⚠️ 평균값 주의: 치명 3건이 두 영역(①·②)에 몰려 있어 10개 영역 평균이 실제 위험을 희석합니다 — 종합 86점보다 ②번 영역 30점이 지금의 실제 상태에 가깝습니다. 나눔콘 3차에서 KJH 자산으로 판명된 3건(무인증 GAS·광고 GAS 쓰기창구·네이버 키 평문)을 이 회차로 이관했습니다.

치명 3높음 2중간 9낮음 8참고 1

🚨 최우선 조치 Top 10

영향도 × 시급성 순 — 위에서부터 처리 권장 (본문 항목 ID 병기)

  1. 치명수료증 컬렉션이 무인증 목록조회를 허용 → 신청자 명단 보호가 통째로 우회됨 (①) — 실측: 로그인 없이 수료증 54건(이름·이메일·appId) 조회 성공, 거기서 얻은 appId로 신청서 단건 조회까지 성공(이름·전화·이메일·소속·비고). 규칙이 지키려던 "명단 일괄 유출 차단"이 무력화. K1-01
  2. 치명도구 GAS 13종 전량이 인증 없이 200 — 매출·연락처·통화기록이 익명에게 반환 (②) — 실측: PayCheck 입금원장 254건(휴대폰·이메일·매출), 교육CRM 411건, CallArchive 2,407건(고객명·상담요약·전화·녹취링크), LectureTrack 2012년~ 원장. K1-06
  3. 치명광고 GAS 쓰기 창구 인증 0줄 — 입찰가·예산·캠페인 조작 가능 (②) — scripts/gas-naver-ad/Code.gs:204 doPost가 검사 없이 곧바로 action 분기(입찰변경·키워드 온오프·캠페인 생성). K1-07
  4. 높음공개 배포된 도구 HTML 14곳에 GAS 배포 주소가 그대로 게시 (②) — "배포 주소는 사실상 비밀번호"라는 전제가 무너진 지점. 위 2건의 전달 경로. K1-08
  5. 높음네이버 검색광고 API 키·시크릿이 소스에 평문 커밋 (②) — scripts/gas-naver-ad/Code.gs:9-10. 저장소는 비공개라 즉시 유출은 아니나, 같은 파일에 안전 보관 패턴이 이미 있어 이전 비용이 낮음. K1-09
  6. 중간CSP가 전 페이지 Report-Only인데 보고 수집처(report-uri)도 없음 — 사실상 무효 헤더 (⑤) — 36개 도구 중 실제 집행은 tax-invoice 1곳뿐. 지금 그대로 켜면 script-srcscript.google.com이 없어 JSONP 도구 14종이 깨짐. K1-13
  7. 중간배포 안전장치가 KJH에만 없음 — 스냅샷·복구·게이트 전부 부재 (⑨) — 나눔콘은 safe-deploy·snapshot·restore + 훅 차단이 있는데 KJH는 CLAUDE.md:68이 날것 firebase deploy. 되돌릴 수단이 콘솔 롤백뿐. K1-21
  8. 중간시트 백업 등록 8종 vs 대장 15종 — 대조 장치 없음 (⑥) — 2026-07-17 시트 대량 영구삭제를 겪은 프로젝트인데, 대장과 백업 목록을 맞춰보는 자동 점검이 없음. K1-16
  9. 중간건강검진이 GAS 5종만 봄 — 실제 가동 13종 중 8종이 감시 사각 (⑦) — PayCheck·교육CRM·공개교육 참가자 등 민감 원장이 통째로 미감시. K1-17
  10. 중간관리자 판별이 이메일 문자열 비교 + email_verified 미검사 (①) — 나눔콘은 2026-06-28에 UID 기준으로 이전했는데 KJH는 이메일 기준 유지. 규칙 파일 전체가 같은 패턴. K1-04

📊 영역별 현재 수준 (100점 만점)

종합 / 기반 · — 점수는 아래 항목의 조치완료 배지를 읽어 자동 계산됩니다.

영역현재기반한 줄 근거

점수는 자동 계산됩니다 — 아래 항목에 조치완료 배지가 붙는 순간 그 항목의 감점이 사라지고 영역 점수·종합 점수가 즉시 다시 계산됩니다. 하드코딩된 숫자가 아닙니다. 다만 평균은 위험을 희석합니다 — 치명이 몰린 영역의 개별 점수를 함께 보세요.

① 접근통제·권한 규칙

5건 · 치명 1 / 중간 3 / 낮음 1

총평: firestore.rules 178줄은 설계 의도가 분명하고 주석도 충실합니다 — 신청자(applications)에서 getlist를 분리해 "개별 조회는 열되 명단 일괄 조회는 막는다"는 고급 패턴을 이미 쓰고 있고, funeral_public·edu_public은 아예 PII를 담지 않는 별도 공개 문서로 분리했습니다. storage.rules는 전 경로 소유자 전용으로 단순·안전합니다. 그런데 같은 파일 안에서 그 원칙이 한 곳에서 깨졌고, 하필 그 한 곳이 나머지를 전부 우회시킵니다 — 수료증 컬렉션이 read: if true(= get+list)로 열려 있어 명단이 통째로 나오고, 그 안에 들어 있는 appId가 신청서 단건 조회의 유일한 방어수단이었습니다. 동급 대비 수준: 규칙 작성 실력은 상위권인데, "규칙끼리 서로를 우회시키는가"를 보는 교차 검증이 없었습니다. 미실측: Storage 실제 객체 목록, App Check 미도입 상태의 실질 영향.

  • 치명수료증 컬렉션 무인증 목록조회 → 신청자 PII 보호가 통째로 우회firestore.rules:100-104certificatesallow read: if true입니다. 주석은 "토큰 보유자 공개 read (다운로드)"라고 적혀 있지만 readgetlist를 모두 포함하므로 컬렉션 전체 목록조회가 열립니다. 라이브 실측: 로그인 없이 공개 Firebase 웹 키만으로 certificates 54건을 조회했고 각 문서에 name·email·appId가 들어 있었습니다(edu-manager/index.html:1104가 그렇게 저장). 같은 조건으로 applications 목록조회는 PERMISSION_DENIED정상 차단됐습니다 — 즉 :80-83의 "전체 명단 조회·수정·삭제는 admin만 → 명단 일괄 유출 차단(PII 보호)"이라는 방어는 제대로 작성돼 있습니다. 문제는 우회 경로입니다: :78-79 주석이 "appId는 20자 랜덤이라 메일 링크 보유자(본인)만 조회 가능"이라는 전제로 allow get: if true를 열어 뒀는데, 그 20자 랜덤값이 수료증 목록에 그대로 실려 나옵니다. 실제로 유출된 appId로 신청서 단건 조회를 시도해 name·phone·email·org·note·userAgent가 반환되는 것까지 확인했습니다. 권고: certificatesapplications와 동일하게 allow get: if true; allow list: if isOwner();로 분리(한 줄 수정). 조회 화면(certificate.html:118)이 where('appId','==',…) 쿼리를 쓰므로 본인 링크 열람은 get이 아닌 list 경로입니다 — 해당 화면을 문서 직접 참조로 바꾸거나 수료증 id를 링크에 담는 변경이 함께 필요합니다.(쉽게: 수료증 목록이 로그인 없이 누구나 볼 수 있게 열려 있습니다. 거기에 수강생 이름·이메일이 들어 있고, 더 큰 문제는 신청서를 여는 열쇠(랜덤 번호)까지 같이 들어 있다는 점입니다. 그래서 "명단은 관리자만"이라고 막아 둔 장치가 이 경로로 완전히 돌아가집니다. 실제로 54명분이 열렸고, 그 열쇠로 전화번호까지 확인됐습니다.)
  • 중간instructor-assessments가 누구나 무제한 문서 생성 — 필드 검증 0firestore.rules:50-51allow create: if true로, 다른 공개 create 규칙들(applications는 이름·이메일 형식·길이 검증, surveys는 rating 범위 검증)과 달리 아무 조건이 없습니다. 익명 스크립트가 임의 크기 문서를 무제한 밀어 넣을 수 있어 쓰기 쿼터·저장 비용이 노출되고, 정상 데이터와 섞여 도구 화면이 오염됩니다. 권고: 같은 파일의 applications 패턴을 복제해 필수 필드·타입·길이 제한을 부여.(쉽게: 강사 평가 문서는 아무나, 아무 내용이나, 몇 개든 만들 수 있습니다. 옆에 있는 신청서 규칙은 "이름은 글자여야 하고 50자 미만" 같은 검사를 하는데 여기만 빠졌습니다.)
  • 중간Mentislido 응답이 전체 공개 읽기 — 참여자 이름이 목록조회로 노출firestore.rules:130responsesallow read: if true입니다(list 포함). 발표 중 응답을 화면에 띄워야 하니 공개 읽기 자체는 설계 의도로 보이지만, 방 id만 알면 종료된 세션의 참여자 이름·응답을 언제든 전량 조회할 수 있습니다. 생성은 status == 'live'인 방으로 잘 제한돼 있는데 읽기에는 같은 제한이 없습니다. 권고: 읽기에도 방 상태 조건을 걸거나, 세션 종료 시 응답을 집계 문서로 접고 원본을 소유자 전용으로 좁히기.(쉽게: 실시간 설문 응답이 발표가 끝난 뒤에도 계속 공개 상태로 남습니다. 방 주소를 아는 사람은 참여자 이름과 답변을 나중에도 전부 받아볼 수 있습니다.)
  • 중간관리자 판별이 이메일 문자열 비교 + email_verified 미검사 — 나눔콘과 기준 불일치firestore.rules:5-8isOwner()request.auth.token.email == '…'만 봅니다. 파일 전체에서 같은 비교가 12곳 반복되고 storage.rules도 동일합니다. 나눔콘은 2026-06-28에 최고관리자 판별을 이메일→구글 UID로 이전했는데 KJH는 이메일 기준 그대로입니다. 이메일 클레임은 검증되지 않은 공급자에서 위조 여지가 있고(email_verified 검사 부재), 함수 쪽 verifyAdmin()(functions/index.js:83)은 이미 email_verified !== true를 검사하고 있어 규칙과 함수의 기준이 서로 다릅니다. 권고: isOwner()를 UID 비교로 바꾸고 12곳을 헬퍼 호출로 통일(값 한 곳 변경으로 끝나도록).(쉽게: "관리자인지"를 이메일 주소가 같은지로만 판단합니다. 나눔콘은 이미 더 안전한 방식(구글 고유번호)으로 바꿨는데 여기만 옛 방식입니다. 게다가 서버 함수는 새 방식에 가깝게 검사하고 있어서 같은 시스템 안에 기준이 두 개입니다.)
  • 낮음익명 create 창구 3종의 물량 제한 부재checkins(:93-95surveys(:108-113mailOptOut(:169)이 모두 형식 검증은 하지만 건수 제한이 없습니다. 위 instructor-assessments와 달리 필드 검증이 있어 오염도는 낮지만, 문서 폭증에 의한 비용·쿼터 노출은 동일합니다. 권고: 우선순위는 낮으며, App Check 도입 시 한 번에 해소되는 계열이라 그 시점에 묶어 처리 권장.(쉽게: 출석 체크·설문·수신거부는 형식은 검사하지만 몇 개까지라는 제한이 없습니다. 당장 위험하진 않고, 나중에 App Check라는 장치를 켜면 한꺼번에 해결됩니다.)

② 외부 백엔드(GAS) 인증

4건 · 치명 2 / 높음 2

총평: 이번 회차 최대 취약 구간이며, 이 프로젝트에서 유일하게 "설계가 없는" 영역입니다. 도구가 쓰는 GAS 웹앱 13종을 전부 라이브 프로브한 결과 13/13이 인증 없이 HTTP 200이었고, 그중 4종은 원장을 통째로 반환했습니다(PayCheck 254건·교육CRM 411건·CallArchive 2,407건·LectureTrack 2012년~). 쓰기 쪽은 더 나빠서, 광고 GAS의 doPost는 검사 한 줄 없이 입찰가·예산·캠페인 조작을 처리합니다. 이 구조를 지탱해 온 유일한 전제는 "배포 주소를 아무도 모른다"였는데, 그 주소가 공개 배포된 도구 HTML 14곳에 평문으로 실려 있어 전제가 성립하지 않습니다. 구조적 원인: 도구가 JSONP(<script src>)로 GAS를 부르기 때문에 브라우저가 인증 헤더를 실을 수 없고, GAS 웹앱은 요청 헤더를 읽을 수 없어 표준 토큰 방식이 아예 불가능합니다 — 그래서 "공유 시크릿을 쿼리로 전달 + 브라우저는 서버 프록시 경유"가 사실상 유일한 해법입니다. 참고로 그 해법의 선례가 이미 이 저장소에 있습니다(autoproposeServerProxy + PropertiesService 패턴, edu-history 2026-05-21 보안 마이그레이션). 동급 대비 수준: 프록시·시크릿 패턴을 이미 구현해 본 팀이 정작 가장 민감한 원장 13종에는 적용하지 않은 상태입니다. 미실측: 각 GAS 웹앱의 콘솔 접근권한 설정값(익명 200 응답으로 '모든 사용자' 추정), 녹취 파일 링크(file_url)의 실제 공개 범위 — 확인 시 타인의 통화 녹음을 열게 되므로 의도적으로 미검증, 쓰기 요청은 일절 발송하지 않음(읽기 GET만).

  • 치명도구 GAS 13종 전량 무인증 — 매출·연락처·통화기록이 익명에게 JSON으로 반환 — 배포된 도구 HTML에서 GAS 주소를 추출해 13종을 프로브했고 전부 200이었습니다. 응답 스키마만 확인한 결과(값은 기록하지 않음): PayCheck 254건 — 담당자·휴대폰·이메일·교육비·커미션·매출·강사 + bankData, 교육CRM 411건 — contact·contactPhone·contactEmail·revenue·commission, CallArchive 2,407건 — client_name·summary·phone_number·file_url(녹취 링크), LectureTrack ?action=data 1.76MB — 2012-03-02부터의 전 교육 원장(엔드유저·담당자·휴대폰·이메일 포함). 소스에서도 확인됩니다: scripts/gas-call-archive/Code.js:1024·scripts/gas-edu-history-bound/Code.js:2952doGet은 첫 줄부터 파라미터 분기로 들어가며 인증 검사가 없습니다. 권고: ⓐ 각 GAS doGet 첫 줄에 PropertiesService 공유 시크릿 검증 추가 ⓑ 브라우저 도구는 키를 실을 수 없으므로 verifyAdmin 게이트가 붙은 서버 프록시(/api/gasProxy) 경유로 전환 ⓒ 서버 소비처(나눔콘 functions/index.js:2151·2153, KJH functions/index.js:1038-1044)에 키 적용. 순서가 중요합니다 — 프록시·소비처를 먼저 배포하고 GAS 차단을 마지막에 켜야 도구가 죽지 않으며, clasp 재배포 시 접근권한이 '제한됨'으로 리셋되므로 UI에서 권한 재설정까지 한 세트로 진행해야 합니다.(쉽게: 입금 장부·영업 장부·통화 기록·14년치 교육 장부를 돌려주는 주소가 13개 전부 로그인 없이 열립니다. 고객 회사명, 담당자 이름·휴대폰·이메일, 매출, 상담 내용이 그대로 나옵니다. 지금까지 안전했던 이유는 오직 "주소를 아무도 모를 것"이었는데, 그 주소가 공개된 페이지에 적혀 있습니다.)
  • 치명광고 GAS 쓰기 창구 인증 0줄 — 광고비 소진·캠페인 조작이 인터넷에 열림scripts/gas-naver-ad/Code.gs:204doPostJSON.parse(e.postData.contents) 직후 곧바로 switch(action)으로 들어가며 인증 검사가 한 줄도 없습니다. 처리 액션에 입찰가 변경(updateBid)·키워드 온오프·키워드 추가삭제·캠페인 생성·광고그룹 생성·제외키워드 추가가 포함됩니다. 구글 광고 쪽도 쓰기 창구가 무인증이며 시트 큐 경유라 반영이 지연될 뿐입니다. 나눔콘 admin은 읽기 3종만 사용하므로 이 쓰기 액션들은 순수한 공격면입니다. 주소를 알아낸 사람은 하루 만에 광고비를 태우거나 광고를 전부 끌 수 있습니다. ※ 검증은 읽기(GET)만 수행했고 쓰기 요청은 일절 보내지 않았습니다. 권고: 쓰기 창구 첫 줄에 PropertiesService 공유 시크릿 검증 + 나눔콘 admin의 GAS 직접 호출을 서버 프록시로 이동. (나눔콘 3차 R3-69에서 발견 → 자산 소재지가 KJH이므로 이 회차로 이관)(쉽게: 광고 입찰가와 예산을 바꾸는 창구가 아무 확인 없이 열려 있습니다. 우리 관리자 화면은 조회만 쓰기 때문에, 이 쓰기 창구는 지금 아무 쓸모 없이 위험만 있는 상태입니다.)
  • 높음공개 배포된 도구 HTML 14곳에 GAS 배포 주소가 평문 게시 — "주소=비밀번호" 전제 붕괴 — 배포본 index.html을 전수 스캔한 결과 14개 도구가 GAS 주소를 하드코딩하고 있습니다(call-archive·lecture-track·paycheck·edu-crm·Tax-planner·open-edu-manager·edu-receipt·birthday-coupon·quiz-reward·cross-writer·curriculum-editor·detail-page-maker·task-todo·Presenter-analyzer). 라이브에서 kjh-nexus.web.app/tools/KJH_tools/call-archive/index.html비로그인 HTTP 200으로 본문에 주소를 포함해 응답하는 것을 확인했습니다. 페이지에 로그인 게이트(js/firebase-init.js:14-26)가 있지만 그것은 렌더 시점 리다이렉트일 뿐 HTML 자체는 이미 전송되며, robots.txtnoindex도 없어 차단 신호는 0입니다. 나눔콘 R2-118은 "GAS 배포 주소는 사실상 비밀번호"라는 원칙으로 자산 대장에서 값을 가렸는데, 그 비밀번호가 형제 프로젝트 공개 호스팅에 게시된 상태였습니다. 권고: 위 K1-06의 프록시 전환과 함께 HTML에서 주소를 제거. 프록시 전환을 미룰 경우의 최소안은 주소를 HTML에서 빼고 로그인 후 관리자 전용 Firestore 문서에서 받아오게 하는 것으로, GAS·서버를 건드리지 않아 재배포 함정을 피할 수 있습니다(단 인증이 아니라 전제 회복에 그침).(쉽게: 도구 페이지 14개가 "장부 주소"를 페이지 안에 그대로 적어 두고 있습니다. 로그인 화면이 뜨긴 하지만 그건 화면을 가리는 것뿐이고, 페이지 내용 자체는 이미 전달됩니다.)
  • 높음네이버 검색광고 API 키·시크릿이 소스에 평문 커밋scripts/gas-naver-ad/Code.gs:9-10scripts/naver-ad-sync.py:9에 값이 그대로 들어 있습니다(값은 여기 적지 않습니다). 저장소가 비공개이고 firebase.jsonignore**/*.gs가 있어 공개 호스팅에는 배포되지 않는 것을 실측 확인했으므로 즉시 유출은 아닙니다. 다만 같은 파일 20행에 이미 PropertiesService 보관 패턴이 있어 이전 비용이 매우 낮습니다. 위 무인증 문제를 고치면 키 자체의 중요도가 올라가므로 인증 추가와 같은 작업에서 함께 처리하는 것이 효율적입니다. 권고: 스크립트 속성으로 이전 + 네이버 광고 콘솔에서 키 재발급. (나눔콘 3차 R3-70에서 발견 → 파일 소재지가 KJH이므로 이 회차로 이관)(쉽게: 광고 시스템 접속용 아이디·비밀번호가 코드 파일에 그대로 적혀 있습니다. 저장소가 비공개라 새어나간 건 아니고, 같은 파일에 이미 "안전 보관" 방식을 쓰는 곳이 있어 옮기기 쉽습니다.)

③ 시크릿·자격증명 위생

2건 · 중간 1 / 낮음 1

총평: Cloud Functions 쪽 위생은 양호합니다 — API 키가 전부 defineSecret으로 선언돼 Secret Manager에 있고, 함수 소스에 평문 키가 없습니다. 특히 firebase.jsonignore 목록이 정교하게 설계돼 있어 **/*.gs·**/gas-backend.js·**/*.md·**/.*·**/paycheck/bank/**가 공개 배포에서 제외됩니다. 이 항목은 실측으로 확인했습니다 — 평문 시크릿이 든 quiz-reward/gas-backend.js를 라이브에서 요청하면 파일이 아니라 앱 셸(index.html)이 돌아오며, 즉 배포되지 않았습니다. 만약 이 ignore 한 줄이 빠졌다면 카카오 시크릿이 인터넷에 그대로 노출됐을 것이므로, 설계가 실제로 사고를 막고 있는 사례입니다. 남은 결함은 GAS 소스 쪽 평문 잔존 2건이며 둘 다 저장소 내부에 머무릅니다. 동급 대비 수준: 배포 경계 설계는 상위권, GAS 자격증명 관리만 뒤처짐. 미실측: Secret Manager 실제 버전·순환 이력, 사용되지 않는 시크릿 상주 여부.

  • 중간카카오 클라이언트 시크릿 평문 커밋(공개 배포는 아님 — 실측 확인)public/tools/KJH_tools/quiz-reward/gas-backend.js:16과 사본 scripts/gas-quiz-reward/Code.js:16KAKAO_CLIENT_SECRET이 평문입니다. 다만 firebase.jsonignore**/gas-backend.js가 있어 배포 대상에서 제외되며, 라이브 요청 시 앱 셸이 반환되는 것으로 미배포를 확인했습니다. 따라서 노출면은 저장소 내부로 한정됩니다. 다만 위치가 public/ 아래라는 점이 구조적 위험입니다 — ignore 한 줄이 실수로 빠지거나 파일명이 바뀌는 순간 즉시 공개됩니다. 같이 검출된 AIzaSy… Firebase 웹 API 키(6곳)는 설계상 공개되는 값이라 결함이 아닙니다(오탐으로 분류). 권고: 시크릿을 PropertiesService로 이전하고, GAS 소스 사본은 public/ 밖(scripts/)으로 일원화.(쉽게: 카카오 비밀키가 코드에 적혀 있는데, 다행히 배포 제외 목록에 걸려 인터넷에는 안 나갑니다(직접 확인했습니다). 문제는 그 파일이 하필 "공개 폴더" 안에 있어서, 제외 목록에서 한 줄만 빠지면 바로 새어나간다는 점입니다.)
  • 낮음서울시 지하철 API 키가 평문 HTTP로 전송functions/index.js:2344·2358http://swopenapi.seoul.go.kr/api/subway/${SEOUL_METRO_KEY}/… 형태로 암호화되지 않은 연결에 키를 URL 경로로 실어 보냅니다. 서버-서버 호출이라 노출면은 좁고 키의 가치도 낮지만(공공 오픈API), 경로 중간의 관찰자에게 키가 그대로 보입니다. 권고: https로 변경(해당 API가 HTTPS를 지원하면 한 글자 수정), 미지원 시 키 노출을 감수한다는 판단을 주석에 남기기.(쉽게: 지하철 정보를 받아올 때 암호화 안 된 연결로 열쇠를 보냅니다. 공공 데이터용 열쇠라 큰 피해는 없지만, 주소만 https로 바꾸면 끝나는 문제입니다.)

④ 백엔드 함수(Cloud Functions)

1건 · 중간 1

총평: 이번 회차에서 가장 건강한 영역입니다. functions/index.js 3,112줄에 HTTP 함수 22개·스케줄 함수 8개가 있는데, 22개를 전수 확인한 결과 인증이 누락된 함수가 없습니다 — 사용자 대면 프록시 10종은 verifyAdmin()(Bearer ID 토큰 + email_verified 검사 + 관리자 일치), 서버-서버 창구 7종은 공유 시크릿 헤더(x-autopropose-secret/x-callarchive-secret), 웹훅 2종은 쿼리 키 검증, trainProxy는 별도 verifyTrainUser()로 가족 2계정만 허용합니다. invoker: 'public'이 22곳에 있지만 이는 Cloud Run IAM을 열고 인증을 코드 안에서 하는 의도적 설계이며 소스 주석에도 명시돼 있습니다. 부가 품질도 좋습니다 — Brevo 웹훅은 sha1(messageId|event|ts|email) 멱등키로 재전송 중복을 막고, 나눔콘과 계정을 공유하는 상황을 kjh_ 태그 라우팅으로 처리하며, 건강검진은 GAS 특유의 302→2차 홉 404 블립을 재시도로 흡수합니다. 유일하게 열린 창구가 수신거부 1종이며 이는 공개여야 하는 성격이라 결함은 "물량 제한 부재"로 한정됩니다. 동급 대비 수준: 개인 프로젝트 기준으로는 이례적으로 높습니다. 미실측: 실제 호출량·비용, 스케줄 함수 8종의 최근 실행 성공률.

  • 중간mailUnsubscribe에 토큰·레이트리밋 부재 — 제3자가 임의 주소를 수신거부 목록에 등록 가능functions/index.js:2862-2881은 인증 없이 {email}만 받아 형식 검사 후 mailOptOut/{emailKey}에 기록합니다. 공개 창구인 것 자체는 맞는 설계(메일 하단 링크에서 호출)지만, 수신거부 링크가 서명 토큰을 담지 않아 임의의 이메일을 대신 등록할 수 있습니다. 결과는 조용한 발송 차단입니다 — 대상자는 메일이 안 온다는 사실을 모르고, 발송 측도 원인을 알기 어렵습니다. 레이트리밋도 없어 대량 등록이 가능합니다. 권고: 수신거부 URL에 hmac(email, secret) 토큰을 실어 발급하고 함수에서 검증(발송 코드 한 곳·검증 한 줄), IP당 분당 상한 추가.(쉽게: 메일 하단의 "수신거부"를 남이 대신 눌러줄 수 있습니다. 주소만 알면 그 사람을 수신거부 목록에 넣어버릴 수 있고, 당사자는 메일이 안 온다는 것도 모른 채 넘어갑니다.)

⑤ 호스팅·보안헤더

3건 · 중간 1 / 낮음 2

총평: 라이브 응답 헤더를 실측한 결과 X-Content-Type-Options·X-Frame-Options·Strict-Transport-Security(preload 포함)·Referrer-Policy·Permissions-Policy모두 적용돼 있습니다. 배포 제외 목록도 앞서 본 대로 정교합니다. 문제는 CSP 하나에 몰려 있습니다 — 정책 문자열 자체는 잘 작성돼 있는데(object-src 'none'·frame-ancestors 'none'·base-uri 'self' 등) 전 페이지에 Content-Security-Policy-Report-Only로 붙어 있고 report-uri/report-to도 없습니다. 즉 차단도 안 하고 보고도 안 받는, 아무 일도 하지 않는 헤더입니다. 그리고 지금 그대로 집행 모드로 바꾸면 도구 14종이 즉시 깨집니다script-srcscript.google.com이 없는데 그 도구들이 JSONP로 GAS를 부르기 때문입니다. 결국 ②번 영역의 프록시 전환이 CSP 집행의 선행 조건이기도 합니다. 동급 대비 수준: 헤더 커버리지는 상위권, CSP 성숙도만 최하위. 미실측: 도구별 실제 CSP 위반 건수(수집처가 없어 측정 불가 — 이것이 결함의 핵심).

  • 중간CSP가 전 페이지 Report-Only + 보고 수집처 없음 = 무효 헤더, 집행 전환은 도구 14종과 충돌firebase.json**/*.html 블록이 Content-Security-Policy-Report-Only이며 라이브 응답에서도 동일 확인했습니다. report-uri/report-to가 없어 위반 보고를 받는 곳이 없습니다 — 차단도 관측도 하지 않으므로 현재 이 헤더의 실효는 0입니다. 실집행(Content-Security-Policy)은 **/tools/KJH_tools/tax-invoice/** 1곳뿐이며 도구는 37종입니다. 또한 정책의 script-src'self' 'unsafe-inline' gstatic cdnjs jsdelivr인데 script.google.com이 없어, K1-08의 JSONP 도구 14종은 집행 전환 즉시 데이터 로딩이 차단됩니다. 권고: 순서가 중요합니다 — ⓐ 먼저 report-to 수집처를 붙여 실제 위반을 며칠 관측 ⓑ ②번 영역 프록시 전환으로 JSONP 의존 제거 ⓒ 그 뒤 집행 모드 전환. 프록시 전환 전에 급히 켜면 도구가 죽습니다.(쉽게: 웹페이지 보안 장치(CSP)가 "감시만 하고 막지는 않는" 모드인데, 그 감시 기록을 받아보는 곳도 없습니다. 그래서 지금은 있으나 마나입니다. 그렇다고 바로 "막는 모드"로 바꾸면 도구 14개가 즉시 고장납니다 — 먼저 ②번을 고쳐야 이걸 켤 수 있습니다.)
  • 낮음catch-all 재작성 때문에 존재하지 않는 경로도 HTTP 200 — 404가 없음firebase.json 재작성 22개 중 마지막이 {"source":"**","destination":"/index.html"}입니다. 실측 결과 임의 경로도 200(9,445B 앱 셸)을 반환합니다. 부작용: ⓐ 배포 누락을 알아채기 어렵습니다 — 파일이 안 올라가도 200이 오므로 "있다"고 오판하게 됩니다(이번 감사에서도 gas-backend.js 200을 처음에 노출로 오해할 뻔했고, 내용을 열어보고서야 앱 셸임을 확인했습니다) ⓑ 링크 검사·모니터링 도구가 깨진 링크를 잡지 못합니다. 권고: 정적 자산 확장자(.js·.css·.json·.md)는 재작성에서 제외해 실제 404가 나오게 하기.(쉽게: 없는 주소를 요청해도 "정상"이라고 답합니다. 그래서 파일을 빠뜨리고 배포해도 티가 안 납니다. 실제로 이번 점검에서 저도 한 번 헷갈렸습니다.)
  • 낮음버전 쿼리를 쓰면서 캐시는 5분 — 불변 캐시 미적용firebase.json** 규칙이 Cache-Control: public, max-age=300입니다. 그런데 CSS·JS는 ?v=20260523j 형태의 버전 쿼리로 이미 캐시 무효화를 제어하고 있으므로, 5분마다 재검증할 이유가 없습니다. 나눔콘은 같은 상황에서 immutable 장기 캐시를 적용해 둔 상태라 두 프로젝트 정책이 어긋납니다. 권고: 버전 쿼리를 쓰는 css/**·js/**max-age=31536000, immutable 적용(HTML은 지금대로 짧게 유지).(쉽게: 디자인 파일에 이미 "버전 번호"를 달아 두고도 5분마다 다시 확인하고 있습니다. 버전 번호가 있으면 1년을 캐시해도 안전하고, 그만큼 페이지가 빨라집니다.)

⑥ 데이터 보존·백업

1건 · 중간 1

총평: 2026-07-17 시트 대량 영구삭제 사고 이후 체계가 실제로 세워졌습니다 — Firestore export 매일 03:20(functions/index.js:966), 구글시트 xlsx 백업 04:00(:991), 저장 위치 gs://kjh-nexus-backups-asia 30일 수명주기, 추가로 외장 미러(scripts/backup-to-external.sh) 영구 누적. 시트를 한 폴더(00.sheets)로 모으고 백업 로봇 서비스계정에 뷰어 공유까지 해 둔 것도 정석입니다. 남은 구멍은 "목록끼리 맞춰보는 사람이 없다"는 점입니다 — DATASOURCES.md 대장에 시트가 15종인데 KJH_BACKUP_SHEETS(:955-964)에 등록된 것은 8종이고, 그 차이 7종이 사고로 사망한 시트인지 등록 누락인지 대장만으로는 판별할 수 없습니다. 대장 자체에 "새 시트를 만들면 이 목록에 추가할 것"이라는 지침이 적혀 있는데 그 지침의 준수 여부를 검사하는 장치가 없습니다. 동급 대비 수준: 백업 자산은 상위권, 목록 정합성 감사만 부재. 미실측: 백업본 실제 복원 테스트(드릴 미실시), 외장 미러의 최근 성공 여부(나눔콘 3차에서 8일 실패가 확인된 계열이라 KJH 쪽도 점검 대상).

  • 중간시트 대장 15종 vs 백업 등록 8종 — 차이 7종의 성격을 대장만으로 판별 불가DATASOURCES.md의 시트 목록은 15종인데 functions/index.js:955-964의 백업 대상은 8종입니다. 미등록 7종에는 Open-edu-manager(참가자 PII)·API_Key_Manager_BD·API_TokenTrack_BD·NanumCon/NanumCam 대시보드·BPT CRM 2종이 포함됩니다. 이 중 일부는 2026-07-17 사고로 이미 사망했을 가능성이 있으나, 대장에 생사 표시가 없어 "죽은 시트"와 "백업 누락"이 구분되지 않습니다 — 이 상태 자체가 결함입니다. 사고를 한 번 겪은 프로젝트에서 가장 값싸게 재발을 막는 장치가 바로 이 대조입니다. 권고: ⓐ 대장 각 행에 생사·백업등록 여부 열 2개 추가 ⓑ 백업 함수 실행 시 KJH_BACKUP_SHEETS와 실제 접근 가능한 시트를 대조해 불일치가 있으면 메일 1통(기존 백업 함수 안에 몇 줄) ⓒ 사망 확인된 시트는 대장에서 아카이브 표기로 이동.(쉽게: 시트 목록표에는 15개가 적혀 있는데 백업하는 건 8개입니다. 나머지 7개가 "이미 없어진 시트"인지 "백업을 빠뜨린 시트"인지 표만 봐서는 알 수 없습니다. 시트를 한 번 대량으로 잃어본 적이 있으니, 이 대조표 하나가 가장 싼 보험입니다.)

⑦ 관측성·경보

2건 · 중간 1 / 낮음 1

총평: 감시 장치의 설계 품질 자체는 상당히 높습니다opsHealthCheck(functions/index.js:1169)는 단순 도달 확인을 넘어 ⓐ LectureTrack 데이터 신선도(동기화 정지 감지, freshDays:3) ⓑ 자동제안 GAS의 익명 접근 가능 여부(clasp 재배포가 권한을 '제한됨'으로 리셋하는 재발 사고 감지) ⓒ 생일쿠폰 GAS의 healthz 딥체크(2026-07-20 OAuth 승인 회수 사고 재발 대비)까지 봅니다. GAS 특유의 302→2차 홉 404 블립을 오탐하지 않도록 25초 타임아웃·3회 백오프 재시도를 둔 것도 실전 경험이 반영된 설계입니다. 문제는 설계가 아니라 커버리지입니다 — 감시 대상이 5종인데 도구가 실제로 쓰는 GAS는 13종이라 8종이 사각이고, 사각에 하필 PayCheck·교육CRM·공개교육 참가자 같은 민감 원장이 들어 있습니다. 동급 대비 수준: 감시자 한 개의 완성도는 상위권, 감시 범위 관리가 부재. 미실측: 최근 경보 발송 이력, 외장 백업 스크립트의 KJH 쪽 최근 성공 여부.

  • 중간건강검진이 GAS 5종만 감시 — 실제 가동 13종 중 8종이 사각functions/index.js:1038-1044OPS_ENDPOINTS는 LectureTrack·CallArchive·CrossWriter·상세페이지 메이커·생일 축하 5종입니다. 그런데 배포된 도구 HTML에서 추출한 실제 사용 GAS는 13종이며, 미감시 8종에 PayCheck(입금 원장)·교육CRM(영업 원장)·공개교육 참가자(PII)·교육비 영수증·퀴즈리워드·Presenter-analyzer·curriculum-editor·task-todo가 포함됩니다. 이들이 조용히 죽으면 도구를 열어봐야만 압니다 — 이 프로젝트는 이미 그 방식으로 두 번(2026-07-17 시트 삭제, 2026-07-20 OAuth 회수) 뒤늦게 알아챈 이력이 있습니다. 권고: OPS_ENDPOINTS에 8종 추가(각 1줄). 목록의 단일 원본을 DATASOURCES.md와 맞추는 것이 이상적이며, ⑥번 대조 장치와 같은 작업으로 묶으면 한 번에 끝납니다.(쉽게: 시스템이 살아 있는지 매일 확인하는 장치가 5개만 보고 8개는 안 봅니다. 안 보는 8개에 하필 입금 장부·영업 장부·참가자 명단이 들어 있습니다. 이것들이 조용히 멈추면 도구를 열어보기 전엔 모릅니다.)
  • 낮음경보 채널이 메일 단일 경로 — 발송사가 죽으면 "경보가 죽었다"는 사실도 경보되지 않음 — 건강검진·백업·클라이언트 알림 등 모든 관리자 경보가 Brevo 한 곳을 거쳐 jazzmania74@gmail.com으로 갑니다. 나눔콘 3차에서도 동일 구조가 지적됐고(완화안 미채택), KJH는 나눔콘과 같은 Brevo 계정을 공유하므로 장애가 나면 두 프로젝트의 경보가 동시에 침묵합니다. 권고: 치명·높음 경보만이라도 2차 채널(문자)을 붙일지 결정 항목으로 상정. 나눔콘에 이미 솔라피 연동이 있으므로 재사용 가능합니다.(쉽게: 모든 알림이 메일 한 곳을 통해 나갑니다. 그 회사에 장애가 나면 "알림이 안 온다"는 사실 자체를 알 방법이 없고, 나눔콘과 같은 계정이라 두 곳이 동시에 조용해집니다.)

⑧ 프런트엔드 도구 품질

2건 · 낮음 1 / 참고 1

총평: 도구 37종을 innerHTML 사용량 대비 이스케이프 함수(esc/escapeHtml/textContent) 보유 여부로 스캔했습니다. 대부분은 방어가 있습니다 — lecture-track(26:56)·nanum-rag(25:34)·curriculum-maker(23:34)·tax-invoice(22:38)처럼 이스케이프 호출이 innerHTML보다 많은 도구가 다수이고, call-archive는 아예 "시트 데이터를 innerHTML에 안전하게 삽입"이라는 주석과 함께 esc()를 정의해 두었습니다. 무방비는 소수이며 그마저도 데이터 출처가 본인·가족 입력이라 실질 위험은 낮습니다. 다만 이 프로젝트에서 진짜 프런트엔드 결함은 XSS가 아니라 "인증수단을 화면 코드에 넣은 것"이며, 그것은 ②번 영역에서 다뤘습니다. 동급 대비 수준: 개인 도구 모음 기준으로 방어 수준이 높은 편. 미실측: 도구별 실제 페이로드 주입 시험(운영 데이터를 오염시키므로 미수행), 37종 전수 코드 리뷰(사용량 상위만 표본 점검).

  • 낮음이스케이프 없이 innerHTML을 다수 쓰는 도구 2종train-tracker(innerHTML 66회 / 이스케이프 0)와 woncheon-Tax(21회 / 0)가 무방비입니다. 다만 두 도구 모두 데이터 출처가 본인 또는 가족 계정의 직접 입력이고(train_shotsfirestore.rules:143-149로 2계정만 쓰기 가능), 외부 입력을 받는 경로가 없어 실현 가능한 공격자가 없습니다. 등급을 낮음으로 둔 이유입니다. 권고: 다음에 해당 도구를 손볼 때 esc() 헬퍼 하나를 추가하고 렌더 함수에서 통과시키는 정도로 충분합니다. 지금 별도 작업으로 잡을 필요는 없습니다.(쉽게: 화면에 글자를 그릴 때 안전 처리를 안 하는 도구가 2개 있습니다. 그런데 그 도구에 글을 넣을 수 있는 사람이 대표님과 가족뿐이라, 지금은 실제로 문제가 생길 수 없습니다. 나중에 그 도구를 고칠 일이 생기면 그때 같이 처리하면 됩니다.)
  • 참고GAS 주소가 14개 도구에 흩어져 하드코딩 — 주소 교체 시 14곳 동시 수정 — 결함이라기보다 다음 작업의 비용 지표로 기록합니다. K1-06·K1-08 조치로 GAS 주소를 바꾸거나 프록시로 옮길 때, 도구 14곳을 각각 고쳐야 하고 하나라도 빠지면 그 도구만 조용히 죽습니다(⑦번의 감시 사각과 겹치면 발견이 늦습니다). 권고: 조치 시 주소를 도구마다 적지 말고 공용 설정 한 곳(관리자 전용 Firestore 문서 또는 /js/의 공용 모듈)에서 받아오는 형태로 통일. 이렇게 하면 이후 주소 변경이 1곳 수정으로 끝나고, K1-08의 "HTML에 주소 게시" 문제도 같이 해소됩니다.(쉽게: 장부 주소가 도구 14곳에 따로따로 적혀 있습니다. 나중에 주소를 바꾸면 14군데를 다 고쳐야 하고, 한 곳을 빠뜨리면 그 도구만 조용히 고장납니다. 고칠 때 주소를 한 곳에 모아두는 방식으로 바꾸면 이 문제가 같이 없어집니다.)

⑨ 배포·레포 위생

2건 · 중간 1 / 낮음 1

총평: 버전관리 위생은 좋습니다 — 감사 시점 미커밋 변경 0건이고, GAS 10종이 clasp로 저장소에 들어와 있어 구글 콘솔 밖에서도 소스가 추적됩니다(이번 감사에서 GAS 인증 부재를 코드로 확인할 수 있었던 것도 이 덕분입니다). ignore 설계도 앞서 본 대로 우수합니다. 사전 차단은 일부 작동하고, 사후 복구가 통째로 없습니다 — 나눔콘의 Claude 훅(001.Nanumcon.com/scripts/hooks/deploy-gate.sh)은 규칙 ②로 "타 프로젝트 배포도 대상 폴더가 미커밋이면 차단"하므로 KJH 배포에도 걸립니다. 이 감사 중 실제로 차단되는 것을 확인했습니다(미커밋 상태로 KJH 호스팅 배포를 시도 → 훅이 대상 폴더를 000.KJH Nexus로 식별해 중단). 즉 "안전장치가 0"이라는 서술은 과장이며 이 자리에서 정정합니다. 그러나 나눔콘과 KJH가 받는 보호 수준은 다릅니다 — 나눔콘 hosting은 훅이 무조건 차단하고 safe-deploy.sh로 유도하는 반면(스냅샷·preflight·JS압축·배포후검증이 전부 그 경로에만 있음), KJH는 커밋만 돼 있으면 날것 firebase deploy가 그대로 통과합니다. KJH의 scripts/에는 gen-architecture.mjs 하나뿐이라 유도할 정문 자체가 없고, CLAUDE.md:66-68firebase deploy 한 줄입니다. 결과: 배포 전 스냅샷이 남지 않아 되돌릴 수단이 Firebase 콘솔 롤백뿐입니다. 덧붙여 그 훅의 등록 파일(.claude/settings.local.json)은 git 제외 대상이라 이 컴퓨터 한 대에만 존재합니다(나눔콘 3차 R3-72와 동일 뿌리) — 즉 다른 머신에서는 KJH에 사전 차단조차 없습니다. 도구 37종·함수 30종이 도는 프로젝트의 규모에 비하면 이 격차가 가장 큽니다. 동급 대비 수준: 나눔콘 대비 명확히 뒤처지며, 두 프로젝트를 같은 사람이 같은 방식으로 운용한다는 점에서 이전 비용이 낮은 격차입니다. 미실측: 과거 배포 실패·롤백 이력.

  • 중간배포 전 스냅샷·복구·preflight 부재 — 되돌릴 수단이 콘솔 롤백뿐먼저 정정: 초안에서 "안전장치 전무"라고 적었으나 사실이 아닙니다. 나눔콘의 Claude 훅이 타 프로젝트 배포도 미커밋이면 차단하므로 KJH도 커버되며, 이 감사 중 실제 차단을 확인했습니다. 실제 결함은 그 다음 단계입니다 — 나눔콘 hosting은 훅이 무조건 막고 safe-deploy.sh로 유도해 스냅샷·preflight·배포후검증을 강제하는데, KJH는 커밋만 돼 있으면 날것 firebase deploy가 통과합니다. 유도할 정문이 없기 때문입니다(scripts/gen-architecture.mjs 하나뿐, CLAUDE.md:66-68도 날것 명령 한 줄). 따라서 배포 전 복구 지점이 남지 않아 사고 시 Firebase 콘솔 롤백에만 의존합니다. 게다가 훅 등록 파일이 git 제외라 다른 머신에서는 사전 차단도 없습니다. KJH는 공개 페이지가 아니라 업무 도구 모음이라 배포 사고가 "화면이 이상하다"가 아니라 업무 정지로 나타납니다. 권고: 나눔콘 safe-deploy.sh·snapshot.sh·restore.sh를 경로·프로젝트 ID만 바꿔 이식(로직 재작성 불필요)하고 CLAUDE.md 배포 절에 명시. 훅 규칙 ①에 KJH hosting을 추가하면 날것 배포가 원천 차단됩니다.(쉽게: 잘못된 배포를 막아주는 장치는 있습니다(방금 실제로 막히는 걸 확인했고, 초안에 "하나도 없다"고 쓴 건 제 과장이라 고쳤습니다). 없는 건 배포 직전 자동 백업과 되돌리기입니다. 나눔콘은 배포할 때마다 복구 지점을 남기는데 KJH는 안 남겨서, 잘못되면 파이어베이스 콘솔에서 되돌리는 방법밖에 없습니다. 나눔콘 것을 그대로 옮겨오면 됩니다.)
  • 낮음배포 지침이 죽은 경로 + 전체 배포 — 문서대로 하면 실패하고, 고쳐도 함수 30종이 재배포됨CLAUDE.md:66-68의 배포 절이 두 가지로 어긋나 있습니다. ⓐ :67cd 대상이 존재하지 않는 경로입니다(/Volumes/GDriveBD&영상/…/000.KJH Nexus — 2026-07 드라이브 재편으로 코딩 원본이 git으로 일원화된 뒤 남은 화석. 실제로 ls로 부재를 확인했습니다). 즉 문서를 그대로 따라 하면 첫 줄부터 실패합니다. ⓑ :68firebase deploy(대상 미지정)입니다. HTML 한 줄만 바꿔도 Cloud Functions 30종이 함께 재배포되어 ⓐ 배포 시간이 길어지고 ⓑ 무관한 함수까지 사고면에 들어옵니다(빌드 실패·콜드스타트·의존성 변화). 권고: 지침을 --only hosting / --only functions:이름 형태로 분리해 안내. 소스 주석에 이미 firebase deploy --only functions:opsHealthCheck라는 단독 배포 예시가 있어(functions/index.js:1036) 패턴은 이미 알고 계신 상태이며 지침만 갱신하면 됩니다.(쉽게: 화면 파일 하나만 고쳐도 서버 기능 30개가 전부 다시 배포됩니다. 오래 걸리고, 안 건드린 기능까지 위험에 들어갑니다. 코드 주석에는 이미 "필요한 것만 배포하는 방법"이 적혀 있어서 지침만 바꾸면 됩니다.)

⑩ 문서·자산 대장

1건 · 낮음 1

총평: 문서화 수준이 높습니다DATASOURCES.md가 시트·GAS·연결 도구를 한 표로 관리하고 위치 재편(2026-07-19)과 백업 로봇 공유 상태까지 기록돼 있으며, CLAUDE.md·DESIGN.md가 있고, 도구별 HISTORY.md·작업이력 문서가 다수 남아 있습니다. architecture.html 전체구도 대시보드는 노드 의존관계를 다이어그램으로 그려주는 수준이라 개인 프로젝트에서는 보기 드뭅니다. 이번 감사에서도 이 문서들이 실제로 도움이 됐습니다 — GAS 배포 ID 대장이 없었다면 13종 프로브 자체가 불가능했습니다. 남은 문제는 두 프로젝트 간 원칙 불일치 1건이며, ②번 영역 조치 후 자동으로 정리될 성격입니다. 동급 대비 수준: 상위권. 미실측: 문서 최신성(각 HISTORY.md의 실제 갱신일 대조).

  • 낮음GAS 배포 ID 취급 원칙이 나눔콘과 반대 — 대장에 전량 평문DATASOURCES.md의 GAS 표는 배포 ID 10종을 전값 그대로 싣고 있습니다. 나눔콘 R2-118은 "GAS 배포 주소는 사실상 비밀번호"라는 판단으로 대장에서 값을 가렸는데, 같은 자산의 원본 대장인 KJH 쪽은 평문입니다. 저장소가 비공개이고 **/*.md가 배포 제외라 즉시 노출은 아니지만(실측 확인), 두 문서가 같은 값을 서로 다른 등급으로 취급하는 상태 자체가 다음 사람(또는 다음 회차)을 혼란시킵니다. 본질적 해소는 ②번 조치입니다 — GAS에 공유 시크릿이 붙으면 배포 주소는 더 이상 비밀번호가 아니게 되므로, 대장에 평문으로 두는 것이 오히려 정상이 됩니다. 권고: ②번 조치 완료 시점에 나눔콘 R2-118 항목의 "주소=비밀번호" 전제 문구를 함께 갱신해 두 대장의 원칙을 명시적으로 일치시키기.(쉽게: 같은 주소를 두고 나눔콘 문서는 "비밀번호니까 가린다", KJH 문서는 그냥 적어둔다로 서로 다릅니다. ②번을 고쳐서 진짜 자물쇠를 달면 주소는 더 이상 비밀이 아니게 되므로, 그때 두 문서의 설명을 맞추면 깔끔하게 정리됩니다.)