ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Shared Memory — 동북아 역사 공동 기억 프로젝트 누가 옳은가를 AI가 판정하는 것이 아니라, 서로 다른 역사 서술이 어떤 언어와 관점을 사용하고 있는지 분석하고, 사용자가 실제 자료와 다른 참여자의 표현을 살펴본 뒤 자신만의 공동 표현을 제안하도록 돕는다. 한국·일본·중국의 역사 서술을 나란히 놓고 텍스트 분석(NLP)으로 비교한 뒤, 참여자가 직접 '공동 표현'을 제안하고, 모인 제안을 다시 분석해 공동 기억이 형성되는 과정을 데이터로 보여주는 디지털 인문학 웹 플랫폼입니다. 서버 없이 GitHub Pages에서 동작하며, 참여 데이터는 Supabase에 저장됩니다.查看完整 README ↗
누가 옳은가를 AI가 판정하는 것이 아니라, 서로 다른 역사 서술이 어떤 언어와 관점을 사용하고 있는지 분석하고, 사용자가 실제 자료와 다른 참여자의 표현을 살펴본 뒤 자신만의 공동 표현을 제안하도록 돕는다.
한국·일본·중국의 역사 서술을 나란히 놓고 텍스트 분석(NLP)으로 비교한 뒤, 참여자가 직접 '공동 표현'을 제안하고, 모인 제안을 다시 분석해 공동 기억이 형성되는 과정을 데이터로 보여주는 디지털 인문학 웹 플랫폼입니다. 서버 없이 GitHub Pages에서 동작하며, 참여 데이터는 Supabase에 저장됩니다.
본선 시연 준비·당일 점검: docs/finals-demo-checklist.md
한국·일본·중국 역사 서술 비교
→ NLP 분석 (핵심 키워드 · 공통/특징적 표현 · 유사도 · 서술 강조점)
→ 관련 사료 확인 (누가, 언제, 어떤 위치에서 쓴 기록인가)
→ 참여자 공동 표현 작성 (Guided Contribution, 실시간 표현 분석)
→ 공동 표현 NLP 분석 (어휘·개념 · 유사도 · 표현 프레이밍)
→ Shared Memory 형성 (표현군 · 공동 기억 지도 · 탐구 전/후 변화 · 공감)
| 영역 | 기능 |
|---|---|
| 사건 | 임진왜란 · 한중일 문화 교류 · 조공 관계 · 근대화 과정 · 재일조선인 · 히로시마 원폭과 조선인 피해자 (6개) |
| 국가별 서술 | 세 국가 서술 카드, 핵심 키워드, 편집자 해설(사람이 작성), 사건 타임라인 |
| 탐구 전 한 문장 | 서술을 읽기 전에 지금 알고 있는 내용으로 한 문장을 적어 둠 (선택) |
| NLP 비교 분석 | TF-IDF 핵심 키워드, 세 국가 공통 표현, 두 국가만 공유하는 표현, 국가별 특징적 표현, 코사인·Jaccard 유사도와 단어별 기여도, 원문 근거 위치 강조, 서술 강조점 비교, 근거 → 관련 사료 연결 |
| 키워드 비교 | 표현 묶음별 국가별 등장 횟수 — 서술 원문에서 자동 계산 |
| 관련 사료 | 자료명·원문명·연도·1차/2차 구분·작성 주체·소장 기관·관점·설명·링크, 연결된 표현 → 원문 근거 |
| 공동 표현 작성 | STEP 1 핵심 요소 선택 → STEP 2 표현 방식 → STEP 3 참고 예시·문장 틀(+ 작성 언어 선택: 한국어 / 中文 / 日本語 / English, 고르지 않으면 자동 감지) → STEP 4 이유 선택+자유 입력, 실시간 표현 분석 패널 |
| 공동 표현 목록 | 전체 / 한국어 / 中文 / 日本語 / English 필터, 선택 요소·표현 방식·이유, 탐구 전 문장, 공감, 참고 번역 예시 |
| 공동 기억 지도 | 전체·언어별 제안 수, Shared Memory Map(SVG), 공동 표현군(대표 키워드·대표 제안·제안 목록), 국가별 서술 → 참여자 표현 재구성, 자주 쓰인 어휘·개념, 표현 프레이밍, 탐구 전/후 변화 집계, 공감과 표현군 비교 |
| Shared Archive | 상단 메뉴 ‘Shared Archive’. 공개에 동의한 공동 표현을 사건별 카드(문장·닉네임·작성 언어·날짜)로 모아 보고, 기존 공감 기능으로 반응. 사건마다 ‘이 사건 직접 탐구하기’로 4단계 탐구에 연결. 댓글 없음 |
디자인(한지 배경·먹색·글꼴·히어로·지도 SVG·카드·버튼)은 그대로 두고, 처음 온 사람이 무엇을 하는 곳인지 · 무엇을 하면 되는지 · 얼마나 걸리는지를 바로 알 수 있도록 문구와 안내만 추가했습니다.
| 항목 | 내용 |
|---|---|
| 첫 화면 | “당신은 이 역사를 어떻게 기억하고 있나요?”, 5분 역사 인식 조사 참여하기 버튼, 약 5분 · 익명 · 로그인 없음, 버튼 바로 아래 실제 참여자 수, 참여 4단계(연령대 선택 → 역사 하나 보기 → 내 생각 남기기 → 결과 확인). 첫 화면과 사건 선택 화면에서는 ‘탐구’·‘공동 표현’·‘NLP’ 같은 전문 용어를 쓰지 않고, 상세 화면 안에서는 기존 명칭을 유지합니다 |
| 상단 메뉴 | 프로젝트 소개 · 지금 참여하기 · Shared Memory 보기 (휴대폰: 소개 · 참여하기 · 결과 보기). ‘지금 참여하기’는 연령대를 아직 고르지 않았으면 참여 시작 영역부터 열고, ‘Shared Memory 보기’는 사건을 고르면 공동 기억 지도 탭을 바로 엽니다. ‘탐구 의의’ 페이지는 첫 화면 소개 카드 아래 링크로 옮겼습니다 |
| 참여자 수 | Supabase 집계 함수 participation_summary() 결과만 표시(하드코딩 없음). 집계 함수가 없거나 0명이면 문구를 숨깁니다 |
| 연령대 (익명) | 참여 버튼을 누르면 “Shared Memory 참여하기” 영역이 열리고 10대 · 20대 · 30대 · 40대 · 50대 · 60대 이상 · 응답하지 않음 중 하나를 버튼으로 고른 뒤 다음 → 역사 선택으로 넘어갑니다. 공동 표현 작성 폼에서도 바꿀 수 있습니다. → age_group |
| 학습 경험 | 사건을 연 뒤 “탐구 전 한 문장” 카드에서 “이 사건을 이전에 배워보거나 자세히 접한 적이 있나요?”(있다·없다·잘 모르겠다·응답하지 않음) → prior_learning |
| 탐구 진행 표시 | 사건 선택 → 처음 생각 → 서술 탐구 → 공동 표현 → 결과 확인. 권장 흐름을 보여줄 뿐 탭 이동을 막지 않습니다. 각 단계를 누르면 해당 위치로 이동하고, “다음” 안내가 한 줄로 나옵니다 |
| 닉네임 | 선택 항목. 비워 두면 ‘익명’으로 저장합니다 |
| 세대별 필터 | 공동 기억 지도에 전체 · 10대 … 60대 이상 필터. 고른 연령대의 제안만 기존 분석 함수(analyzeExpressions, aggregateBeforeAfter)에 그대로 넣어 제안 수·주요 어휘·개념·프레임·표현군·대표 표현·서술 유사도·탐구 전/후 변화를 다시 계산합니다 |
| 세대별 표현 차이 | ‘전체’ 보기에 연령대별 참여자 수·많이 쓰인 개념·표현 프레임 비율·서술과의 평균 유사도를 나란히 보여주는 작은 표. 차이가 작으면 “차이가 크지 않았습니다”라고 그대로 표시합니다 |
완료 참여자 기준 (completed_flow = true): 사건 선택 + 탐구 시작 기록(사건 상세 열기 또는 탐구 전 문장) + 국가별 서술 확인(서술 카드가 화면의 상당 부분에 5초 이상 보임) + 공동 표현 제출. 단순 방문자는 참여자로 세지 않습니다. NLP·사료 탭 열람(viewed_nlp, viewed_sources)은 추가 분석용으로만 기록하며 완료 조건이 아닙니다. 클릭 로그는 남기지 않고 이 네 가지 milestone만 제출 시점에 함께 저장합니다.
표본이 적은 집단: 연령대별 참여자가 MIN_GROUP_SIZE(기본 5명, generation-analysis.js) 미만이면 세부 분석을 표시하지 않고 “표본이 적어 세부 분석을 표시하지 않습니다.”라고 보여줍니다. 이 제안들도 ‘전체’ 분석과 전체 참여자 수에는 포함됩니다. 표본 크기는 서버 집계(중복 제거된 참여자 수)와 제안 수 중 작은 값을 씁니다.
개인정보: 이름·연락처·생년월일·정확한 나이·학교명은 수집하지 않습니다. 연령대는 10년 단위 구간만, 학습 경험은 4개 선택지만 저장합니다. 화면 안내: “참여 내용은 익명으로 프로젝트 개선과 통계 분석에 활용됩니다. 이름·연락처·생년월일 등 개인정보는 수집하지 않습니다.”
데이터 해석의 한계
DIFF_THRESHOLDS).디자인(한지 배경·먹색·Noto 서체·지도 SVG·국가별 포인트 컬러·카드·버튼)과 기존 기능은 그대로 두고, 쉬운 결과 → 근거 → 자세한 분석 순서로 보이도록 정리했습니다.
| 항목 | 내용 |
|---|---|
| 대표 사건 | 사건 목록을 임진왜란 → 재일조선인 → 나머지 순서로 배치(eventsData 배열 순서, id는 그대로). 두 카드에만 작은 “먼저 살펴보기” 표시(featured) |
| 탭 이름 | 학생이 할 일을 먼저, 기술 이름은 작게: 세 나라의 서술(국가별 서술) · 어떻게 다를까?(NLP 비교 분석) · 단어 빈도(키워드 비교) · 사료로 확인(관련 사료) · 내 한 문장 쓰기(공동 표현 작성) · 모두의 기억 보기(공동 기억 지도) |
| 한눈에 비교 | NLP 탭 맨 위에 ① 세 서술이 공통으로 쓰는 말 ② 서술마다 두드러지게 쓰는 말 ③ 근거 확인 방법을 표시. 모든 단어는 eventNlp(세 서술 텍스트의 실제 계산 결과)에서 가져오며 미리 적어 둔 값이 없습니다. 단어를 누르면 원문 위치 강조 + 연결 사료. 기존 TF-IDF·특징 표현·코사인/Jaccard·기여 단어·강조점·근거는 “자세히 분석하기”에 그대로 |
| 일반화 방지 | 비교 영역마다 “프로젝트에서 선정·요약한 자료이며 각 국가의 모든 교과서나 역사 인식을 대표하지 않는다”는 안내. 편집자 해설(feature, editorNotes)은 “이 서술은 …”, “한국 측 서술: …”을 주어로 다시 씀. 국가 카드 이름 ‘대한민국’ → ‘한국 측 요약 서술’ |
| 서술의 출처 | 각 서술 카드 아래 “이 서술은 어떻게 작성했나요?” — 편집자 요약임을 밝히고, sourceRefs(출처를 확인한 자료만)로 대조할 자료를 연결. 확인된 자료가 없으면 “참고용 관점 요약”이라고 표시 |
| 사료 검증 단계 | verification: verified / partial / unverified (화면: 출처 확인 · 일부 확인 · 검토 전). “원문/기관에서 확인하기” 버튼은 verified 자료에만 표시 |
| 사료 카드 | 1차 화면: 자료명·언제·누가·왜 볼까?·관점 주의·연결된 표현 / “자세히 보기”: 원문명·자료 종류·소장·제공·설명·확인 근거. 출처 확인 자료부터 정렬 |
| 사료 유도 | 사료 탭에 “텍스트 분석 → 근거 추적 → 관점 확인” 흐름 표시. 한눈에 비교 아래 “관련 사료 1개 확인하기 →”, 내 한 문장 쓰기 위에 “근거 자료 하나를 확인해 볼까요?”(사료를 아직 안 본 경우만, 필수 아님) |
| 탐구 전 한 문장 | “먼저 지금 알고 있는 내용만으로 한 문장을 적어 주세요. 검색하거나 정답을 찾지 않아도 됩니다.” 건너뛰기·빈 저장은 한 번만 이유를 설명하고 다시 묻습니다(강제하지 않음) |
| 빈칸 검사 | 밑줄 3개 이상(문장 틀 사용 시 2개 이상, 전각 _ 포함)이 남거나 빈칸을 지우기만 한 경우(“이 사건은 로 시작되어,”) 제출을 막고 고칠 위치를 알려줌. 일반 문장의 밑줄 1개는 허용 (findPlaceholderIssues) |
| 역사 용어 확인 | 임진왜란 + 대한민국/대한제국만 안내(“1592년 당시 국호는 ‘조선’”). 같은 문장에 ‘오늘날·부른다·교과서’ 등이 있으면 안내하지 않음. 점수·정답 판정 없음, 한 번 더 누르면 그대로 등록 (checkHistoricalTerms) |
| 닉네임 | “닉네임은 선택사항이며 다른 참여자에게 공개됩니다. 실명·학교명·학년/반·연락처 등 개인정보는 입력하지 마세요.” 연락처·이메일·학교명·학년/반 형태는 등록 차단 (checkNickname) |
| 자주 묻는 질문 | 소개 페이지에 교사·심사위원용 질문(서술 작성자, 대표성, 출처, AI 판정 여부, LLM을 어디에 쓰는지 — 네 번째 서술자 기록 + ③ 질문하는 AI, 하드코딩 여부, 학생 데이터 보호, 수업 활용, 교육 효과 해석, 사료–분석 연결) |
교실 PC·태블릿처럼 한 기기를 여러 학생이 쓰면, 브라우저 저장소(localStorage)에 남은 앞 학생의 연령대·탐구 전 문장·학습 경험·진행 기록·참여자 키가 다음 학생에게 이어질 수 있었습니다.
| 항목 | 내용 |
|---|---|
| 누가 참여하나요? | 이 기기에 이전 참여 기록이 있으면 참여를 시작하는 동작(참여하기 버튼·상단 메뉴·사건 카드·제출) 전에 “다른 학생입니다 · 새 참여자로 시작 / 같은 학생입니다 · 이어서 참여하기”를 고르게 합니다. 선택은 이 창(탭)에만 기억합니다(새로고침해도 유지, 새 창은 다시 물음). 제출에 성공한 뒤와 15분 동안 조작이 없을 때는 다음 참여 시작 때 다시 묻습니다 → 다음 학생이 일반 참여 버튼을 눌러도 앞 학생의 상태를 이어받지 않습니다 |
| 제출 직전 안전장치 | 선택하지 않은 채로 제출하면 저장 전에 같은 선택 창을 띄웁니다. 이때 “새 참여자”를 고르면 지금 쓰던 문장·이유는 남기고 이전 참여자의 연령대·탐구 전 문장·참여자 키만 지운 뒤 다시 등록하게 합니다 |
| 새 참여자로 시작 | 선택 창 외에도 첫 화면·사건 선택 화면(이전 기록이 있을 때만), 진행 표시 아래, 제출 완료 영역에 링크가 있습니다. Shared Memory 키(sm_ 접두어)만 지우고 연령대 선택부터 다시 시작합니다 (SharedStore.resetParticipant) |
| 지우는 것 | sm_age_group, sm_before_*(탐구 전 문장·건너뛰기·학습 경험·제출 표시), sm_flow_*, sm_participant_key, sm_voter_key, sm_empathized + 화면의 입력창·작성 가이드·작성 언어 선택 등 메모리 상태 |
| 지우지 않는 것 | 같은 주소(GitHub Pages)의 다른 사이트 저장소, 데이터베이스에 이미 등록된 제안(삭제·수정 요청을 보내지 않음) |
| 작성 중 확인 | 작성 중인 문장·이유·제출하지 않은 탐구 전 문장이 있으면 지우기 전에 확인 창을 띄웁니다 (선택 창에서 “새 참여자”를 골라도 동일). 선택 창은 Esc로 닫을 수 있으며 닫으면 아무것도 바뀌지 않습니다 |
| 제출 후 정리 | 제출에 성공하면 그 사건의 탐구 전 문장을 이 기기에서 지우고 “제출됨”만 표시합니다. 실패하면 문장과 입력 내용이 그대로 남습니다 |
| 같은 학생이 계속 | 버튼을 누르지 않으면 아무것도 지우지 않습니다(새로고침해도 유지) |
| 저장소를 쓸 수 없을 때 | 사생활 보호 모드·차단 등으로 localStorage를 못 쓰면 페이지가 열려 있는 동안 메모리에 보관합니다(오류 없이 참여 가능) |
브라우저 E2E — tests/e2e/shared-device.e2e.js (가짜 Supabase, 실제 DB 접속 없음): 학생 A 제출 → 같은 창에서 일반 참여 버튼 → 선택 창, 이어서 참여(키·연령대 유지), 15분 무조작 후 재확인, 새 참여자 B(연령대부터·탐구 전 문장·키 분리), 선택 없이 제출하는 C(문장 유지·정보 분리), 작성 중 삭제 방지, 제출 실패 보존, 저장소 차단 환경, 모바일.
역사 용어 확인을 더 넓히지 않은 이유: 국호·연대처럼 확실한 경우가 아니면 오탐(현재 시점 문장, 인용, 명칭 설명)이 커지고, 해석 판단으로 보일 위험이 있습니다. 그래서 임진왜란의 국호 한 가지만 두었습니다. 다른 사건(문화 교류·조공 등)은 기간이 길어 같은 규칙을 적용하면 정당한 문장까지 걸리므로 넣지 않았습니다.
QR코드로 들어와 약 5분 동안 한 사건을 탐구하고, 자신의 생각이 어떻게 움직였는지 담긴 개인 카드를 받아 가는 흐름입니다. 기존 서술 비교·NLP·사료·공동 표현·공동 기억 지도는 그대로이며, 디자인(한지·먹색·서체·카드·버튼)도 유지했습니다.
| 항목 | 내용 |
|---|---|
| 사건별 링크 | …/#/e/imjin 처럼 사건 id를 붙이면 첫 화면 없이 그 사건이 바로 열립니다(?e=imjin도 지원). 해시 주소라 GitHub Pages에서 서버 설정 없이 동작하고, 없는 사건이면 첫 화면으로 갑니다. 사건 화면에 있는 동안 주소창에도 이 링크가 남습니다(history.replaceState, 뒤로 가기 기록을 늘리지 않음). 단, 새로고침은 예전처럼 첫 화면에서 시작합니다(공용 기기에서 앞 사람이 보던 사건이 저절로 열리지 않도록) |
| 수업·전시용 QR 시트 | 첫 화면 ‘수업·전시용 사건별 QR 만들기’ → 사건 6개의 QR·링크·역사적 질문을 한 장으로 보여주고, ‘QR 시트 인쇄 · PDF로 저장’으로 A4에 2열로 인쇄합니다. QR은 vendor/qrcode-generator로 기기 안에서 만들며 외부 QR 서비스를 쓰지 않습니다. QR은 지금 열린 사이트 주소로 만들어지므로, 배포된 사이트에서 인쇄해야 합니다 |
| 역사적 질문 | 사건마다 편집자가 쓴 열린 질문(events-data.js의 question)을 사건 화면 맨 위 ‘오늘의 질문’과 카드에 표시합니다. 정답을 정해 둔 질문이 아닙니다 |
| 5분 흐름 | 진행 표시를 질문 → 처음 생각 → 서술 탐구 → 탐구 후 생각 → 나의 카드로 바꿨습니다. 탭 이동은 여전히 자유롭고, 진행 표시는 권장 순서만 안내합니다. ‘탐구 후 생각’은 ‘내 한 문장 쓰기’의 STEP 3 문장입니다(입력 항목을 새로 늘리지 않음) |
| My Memory Card | ‘내 한 문장 쓰기’ 아래 My Memory Card 만들기. 사건·역사적 질문, 탐구 전 문장 → 탐구 후 문장(입력 그대로), 새로 쓴 말·계속 쓴 말(compareBeforeAfter의 실제 단어 차이), 새롭게 발견한 관점(최대 3개), 아직 남은 질문, 탐구 기록 도장(세 나라 서술·비교 분석·사료 확인·공개 참여 — 실제로 한 단계만 채워짐), Shared Memory 브랜드를 담습니다. 문장을 고치거나 요약하지 않고, “이해도 향상” 같은 평가를 만들지 않습니다 |
| 새롭게 발견한 관점 | 카드 화면의 ‘카드 다듬기’에서 고릅니다. 후보는 사건 이름 차이(예: 임진왜란 · 文禄·慶長の役 · 万历朝鲜战争)와 국가별 서술 NLP 결과의 실제 단어(특징적 표현·세 서술 공통 표현)로만 만들고, 직접 적을 수도 있습니다 |
| 저장 · 인쇄 | 이미지로 저장: Canvas로 1080px 폭 PNG(높이 1350–1920px, 내용에 맞춤)를 기기에서 만듭니다. 글자 크기는 문장 길이에 따라 4단계로 줄고, 그래도 넘치면 줄 수를 제한하고 “(일부 생략)”을 표시합니다(화면 카드에는 전체 문장). 공유하기(휴대폰 공유 기능이 있을 때), 인쇄 · PDF로 저장(카드만 인쇄) |
| 공개는 별도 동의 | 카드는 공개 등록 없이 받을 수 있습니다. 공개하려면 ‘Shared Memory에 공개하기 (선택)’에서 동의 상자에 체크한 뒤 ‘동의하고 공개 등록하기’를 눌러야 서버로 보냅니다. 등록 후 동의 상자는 다시 해제됩니다 |
| 카드 데이터 | 카드에 쓴 발견·질문·카드 내용은 서버로 보내지 않고 저장소에도 남기지 않습니다(이 창의 메모리만). 공개 등록 뒤에도 같은 창에서는 카드를 만들 수 있도록 탐구 전·후 문장을 메모리에만 잠시 둡니다. 새로고침하거나 ‘새 참여자로 시작’하면 사라집니다 |
| 공용 기기 | 카드 화면에도 ‘새 참여자로 시작’이 있으며, 누르면 카드·메모리·사건 주소를 지웁니다. 기존 공용 기기 보호(제출 후 탐구 전 문장 삭제, 참여자 선택 창)는 그대로입니다 |
완료 참여자 기준은 바꾸지 않았습니다. completed_flow는 여전히 공개 등록 시점에만 계산·저장됩니다. 카드만 받고 공개하지 않은 참여는 서버에 기록되지 않으므로 참여자 수·통계에 포함되지 않습니다(의도된 개인정보 보호 효과이자 통계상 한계).
검증 — tests/memory-card.test.js(링크 해석·카드 모델·실제 변화만 표시·발견 후보 출처·줄바꿈·QR 라이브러리 해시), tests/e2e/memory-card.e2e.js(휴대폰에서 QR 링크 진입 → 카드 → PNG 저장 → 인쇄 화면 → 동의 분리 → 공개 후 카드 → 새 참여자 초기화, 1000자 문장, QR 시트). 생성한 QR 6개는 별도 디코더(jsQR)로 읽어 링크와 일치함을 확인했습니다(디코더는 저장소에 추가하지 않음).
남은 한계
discoveries text[])을 설계해 승인 후 적용해야 합니다.참여를 마친 뒤 카드 화면 맨 위 카드 디자인에서 동아시아 전통 미술에서 영감을 받은 디자인을 고르고, 카드(4:5) 또는 인스타 스토리(9:16) 형식으로 저장·공유합니다. 카드 내용(문장·발견·질문·도장)과 기존 기능은 그대로이며, 바뀌는 것은 모양뿐입니다.
| 디자인 | 색감 | 장식 (모두 코드로 직접 그린 창작 그림) |
|---|---|---|
| 고문서 (기본) | 한지색, 주묵 | 한지 섬유 질감, 선장(線裝) 제본 실, 괘선, 제첨 ‘共同記憶帖’, 붙인 쪽지, 인장 |
| 민화 화조도 | 살구빛 바탕, 석류 주홍, 연두 잎 | 겹꽃 가지 두 갈래, 앉은 새·나는 새 한 쌍, 또렷한 윤곽선 |
| 우키요에 | 붉은 보카시 하늘, 짙은 녹색 땅 | 판화 여백과 굵은 먹 테두리, 세로 제목 칸 ‘共同記憶’, 화면을 가로지르는 매화 줄기 |
| 청록산수 | 석청·석록, 황토 | 비단 결, 겹친 봉우리(준법·태점), 구름, 소나무, 물결 |
| 기본 | 이전 디자인 | 한지색 + 이중 테두리 + 세 나라 색 띠 |
card-themes.js)로 캔버스를 깔고, 저장 이미지는 drawCard가 같은 함수로 그립니다. 스토리 미리보기는 저장될 이미지를 그대로 그립니다. 그림은 사건·테마별 시드로 고정되어 다시 그려도 같습니다.shared-memory-card-imjin-20261020-ukiyoe-story.png).저작권 · 출처
card-themes.js가 Canvas로 직접 그리는 창작 벡터 그림이며, 테스트(tests/card-themes.test.js)가 외부 이미지 참조가 없음을 확인합니다.sources-data.js와 이 문서에 기록합니다.검증 — tests/card-themes.test.js(테마 5종·기본값, 대비, 스토리 안전 영역, 모든 테마 × 형식 × 최대 길이 그리기, 문장 전체가 패널 안에 그려지는지, 스토리 글자 크기 맞추기, 시드 고정, 외부 이미지 없음), tests/e2e/memory-card.e2e.js(디자인 고르기·화살표 키, 디자인을 바꿔도 문장 유지, 고른 디자인으로 PNG 저장, 스토리 미리보기·1080×1920 PNG, 가로 스크롤 없음, 새 참여자 초기화).
카드 이미지 저장 (2026-10-23) — 카드가 보이거나 디자인·형식이 바뀌면 저장용 PNG를 미리 만들어 두고, ‘이미지로 저장’을 누르면 바로 내려받습니다(전에는 누른 뒤 글꼴을 최대 3초 기다려 그렸음). 같은 카드를 2초 안에 다시 누르면 한 번만 내려받고(브라우저의 ‘여러 파일 자동 다운로드’ 차단 방지), 만드는 중에는 추가 누름을 무시합니다. 안내는 페이지가 저장 여부를 알 수 없으므로 ‘내려받기를 시작했습니다’로 하고, 내려받기가 막힌 브라우저를 위해 ‘이미지로 열기(길게 눌러 저장)’ 창을 둡니다. 글꼴이 3초 안에 오지 않으면 기본 서체로 임시 이미지를 만들고 글꼴이 도착하면 다시 만듭니다. 미리 만든 이미지는 이 창의 메모리에만 있고 ‘새 참여자로 시작’하면 지웁니다. 검증: tests/e2e/card-save.e2e.js(PC·Android Chrome 화면).
남은 한계
전통 동아시아 서체의 결을 살리되 붓글씨·과한 장식은 쓰지 않고, 역사 전시관처럼 단정한 명조 + 현대적인 고딕 UI로 정리했습니다. 기존 레이아웃·색은 그대로입니다.
| 역할 | 서체 | 라이선스 · 공식 배포처 |
|---|---|---|
메인 타이틀 · 큰 제목 (h1, h2, 첫 화면·사건·페이지 제목) | 마루부리 Bold | SIL OFL 1.1 · 네이버 한글(hangeul.naver.com) 공식 웹폰트 서버 hangeul.pstatic.net |
| 메뉴 · 버튼 · 안내 · 본문 | Pretendard (가변) | SIL OFL 1.1 · 공식 GitHub orioncactus/pretendard v1.3.9 배포본(jsDelivr), 다이내믹 서브셋 |
| 카드 제목 · 서술 · 질문 | Noto Serif KR | SIL OFL 1.1 · Google Fonts |
| 참여자의 문장 (공동 표현 · 카드 인용) | 고운바탕 | SIL OFL 1.1 · Google Fonts (디자이너 류양희) |
일본어 (lang="ja") | Zen Old Mincho | SIL OFL 1.1 · Google Fonts (디자이너 Yoshimichi Ohira) |
중국어 간체 (lang="zh-Hans") | Noto Serif SC | SIL OFL 1.1 · Google Fonts |
| 영문 라벨 · 연도 | Cormorant Garamond (기존) | SIL OFL 1.1 · Google Fonts |
LXGW WenKai(霞鹜文楷)를 쓰지 않은 이유 — 라이선스(OFL 1.1)는 문제가 없지만, Google Fonts에는 번체판(LXGW WenKai TC, chinese-traditional)만 있습니다. 이 프로젝트의 중국어 콘텐츠는 간체(사건 이름 ‘万历朝鲜战争’, AI 답변, 참여자 문장)라 번체판으로는 일부 글자가 다른 서체로 섞여 보일 수 있습니다. 간체 웹폰트는 공식 경로가 아닌 개인 패키지로만 배포됩니다. 또 해서(楷書) 계열이라 ‘붓글씨 지양’ 방향과도 맞지 않아, 같은 명조 계열인 Noto Serif SC를 썼습니다.
정리한 것
style.css 맨 위 :root에 서체 역할(--font-display/serif/quote/ui/latin/ja/zh), 글자 크기 7단계(--fs-2xs 12px ~ --fs-xl 18px), 행간(--lh-*), 자간(--ls-*) 변수를 두었습니다. 흩어진 크기 25종 311곳, 자간 29곳, 행간 63곳을 이 단계로 옮겼고, 가장 작은 글자를 10–11.5px에서 12px로 올렸습니다(모바일 가독성). 큰 제목(1.2rem 이상)은 기존 값을 유지했습니다.
사건 이름(일·중), 공동 표현, 표현군 대표 문장에 lang 속성을 붙여 언어별 서체가 적용됩니다(기존 AI 답변 포함).
Memory Card 테마별 서체 조합 (card-themes.js의 fonts, 화면 카드와 저장 이미지가 같음)
| 테마 | 제목 | 참여자 문장 | 장식 속 한자 |
|---|---|---|---|
| 고문서 | Noto Serif KR 700 | 고운바탕 | Noto Serif KR |
| 민화 화조도 | 고운바탕 | 고운바탕 | Noto Serif KR |
| 우키요에 | Noto Serif KR 900 (판화의 굵은 윤곽) | Noto Serif KR 700 | Zen Old Mincho (제목 칸 ‘共同記憶’) |
| 청록산수 | Noto Serif KR 500 (문인화처럼 가볍게) | 고운바탕 | Noto Serif SC (인장) |
| 기본 | Noto Serif KR 700 | Noto Serif KR 700 | Noto Serif KR |
안내·라벨은 Pretendard, 영문 라벨은 Cormorant Garamond로 모든 테마에 공통입니다.
이미지 저장 시 서체 — 이전에는 document.fonts.load()에 글자를 넘기지 않았습니다. Google Fonts·Pretendard는 글자 범위별로 파일이 나뉘어 있어, 이렇게 하면 영문 조각만 내려받고 한글·한자 조각은 빠질 수 있었습니다. 특히 화면 카드가 숨겨지는 스토리 모드에서는 이미지가 기본 서체로 그려질 수 있었습니다. 이제 MemoryCard.fontLoads(model, { theme, format })가 테마 서체와 실제로 그릴 글자를 함께 넘기고, 다 받은 뒤(최대 3초) 그립니다.
로딩 성능
media="print" → onload, JS가 꺼져 있으면 <noscript>). 학교망처럼 서체 CDN이 응답하지 않는 상황을 흉내 내 측정했더니, 첫 화면 표시가 이전(Google Fonts 1개가 화면을 막던 구조) 8.2초 → 0.3초로 줄었습니다.font-display: swap입니다. 기본 서체로 먼저 보이고, 서체가 도착하면 바뀝니다.font-display: optional(두 번째 방문부터 적용)로 바꾸는 것을 권합니다.preconnect: fonts.googleapis.com, fonts.gstatic.com, cdn.jsdelivr.net, hangeul.pstatic.net검증 — tests/card-themes.test.js(테마별 서체 조합, 카드에 쓰는 서체·굵기를 페이지가 공식 배포처에서 모두 불러오는지, 저장 전 불러올 글자 목록, 캔버스가 테마 서체로 그리는지), tests/e2e/memory-card.e2e.js(저장 전 고운바탕·Noto Serif SC·Pretendard를 실제 문장 글자와 함께 요청), tests/e2e/offline-vendor.e2e.js(서체가 모두 차단돼도 기본 서체로 표시). 개발 환경에서는 외부 서체 서버에 접근할 수 없어, 실제 서체 모양은 배포된 사이트에서 확인이 필요합니다(크기·간격·배치는 기본 서체로 확인).
| 항목 | 내용 |
|---|---|
| 임진왜란 연표 | 날짜는 실록 등 당시 기록의 음력 기준임을 명시. “한 달 만에 한양 함락” → “약 20일 만에”, 옥포해전(첫 승리)과 한산도해전(바닷길 장악)을 구분, 히데요시 사망(1598.8) 추가 |
| 임진왜란 일본 측 | 일본 백과사전 등에서도 ‘文禄・慶長の役’·‘朝鮮出兵’과 함께 ‘朝鮮侵略’이 일반적 명칭으로 소개되므로, 일본 측 요약 서술은 ‘출병’ 계열 표현을 쓰는 서술 유형 하나임을 해설에 명시 |
| 임진왜란 기타 | 태그 ‘명·청 교체기’(1618년 이후) → ‘동아시아 국제질서’. 중국 측 키워드 ‘의병’(본문에 없음, 한국 의병과 혼동) → ‘의로운 전쟁’ |
| 재일조선인 연표 | 막연한 “1952 법적 지위 변화”를 1947 외국인등록령 · 1952 국적 상실 처리 · 1965 협정영주 · 1991 특별영주자로 구체화 (출처 확인 자료와 연결되는 항목) |
| 재일조선인 해설 | “국적·정체성·차별이 세 서술에 공통” → 실제 서술 본문과 맞지 않아(일본 측 요약에는 ‘차별’이 없음) 정정 |
| 사료 메타데이터 | 외국인등록령 URL을 존재가 확인된 원문·영문 대조 페이지로 정정, 1965 협정·입관특례법 설명을 조문 내용(신청 기한, 대상 범위)에 맞게 수정, 징비록 소장 정보를 “병산서원 위탁 보관”으로 구체화 |
심사 대비 핵심 답변
feature, editorNotes)도 사람이 쓴 해석이며 자동 분석과 구분해 표시합니다.sourceRefs로 대조 자료를 연결하고, 자료마다 verification(출처 확인/일부 확인/검토 전)·확인 근거·확인일을 둡니다. 확인된 자료가 없는 서술(임진왜란 일본 측, 재일조선인 중국 측)은 화면에 “참고용 관점 요약”으로 표시합니다.whyRead, relatedTerms).perspective)를 함께 적고 1차 사료/후대 편찬(2차)/기념물을 구분합니다. 예) 『명사』는 사건 약 150년 뒤 청 조정이 편찬한 2차 자료, 『징비록』은 당사자의 변호적 시각이 섞일 수 있는 회고록.아직 확인이 남은 것 (추가 확인 필요): 선조실록 개별 기사 URL, 징비록 원문 제공 페이지, 『조선일일기』(게이넨) 소장처·간행본, 일본·중국 고교 교과서의 실제 용어 사용(교과서별 원문 대조). 이번 점검 환경에서는 외부 사이트에 직접 접속하지 못해 기존에 확인한 URL은 검색 결과로 존재만 재확인했습니다.
| 항목 | 내용 |
|---|---|
| 요청 제한 시간 | 행사장·교실 와이파이에서 Supabase 응답이 오지 않으면 제출 버튼이 ‘등록 중...’, 목록·지도·아카이브가 ‘불러오는 중’에 끝없이 머물렀습니다. supabase-config.js의 fetchWithTimeout이 모든 요청을 15초(SUPABASE_TIMEOUT_MS) 뒤에 멈추고, shared-store.js가 안내 문구를 돌려줍니다 |
| 등록 시간 초과 | 서버에는 저장됐을 수 있으므로 “잠시 후 아래 목록에 내 문장이 없으면 다시 등록해 주세요”라고 안내합니다. 쓰던 문장은 그대로 남습니다. 시간 초과를 ‘컬럼 없음’으로 오인해 같은 문장을 다시 보내지 않습니다 |
| 불러오기 시간 초과 | public_expressions RPC가 시간 초과면 테이블로 다시 묻지 않습니다(기다리는 시간이 두 배가 되지 않도록). 오류 안내 아래 다시 시도 버튼 |
| 최신 참여 불러오기 | ‘모두의 기억 보기’와 Shared Archive에 ↻ 최신 참여(표현) 불러오기 버튼과 불러온 시각. 발표 화면을 띄워 둔 채 청중이 QR로 제출한 문장을 새로고침 없이 반영합니다(새로고침은 공용 기기 보호를 위해 첫 화면으로 돌아감) |
| 탭 아이콘·링크 미리보기 | SVG 파비콘(별도 파일 없음, /favicon.ico 404 방지), 메신저 공유용 og:title·og:description |
검증 — tests/network-timeout.test.js(실제 vendor supabase-js + 응답하지 않는 가짜 서버: 등록·불러오기·참여자 수가 제한 시간 뒤 안내로 끝남, 재전송·테이블 재조회 없음, 정상 응답은 영향 없음), tests/e2e/live-refresh.e2e.js(다른 기기 제출 → 버튼으로 목록·지도·아카이브 갱신, 실패 → 다시 시도, 모바일).
세 나라의 서술처럼 AI도 역사를 서술한다는 관점에서, AI의 답을 정답이 아니라 비판적으로 읽을 ‘네 번째 서술자’로 다룹니다. 사건 화면 STEP 2의 탭 ‘AI는 어떻게 말할까?’(세 나라 서술 카드 아래 안내 버튼으로도 이동).
세 가지를 구분합니다. ① AI 답변 카드는 API로 한 번 수집해 저장한 정적 기록입니다(카드를 보기 위해 AI를 부르지 않음). ② 그 답변을 비교하는 분석(이름·요소·빈자리)은 규칙 기반 NLP(문자열 일치·개념 사전, narrator-analysis.js)이며 AI를 쓰지 않습니다. ③ 탭 맨 아래 ‘질문하는 AI’(3-6-1)만 학생이 ‘시작’을 누른 뒤 서버 함수를 거쳐 AI를 실시간으로 부릅니다(최대 2회, 저장 없음).
ai-narratives-data.js)| 항목 | 내용 |
|---|---|
| 모델 | claude-opus-5-5 (54건 모두 직접 응답 — 대체 모델 응답·거절 0건) |
| 질문 | fourth-narrator-data.js의 questions, 질문 버전 2026-10-19. 사건 이름을 넣지 않고 연도·사실로만 가리킴. 히로시마 질문은 조선인 피해자를 미리 언급하지 않음 |
| 횟수 | 6개 사건 × 한·일·중 × 3회 = 54건. 매번 새 대화, 시스템 프롬프트 없음, 모델 기본 설정, max_tokens 16000 |
| 참고 번역 | 일본어·중국어 답변 36건에 같은 모델로 만든 한국어 번역(화면에 ‘AI 번역’으로 표시). 번역 지시문: “다음 글을 자연스러운 한국어로 번역해 주세요. 내용을 더하거나 빼지 말고, 번역문만 출력하세요.” |
| 대체 모델 설정 | 서버 측 대체(fallbacks: "default", 베타 server-side-fallback-2026-07-01)를 켠 채 수집했고, 대체 응답은 0건. 첫 수집 기록의 params에는 번역 지시문·대체 설정이 빠져 있어, 이후 수집부터 남기도록 스크립트를 고쳤습니다(기존 기록은 수정하지 않음) |
| 원문 | 답변·번역 원문은 고치지 않습니다. Markdown(제목·굵게·목록)은 화면에서만 문단·목록·굵게로 바꿔 보여 줍니다(renderAnswerHtml — 모든 글자를 먼저 이스케이프하고 정해 둔 태그만 붙임). 분석은 원문 그대로 합니다 |
| 항목 | 내용 |
|---|---|
| 언어별 카드 | 회차 버튼(1·2·3회), 한국어 참고 번역, 가장 먼저 쓴 이름(답변이 앞세운 이름)과 답변에 나온 이름(다른 나라 이름 소개 포함), 자주 나온 요소(“3/3”), 모델·수집일. 휴대폰에서는 언어 버튼으로 한 카드씩 |
| 읽는 방법 | ① 어떤 이름을 먼저 쓰는지 ② 누가 무엇을 했다고 쓰는지(주어·동사) ③ 무엇이 빠졌는지. 사건마다 “계산된 빈자리 — 세 나라 요약 n곳 · AI 세 언어 m곳”을 함께 표시 |
| 기억의 빈자리 | 서술자(세 나라 요약 + AI 언어별) × 요소 표. 다른 서술자 중 2곳 이상이면서 절반 이상에게 있는데 이 서술자에게 없는 요소를 빈자리로 표시. AI는 3회 중 2회 이상 나오면 ‘있음’ |
| 편집자 관찰·핵심 빈자리 | observations(사건마다 1keyGaps(사건마다 1 |
| 기록 검사 | 질문 원문이 다르거나 모델·수집 시각·답변이 빠진 기록은 화면에 쓰지 않음(validateRecord). 기록이 없으면 ‘아직 기록 전’과 질문 원문만 표시 |
docs/fourth-narrator-findings.md)| 경우 | 실제 표현 | 보정 |
|---|---|---|
| 일본어 임진왜란 ‘명나라의 참전’ | 明の援軍 (3/3) | 사건 요소 표기 明の 추가 (#25) |
| 중국어 히로시마 ‘조선인 피해자’ | 朝鲜半岛劳工·朝鲜半岛人 (3/3) | 朝鲜半岛·朝鮮半島 추가 (#25) |
| 일본어·한국어 근대화 ‘한국 병합’ | 日本に併合され (3/3) · 국권을 빼앗겼습니다·식민지가 되고·나라를 빼앗겼어요 | 併合·병합·국권을 빼앗·나라를 빼앗·식민지가 되·국권을 상실 추가 |
| 중국어 임진왜란 ‘피해·희생’ | 遭受了严重破坏 (3/3) | conceptExtraForms.damage (이 화면에서만) |
| 한국어 히로시마 ‘전쟁’ | 제2차 세계대전 | conceptExtraForms.war (이 화면에서만) |
| 이름 | 일본어 1회 壬辰祖国戦争(북한 명칭), 한국어 조일전쟁 | 이름 목록에 추가 |
공동 표현 분석이 쓰는 개념 사전(concept-lexicon.js)은 바꾸지 않았습니다. 보정 후에도 남은 AI 빈자리는 원문에서 실제로 없는 것을 확인했습니다(예: 중국어 임진왜란 백성·의병 0/3, 일본어 임진왜란 ‘東アジア·国際’ 0/3, 중국어 근대화 ‘侵略’ 0/3).
한계: 언어마다 3회뿐인 작은 표본이며 특정 날짜·모델의 기록이라 ‘AI 전체’를 대표하지 않습니다. 답변 길이 차이가 큽니다(조공 9회 모두 6591,403자로 “다섯 문장” 요청을 넘음, 히로시마는 337502자) — 긴 답변이 요소를 더 많이 건드립니다. 사전에 없는 표현·비유는 찾지 못하고 부정문도 언급으로 셉니다. 빈자리는 “덜 언급했다”는 관찰이며 왜곡이나 의도를 뜻하지 않습니다. 이름이 ‘나왔다’는 것은 그 이름을 썼다는 뜻이 아니라 소개했을 수도 있다는 뜻이라 ‘가장 먼저 쓴 이름’을 따로 셉니다.
다시 수집할 때 (tools/collect-ai-narratives.mjs, 유료 API — 승인 후)
npm ci # SDK(@anthropic-ai/sdk, 고정 버전) — 사이트 실행에는 필요 없음
npm run collect:ai -- --dry-run # 무엇을 몇 번 물을지 미리 보기 (API 호출 없음)
npm run collect:ai # 빠진 (사건, 언어, 회차)만 채움
npm test # 기록 형식 확인 (질문 원문 일치 등)
ANTHROPIC_API_KEY로만 읽습니다. 코드·파일·채팅에 붙여 넣지 마세요.questionsVersion을 올리고 기존 기록을 따로 보관한 뒤 다시 수집합니다(섞이지 않도록 스크립트가 멈춤).세 나라 서술과 AI 답변(①②)을 본 뒤, 답을 썼던 그 AI가 이번엔 묻습니다. 심화 탐구의 네 번째 서술자 탭 맨 아래 블록(ask-ai.js, #askAi)이며, 기본 체험(quick)에서는 탭과 함께 보이지 않습니다.
| 항목 | 내용 |
|---|---|
| 흐름 | 첫 질문(미리 써 둔 것, 호출 없음) → 학생 답 → 호출 1(짧은 반응 + 근거 칩 + 두 번째 질문) → 학생 답 → 호출 2(짧은 마무리 + 근거 칩) → 입력 잠김. ‘시작’을 눌러야만 부르고, 자동 호출·스트리밍·재시도는 없습니다 |
| 호출 경로 | 브라우저 → db.functions.invoke('ask-ai') → Supabase Edge Function supabase/functions/ask-ai/(Deno) → Anthropic API claude-opus-5-5(effort low, 서버 측 대체 모델 켬, JSON 스키마 구조화 출력). API 키는 Supabase secret에만 있고 브라우저·저장소에 없습니다 |
| 근거 제한 | 함수의 system prompt에 사이트 자료(세 나라 서술·AI 답변 9건·사료 메타·편집자 관찰, data.json — node tools/build-ask-ai-data.mjs로 생성)를 전부 넣고, 응답의 근거 id가 그 자료에 있는 것만 통과시킵니다. 근거가 하나도 없으면 답을 보여 주지 않고 폴백. 전송 자료: 세 나라 요약 서술 전문, AI 답변 9건(한국어 텍스트+원문 앞 200자), 사료 id·제목·작성 주체·연도·관점·설명, 편집자 관찰·핵심 빈자리 note, 학생 답. 규칙: 나라 역할극·“편향됐다” 판정·학생 문장 대신 쓰기 금지, 자료 밖은 “이 사이트 자료로는 확인할 수 없다” |
| 근거 칩 | narrative(비교 탭의 그 나라 카드로 이동·강조) · ai(네 번째 서술자 언어 카드·회차 전환) · source(사료 카드) — 대상이 없으면 무시 |
| 폴백 | 함수 실패·시간 초과(15~20초)·429(한도)·503(끔)·모델 거절({fallback:true})·db 없음 → “AI가 응답하지 않아 준비한 질문으로 이어갑니다” + fourth-narrator-data.js의 dialogue(사건마다 3개, 초안 — 운영자 확정 전) 중 다음 질문·정적 칩. 흐름은 끊기지 않고, CI(외부 차단)는 이 경로를 자동 검증합니다 |
| 개인정보 | 블록 상단에 “이 사이트는 답을 저장하지 않습니다. 답은 AI 서비스(Anthropic)로 전송되어 그 쪽 정책에 따라 처리됩니다. 이름·개인정보는 쓰지 마세요.” 전송 전 빈 답·600자 초과·이메일·전화번호 차단. 대화는 저장·로그하지 않음(함수에 console.log 없음 — 테스트가 검사). 공동 표현·참여 문장은 보내지 않습니다 |
| 한도 | IP당 시간당 200회, 전체 하루 250회, 한 대화 최대 2회 — ask_ai_calls(IP 원문은 저장하지 않음 — ASK_AI_IP_SALT로 솔트한 해시·시각, 솔트 설정 권장; 서비스 롤 전용, 7일 뒤 정리; supabase/2026-10-ask-ai-limit.sql). 허용 Origin은 배포 주소·localhost만 |
| 동의·켜기 | 외부 AI 전송은 시작 전 동의(선택)한 경우에만 — 동의하지 않으면 호출 없이 준비한 질문으로. 함수는 secret ASK_AI_ENABLED가 정확히 true일 때만 동작(없으면 꺼짐) |
| 운영 | 배포·secret·SQL·끄는 법(ASK_AI_ENABLED를 true가 아닌 값으로 → 503 → 폴백)·비용: docs/ask-ai-runbook.md |
검증 — tests/ask-ai.test.js(입력 검증·system prompt·스키마·근거 id 대조·폴백 규칙·상태기·답 검사·dialogue 실재·data.json 동기화·배선·비밀 키 없음·SQL 규칙), tests/e2e/ask-ai.e2e.js(폴백 경로 끝까지, 함수 응답 모의 성공 경로의 칩 렌더·클릭 이동·호출 2회·저장 없음, 기본 체험 비노출, 모바일).
한계 — 모델 응답은 매번 다를 수 있고 자료 밖 내용을 완전히 막지는 못합니다(근거 id 대조와 규칙으로 줄일 뿐). 폴백 질문 문구와 FAQ·README 문구는 초안입니다.
학생이 사용법 설명 없이 3–5분 안에 끝낼 수 있도록, 임진왜란·재일조선인의 기본 동선을 객관식 중심으로 줄였습니다. 기존 기능(탐구 전 문장·AI 네 번째 서술자·NLP·사료·자유서술 공동 표현·공동 기억 지도·Shared Archive)은 심화 탐구로 그대로 남아 있고, 나머지 4개 사건은 기존 흐름 그대로입니다.
첫 화면 큰 사건 카드(임진왜란 / 재일조선인) ─▶ ① 세 나라 기록 비교 ─▶ ② 관점 고르기(질문 3개, 한 화면에 하나)
─▶ ③ 한 줄 소감(건너뛰기 가능) ─▶ ④ My Memory Card
| 항목 | 내용 |
|---|---|
| 어디로 들어오면 기본 체험인가 | 첫 화면 큰 사건 카드, 상단 메뉴 ‘지금 참여하기’(첫 화면 사건 카드로 이동해 첫 카드에 초점), QR·사건 링크(#/e/imjin, #/e/zainichi). 사건 목록(‘다른 사건 · 전체 탐구 둘러보기’)·‘Shared Memory 보기’로 들어오면 기존 심화 탐구 4단계입니다 |
| 첫 화면 | 제목 바로 아래 **“어떤 역사부터 볼까요? 하나를 누르면 바로 시작해요”**와 두 사건 큰 카드(48px 이상 터치 영역). ‘시작’은 채운 버튼 모양, 임진왜란 카드에 ‘처음이라면 여기부터’. 휴대폰에서는 세 이름 칩을 숨겨 두 카드가 스크롤 없이 보입니다. 3–5분 · 익명 · 로그인 없음 · 정답 없음. 연령대는 묻지 않습니다(‘다른 사건 · 전체 탐구 둘러보기’ 경로에서만) |
| 한 화면에 하나 | 단계마다 질문 하나와 화면 아래 고정 버튼 하나, 2단계부터 이전 버튼. 진행 표시는 위쪽 한 곳(4단계 칩)과 질문 n/3만 — 같은 ‘STEP n/4’를 여러 번 보여주지 않습니다. 탭·탐구 전 문장 칸은 숨김(‘심화 탐구’ 링크로 열기) |
| 누르기·스크롤 줄이기 | ① 비교 화면은 세 서술의 제목·본문과 ‘같은 사건, 서로 다른 이름’ 카드만(키워드·편집자 해설·근거 자료·형광펜·NLP 공통점/차이점·연표는 심화 탐구에 그대로) → 첫 화면 안에서 서술이 시작되고 길이 4.5 → 2.2화면. ② 선택지를 누르면 체크(✓)만 표시하고 화면은 그대로 — 아래 ‘다음 질문’(마지막 질문은 ‘한 줄 소감 쓰러 가기’)을 눌러야 넘어가며, 그 전까지는 선택을 얼마든지 바꿀 수 있습니다(자동으로 넘어가지 않아 잘못 누른 선택도 바로잡을 수 있음). 고르지 않았을 때 아래 버튼은 ‘이 질문 건너뛰기’(덜 눈에 띄게), 고르면 붉은 먹색 ‘다음 질문’으로 바뀝니다. 화살표 키로도 선택만 바뀝니다. ③ 소감 단계는 버튼 하나(비어 있으면 ‘소감 없이 카드 만들기’). ④ 카드 화면은 카드 → 이미지로 저장(휴대폰에서 아래 고정) → 디자인 고르기 순서, 심화용 ‘카드 다듬기’·진행 표시는 숨김 |
| 객관식은 퀴즈가 아님 | “정답은 없어요. 내가 가장 중요하다고 생각하는 것 하나”를 고릅니다. ‘이 질문 건너뛰기’를 누르면 그 질문은 건너뛴 것으로 남고(막지 않음), 카드·저장에 넣지 않습니다 |
선택지 작성 원칙 (choice-questions.js) | 모두 사실에 맞는 “무엇을 중요하게 볼 것인가”의 차이이며, 사실을 부정하는 선택지는 없습니다. 선택지에 나라 이름을 붙여 학생을 편 가르지 않습니다. 각 선택지에 근거(basis: 서술·연표·이름의 실제 낱말)를 적어 두고 테스트가 그 낱말이 데이터에 있는지 확인합니다. 순서 효과를 줄이려고 선택지 순서를 참여자마다 돌려서 보여줍니다(같은 창에서는 고정) |
| 한 줄 소감 | 최대 300자, 비워 두고 카드를 만들어도 됩니다. 카드에 담기고, 이름 없이 비공개 테이블(exploration_reflections)에 저장됩니다(다른 참여자에게 공개하지 않음 · 운영자만 조회). 공개하고 싶으면 ‘공동 표현 쓰기(심화 탐구)’로 옮겨 기존 공개 동의를 거칩니다(소감을 입력칸에 옮겨 줌) |
| My Memory Card | **“내가 고른 관점 · 객관식”**과 **“나의 한 줄 소감 · 직접 쓴 문장”**을 다른 칸·다른 이름으로 담습니다(화면·4:5 이미지·9:16 스토리 모두). 소감을 건너뛰면 “소감은 남기지 않았어요”. 도장: 세 나라 서술 · 관점 고르기 · 한 줄 소감 · 공개 참여(실제로 한 것만). 고른 관점·소감이 둘 다 없으면 카드를 만들지 않고 이유를 알려 줍니다 |
| 객관식 응답·소감 저장 | 2026-10-23 운영자 결정으로 동의 상자 없이 저장합니다(이전: ‘익명 통계에 보태기’에 체크한 경우에만). 카드를 만들면 exploration_choices에 사건·질문·선택지 id와 묶음 번호(submission_id, 무작위 UUID)를, 소감은 exploration_reflections에 사건·묶음 번호·문장을 보냅니다. 닉네임·연령대·참여자 키는 보내지 않습니다. 같은 답·같은 소감으로 카드를 다시 만들면 다시 보내지 않고, 소감을 고치면 고친 소감만 새로 저장합니다(같은 창 기준). 화면(소감 칸 아래)과 FAQ에 저장 사실을 안내합니다 |
운영 DB — supabase/2026-10-exploration-choices.sql (운영 DB에 적용됨 · 2026-10). 추가만 합니다: 테이블 exploration_choices(anon은 세 칸 등록만, 조회 정책 없음), 1분 600건 도배 방지 트리거, 집계 함수 choice_counts(작은 표본 숨김). 테이블이 없으면 저장만 건너뛰고 카드는 그대로 만들어집니다.
운영 DB — supabase/2026-10-save-answers.sql (미적용 · SQL Editor에서 실행). 추가만 합니다: exploration_choices.submission_id 칸, 비공개 테이블 exploration_reflections(anon은 세 칸 등록만, 조회 정책 없음, 1~300자, 1분 300건 제한). SQL 실행 전에는 객관식 답은 묶음 번호만 빼고 저장되고, 소감은 저장되지 않습니다(카드는 그대로). 소감과 같은 카드의 답을 함께 보는 쿼리는 SQL 파일 끝에 있습니다.
편집자 확인이 필요한 것
sources-data.js에 확인한 자료를 추가하거나 문구를 조정해 주세요.choice-questions.js, 화면 문구 label은 바꿔도 되고 id는 바꾸지 마세요).검증 — tests/quick-experience.test.js(질문 수·근거 낱말 실재·형식·중립 표현·순서 돌리기·응답 검증·저장 행·작은 표본 숨김·카드 모델·모든 테마×형식×소감 길이 그리기·하단 안내 폭·저장 모듈·SQL 규칙), tests/e2e/quick-experience.e2e.js(휴대폰: 첫 화면 → 카드 탭 8번 이하, 첫 화면 안에 서술 시작·비교 화면 2.5화면 이하, 단계 표시 중복 없음, 누르면 체크만·자동으로 넘어가지 않음·다음 전 선택 바꾸기·두 번 눌러도 그대로·마지막 질문 ‘한 줄 소감 쓰러 가기’·PC 클릭, 한 화면 한 질문, 화살표 키(넘어가지 않음), 카드 먼저·저장 버튼 고정, 이전/다음 답 유지, 건너뛰기, 소감 건너뛰기, 카드 칸 구분, 동의 없이 저장 0건, 동의 시 id만 별도 테이블·중복 없음, 소표본 숨김·비율, SQL 실행 전, 심화 탐구 전환·공개 동의 유지·복귀, 새 참여자 초기화, QR·360px·다른 사건·전체 탐구 경로).
한계 — “3–5분”은 목표이며 실제 학생으로 측정하지 않았습니다(자동 테스트는 탭 수만 확인). 객관식 비율은 동의한 참여자만의 자발적 표본이며, 선택지 구성 자체가 응답에 영향을 줄 수 있습니다.
vendor/supabase-js/에 보관 (학교망에서 CDN이 막혀도 동작)npm:@anthropic-ai/[email protected])이 대신 호출하며 브라우저 쪽은 vanilla JS 그대로memory-map.js) — 원 몇 개와 선만 필요해 D3 등 수백 KB 의존성을 추가할 이유가 없고, GitHub Pages에서 그대로 동작하기 때문입니다.node:test (추가 설치 없음)모든 결과는 입력 텍스트에서 매번 계산됩니다. 분석 결과·비율·군집 이름을 코드에 미리 적어 두지 않습니다.
nlp-analyzer.js)TF × IDF, IDF = ln((1+N)/(1+df)) + 1. 말뭉치는 사이트의 전체 서술(6개 사건 × 3개국).concept-lexicon.js)사람이 만든 다국어 개념 사전입니다. 예) 침략 · 침공 · 侵略 · invasion → invasion.
26개 개념을 갈등·책임 / 사실·기술 / 화해·협력 세 가지 표현 프레임으로 나눕니다.
기계학습 모델이 아니며, 목록에 없는 동의어·문맥은 인식하지 못합니다.
emphasisProfile)개념 사전의 한국어 표현이 각 국가 서술에 나온 횟수 ÷ 서술의 어휘 토큰 수(밀도)를 구하고, 세 서술 중 최댓값을 3으로 두어 ●(0~3단계)로 표시합니다. 편향 탐지가 아닙니다. 덜 언급했다고 왜곡이라고 판단할 수 없으며, 부정문도 언급으로 셉니다.
keywordGroupCounts)events-data.js의 keywordGroups에는 사람이 정한 "표현 묶음"(예: '침략': ['침략', '침공'])만 있고,
숫자는 각 서술(제목+본문)에서 실제 등장 횟수를 세어 만듭니다. 예전의 수동 입력 숫자(keywordFreq)는 삭제했습니다.
shared-memory-analyzer.js)지원 언어: 핵심 언어 한국어(ko) · 중국어(zh) · 일본어(ja), 추가 지원 영어(en). 화면 표시 순서는 한국어 · 中文 · 日本語 · English 입니다.
unknown은 언어를 판별할 수 없었던 기존 데이터 호환용으로만 남겨 두며, 언어 탭으로 따로 보여주지 않습니다(‘전체’에는 포함).
resolveLanguage).ko, 가나가 있으면 ja, 라틴 문자가 주되면 en, 가나 없이 한자가 주되면 zh 후보.
朝鮮侵略)이 있으므로, 한자 문장은 중국어 기능어·간체자(的·们·这·对·战·历 …) 같은 단서가 있을 때만 확정(confident: true)하고, 단서가 없으면 zh(확인 필요) + 후보 ['zh','ja']로 둡니다. 일본 신자체(戦·歴·対 …)만 있으면 ja 후보입니다 (detectLanguageDetail).和平·对话처럼 사전에 있는 단어 안의 기능어는 경계로 쓰지 않음). ③ 한 글자 조각(明·清·唐·宋 등 왕조명 제외)과 我们·因为·可以 같은 일반 기능 표현은 버려 과대평가를 막습니다.단어 + 문자 2-gram(가중치 0.5) + 개념(가중치 1.5)을 TF-IDF로 가중. IDF는 해당 사건의 공동 표현 + 국가별 서술에서 계산.
的东 등)은 제외합니다.‘국제·동아시아 · 전쟁’ 중심 표현군처럼 자동 생성. 대표 제안 = 다른 제안들과 평균 유사도가 가장 높은 제안(medoid).브라우저용 다국어 문장 임베딩(예: transformers.js + 다국어 MiniLM)을 검토했지만 사용하지 않았습니다.
대신 TF-IDF + 문자 n-gram + 다국어 개념 사전 + 코사인으로 "표현은 다르지만 뜻이 가까운" 문장의 일부를 잡아냅니다.
따라서 화면과 문서에서 이 분석을 "AI", "임베딩", "의미 분석"이라고 부르지 않고 "텍스트 유사도"라고 부릅니다.
analyzeExpressions는 특징 벡터를 만드는 부분이 분리되어 있어, 나중에 서버(Edge Function)에서 임베딩을 계산하는 방식으로 확장할 수 있습니다.
人民带来처럼), 기능어 글자가 실제 단어의 일부일 때 잘못 나뉠 수 있습니다. 문자 2-gram과 개념 특징이 이를 일부 보완합니다.verified)는 5건, 일부 확인(partial) 2건이며 나머지 14건은 검토 전입니다(2026-10-16 기준, 임진왜란·재일조선인 우선). 검토 전 자료는 화면에 그대로 표시됩니다.events-data.js — 사건별 korea/japan/china(title·text·keywords·feature·sourceRefs), timeline, keywordGroups(표현 묶음), sampleExpression(참고 예시, 정답 아님), featured(대표 사례). 배열 순서 = 화면 순서
title·text는 교과서 원문이 아니라 편집자가 쓴 비교용 요약입니다. sourceRefs에는 서지·제공처를 확인한(verified/partial) 자료만 넣습니다(테스트로 검사).sources-data.js — 자료 메타데이터. id, eventId, country(korea|japan|china|other), title, originalTitle, year, sourceType(primary|secondary|memorial), typeNote, creator, institution, archive, description, whyRead, perspective, url, urlNote, relatedTerms, verification, verificationNote, checkedAt, reviewed
verification: verified(자료명·작성 주체·연도·제공 기관·해당 페이지 URL 확인) / partial(자료와 기본 서지는 확인, 세부 URL·일부 정보 미확인) / unverified. reviewed는 verification === 'verified'와 같습니다(하위 호환).relatedTerms: NLP 결과와 자료를 잇는 사람이 검증한 표현 목록url: 확인한 페이지만 넣고 나머지는 null. 기관 대표 페이지를 넣은 경우 urlNote에 “대표 페이지”라고 밝힙니다. 추측으로 단계를 올리지 말고, 올릴 때는 근거를 verificationNote에 적으세요.concept-lexicon.js — FRAMES, CONCEPTS[{id, label, frame, forms:{ko, zh, ja, en}}] (zh: 현대 표준 중국어 간체 기본 + 역사·동아시아 맥락에서 쓰이는 번체, 확신할 수 있는 핵심 표현만)contribution-guide.js — STYLES, TEMPLATES, REASONS (id는 DB에 저장되므로 바꾸지 마세요), 제출 전 확인 findPlaceholderIssues, checkHistoricalTerms, checkNicknameshared_expressions| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | bigint (identity, PK) | |
| event_key | text | 사건 id (imjin 등) |
| author_name | text (기본 '익명') | 닉네임 |
| country_code | text | 작성 언어 ko/zh/ja/en/unknown — 이름과 달리 국가가 아니라 언어입니다 (기존 데이터 호환을 위해 이름은 그대로 유지, 기본값 ETC는 화면에서 unknown으로 처리) |
| content | text | 공동 표현 |
| reason | text, null | 자유 입력 이유 |
| created_at | timestamptz |
supabase/2026-09-shared-memory-upgrade.sql| 대상 | 내용 |
|---|---|
shared_expressions.expression_style | text, null — fact/perspective/shared/future |
shared_expressions.selected_concepts | text[], null — STEP 1에서 고른 요소 |
shared_expressions.reason_tags | text[], null — STEP 4 이유 코드 |
shared_expressions.before_content | text, null — 탐구 전 한 문장 |
| CHECK 제약 (NOT VALID) | 길이·값 범위 제한. 기존 행은 검사하지 않음 |
expression_empathy 테이블 | (expression_id, voter_key) UNIQUE로 같은 브라우저의 중복 공감 차단, insert만 허용 |
empathy_counts(p_event_key) 함수 | 사건별 공감 수 집계 (security definer, voter_key는 노출하지 않음) |
supabase/2026-10-participant-demographics.sql| 대상 | 내용 |
|---|---|
shared_expressions.age_group | text, null — 10s/20s/30s/40s/50s/60plus/no_answer |
shared_expressions.prior_learning | text, null — yes/no/unsure/no_answer |
shared_expressions.viewed_compare · viewed_nlp · viewed_sources · completed_flow | boolean, null — 제출 시점의 탐구 milestone |
| CHECK 제약 (NOT VALID) | 허용 값 외 저장 차단. 기존 행은 검사하지 않음 |
expression_participants 테이블 | (expression_id PK, participant_key) — 브라우저별 무작위 익명 키. insert만 허용, 조회 정책 없음(키는 외부에서 읽을 수 없음). 10분 이내에 등록된 제안에만 연결 가능 |
participation_summary() 함수 | 전체 참여자 수(서로 다른 키 수 + 키가 없는 기존 제안 수), 완료 참여자 수, 제안 수 |
age_group_participants(p_event_key) 함수 | 사건별·연령대별 중복 제거 참여자 수 (표본 기준에 사용) |
→ 아래 본선 전 하드닝에서 서버 단 소표본 보호로 바꿨습니다.age_group은 원자료 API에서 행 단위로 보입니다
하위 호환: SQL을 실행하기 전에도 사이트는 동작합니다. 저장은 ① 참여자 통계 컬럼 포함 → ② 작성 가이드 확장 컬럼까지 → ③ 기본 컬럼만 순서로 다시 시도합니다. 기존 행은 age_group이 없어 ‘전체’ 분석에만 포함되고, 참여자 수 문구는 집계 함수가 생길 때까지 숨겨집니다.
확장 컬럼이 없으면 shared-store.js가 기존 컬럼만으로 다시 저장하고, 공감 기능은 숨겨집니다. 기존 행은 새 컬럼이 비어 있는 상태로 그대로 분석에 포함됩니다.
supabase/2026-10-add-chinese-language.sql (선택)country_code에는 원래 CHECK 제약이 없어 이 SQL 없이도 zh가 저장됩니다. 이 SQL은 additive 변경만 합니다.
| 대상 | 내용 |
|---|---|
shared_expressions.country_code 컬럼 설명 | ‘작성 언어’ 컬럼임을 기록 |
shared_expressions_lang_chk (NOT VALID) | 새 행의 값을 ko/zh/ja/en/unknown/ETC/null 로 제한. 기존 행은 검사하지 않음 |
하위 호환: 기존 ko/ja/en 행은 그대로, 기존 unknown(및 ETC) 행은 unknown으로 유지합니다. 한자만 있는 기존 unknown 행을 zh로 다시 분류하지 않으며, 그 토큰화 방식도 이전과 같습니다. 개선된 언어 판별·선택은 새로 등록되는 제안부터 적용됩니다.
supabase/2026-10-prefinal-hardening.sql (1단계, 적용 완료) · supabase/2026-10-after-deploy-hide-age-column.sql (2단계, 적용 완료 2026-10-10)| 대상 | 내용 |
|---|---|
| 테이블 권한 | anon/authenticated의 UPDATE·DELETE·TRUNCATE·REFERENCES·TRIGGER 회수. INSERT는 사이트가 보내는 컬럼에만 허용 → id·created_at 지정 불가(날짜를 미래로 넣어 목록 상단 고정 차단). 공감·참여자 테이블은 INSERT만, 조회 권한 없음 |
| CHECK 제약 | event_key ∈ {imjin, zainichi, culture, tribute, modern, hiroshima}, 작성 언어, 공백뿐인 내용 금지, reason_tags 허용 코드, 선택 요소 총 길이. 기존 NOT VALID 제약까지 모두 VALIDATE(기존 10건 모두 통과) |
| RPC | participation_summary·empathy_counts·age_group_participants·public_expressions 모두 SECURITY DEFINER + search_path = '' + 객체 전체 경로 + REVOKE ALL FROM PUBLIC, anon, authenticated 후 anon에만 EXECUTE. 동적 SQL 없음 |
| 소표본 보호 | age_group_participants: 5명 미만인 실제 연령대는 participants = null. 새 함수 public_expressions(event_key, limit): 5명 미만 연령대의 age_group을 null로 가리고 age_group_suppressed = true. 화면은 “5명 미만”으로만 표시 |
| 2단계(적용 완료) | 테이블에서 age_group 직접 조회를 막는 컬럼 단위 SELECT 권한. 새 shared-store.js 배포 뒤에 적용했습니다(이전 버전 사이트는 select('*')를 썼기 때문) |
⚠ 사건을 새로 추가하면
shared_expressions_event_key_chk의 목록도 함께 바꿔야 저장됩니다. 테이블에 컬럼을 추가하면 공개 여부를 판단한 뒤 2단계 SQL의grant select (...)와public_expressions반환 목록에 추가하세요.
Security Advisor: 하드닝 전 6건(anon 3 + authenticated 3) → 하드닝 후 4건. 남은 4건은 anon_security_definer_function_executable(위 4개 함수)이며 의도된 구성입니다. 이 함수들은 조회 정책이 없는 비공개 테이블(참여자 키·공감 키)을 읽어 집계값 또는 가려진 행만 돌려주기 위해 DEFINER가 필요합니다. INVOKER로 바꾸면 집계가 0이 되거나 비공개 키를 공개해야 합니다. 반환값에는 키·개인 식별 정보가 없고, 입력값은 SQL 파라미터로만 비교합니다.
supabase/2026-10-server-moderation.sql (적용 완료 2026-10-22, 마이그레이션 server_moderation_hidden_and_rate_limit)| 대상 | 내용 |
|---|---|
shared_expressions.is_hidden | boolean, 기본 false. 운영자가 true로 바꾸면 재배포 없이 즉시 테이블 직접 조회(RLS)·public_expressions·공감 수·참여자 수·연령대 집계에서 모두 빠집니다. 행은 지우지 않습니다. anon은 등록·수정 권한이 없어 지정할 수 없습니다 |
| 숨기는 방법 | Table Editor → shared_expressions → is_hidden = true, 또는 update public.shared_expressions set is_hidden = true where id = …; |
sm_insert_guard 트리거 | ① 같은 사건에 모든 항목이 같은 제안이 5분 안에 다시 오면 sm_duplicate(응답 시간 초과 뒤 다시 누름·도배) ② 전체 등록이 1분에 120건 이상이면 sm_rate_limited. 같은 문장 틀을 쓴 다른 학생은 닉네임·이유·탐구 전 문장·연령대 등까지 모두 같아야 걸리므로 거의 막히지 않습니다. 사이트는 두 오류를 안내 문구로 보여 주고 쓰던 문장을 남깁니다 |
| 운영 반영 전 리허설 | 운영 DB에서 트랜잭션 안에 적용 → anon 권한으로 확인 → 강제 롤백 (2026-10-22). 숨긴 행 직접 조회·RPC 모두 제외, 참여자 수 재계산, 등록+select('id')·참여자 연결 정상, 동일 제안 차단·같은 문장 다른 학생 허용, anon의 is_hidden 지정·수정 거부, 1분 120건에서 차단 |
공감(expression_empathy) 빈도 제한은 넣지 않았습니다(브라우저 키 기준 중복 방지만). HIDDEN_EXPRESSION_IDS(코드 숨김)는 SQL 적용 전·후 모두 그대로 동작합니다.
supabase/2026-10-exploration-choices.sql (미적용 · 승인 후 SQL Editor에서 실행)| 대상 | 내용 |
|---|---|
exploration_choices 테이블 | event_key(imjin·zainichi만) · question_id · option_id(영문 소문자·숫자·_ 40자) · 서버가 정하는 id·created_at. 이름·문장·연령대·참여자 키 칸 없음 |
| 권한 | anon은 세 칸 등록만(grant insert (event_key, question_id, option_id)), 조회 정책 없음 → 행 단위로 읽을 수 없음 |
sm_choice_insert_guard 트리거 | 전체 등록이 1분에 600건 이상이면 멈춤(자동 도배 방지) |
choice_counts(p_event_key) 함수 | 선택지별 수만 공개. 질문별 응답이 sm_min_group_size()(5) 미만이면 그 질문은 돌려주지 않음 |
supabase/2026-10-count-choice-participants.sql (적용 완료 2026-10-23, 마이그레이션 count_choice_participants)participation_summary()의 participants(첫 화면 "현재 N명이 참여")에 기본 체험 객관식 참여를 더합니다. 함수 하나만 바꾸며 반환 형식·권한은 그대로라 사이트 코드 변경은 없습니다.
(event_key, created_at)을 참여 1회로 셉니다(사이트는 한 사람의 응답을 INSERT 한 번으로 보냄).participation_pings·2026-10-save-answers.sql로 모든 참여를 기록합니다.completed_participants·expressions는 공동 표현 기준 그대로입니다.supabase/2026-10-participation-pings.sql설문(기본 체험 객관식)을 마치거나(소감 단계 도착), My Memory Card를 만들거나, 공동 표현을 등록하면 사건마다 한 번 (사건 이름, 무작위 참여자 키) 두 칸만 participation_pings에 보냅니다. 통계 동의(객관식 답)와 공개 동의(공동 표현)와는 별개이며, 답·소감·문장·닉네임·연령대는 보내지 않습니다.
| 대상 | 내용 |
|---|---|
participation_pings 테이블 | event_key · participant_key · 서버가 정하는 id·created_at. unique (participant_key, event_key) |
| 권한 | anon은 두 칸 등록만, 조회 정책 없음. 1분 600건 이상이면 멈춤(sm_ping_insert_guard) |
participation_summary() | 참여자 수 = (참여 기록 키 ∪ 숨기지 않은 공동 표현 키)의 수 + 키 없는 예전 공동 표현 + 참여 기록 시작 전 객관식 묶음. 같은 사람(키)은 사건·공동 표현을 여러 번 해도 1명 |
sm_participant_key입니다. ‘새 참여자로 시작’을 누르면 바뀌므로 공용 기기에서도 학생마다 따로 셉니다. 브라우저 저장소를 지우거나 다른 기기를 쓰면 같은 사람도 다시 셀 수 있습니다.supabase/2026-10-count-everyone.sql (위 SQL 포함, SQL Editor에서 이 파일 하나만 실행)참여 기록을 시작하기 전에는 동의하지 않은 참여자의 행이 서버에 없으므로, API 요청 로그로 추정치를 만들어 participation_baseline에 숫자 하나로 넣었습니다.
participation_baseline은 RLS가 켜져 있고 사이트(anon)에서 읽기·쓰기가 불가합니다. 고치거나 빼는 방법은 SQL 파일 끝에 있습니다.service_role 키·비밀 키를 절대 코드에 넣지 마세요. Anthropic API 키는 Edge Function ask-ai의 Supabase secret에만 있고, 함수는 Origin 검사·호출 한도(ask_ai_calls, 서비스 롤 전용)로 보호합니다..claude/settings.json): Supabase 조회 도구(목록·스키마·advisor)만 자동 허용하고, execute_sql·apply_migration 등 데이터·스키마·배포를 바꿀 수 있는 도구는 ask로 두어 실행 전에 확인을 받습니다(tests/claude-settings.test.js가 검사). 이 설정은 이 저장소에서 Claude Code를 쓸 때만 적용되며, 사용자·전역 설정, 세션 권한 모드, Supabase 대시보드·다른 도구에서의 변경까지 막지는 않습니다.shared_expressions: 조회·등록만 허용, 수정·삭제 정책·권한 없음. 등록은 허용 컬럼·CHECK 제약 범위 안에서만.expression_empathy: 등록만 허용, 조회 정책 없음. 집계는 함수로만.auth.uid() 기준 UNIQUE를 권장합니다.2026-10-server-moderation.sql 적용 후 같은 제안 5분 내 재등록·전체 1분 120건 이상 차단. 공감 빈도 제한은 없습니다.DB를 바꾸지 않는 최소 방식입니다. 기존 public_expressions RPC와 공감 테이블을 그대로 씁니다.
| 항목 | 내용 |
|---|---|
| 아카이브에 넣는 것 | 공개 등록에 별도 동의를 받기 시작한 배포(2026-10-16, PR #9) 이후 등록된 제안만 (SharedStore.ARCHIVE_SINCE). 그 전 시범 운영 제안 10건은 사건 화면 목록에는 남지만 아카이브에는 넣지 않습니다 |
| 등록 전 예방 | 공개 문장·이유에 링크·이메일·전화번호가 있으면 등록하지 않고 안내합니다 (checkPublicText). 닉네임의 연락처·학교·학년/반 차단(checkNickname)은 그대로입니다 |
| 숨김 이력 | 2026-10-22: 시범 운영(10-08) 제안 중 빈칸 검사 도입 전 문장 틀 밑줄이 남은 id 11·14 (운영 책임자 확인, DB 행 유지). 코드 목록과 DB is_hidden = true 모두 적용 |
| 신고·숨김 | 교사·참여자가 운영자에게 알리면, 운영자가 확인 후 Supabase Table Editor에서 해당 제안의 is_hidden을 true로 바꿉니다 → 즉시 목록·공동 기억 지도·아카이브·참여자 수에서 빠집니다(재배포 불필요). DB 행은 지우지 않습니다. DB에 접근할 수 없을 때는 shared-store.js의 HIDDEN_EXPRESSION_IDS에 id를 추가해 배포합니다 |
| 영구 삭제 | 개인정보 노출 등 꼭 필요한 경우에만 운영 책임자 승인 후 Supabase 대시보드에서 삭제합니다 |
| 판단 기준 | 개인정보, 특정인·집단 비하·혐오, 광고·도배, 사건과 무관한 글. 역사 해석이 다르다는 이유로는 숨기지 않습니다 |
| 서버 단 숨김 | 2026-10-22부터 is_hidden으로 재배포 없이 공개 API·목록·집계에서 숨깁니다(9장 ‘서버 단 숨김 · 등록 빈도 제한’). 코드 목록 HIDDEN_EXPRESSION_IDS는 보조 수단으로 유지 |
escapeHtml로 처리하고, 지도 텍스트는 textContent로 넣습니다.index.html 페이지 구조 (SPA: section 전환)
style.css 디자인 (한지 베이지·먹색·국가별 포인트 컬러)
supabase-config.js Supabase 공개 클라이언트 (라이브러리를 못 불러오면 null)
vendor/supabase-js/ Supabase JS 2.117.2 (저장소 내 보관 · 버전 고정 · MIT 라이선스 · VERSION.json에 무결성 해시)
vendor/qrcode-generator/ qrcode-generator 2.0.4 (QR 시트용 · 저장소 내 보관 · MIT · VERSION.json에 무결성 해시)
card-themes.js My Memory Card 아트 컬렉션 테마 (고문서·민화·우키요에·청록산수·기본) — Canvas로 직접 그리는 창작 장식
memory-card.js 사건별 QR 링크 해석 · My Memory Card 내용·이미지(Canvas, 4:5 카드 · 9:16 스토리) 만들기
choice-questions.js 기본 체험 객관식 질문(임진왜란·재일조선인) · 근거 · 순서 돌리기 · 저장 행 · 집계
events-data.js 사건·국가별 서술·키워드 묶음·참고 예시
sources-data.js 관련 사료·자료 메타데이터
concept-lexicon.js 다국어 개념 사전·표현 프레임
contribution-guide.js 작성 가이드 문구 (표현 방식·문장 틀·이유) + 제출 전 확인 (빈칸·역사 용어·닉네임)
nlp-analyzer.js 국가별 서술 NLP 분석
shared-memory-analyzer.js 공동 표현 분석·군집·탐구 전/후·강조점·키워드 차트
generation-analysis.js 연령대 필터·세대별 비교·소수 표본 기준·완료 참여 기준 (기존 분석 함수 재사용)
fourth-narrator-data.js 네 번째 서술자 편집 데이터: AI 질문 원문(3개 언어)·사건 명칭·사건별 확인 요소·핵심 빈자리·편집자 관찰·질문하는 AI 사전 질문(dialogue)
ask-ai.js ③ 질문하는 AI: 상태기·입력 검사·Edge Function 호출·폴백·근거 칩 이동
ai-narratives-data.js AI 답변 기록 54건 (2026-10-22 수집, 손으로 고치지 않음)
narrator-analysis.js 네 번째 서술자·기억의 빈자리 계산 (DOM 없음, Node 테스트 가능)
tools/collect-ai-narratives.mjs AI 답변 수집 스크립트 (Anthropic API, 로컬·클라우드 환경에서 실행)
tools/build-ask-ai-data.mjs 질문하는 AI 함수용 data.json 생성 (사이트 데이터만, API 호출 없음)
supabase/functions/ask-ai/ Edge Function: index.ts(Deno 핸들러) · core.mjs(검증·프롬프트·정규화) · data.json(근거 자료)
memory-map.js 공동 기억 지도 SVG
shared-store.js Supabase 읽기·쓰기 (하위 호환)
script.js 화면 렌더링·이벤트 처리
supabase/*.sql DB 확장 SQL
tests/*.test.js Node 단위 테스트
tests/e2e/ 브라우저 E2E (Playwright · 가짜 Supabase)
.github/workflows/test.yml GitHub Actions 자동 테스트
docs/fourth-narrator-findings.md AI 답변 54건 분석 근거·검토 필요 항목·발표 요약
docs/finals-demo-checklist.md 본선 시연 점검표
docs/ask-ai-runbook.md 질문하는 AI 배포·운영 런북 (MCP 배포·secret·SQL·끄는 법·한도)
학교망에서 외부 CDN(jsDelivr 등)이 막히거나 느려도 동작하도록, Supabase JS를 저장소 안에 보관하고 정확한 버전(2.117.2)으로 고정했습니다. 페이지가 불러오는 외부 스크립트는 없습니다(글꼴만 Google Fonts, 실패해도 기본 글꼴로 표시).
| 파일 | 내용 |
|---|---|
vendor/supabase-js/supabase.js | 공식 npm 패키지 @supabase/[email protected]의 dist/umd/supabase.js를 수정 없이 복사 |
vendor/supabase-js/LICENSE | MIT 라이선스 원문 (Copyright Supabase) |
vendor/supabase-js/VERSION.json | 버전, 배포일, npm 무결성(sha512), 파일 해시(sha256). tests/vendor-supabase.test.js가 파일 해시를 검사해 변조·잘못된 교체를 막습니다 |
업데이트 방법 (2주 이상 지난 2.x 버전 권장):
npm pack @supabase/supabase-js@<버전> # 공식 레지스트리에서 받기 (출력의 integrity 확인)
tar xzf supabase-supabase-js-<버전>.tgz
cp package/dist/umd/supabase.js vendor/supabase-js/supabase.js
cp package/LICENSE vendor/supabase-js/LICENSE
sha256sum vendor/supabase-js/supabase.js # → VERSION.json의 fileSha256, version, npmIntegrity 갱신
npm test && node tests/e2e/offline-vendor.e2e.js
빌드가 필요 없습니다. 모듈을 <script>로 불러오므로 로컬 서버로 여는 것을 권장합니다.
python3 -m http.server 8000
# 브라우저에서 http://localhost:8000
GitHub Pages: 저장소 Settings → Pages → Branch main / root 로 배포.
환경: Node.js 22 LTS (CI와 같은 버전, 18 이상이면 단위 테스트는 동작) · 테스트 의존성은 package-lock.json으로 고정 (playwright 1.56.1 → Chromium 141, 리비전 1194)
npm ci # 테스트 의존성 설치 (lockfile 그대로, 사이트 실행에는 필요 없음)
npm test # 단위 테스트 = node --test tests/*.test.js
npx playwright install --with-deps chromium # 브라우저 E2E용 Chromium (처음 한 번)
npm run test:e2e # tests/e2e/*.e2e.js 전체 실행 후 요약
# 종료 코드: 0 = 브라우저에서 실제로 실행되어 통과 / 1 = 실패(확인 항목 0개 포함) / 2 = 실행하지 못함(SKIPPED)
# 각 결과에 Playwright·Chromium 버전과 실행 시각이 출력됩니다. Playwright가 없으면 SKIPPED이며 통과가 아닙니다.
# 다른 경로의 Playwright·브라우저를 쓰려면 PLAYWRIGHT_MODULE, CHROMIUM_PATH 환경 변수로 지정
.github/workflows/test.yml| 항목 | 내용 |
|---|---|
| 실행 조건 | 모든 Pull Request, main 브랜치 push, Actions 탭의 수동 실행(workflow_dispatch) |
| 작업 | Unit tests — npm ci → npm test (실행된 테스트가 0개면 실패) / Browser E2E — npm ci → Chromium 설치 → npm run test:e2e (실제 Chromium, 가짜 Supabase) |
| 실패 처리 | 테스트 실패·확인 항목 0개·E2E를 실행하지 못함(SKIPPED) 모두 워크플로 실패. E2E_ALLOW_SKIP은 사용하지 않습니다 |
| 권한·보안 | permissions: contents: read만 사용, 비밀 키·운영 DB 인증 정보 없음. 공식 actions는 커밋 SHA로 고정. E2E 작업은 러너의 hosts 파일에서 운영 Supabase 주소를 막아, 테스트가 실수로 운영 DB에 요청해도 연결되지 않게 합니다 |
| 결과 확인 | PR 아래 Checks 또는 Actions 탭 → 실행 → 요약(Summary)에 단위 테스트 수와 E2E 요약 표(PASS/FAIL/SKIPPED, 통과 수, Chromium 버전). 자세한 로그는 아티팩트 unit-test-log, e2e-log(14일 보관)에서 내려받아 FAIL 줄과 바로 앞의 확인 항목을 찾으면 됩니다 |
가짜 DB(Mock Supabase) 테스트의 한계: E2E는 tests/e2e/mock-supabase.js나 가로챈 HTTP 응답을 쓰므로 RLS 정책, 컬럼 단위 권한, CHECK 제약, RPC의 실제 SQL(소표본 가림 등)은 브라우저 테스트로 검증되지 않습니다. 이 부분은 단위 테스트의 SQL 파일 검사(tests/prefinal-stabilization.test.js)와 운영 반영 시 수동 확인으로 보완합니다. 실제 휴대폰·교실 네트워크 환경도 CI에서 재현되지 않습니다.
tests/ask-ai.test.js — ③ 질문하는 AI: 함수 순수 부분(입력 검증·system prompt·JSON 스키마·근거 id 대조·폴백)·화면 상태기·답 검사·dialogue·data.json 동기화·비밀 키 없음·SQL 규칙tests/nlp-analyzer.test.js — 국가별 서술 분석 (기존)tests/generation-analysis.test.js — age_group 없는 기존 데이터 호환·연령대 필터·prior_learning null·소수 표본 숨김·completed_flow·전체 필터 = 기존 분석 결과·단계적 저장(하위 호환)·비공개 참여자 연결tests/multilingual-zh.test.js — 한/중/일/영 언어 판별, 한자만 있는 표현의 비확정 판별, 사용자 선택 언어 우선, 중국어 개념 매핑(간체·번체), 중국어 토큰화, 중↔한 개념 연결, 4개 언어 혼합 군집화, 기존 ko/ja/en·unknown 데이터 호환, zh 언어 필터, 결정성tests/shared-device.test.js — 새 참여자로 시작(‘sm_’ 키만 삭제, 새 참여자 키), 새로고침 후 분리 유지, localStorage 차단·없음·용량 초과 환경, 초기화 후 제출이 새 키로 연결되고 삭제 요청이 없음tests/prefinal-stabilization.test.js — 사건 순서(imjin→zainichi)·문장 틀 빈칸 잔존 검출·역사 용어 확인·닉네임 확인·사료 검증 단계와 메타데이터·sourceRefs 유효성·편집자 해설 일반화 방지·허용 event_key·정상 insert 컬럼 = 허용 컬럼·RPC 권한/search_path·집계 RPC 형식·소표본 보호(서버 기준 = 화면 기준)·RPC/테이블 하위 호환·기존 언어/연령 데이터 호환tests/shared-memory-analyzer.test.js — 빈 배열·한 개·거의 같은 표현·완전히 다른 표현·한/일/영 혼합·잘못된 입력·유사도 범위·군집 결과·결정성·대량 데이터·서술 연결·프레이밍·탐구 전/후·실시간 분석·키워드 차트 자동 계산·강조점·작성 후보·공감 비교·사료 데이터 검증·지도 배치tests/quick-experience.test.js — 기본 체험: 질문 근거 낱말 실재·중립 표현·순서 돌리기·응답 검증·저장 행(id만)·작은 표본 숨김·카드 모델(고른 관점/소감 구분)·모든 테마×형식 그리기·저장 모듈·SQL 규칙tests/e2e/quick-experience.e2e.js — 휴대폰 기본 체험 전 과정(첫 화면 → 카드 탭 수, 동의 분리, 별도 저장, 심화 탐구 전환, 새 참여자, 다른 사건 회귀)tests/memory-card.test.js — 사건 링크 해석(형식·없는 사건·이상한 값)·사건별 역사적 질문·카드 모델(입력 그대로·없는 항목 비움·도장=실제 기록·발견 최대 3개·길이 제한)·글자 크기 단계·탐구 전/후는 실제 차이만·발견 후보가 NLP 결과에서 옴·줄바꿈·QR 라이브러리 해시·스크립트 순서·공개 동의 분리tests/network-timeout.test.js — Supabase 응답이 없을 때 제한 시간 뒤 안내(등록·불러오기·참여자 수), 시간 초과 시 재전송·테이블 재조회 없음tests/e2e/live-refresh.e2e.js — 최신 참여 불러오기(공동 기억 지도·아카이브), 불러오기 실패 → 다시 시도, 모바일tests/e2e/memory-card.e2e.js — 휴대폰 QR 링크 진입, 탐구 전/후 기록 정확성(특수문자), 공개 없이 카드(서버 0건·저장소 없음), 발견·질문, PNG 저장(크기), 인쇄 화면, 동의 없이 공개 불가, 공개 후 카드·도장, 새 참여자 초기화, 잘못된 링크, 1000자 문장, QR 시트 6개tests/vendor-supabase.test.js — vendor 파일 버전 고정·해시·라이선스, index.html에 외부 CDN 스크립트 없음, 비밀 키 없음, 실제 vendor 라이브러리 + shared-store로 조회·등록·공감·집계 RPC 요청 형식(가짜 HTTP), 네트워크 실패 시 안내 문구tests/ci-workflow.test.js — CI 실행 조건(PR·main push), 최소 권한·비밀 값 미사용, npm ci·npm test·npm run test:e2e 실행, 테스트 0개·SKIPPED 성공 처리 금지, 공식 actions SHA 고정, Playwright 정확한 버전·lockfile 일치, node_modules 제외partial 자료의 세부 원문 링크(선조실록 개별 기사, 징비록 원문 제공처) 확인| 주관식과 구분 | 객관식은 exploration_choices(+ 집계 choice_counts), 자유서술 공동 표현은 기존 shared_expressions — 테이블과 집계가 완전히 따로입니다 |
| 다른 참여자들의 선택 | 카드 화면 아래 “다른 참여자들은 어떤 관점을 골랐을까?” — 질문별 비율과 ‘내 선택’. 질문별 응답이 5명 미만이면 숨깁니다(서버 sm_min_group_size() 기준). “많이 고른 관점이 옳다는 뜻은 아니에요”를 함께 표시 |
| 공용 기기 | 고른 답·소감·묶음 번호는 이 창의 메모리에만 두며(저장소에 남기지 않음), ‘새 참여자로 시작’하면 지웁니다. 이전 기록이 있는 기기에서 사건 카드를 누르면 기존처럼 ‘이어서/새 참여자’를 묻고, ‘새 참여자’를 고르면 초기화한 뒤 누르던 사건을 바로 기본 체험으로 엽니다(연령대 묻지 않음). 기본 체험 중 ‘새 참여자로 시작’은 첫 화면 사건 카드로 돌아갑니다 |