Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"site:preview": "vite preview --config site/vite.config.ts",
"site:check": "tsc -p site/tsconfig.json",
"site:test": "vitest run --config site/vitest.config.ts",
"site:icons": "node site/scripts/generate-icons.mjs"
"site:icons": "node site/scripts/generate-icons.mjs",
"site:sizes": "node site/scripts/measure-samples.mjs"
},
"devDependencies": {
"@astryxdesign/core": "^0.5.2",
Expand All @@ -31,6 +32,15 @@
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@testing-library/user-event": "^14.6.8",
"@tiptap/core": "3.32.0",
"@tiptap/extension-highlight": "3.32.0",
"@tiptap/extension-list": "3.32.0",
"@tiptap/extension-subscript": "3.32.0",
"@tiptap/extension-superscript": "3.32.0",
"@tiptap/extension-table": "3.32.0",
"@tiptap/extension-text-align": "3.32.0",
"@tiptap/extension-text-style": "3.32.0",
"@tiptap/pm": "3.32.0",
"@tiptap/react": "^3.32.0",
"@tiptap/starter-kit": "^3.32.0",
"@types/node": "^22.20.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webcraft",
"version": "0.9.1",
"version": "0.10.0",
"description": "웹 플랫폼 개발 지침(프론트·디자인 시스템·백엔드·API·DB·아키텍처)과 프로젝트 연결",
"author": {
"name": "dev-goraebap"
Expand Down
2 changes: 1 addition & 1 deletion plugins/webcraft/dist/claude/plugins/webcraft/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webcraft",
"version": "0.9.1",
"version": "0.10.0",
"private": true,
"type": "module"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ arc42: "8.1"

주소·인증·설정 입력 방식은 각 에이전트의 현재 지원을 확인합니다. 한 에이전트의 변수 치환이나 비밀값 저장 방식이 다른 에이전트에서도 작동한다고 가정하지 않습니다. 필요한 설정만 온보딩에서 확인하고 토큰은 지원되는 로컬 비밀 저장 수단으로 받습니다. 설치 명령이 성공한 뒤에는 실제 연결과 사용자 조회까지 별도로 확인합니다.

## 개선 피드백 받기

사용자에게 MCP를 제공하는 제품은 피드백 접수처(예: 이슈 등록 도구, 접수 양식)와 제출 방식을 정하고, 사용자용 스킬에 그 경로를 적습니다. Webcraft를 쓰지 않는 사용자의 에이전트는 Webcraft 지침을 읽지 않습니다. 판단 기준은 [도구의 부족을 발견했을 때](../../goraebap-mode/references/tool-improvements.md)를 따르되, 사용자용 스킬에는 짧은 요약과 그 제품의 접수처·인증 방식만 둡니다.

MCP 서버를 만드는 제품은 서버 안내(instructions)에도 같은 짧은 요약을 둘 수 있습니다. 플러그인이 없는 에이전트에도 원칙이 닿게 하기 위한 선택지입니다. 스킬과 서버 안내에 긴 절차를 각각 복제하지 않습니다.

Webcraft가 모든 MCP 서버를 관리한다고 가정하지 않습니다.

## 변경과 검증

MCP 도구가 바뀌면 서버 계약과 사용자용 설명을 함께 맞춥니다. 스킬에 없는 도구나 아직 제공하지 않는 기능을 약속하지 않습니다. 패키지 버전과 서버의 호환 범위, 이름이 바뀐 경우 기존 사용자의 전환 방법을 기록합니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ arc42: "8.2, 5, 2"
- [조회 포트](references/queries.md): 화면 조회의 소유권·결과 타입·조립 경로를 정할 때 읽습니다.
- [모듈 간 조회](references/cross-module-read.md): 다른 모듈의 표시값·검색·정렬이 필요할 때 읽습니다.
- [도메인 오류와 서비스 오류](references/errors.md): 규칙 위반·부재·권한·충돌을 처리할 때 읽습니다.
- [사용자가 올린 파일과 고른 주소](references/untrusted-content.md): 올린 HTML을 화면 안에 보이거나, 사용자가 준 주소를 서버가 읽을 때 읽습니다.
- [코드 규칙](references/code-style.md): Java 파일·DTO·의존성을 작성할 때 읽습니다.

완료 시 프로젝트의 정적 검사·테스트와 변경한 유스케이스의 성공·실패·권한을 검증합니다. HTTP 계약이 바뀌면 명세·생성 타입·호출자도 함께 맞춥니다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
arc42: "8.2"
---

# 사용자가 올린 파일과 고른 주소

사용자가 올린 HTML을 본문 안에 미리 보이거나, 사용자가 고른 웹 주소를 서버가 읽어(링크 카드 등) 보여 줄 때 적용합니다. 둘 다 남의 내용을 우리 출처나 우리 서버 망에서 실행·호출하게 되는 일이라 따로 경계를 둡니다.

## 올린 HTML을 틀 안에 보이기

저장소의 서명 주소(presigned URL)나 `/api/**` 응답으로는 화면 안 틀(`iframe`)에 보일 수 없습니다. API 응답에는 `X-Frame-Options: DENY`가 붙고, 저장소 주소는 응답 헤더를 우리가 정할 수 없기 때문입니다. 그래서 앱이 전용 주소로 직접 내보냅니다. 예: `GET /api/embeds/{exp}.{sig}/{key}`. `{key}`는 저장 키를 URL-safe Base64로 쓴 것, `{exp}`는 만료 시각(초), `{sig}`는 서명입니다.

| 항목 | 기준 |
| --- | --- |
| 서명 | 서버 비밀키로 HMAC-SHA256을 계산하고, 서명 대상에 용도 문자열(예: `html-embed`)·만료 시각·키를 함께 넣습니다. 용도 문자열이 있어 다른 서명과 섞여 쓰이지 않습니다. 비교는 시간이 일정한 비교(`MessageDigest.isEqual`)로 합니다 |
| 만료 | 1시간. 만료 시각을 10분 단위로 올려 가까운 시각에 만든 주소가 같아지게 하여 브라우저가 캐시할 수 있게 합니다. 본문을 다시 읽으면 새 주소를 받습니다 |
| 권한 | 서명한 주소가 곧 권한입니다(서명 주소와 같음). 권한 확인은 기존 파일 열기 주소(예: 모듈의 `…/files/{id}`)가 하고, HTML을 제자리에서 열 때만 전용 주소로 돌려보냅니다. 내려받기는 기존 경로를 씁니다 |
| 응답 | `Content-Type: text/html;charset=UTF-8`, `Cache-Control: private, max-age=300`. 서명이 틀리거나 만료됐거나 객체가 없으면 404입니다. 이 주소는 브라우저가 돌려보내기로만 오므로 OpenAPI 문서에서 뺍니다 |

보안 체인(Spring Security)은 그 주소만의 `SecurityFilterChain`을 다른 체인보다 앞(`@Order(0)`, `securityMatcher("/api/embeds/**")`)에 둡니다.

- GET·HEAD만 허용하고 나머지는 `denyAll`입니다.
- 세션 없음(`STATELESS`), CSRF·폼 로그인·HTTP Basic·로그아웃·요청 캐시를 끕니다.
- `X-Frame-Options`를 끕니다.
- `Content-Security-Policy: sandbox allow-scripts allow-modals allow-forms allow-popups; frame-ancestors 'self'`, `Referrer-Policy: no-referrer`를 붙입니다.

응답 문서는 출처가 없는 상태(sandbox, `allow-same-origin` 없음)로 실행되어 이 앱의 쿠키·저장소·API에 닿지 못하고, 우리 화면 안 틀에서만 열립니다. 주소를 새 탭으로 바로 열어도 같은 정책이 붙습니다.

화면 쪽 틀도 같은 격리를 겁니다: `sandbox="allow-scripts allow-modals allow-forms allow-popups"`, `referrerPolicy="no-referrer"`. 크게 보기 창의 "새 탭에서 열기"는 서버 주소일 때만 둡니다. 저장 전의 `blob:` 주소는 새 탭에서 앱의 출처로 열리기 때문입니다.

서명·만료·변조된 서명·다른 키·잘못된 형식을 단위 테스트로 확인합니다.

## 사용자가 고른 주소를 서버가 읽기

사용자가 고른 주소로 서버가 안쪽 망(사내 서비스, 클라우드 메타데이터)을 부르지 않게 합니다(SSRF 막기).

받는 주소는 HTTPS, 포트 443(생략 포함)만입니다. 사용자 정보(`user:pass@`)가 든 주소, 2000자 넘는 주소는 거부합니다.

호스트의 모든 IP가 공개 주소일 때만 부릅니다. 막는 대역은 미지정(`0.0.0.0/8`, `::`), 루프백, 링크 로컬, 사설(site local), 멀티캐스트, CGNAT 공유 대역 `100.64.0.0/10`, 벤치마크 `198.18.0.0/15`, 예약 `240.0.0.0/4`, IPv6 고유 로컬 `fc00::/7`입니다. IPv4를 품은 IPv6(IPv4-mapped `::ffff:a.b.c.d`, IPv4-compatible)와 NAT64 `64:ff9b::/96`은 안의 IPv4로 다시 판정합니다.

리다이렉트는 자동으로 따르지 않습니다(`HttpClient.Redirect.NEVER`). `Location`을 직접 풀어 다음 주소에도 같은 검사를 하고 최대 3번까지 따릅니다.

연결·응답은 각 5초이고, 상태 200이고 `Content-Type`이 HTML일 때만 읽습니다. 본문은 앞 512KB만 읽습니다(메타데이터는 `head`에 있음). 쿠키·토큰 등 우리 쪽 인증을 보내지 않습니다.

검사할 때 이름을 해석하고 연결할 때 다시 해석하면, 그 사이에 DNS 답이 바뀌는 공격(DNS rebinding)은 위 검사로 막지 못합니다. 검사한 IP로 연결을 고정하거나, 안쪽 망으로 나가지 못하는 출구 프록시·방화벽으로 막습니다. 둘 다 못 하면 그 한계를 프로젝트 기록에 남깁니다.

결과는 제목·설명·대표 사진·사이트 이름을 `og:*`, `twitter:*`, `description`, `<title>` 순으로 읽습니다. 제목은 200자, 설명은 300자에서 자릅니다. 대표 사진 주소는 페이지 주소 기준으로 풀어 HTTPS일 때만 받습니다(HTTP·`data:`는 사진 없음). 대표 사진은 읽는 사람의 브라우저가 그 사이트에서 직접 받으며, 화면은 `referrerPolicy="no-referrer"`로 받습니다.

결과는 6시간 기억합니다(최대 1000개, 오래 안 쓴 것부터 버림). 읽지 못한 주소는 오류 대신 주소(와 호스트)만 있는 카드로 돌려줘 본문이 죽은 링크로 깨지지 않게 합니다. 외부 사이트를 기다리는 동안 DB 트랜잭션을 잡지 않습니다(트랜잭션 서비스가 아닌 컴포넌트로 둠).

공개·사설·루프백·CGNAT·IPv4-mapped·NAT64·포트·스킴 판정, 리다이렉트 횟수, 메타 읽기·자르기·엔티티 풀기를 단위 테스트로 확인합니다.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ arc42: "8.5, 2"
| 대기·빈 상태·실패·전환 | [데이터 상태](references/data-states.md) |
| 표면 고르기·누르는 대상·키보드 | [입력과 상호작용](references/interaction-patterns.md) |
| UI 문구·본문 편집·검증 | [문구·본문과 검증](references/content-and-review.md) |
| 본문 인쇄·PDF 저장 | [인쇄와 PDF](references/print.md) |
| 지침·코드·가이드 견본의 연결 | [패턴 카탈로그](references/pattern-catalog.md) |
| 프로젝트 지정이 없는 기본 화면 | [레퍼런스 사용과 갱신](references/reference-kit.md) |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,19 @@ arc42: "9"

- 결정(사용자): 가이드를 Astro에서 키트와 같은 Vite + React + TanStack Router + MDX SPA로 옮겼습니다. Astro 때문에 둔 우회(셸 유지, 테마 스타일 붙잡기, 넘겨받을 때까지 감추기, 본문 옮겨 끼우기)가 사라지고, 전환은 라우터 대기 + 180ms 덮개 + 160ms 페이드로 키트 규칙 그대로 구현했습니다. 첫 화면은 키트의 bootCover·BootCover를 씁니다.
- 사이트는 플러그인 지침 전체를 보여 줍니다. 지침 문서는 스킬 Markdown을 원본 그대로 빌드합니다(사본 없음). 문서별 정적 HTML(SSG)은 아직 없습니다. 공개할 때 프리렌더를 붙입니다.

## 2026-11-10 · 사내 업무 앱의 리치 에디터와 화면 규칙

- 증상: 키트의 본문 칸은 자리를 보이는 Tiptap 예시뿐이라, 표·블록 메뉴·미디어처럼 실제 업무 본문에 필요한 동작을 지침과 견본으로 확인할 수 없었습니다.
- 수정: 사내 업무 앱에서 다듬은 리치 에디터(표·블록 메뉴·선택 서식 도구·링크 입력·올리는 칸·미디어·파일 카드·도표·HTML 임베드·링크 카드)를 키트 본문 칸으로 옮겼습니다. 키트 리비전 8입니다.
- 기존 규칙을 바꾼 것(이미 Webcraft를 따르는 프로젝트에 영향): 대화상자 안 여백을 데스크톱 40/24/12px로 바꿨습니다(전에는 1rem·0.5rem). 본문 편집을 상세 안 전환에서 전용 수정 화면으로 옮겼습니다. 너비를 조절하는 옆 목록의 기본 너비를 최대값으로 정했습니다(키트 사이드바 380px, 받은함 목록 560px). 본문 칸의 Tiptap 예시를 자리 표시에서 참고 구현으로 바꿨고, 본문의 1단계 제목을 쓰지 않게 했습니다.
- 원인(옮기며 겪은 함정):
1. `@scope (…) { }` 안의 규칙은 같은 우선순위의 바깥 규칙을 이깁니다. 덮어쓰는 규칙은 같은 scope 안에 두거나 우선순위를 올립니다.
2. 조상에 `transform`이 있으면 `position: fixed` 메뉴의 기준이 그 조상이 되어 엉뚱한 곳에 뜹니다. 손잡이 정렬에 transform 대신 margin을 씁니다.
3. 표 열 너비 손잡이가 셀 밖으로 2px 나가 있으면 표를 감싼 칸에 스크롤바가 생겼다 사라지며 행이 흔들립니다. 손잡이는 셀 안에 두고 감싸는 칸은 `overflow-y: hidden`으로 둡니다.
4. NodeView의 바깥 감싸개(`.react-renderer`)가 실제 블록이므로, 사진 나란히 놓기 같은 배치 규칙은 감싸개에 `:has()`로 겁니다.
- 키트 규칙 때문에 원본과 달라진 점: 색을 hex 대신 테마 변수로 저장합니다. Stone 테마의 색 토큰을 그대로 쓰면 색끼리 구분되지 않아 테마 색의 색상(hue)으로 값을 만들었습니다. 갈색은 테마에 없어 주황 색상을 낮은 채도로 씁니다. 메뉴·도구는 Astryx 부품(`Button`, `HStack`/`VStack`, `Kbd`, `Selector` 등)으로 그리고, 휴대폰 도구는 44px입니다.
- 원본에서 고친 결함: 포인터로 연 하위 메뉴가 누르면 닫히던 것, 블록 메뉴가 열린 채 Alt+↑/↓가 먹지 않던 것, 편집 전환만으로 "고친 내용 있음"이 되던 것.
- 키트에 넣지 않은 것: 토글·열 배치·수식·콜아웃·코드 강조, 댓글·버전 기록, 글꼴 설정 화면, 인쇄 화면.
- 확인: `npm run site:check`, `npm run site:test`(28개 파일, 349개 테스트 통과), `npm run site:build`, `npm test`가 통과했습니다. 가이드 사이트에서 1440 라이트·다크, 390 휴대폰으로 견본을 봤습니다.
- 범위: 실제 서버(올리기, 링크 미리보기, HTML 격리 응답), 터치 기기, 화면 낭독기, 2560 화면은 확인하지 않았습니다.
Loading
Loading