저는 산림청, 교육청 및 지자체를 대상으로 한 도시숲 안전관리 플랫폼 'TreeSafe'(thetreesafe.com)를 준비하고 있습니다. 나무의 전도, 낙지, 낙과 및 고사 같은 중대 안전사고를 IoT, AI, GIS 및 디지털트윈으로 사전 예방하는 B2G 서비스입니다.
문제는 기술명세서가 하드웨어까지 포함한 많은 규모의 엔터프라이즈라, 한 번에 만들 수 없다는 점이었습니다. 그래서 "웹 관제 SaaS를 먼저 빠르게 만들고, IoT 및 AI는 인터페이스만 열어둔다"는 전략으로 Claude Code와 함께 MVP를 만들어 왔고, 이번에는 그 위에 실제 영업 및 제안에 바로 쓸 수 있는 기능들을 얹는 작업을 진행했습니다.
이번 세션에서 실제로 라이브 배포까지 끝낸 것:
📄 PDF 종합 리포트 (한글 임베딩, 정부 제출용)
🖥️ 공개 랜딩 페이지 (관제 콘솔과 분리)
🎨 공식 로고 + ㈜더코어 코퍼레이트 푸터
📱 QR 스캔 현장점검 (모바일 카메라 → 수목 상세)
🗂️ 녹지 유형별 필터 및 구분 UI (학교숲→도시공원→가로수→생활권녹지→도시숲)
🌱 5개 유형 데모 데이터 시드
[이미지 첨부: 완성된 랜딩 페이지 전체 화면 — 히어로 "도시숲을 지키는 디지털트윈 AI 안전관리 플랫폼"]
Claude Code (Opus 4.8) — 메인 개발/배포 에이전트
Next.js 16 + TypeScript + Tailwind / Supabase(PostgreSQL+PostGIS) / Vercel(서울 icn1 리전)
Claude in Chrome (브라우저 MCP) — 라이브 사이트 로그인 및 검증
Supabase MCP — DB 마이그레이션 및 쿼리
Firecrawl MCP — 회사 홈페이지(푸터/로고) 정보 스크래핑
가장 큰 꿀팁입니다. Claude Code에 프로젝트 상태를 통째로 적어둔 메모리 파일을 두고, 매 세션 첫 메시지에서 "메모리 확인 후 이어서 작업"이라고만 하면 됩니다. 제가 실제로 쓴 시작 프롬프트 전문:
TreeSafe(thetreesafe.com) 도시숲 안전관리 플랫폼 이어서 작업.메모리(treesafe-mvp-decisions)에 전체 상태 있음. 먼저 확인해줘.
현황(라이브): https://www.thetreesafe.com (로그인 [email protected] / ...)
- 코드: C:\dev\treesafe (git, push 시 Vercel 자동배포 / 서울 icn1 리전)
- Supabase project: treesafe (서울)
- 완료: 인증 및 RLS / VWorld 지도 / 수목 CRUD 및 점검 / 기상청 연동 / 알림 / 리포트 및 CSV / 커스텀도메인
다음 할 일(순차):
1) PDF 리포트 2) QR 스캔 현장점검 3) 카카오 로드뷰 ...
먼저 dev 서버 띄우고, 1번 PDF 리포트부터 진행해줘.
이후로는 "랜딩 페이지 만들어 줘", "로고 이걸로 바꿔줘", "유형별 필터 만들어줘" 처럼 짧게만 지시해도, Claude가 메모리와 코드베이스를 읽고 알아서 이어갔습니다.
@react-pdf/renderer로 서버에서 PDF를 생성했는데, 한글이 깨지지 않게 Pretendard(OFL) 폰트를 레포에 직접 번들하고 등록한 게 핵심이었습니다.
// src/lib/pdf/font.tsimport path from 'node:path'
import { Font } from '@react-pdf/renderer'
export function registerFonts(): void {
const dir = path.join(process.cwd(), 'src/fonts')
Font.register({
family: 'Pretendard',
fonts: [
{ src: path.join(dir, 'Pretendard-Regular.otf'), fontWeight: 'normal' },
{ src: path.join(dir, 'Pretendard-Bold.otf'), fontWeight: 'bold' },
],
})
Font.registerHyphenationCallback((word) => [word]) // 한글 단어 중간 줄바꿈 방지
}
Vercel 서버리스에서 폰트 파일이 누락되지 않도록 next.config.ts에 한 줄을 추가한 게 포인트입니다.
const nextConfig: NextConfig = {serverExternalPackages: ['@react-pdf/renderer'],
outputFileTracingIncludes: { '/api/reports/pdf': ['./src/fonts/**'] },
}
[이미지 첨부: 생성된 PDF "수목 안전관리 종합 리포트" — 요약카드, 위험등급분포 및 중점관리수목 표]
QR 스캔 기능을 만들 때, 데스크톱엔 카메라가 없어 실제 스캔 테스트가 어려웠습니다. 그래서 QR 인코딩 → (스캐너가 쓰는 바로 그 디코더 jsQR로) 디코딩 라운드트립으로 검증했습니다.
import QRCode from 'qrcode'import jsQR from 'jsqr'
const url = 'https://www.thetreesafe.com/trees/3'
const qr = QRCode.create(url, { errorCorrectionLevel: 'M' })
// QR 매트릭스를 4px/모듈로 래스터화해서 jsQR에 넣고 복원 확인
const decoded = jsQR(rgba, dim, dim)
console.log('match:', decoded?.data === url ? 'PASS' : 'FAIL')
// → match: PASS (인코딩→디코딩 파이프라인 검증 완료)
[이미지 첨부: /scan 카메라 스캔 화면 (타겟 프레임 + 수동입력 폴백) / QR 라벨 인쇄 페이지]
"학교숲 → 도시공원 → 가로수 → 생활권녹지 → 도시숲" 순차 확산을 위해, PostGIS RPC에 유형 필터를 넣고(지도, 목록 및 리포트 공통), Supabase MCP로 라이브 DB에 바로 마이그레이션했습니다.
-- trees_geojson: forest_type 노출 + 유형 필터(p_forest_type[]) 추가-- SECURITY INVOKER → RLS(기관 단위 권한) 그대로 유지
create or replace function public.trees_geojson(
p_forest bigint default null, p_forest_type text[] default null
) returns jsonb language sql stable as $$
select jsonb_build_object('type','FeatureCollection','features', coalesce(jsonb_agg(
jsonb_build_object('type','Feature',
'geometry', extensions.st_asgeojson(t.geom)::jsonb,
'properties', jsonb_build_object('tree_id', t.tree_id, 'risk_level', t.risk_level,
'forest_type', f.forest_type::text)))), '[]'::jsonb))
from public.tree_info t join public.forest_info f on f.forest_id = t.forest_id
where (p_forest_type is null or f.forest_type::text = any(p_forest_type))
and t.geom is not null;
$$;
[이미지 첨부: 리포트 "녹지 유형별 분포" — 학교숲50%, 도시공원14%, 근린11%, 가로수14% 및 생활권11% / 지도 레이어 패널의 "녹지 유형" 필터 체크박스]
Claude가 코드만 짜고 끝내는 게 아니라, Chrome MCP로 실제 라이브 사이트에 로그인해 결과를 눈으로 확인하게 했습니다. 예를 들어 PDF가 Vercel에서 폰트 임베딩까지 제대로 되는지를, 로그인 상태에서 직접 fetch해 확인:
status 200 · application/pdf · 67KB · 매직바이트 %PDF→ 로컬 렌더(67KB)와 동일 크기 = Vercel 폰트 임베딩 정상
PDF 리포트 /api/reports/pdf — 한글 임베딩 제출용
랜딩 페이지 / (공개) + 관제 콘솔 /map 분리
공식 로고/파비콘 + ㈜더코어 푸터
QR 현장점검 — /scan 카메라 + 수목별 QR 라벨 인쇄
유형 필터 + 5개 유형 데모 데이터 (5개 관리단위 및 36그루)
폰트 CDN 경로 404 — jsDelivr GitHub 소스엔 정적 TTF가 없고 릴리스에만 있었음. npm CDN의 OTF 경로로 우회.
tsx에서 top-level await 에러 — async function main()으로 감싸 해결.
@react-pdf 타입 불일치 — renderToBuffer가 ReactElement<DocumentProps>를 요구. 엘리먼트 캐스팅으로 해결.
Chrome이 localhost 접속 불가(loopback) — 그래서 검증은 전부 라이브 https로 진행. (이게 오히려 "진짜 배포본"을 검증하게 만든 전화위복)
Vercel 미국 리전 → VWorld 502 — vercel.json에 {"regions":["icn1"]} 서울 리전으로 해결(예전 세션).
배포 폴링 false positive — 인증 보호 페이지는 미인증 curl이 로그인으로 리다이렉트돼 항상 200. 공개 자산(예: /thecore-logo.png) 등장 여부를 배포 완료 마커로 사용.
qr_code 필드가 URL이 아니었음 — "QR-SEOULFOREST-0001" 형식. 그래서 QR엔 상세 URL을 인코딩하고, 스캐너는 URL, 숫자 및 코드값을 모두 해석하도록 설계.
메모리 파일 = 프로젝트의 단일 진실. 세션이 끊겨도 "메모리 확인하고 이어줘"면 끝.
"증거 없이는 완료 선언 금지." 빌드 통과 및 라이브 fetch 및 라운드트립 PASS 같은 실행 증거를 매번 남기게 하면 결과 신뢰도가 확 올라갑니다.
외부 자산은 핫링크 말고 번들(폰트 및 로고). 서버리스에선 outputFileTracingIncludes까지 챙기면 안전.
카카오 로드뷰가 카카오맵 추가기능 심사 대기 중(콘솔 확인: 카카오맵 및 비즈니스정보 둘 다 "심사 중"). 코드는 완성돼 있어 승인되면 자동 작동 예정.
카카오 로드뷰 승인 후 켜기 (+ 도메인 등록 확인)
길찾기/점검 경로
실서비스 전 지자체 org 분리(현재 데모는 가시성 위해 교육청 org에 모아둠)
IoT 센서 및 AI 위험예측 실연동 (지금은 인터페이스 + 시뮬레이터)
시스템 명세 근거: 자사 기술명세서 「도시숲 디지털트윈 AI 안전관리 플랫폼」 제1장 시스템 개요
적용 범위/UI 톤 참고: 산림빅데이터거래소 (bigdata-forest.kr)
폰트: Pretendard (OFL), 지도: 공간정보 오픈플랫폼 브이월드(VWorld)

한국사이트 스크린샷

한국사이트 스크린샷

한국어 웹사이트 스크린샷

도시의 위치를 보여주는 지도

한국 검색 엔진의 스크린샷

다양한 색상이 표시된 대시보드의 스크린샷

한국증권거래소 스크린샷
원문: 천동암, 지피터스 GPTers (2026.06.04) gpters.org 원문 보기