TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
링크, 이미지, PDF를 칼럼으로 정리하는 개인용 보드입니다. Next.js로 만들었고 Supabase(Auth, Postgres, Storage)를 사용합니다. 배포는 Vercel이 GitHub main 브랜치를 지켜보다가 push마다 자동으로 합니다. • 저장소: https://github.com/dacisosl/callum • 데이터베이스 스키마: supabase/schema.sql檢視完整 README ↗
링크, 이미지, PDF를 칼럼으로 정리하는 개인용 보드입니다. Next.js로 만들었고 Supabase(Auth, Postgres, Storage)를 사용합니다. 배포는 Vercel이 GitHub main 브랜치를 지켜보다가 push마다 자동으로 합니다.
supabase/schema.sqllib/pdf-render.ts, pdf.js). 카드 타일은 이 썸네일을 쓰고, 휴대폰 뷰어는 쪽마다 캔버스로 그립니다. 안드로이드·카카오톡 인앱 브라우저는 PDF <iframe>을 파일 다운로드로 넘겨 버리기 때문입니다. 썸네일 없이 올라간 옛 PDF는 주인이 보드를 열면 자동으로 채워지고, 그 전까지 휴대폰에서는 아이콘 자리표시로 보입니다.+ 버튼 색 선택 (기본값은 모든 칼럼이 같은 색)?board=...) 새로고침해도 그 보드가 다시 열립니다. 뒤로가기로 홈에 갑니다Node.js 20 이상이 필요합니다.
npm install
npm run dev
브라우저에서 http://localhost:3000을 엽니다. 서버 없이 화면만 확인하려면 .env.local에 NEXT_PUBLIC_DEMO_MODE=1을 넣으면 브라우저 로컬 데모 모드로 실행됩니다.
Vercel은 배포마다 callum-xxxxxxxx-....vercel.app 같은 배포 전용 주소도 만들지만, 그 주소는 그 배포 시점의 코드에 고정되어 이후 변경이 반영되지 않습니다. 고정 도메인은 Vercel 대시보드 → 프로젝트 → Settings → Domains 에 있습니다(callum-<팀>.vercel.app 또는 연결한 도메인).
앱이 만드는 공유 링크와 링크 미리보기 주소는 현재 열려 있는 주소와 관계없이 항상 고정 도메인으로 만들어집니다(lib/site-url.ts). 기준은 NEXT_PUBLIC_SITE_URL → Vercel이 넣어 주는 NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL → 코드에 적힌 기본값 https://callum-eight.vercel.app 순서입니다. 나만의 도메인을 연결하면 Vercel 값이 그 도메인으로 바뀌고, 다른 주소를 쓰고 싶으면 NEXT_PUBLIC_SITE_URL에 넣으면 됩니다. 로컬 개발 서버(localhost)에서만 현재 주소를 그대로 씁니다.
proxy.ts는 프로덕션에서 배포 전용 주소로 들어온 요청을 고정 도메인으로 308 리디렉션하므로, 예전에 보낸 배포 전용 링크도 최신 화면으로 연결됩니다.
get_my_usage 함수가 돌려줍니다. 홈을 보는 동안 1분마다 다시 셉니다. 한 달 전송량(5GB)은 클라이언트에서 셀 수 없어 Supabase 대시보드 링크로 안내합니다. 오른쪽에는 고른 폴더(또는 전체)의 보드가 카드로 나오고, 보드 검색, 새 보드, 이름 변경, 삭제, 복사를 할 수 있습니다. 공유 중인 보드가 있으면 보드 모음 위에 "공유 중인 보드" 줄이 접힌 채로 있고, 펼치면 링크를 복사하거나 공유를 중지할 수 있습니다. 휴대폰에서는 폴더가 위쪽 가로 줄로, 사용량은 맨 아래로 내려갑니다.position 숫자로 저장되어(lib/board-order.ts) 어느 기기에서나 같고, 폴더 안에서 옮겨도 다른 폴더의 순서는 흐트러지지 않습니다. 새 보드는 맨 앞에 옵니다. 아직 숫자가 없는 보드는 숫자가 있는 보드 뒤에 최근 수정 순으로 놓입니다.…)에서 이름을 바꾸거나 없앱니다. 폴더를 없애도 보드는 지워지지 않고 폴더 밖으로 나옵니다. 보드 메뉴의 "폴더로 이동"으로 넣고 빼며, 폴더를 보는 중에 새 보드를 만들면 그 폴더에 들어갑니다. 보드 메뉴의 색 견본으로 보드마다 색을 정하면 홈의 보드 카드 전체가 그 색으로 물들고, 보드를 열었을 때의 배경도 같은 색조가 됩니다. 폴더는 따로 저장하지 않고 보드마다 적힌 folder 이름에서 끌어내므로(lib/board-folders.ts) 데이터베이스에 표를 더하지 않아도 어느 기기에서나 같은 폴더가 보입니다. 아직 보드가 없는 빈 폴더만 이 브라우저에 기억합니다.+ 버튼이 있고, 새 카드는 맨 위에 추가됩니다. 질문 섹션은 제목 막대에 물음표 표시가 붙고, + 대신 "답변 쓰기" 버튼이 나옵니다. 왼쪽 위 로고나 보드 이름 메뉴의 "모든 보드 보기"로 홈에 돌아갑니다. 칼럼 메뉴(…)에서 이름과 색을 바꿉니다.?share=토큰 주소로 열리며 로그인 없이 읽기 전용으로 표시됩니다. 댓글이 켜진 보드라면 댓글은 남길 수 있습니다. 이 주소를 카카오톡·슬랙 등에 붙이면 보드 제목, 칼럼·카드 수, 보드 제목이 그려진 미리보기 이미지(/api/og)가 링크 미리보기로 붙습니다. 휴대폰에서는 보드 위에 칼럼 탭이 나타나 눌러서 칼럼 사이를 오갈 수 있고, 카드의 썸네일을 눌러도 카드가 열립니다.…)의 "질문 섹션으로 만들기"로 설정하거나, 칼럼 추가 폼의 "질문 (선택)" 칸을 채워 만듭니다. 질문은 500자까지입니다.question 필드에 들어갑니다. 보드 전체가 boards.data JSONB 한 덩어리로 저장되므로 Supabase 스키마를 다시 실행할 필요가 없습니다.이미지·PDF 말고도 HTML, 한글(HWP), 엑셀, 워드, ZIP, TXT 같은 파일을 카드에 붙일 수 있습니다(파일당 30MB). 동영상과 실행 파일(exe, bat, msi 등)은 막습니다.
HTML quiz.html 처럼 형식 이름표가 붙고, 카드 뷰어에서 내려받기를 누르면 원래 이름으로 저장됩니다srcdoc, allow-same-origin 없음)에 그려서, 페이지 안의 스크립트는 돌아가지만 이 앱의 화면·저장소·로그인 정보에는 손대지 못합니다. txt·csv·md·json 같은 글 파일은 내용을 그대로 보여 줍니다(3MB 이하)application/octet-stream(내려받기 전용)으로 올립니다. 공개 주소를 직접 열어도 웹 페이지로 실행되지 않고 내려받기만 됩니다. SVG 도 스크립트를 담을 수 있어 같은 방식으로 다룹니다clean_guest_attachments 가 실행 파일 확장자를 한 번 더 거릅니다. 막는 목록은 lib/file-kinds.ts 와 같습니다supabase/file-attachments.sql 을 한 번 실행해야 합니다(버킷 허용 형식에 octet-stream 추가). 공유 설정 창에 "일반 파일 첨부"가 빠졌다고 뜨면 SQL 복사로 넣으세요. 실행 전에는 일반 파일을 올리면 그 안내가 뜨고, 이미지·PDF 는 그대로 됩니다칼럼 메뉴(…)의 설문 섹션으로 만들기로 칼럼 맨 위에 설문을 둡니다. 한 섹션에 질문을 여러 개(최대 30개) 넣을 수 있고,
질문마다 객관식(하나 고르기) · 복수 선택 · 단답형 중에서 고르며 필수 여부를 정합니다. 응답 마감도 설문 편집 창에서 켭니다.
survey_responses 표에 따로 두고, 주인만 RLS 로 읽고 지웁니다. 손님은 submit_survey_response 함수로만 쓰며, 함수가 설문 정의에 맞춰 답을 다시 거릅니다(없는 질문·보기는 버리고, 필수 질문이 비면 거부, 마감된 설문은 거부, 보드당 5,000개 상한)supabase/surveys.sql 을 한 번 실행해야 합니다. 공유 설정 창에 "설문 응답"이 빠졌다고 뜨면 SQL 복사로 넣으세요lib/survey.ts, 화면은 app/survey-ui.tsx 에 있습니다주인 화면은 보드 전체를 통째로 저장합니다. 예전에는 주인이 보드를 열어 둔 사이 손님이 올린 카드가 다음 저장 때 덮어써져 사라졌습니다. 이제 글쓰기가 켜진 보드는 저장 직전에 서버 쪽 데이터를 읽어, 내 화면에 없는 손님 카드를 되살려 함께 저장합니다(lib/supabase-client.ts의 withGuestCards). 주인이 방금 지운 카드는 되살아나지 않습니다.
손님은 로그인을 하지 않아 "이 글을 쓴 사람이 나"라는 것을 증명할 방법이 없습니다. 그래서 글을 올릴 때
브라우저에 비밀 열쇠를 저장하고(pillar-card-keys-v1), 서버에는 그 열쇠의 SHA-256 지문만
카드의 editKeyHash 에 남깁니다. 수정할 때 열쇠를 함께 보내면 서버가 지문을 다시 계산해 맞을 때만
통과시킵니다(update_shared_card). 지문은 공유 링크로 읽히지만 지문에서 열쇠를 되돌릴 수 없어
남의 글은 고칠 수 없습니다.
supabase/guest-cards.sql 을 SQL Editor에서 한 번 실행해야 합니다
(supabase/schema.sql 전체를 실행해도 같습니다). 실행하지 않아도 글 올리기와 파일 첨부는
예전처럼 동작합니다. 앱이 새 함수가 없는 것을 알아채면 예전 방식으로 올리고, 그 글은 수정만
안 됩니다.편집창의 링크 칸 옆 검색 을 누르면 검색 창이 열립니다. 키나 설정이 필요 없습니다.
홈 화면 보드 메뉴(…)의 "보드 복사" 로 보드를 통째로 베낍니다. 같은 양식으로 다음 기수를 받을 때 씁니다.
첨부 파일은 새 보드 폴더로 복사됩니다. 경로만 베끼면 복사본이 원본 폴더의 파일을 가리키게 되는데, 정리 기능은 보드 폴더를 훑어 그 보드가 쓰지 않는 파일을 지우므로 원본 카드를 지우는 순간 복사본의 첨부가 깨집니다. 그래서 Supabase Storage 의 서버 내 복사로 파일을 새 폴더에 따로 만듭니다. 저장 공간을 그만큼 더 쓰므로 복사 창에 용량을 미리 보여 줍니다. 복사에 실패한 파일은 원래 경로를 유지하고 몇 개인지 알려 줍니다.
규칙은 lib/board-copy.ts 에 저장소와 무관한 순수 함수로 두어 따로 시험합니다.
손님 기능(글쓰기·수정·삭제)은 데이터베이스 함수에 기대므로, 새 기능이 배포돼도 SQL을 실행하기 전에는 동작하지 않습니다. 보드 주인이 공유 설정 창을 열면 무엇이 빠졌는지 한 줄로 알려 줍니다.
손님의 글쓰기·수정·삭제·파일 업로드와 동시 저장 보호가 모두 준비되었습니다.확인은 각 함수를 빈 토큰으로 한 번씩 불러 보고 "함수 없음"(PGRST202) 오류인지만 보는 방식입니다.
세 함수 모두 보드를 찾는 조건에 token <> '' 가 있어 아무것도 쓰기 전에 예외로 빠지므로 어떤 보드도
건드리지 않습니다. 판정 규칙은 lib/rpc-errors.ts 에 순수 함수로 두어 따로 시험합니다.
경고가 뜨면 그 자리의 SQL 복사 버튼을 누르세요. 빠진 것에 꼭 맞는 SQL이 클립보드에 들어갑니다. 저장소에서 파일을 찾을 필요가 없고, 일부만 선택돼 잘릴 일도 없습니다.
붙여 넣을 때는 Supabase SQL Editor 를 먼저 비우세요. 기존 내용 밑에 덧붙이면 예전 스크립트까지
다시 길게 실행되어, 지금까지 겪은 잘림 문제가 되풀이됩니다. Ctrl+A 로 지우거나 새 쿼리 탭을 여세요.
파일로 직접 실행하고 싶다면 상황에 맞는 것을 고르세요.
| 증상 | 실행할 파일 |
|---|---|
| 삭제만 안 됨 | supabase/guest-card-delete.sql |
| 손님이 파일을 못 올림 | supabase/fix-guest-uploads.sql |
| 수정·삭제가 안 됨 | supabase/guest-cards.sql |
| "동시 저장 보호" 가 빠졌다고 뜸 | supabase/save-board.sql |
| "설문 응답" 이 빠졌다고 뜸 | supabase/surveys.sql |
| "일반 파일 첨부" 가 빠졌다고 뜸 | supabase/file-attachments.sql |
| 처음부터 설치하거나 전부 맞추고 싶을 때 | supabase/schema.sql |
supabase/schema.sql 이 유일한 원본입니다. 나머지 짧은 파일들과 앱이 복사해 주는 문자열은
scripts/build-sql.mjs 가 거기서 떼어 만듭니다. 손으로 떼어 두면 schema.sql 이 바뀔 때 조용히
어긋나기 때문입니다.
supabase/schema.sql 에 반영하고 npm run sql 을 돌립니다npm run build 앞에서도 자동으로 돌아, 배포에 낡은 내용이 실릴 수 없습니다supabase/fix-guest-uploads.sql, supabase/guest-cards.sql, supabase/guest-card-delete.sql,
supabase/save-board.sql, supabase/surveys.sql, supabase/file-attachments.sql, lib/generated-sql.ts 는 모두 생성물이니 직접 고치지 마세요save_board)주인 화면은 보드를 통째로 저장합니다. 손님이 올린 카드는 주인 화면에 아직 없을 수 있어, 그대로 덮어쓰면 사라집니다. 앱은 저장 전에 서버 것을 읽어 합치지만, 읽고 쓰는 사이(수백 ms)에 올라온 카드는 여전히 잃을 수 있었습니다. 20~30명이 한꺼번에 올리는 동안 주인이 카드를 옮기면 실제로 생기는 일입니다.
supabase/save-board.sql 의 save_board 함수는 행을 잠근 채 손님 카드를 합쳐 저장합니다. 손님의
add_shared_card 와 서로 줄을 서므로 어느 쪽이 먼저 오든 둘 다 남습니다. 앱은 이 함수가 있으면
그것으로 저장하고, 없으면(SQL 을 아직 안 돌림) 예전 방식으로 저장합니다. 공유 설정 창에
"동시 저장 보호" 가 빠졌다고 뜨면 SQL 복사 로 넣으세요.
Supabase 무료 프로젝트는 일주일 동안 요청이 없으면 일시정지되어, 연수 당일 로그인도 공유 링크도
열리지 않습니다. vercel.json 의 cron 이 매일 한 번(UTC 21시, 한국 아침 6시) /api/keepalive 를
불러 데이터베이스에 가벼운 질의를 보내므로 잠들지 않습니다. 따로 설정할 것은 없습니다.
https://callum-eight.vercel.app/api/keepalive 를 열어 "ok":true 가 나오는지 봅니다CRON_SECRET 을 두면 그 값을 아는 호출만 받습니다. 없어도 동작하며, 이 경로는
아무것도 바꾸지 않아 열려 있어도 해가 없습니다공유 설정에서 글쓰기를 켰는데도 손님의 파일 업로드가 거부된다면, 저장소 정책이 옛 버전입니다.
supabase/fix-guest-uploads.sql 을 SQL Editor에 붙여 넣고 실행하세요. 이 문제만 고치는 짧은
스크립트라 붙여넣기가 잘릴 걱정이 없습니다. supabase/schema.sql 전체를 실행해도 같은 내용이 들어갑니다.
unterminated dollar-quoted string 오류가 날 때Supabase 대시보드의 SQL Editor는 select ... into 변수 를 "새 표를 만드는 SELECT INTO"로 오해해,
함수 본문 한가운데에 ALTER TABLE 변수 ENABLE ROW LEVEL SECURITY; 를 몰래 끼워 넣습니다.
그러면 $$ 로 감싼 본문이 중간에서 끊겨 이 오류가 납니다. 오류 메시지 안에 그 ALTER TABLE 줄이
같이 보이면 이 경우입니다.
그래서 이 저장소의 함수들은 select ... into 대신 변수 := (select ...) 대입문만 씁니다.
새 함수를 추가할 때도 같은 규칙을 지켜야 합니다.
원인은 이렇습니다. 예전 정책은 손님 업로드를 허용할지 판단할 때 정책 안에서 boards 테이블을
직접 조회했습니다. 그런데 정책 안의 조회에도 boards의 RLS가 그대로 걸리고, boards는
로그인한 주인만 읽을 수 있습니다. 손님은 익명(anon)이라 조회 결과가 언제나 0건이 되어,
공유와 글쓰기를 아무리 켜 두어도 업로드가 전부 거부되었습니다. 지금은 RLS를 우회하는
board_accepts_guest_files 함수(security definer)로 확인하므로 정상 동작합니다.
정책이 최신인지 확인하려면 SQL Editor에서 실행해 보세요. 공유와 글쓰기를 켠 보드는 마지막 열이
true로 나와야 합니다.
select id, title, share_enabled, public.board_accepts_guest_files(id)
from public.boards order by updated_at desc;
share_enabled가 true인데 마지막 열이 false면 그 보드의 글쓰기 허용이 꺼져 있는 것이니,
앱의 공유 설정에서 켜 주세요.
첨부 파일은 저장소에 올라가고 카드에는 그 경로만 적힙니다. 카드가 사라져도 파일은 남아 무료 1GB를 갉아먹습니다. 카드 삭제, 편집창에서 첨부 빼기, 손님의 글 삭제, 저장하지 않고 편집창 닫기가 모두 이런 파일을 남깁니다.
지우면 안 되는 파일을 지키기 위해 두 가지를 지킵니다. 첫째, 나이 기준을 둡니다. 편집창에 붙여 둔 파일은 저장소에는 있지만 아직 카드에는 없기 때문입니다. 둘째, 판단 기준이 되는 보드를 서버에서 새로 읽습니다. 손님이 방금 올린 글이 내 화면에는 아직 없을 수 있기 때문입니다. 카드를 복제하면 여러 카드가 같은 파일을 가리키므로, 참조는 집합으로 모아 한 카드를 지워도 파일이 남게 합니다.
규칙 자체는 lib/attachment-sweep.ts 에 저장소와 무관한 순수 함수로 두어 따로 시험합니다.
실제 삭제는 lib/supabase-client.ts 의 sweepOrphanAttachments 가 맡습니다. 주인에게 필요한
저장소 권한은 이미 있어 SQL 변경은 없습니다.
한계: 이미 지워진 보드의 guest/{보드ID}/ 폴더에 남은 파일은 이 방식으로 닿지 않습니다. 보드가 없으면
훑을 대상을 정할 수 없기 때문입니다. 보드를 지울 때 카드가 가리키던 파일은 removeBoard 가 이미
정리하므로, 남는 것은 그 보드에서 이미 고아였던 파일뿐입니다.
새로고침하지 않아도 남이 올린 글이 화면에 나타납니다. 세 가지가 함께 동작합니다.
내가 카드를 쓰는 중이거나(편집창이 열려 있음), 아직 저장되지 않은 변경이 있거나, 카드를 끌고 있는 동안에는 다시 읽지 않습니다. 남의 글이 내 작업을 덮어쓰지 않게 하기 위해서입니다.
Northeast Asia (Seoul)을 권장합니다.supabase/schema.sql 내용을 붙여 넣고 실행합니다. 테이블, RLS 정책, 공유 조회 함수, 첨부 버킷이 한 번에 만들어집니다.lib/supabase-config.ts의 defaults에 넣습니다. 로컬에서만 다른 프로젝트를 쓰려면 .env.local의 NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY가 우선합니다.anon 키는 브라우저에 노출되는 공개값입니다. 접근 제어는 schema.sql의 행 수준 보안(RLS) 정책이 담당합니다.
dacisosl/callum을 Import 합니다.이후 GitHub 웹에서 코드를 고쳐 커밋하면 그대로 배포까지 이어집니다.
Supabase 무료 프로젝트는 일주일간 요청이 없으면 자동으로 일시 중지됩니다. 데이터는 유지되며 대시보드에서 Restore를 누르면 1분 안에 다시 켜집니다.
supabase/schema.sql의 버킷 file_size_limit과 app/board-app.tsx의 MAX_CLOUD_FILE을 함께 고치고 SQL을 다시 실행합니다공유 대화상자의 "공유받은 사람의 글쓰기" 스위치로 보드마다 따로 켭니다. 기본값은 꺼짐이라 기존 공유 링크는 그대로 읽기 전용입니다.
+ 버튼이 나타납니다.attachments 버킷의 guest/{보드ID}/ 경로에 올라가며, 저장소 정책이 그 보드가 공유 중이고 글쓰기가 켜져 있는지 확인합니다. 주인은 자기 보드의 손님 파일을 지울 수 있습니다.add_shared_card 함수가 맡습니다. 토큰이 맞고 글쓰기가 켜져 있으며 칼럼이 실제로 있을 때만 통과하고, 제목 120자·내용 3000자·보드당 2000장 상한이 걸려 있습니다. 첨부는 우리 저장소의 guest/{보드ID}/ 경로에 올라간 이미지·PDF만 받습니다.주인이 보드를 열어 둔 채로 손님이 카드를 올리면, 주인 화면은 그 카드를 모른 채로 저장하다가 덮어쓸 수 있습니다. 손님 카드가 보이지 않으면 주인 화면을 새로고침하세요.
공유 대화상자나 보드 이름 메뉴에서 보드별로 댓글을 켜고 끕니다. 설정은 보드 데이터의 commentsEnabled에 저장되고, 댓글은 card_comments 테이블에 따로 저장됩니다.
get_shared_comments, add_shared_comment 함수로만 읽고 씁니다. 두 함수는 토큰이 맞고 commentsEnabled가 true인 보드에서만 동작하며, 주인의 사용자 ID는 노출하지 않습니다.supabase/schema.sql을 다시 한 번 통째로 실행해 테이블과 함수를 추가하세요. 공유 글쓰기용 add_shared_card, 사용량용 get_my_usage 함수도 같은 파일에 들어 있습니다.공유 링크는 보드마다 따로 생깁니다. 공유를 켜면 그 보드에 무작위 shareToken이 생기고, 공유 링크(?share=토큰)는 get_shared_board 함수로 해당 보드 하나만 읽어옵니다. 공유 목록을 열거하는 경로는 없습니다. 공유를 끄거나 링크를 재발급하면 기존 링크는 더 이상 조회되지 않습니다.
공유 링크에는 만료 기간이 없습니다. 링크를 끊으려면 홈 화면의 공유 목록이나 공유 대화상자에서 공유를 중지하거나, 링크를 새로 발급해 기존 링크를 무효로 만듭니다.
첨부파일은 공개 버킷의 URL을 사용합니다. 공유를 끈 뒤에도 파일 주소를 이미 아는 사람은 계속 열 수 있습니다. 이를 막아야 한다면 버킷을 비공개로 바꾸고 서명 URL을 발급하는 구조로 바꿔야 합니다.
npx tsc --noEmit
npm run build
app/board-app.tsx: 보드 UI와 상호작용 전체proxy.ts: 배포 전용 주소를 고정 도메인으로 리디렉션lib/site-url.ts: 공유 링크·미리보기에 쓰는 고정 도메인lib/pdf-render.ts: PDF 첫 쪽 썸네일 생성과 쪽 단위 캔버스 렌더링(pdf.js legacy 빌드)app/api/og/route.tsx: 공유 링크의 미리보기 이미지. 보드 제목에 쓰인 글자만 Google Fonts에서 받아 한글을 그립니다lib/shared-board-server.ts: 서버(메타데이터, OG 이미지)에서 공유 보드를 읽는 함수와 배포 도메인 계산app/api/link-preview/route.ts: 링크 메타데이터 수집 (Vercel 서버 함수로 실행). Google 문서·설문·드라이브, Gemini처럼 로그인이 필요한 서비스는 대표 이미지를 받을 수 없어 화면에서 서비스 표지를 보여 줍니다. og 태그가 없으면 link rel="image_src", JSON-LD, 본문 첫 이미지 순으로 찾고, 그래도 없으면 아이콘을 내려 줍니다. 화면에서는 대표 이미지가 없거나 깨지면 WordPress.com mShots 화면 캡처(s0.wp.com/mshots)를, 그것도 안 되면 아이콘 표지를 보여 줍니다. 캡처는 처음 요청 때 "생성 중" 그림이 먼저 오고 잠시 뒤부터 실제 화면이 나옵니다lib/supabase-client.ts: 로그인, 보드 저장·조회, 첨부 업로드lib/supabase-config.ts: Supabase URL과 anon 키lib/board-types.ts: 데이터 형식supabase/schema.sql: 테이블, RLS 정책, 공유 함수, 버킷