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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webcraft",
"version": "0.7.2",
"version": "0.8.0",
"description": "웹 플랫폼 개발 지침(프론트·디자인 시스템·백엔드·API·DB·아키텍처)과 프로젝트 연결",
"author": {
"name": "dev-goraebap"
Expand Down
23 changes: 21 additions & 2 deletions plugins/webcraft/dist/claude/plugins/webcraft/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,29 @@ codex plugin add webcraft@dev-goraebap

## 갱신

플러그인을 갱신하면 연결된 모든 프로젝트가 새 지침을 따릅니다. 프로젝트 AGENTS.md의 최소 판보다 낮은 플러그인이 설치돼 있으면 먼저 갱신합니다. 커밋·푸시·외부 전송은 이 플러그인 설치만으로 허용되지 않습니다.
플러그인을 갱신하면 연결된 모든 프로젝트가 새 지침을 따릅니다. 프로젝트 AGENTS.md의 최소 판보다 낮은 플러그인이 설치돼 있거나 지침을 읽을 수 없으면 에이전트는 추측으로 진행하지 않고, 작업 상태를 `~/.goraebap/handoff/` 아래 이어받기 문서로 남긴 뒤 설치·갱신 방법과 새 세션에서 읽을 문서 경로를 안내합니다. 커밋·푸시·외부 전송은 이 플러그인 설치만으로 허용되지 않습니다.

Claude Code에서 자동 갱신은 공식 마켓플레이스가 아니면 기본값이 꺼짐입니다. 사용자가 `/plugin` → Marketplaces → `dev-goraebap` → Enable auto-update로 켜거나, 조직 관리자가 managed settings의 `extraKnownMarketplaces` 항목에 `"autoUpdate": true`를 넣습니다. 자동 갱신은 세션 시작 때가 아니라 첫 메시지 뒤 최대 10분 안에 돌며, 실행 중인 세션은 옛 판을 유지하고 `/reload-plugins`를 입력하거나 다음에 실행할 때 새 판이 적용됩니다.

Claude Code 배포본에는 세션 시작 훅(`hooks/hooks.json`, `hooks/update-check.mjs`)이 들어갑니다. 새로 시작할 때만(`startup`) 돌며, 하루 한 번 원본 저장소의 최신 판을 받아 `~/.goraebap/cache/webcraft-latest.json`에 기억합니다. 설치된 판보다 높으면 `Webcraft 새 판이 나왔습니다(0.8.0 → 0.8.1). 에이전트에게 "Webcraft 업데이트해 줘"라고 하면 갱신합니다.` 한 줄을 보이고, 에이전트가 첫 요청 전에 한 번 갱신을 제안합니다. 동의하면 아래 수동 갱신 명령을 실행한 뒤 `/reload-plugins`를 안내하고, 거절하면 다시 묻지 않습니다. 네트워크 실패나 Node가 없는 경우에는 조용히 넘어갑니다. Codex 배포본에는 훅이 없습니다.

직접 갱신할 때는 다음 명령을 실행하고, Claude Code에서는 `/reload-plugins`를 입력하거나 새 세션을 엽니다.

Claude Code:

```text
claude plugin marketplace update dev-goraebap
claude plugin update webcraft@dev-goraebap
```

Codex:

```text
codex plugin marketplace upgrade dev-goraebap
```

## 이전 이름에서 전환

이전 식별자는 `goraebap-workflow@goraebap-workflow`입니다. 새 이름은 `webcraft@dev-goraebap`이며 입구 스킬 이름은 `goraebap-mode`를 유지합니다. 기존 프로젝트는 새 플러그인 설치를 확인한 뒤 AGENTS.md 연결을 새 이름과 최소 판 `0.7.1`으로 바꿉니다. 기존 사본에서 바꾼 규칙과 제품 전용 스킬은 보존합니다. 옛 설치를 제거하는 작업은 새 설치 확인 후 사용자가 허용한 범위에서 수행합니다.

역할별 모델 설정 `~/.claude/rules/goraebap-models.md`와 로컬 제안함 `~/.goraebap/proposals/`는 기존 경로를 유지합니다.
지침 제안은 Webcraft 원본 저장소(`dev-goraebap/dev.goraebap`)의 이슈로 받습니다. 역할별 모델 설정 `~/.claude/rules/goraebap-models.md`는 기존 경로를 유지하며, `~/.goraebap/`에는 최신 판 확인 캐시(`cache/`), 이어받기 문서(`handoff/`), 직접 올리지 못한 이슈 문서(`issues/`)가 남습니다.
17 changes: 17 additions & 0 deletions plugins/webcraft/dist/claude/plugins/webcraft/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"description": "Webcraft 새 판이 있으면 세션을 시작할 때 알리고 갱신을 제안합니다(하루 한 번 확인).",
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/update-check.mjs\"",
"timeout": 10
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Webcraft 새 판 알림(세션 시작 훅). 설치된 판과 원본 저장소의 최신 판을 하루 한 번 비교해, 낮으면 사용자에게 한 줄로
// 알리고 에이전트에게는 갱신을 제안·실행하는 방법을 맥락으로 준다. 네트워크·파일 오류는 조용히 넘긴다: 알림이 실패해도
// 세션 시작을 막거나 경고를 남기지 않는다.
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

export const LATEST_URL = 'https://raw.githubusercontent.com/dev-goraebap/dev.goraebap/main/plugins/webcraft/package.json';
const DAY = 24 * 60 * 60 * 1000;

/** 점으로 나눈 숫자 판을 비교한다. a가 b보다 높으면 양수. 숫자가 아닌 꼬리(-rc 등)는 무시한다. */
export function compareVersions(a, b) {
const parts = value => String(value).split('-')[0].split('.').map(part => Number.parseInt(part, 10) || 0);
const [x, y] = [parts(a), parts(b)];
for (let i = 0; i < Math.max(x.length, y.length); i += 1) {
const diff = (x[i] ?? 0) - (y[i] ?? 0);
if (diff) return diff;
}
return 0;
}

/** 새 판이 있으면 훅 출력(JSON)을, 없으면 null을 돌려준다. */
export function notice(installed, latest) {
if (!installed || !latest || compareVersions(latest, installed) <= 0) return null;
return {
systemMessage: `Webcraft 새 판이 나왔습니다(${installed} → ${latest}). 에이전트에게 "Webcraft 업데이트해 줘"라고 하면 갱신합니다.`,
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: [
`Webcraft 플러그인의 새 판이 있습니다(설치된 판 ${installed}, 최신 판 ${latest}).`,
'사용자의 첫 요청을 처리하기 전에 한 번만 갱신을 제안합니다. 사용자가 동의하면 아래 두 명령을 차례로 실행합니다.',
'`claude plugin marketplace update dev-goraebap`',
'`claude plugin update webcraft@dev-goraebap`',
'성공하면 `/reload-plugins`를 입력하면 바로 적용되고, 아니면 다음 세션부터 적용된다고 안내합니다.',
'작업 중이라 지금 갱신하지 않기로 하면 다시 묻지 않고 하던 작업을 이어갑니다.',
].join('\n'),
},
};
}

async function readJson(file) {
return JSON.parse(await fs.readFile(file, 'utf8'));
}

/** 하루 안에 확인한 최신 판이 캐시에 있으면 그것을, 없으면 원본에서 받아 캐시에 남긴다. 실패하면 null. */
async function latestVersion(cacheFile, now = Date.now()) {
const cached = await readJson(cacheFile).catch(() => null);
if (cached?.latest && now - cached.checkedAt < DAY) return cached.latest;
try {
const response = await fetch(LATEST_URL, { signal: AbortSignal.timeout(3000) });
if (!response.ok) return cached?.latest ?? null;
const { version } = await response.json();
await fs.mkdir(path.dirname(cacheFile), { recursive: true });
await fs.writeFile(cacheFile, JSON.stringify({ latest: version, checkedAt: now }));
return version;
} catch {
return cached?.latest ?? null;
}
}

async function main() {
const root = process.env.CLAUDE_PLUGIN_ROOT ?? fileURLToPath(new URL('../', import.meta.url));
const installed = (await readJson(path.join(root, '.claude-plugin', 'plugin.json')).catch(() => null))?.version;
const latest = await latestVersion(path.join(os.homedir(), '.goraebap', 'cache', 'webcraft-latest.json'));
const output = notice(installed, latest);
if (output) process.stdout.write(`${JSON.stringify(output)}\n`);
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main().catch(() => undefined);
}
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.7.2",
"version": "0.8.0",
"private": true,
"type": "module"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: 웹 플랫폼의 HTTP 계약, 오류 응답, 인증·권한, 첨부
- 인증된 사용자와 자원 접근 권한을 서버에서 검사합니다. 숨겨야 하는 대상의 존재 노출 정책을 프로젝트에서 정하고 REST·MCP·다운로드에 동일하게 적용합니다.
- PATCH의 생략·null·값 전달 의미를 구분합니다. 업무 날짜·지역 시각·절대 시각을 구별하고 절대 시각은 UTC로 전달합니다. 업무 시간대는 프로젝트가 정합니다.
- 목록은 서버 필터·안정된 정렬·페이지/커서 계약을 명시합니다. count와 목록이 같은 조건을 쓰는지 확인합니다.
- 업로드는 임시 파일과 업무 자원 연결의 소유권·만료·권한을 구분합니다. 파일명·확장자를 신뢰하지 않고 크기·타입·경로를 검증합니다. 파일을 읽는 경로에서도 권한을 확인합니다.
- 업로드는 임시 파일과 업무 자원 연결의 소유권·만료·권한을 구분합니다. 파일명·확장자를 신뢰하지 않고 크기·타입·경로를 검증합니다. 파일을 읽는 경로에서도 권한을 확인합니다. 사진 업로드가 선택 값인 대표색·가로·세로를 함께 보내면 파일과 함께 저장하고 첨부 정보로 돌려줍니다.
- 변경은 버전 충돌과 명령 재시도 시 중복 효과를 검토합니다. 브라우저 캐시 무효화가 동시 편집 충돌을 해결한다고 가정하지 않습니다.

완료 시 성공 응답·실패 응답·권한·영향받는 호출자와 생성 타입을 확인합니다.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

주소가 없는 시트는 브라우저의 닫기 요청(CloseWatcher와 같은 장치)으로 안드로이드 뒤로가기와 Esc에 닫힙니다. 네이티브 `<dialog>`를 `showModal()`로 여는 시트(Astryx BottomSheet의 기본, 배경 막 있음)는 지원 브라우저에서 이 요청을 `cancel`로 이미 받으므로 따로 등록하지 않습니다. 겹쳐 등록하면 한 번에 두 번 닫힙니다. 직접 만든 시트나 배경 막 없는 시트만 CloseWatcher를 등록합니다. 외부 제어 API가 시트 열림을 주소 쿼리에 저장하라는 규칙은 아니며, 열림 상태를 쿼리에 두지 않습니다. 짧은 조작마다 방문 기록이 쌓이고 새로고침하면 시트가 다시 열리기 때문입니다. 닫기 요청을 지원하지 않는 브라우저(iOS 사파리 등)에서는 뒤로가기가 이전 페이지로 갈 수 있으므로, 시트마다 보이는 닫기 버튼을 두고 잃으면 아까운 입력은 시트에 담지 않습니다. 입력이 길면 주소가 있는 화면으로 엽니다. 시트는 쌓지 않습니다.

팝오버와 바텀시트는 열렸을 때만 내용을 만듭니다. 행마다 붙는 메뉴의 확인 대화상자는 그 동작을 고를 때만 만듭니다. 닫힌 메뉴 내용과 행마다 미리 만든 확인 대화상자(`<dialog>`)를 화면 이동 때 한꺼번에 정리하면 메인 스레드가 멈춥니다. 행마다 권한 판단에 필요한 데이터는 메뉴에 포인터나 초점이 갈 때 미리 읽습니다.

## 선택지 목록

작은 고정 선택지는 그대로 나열하고, 8개를 넘으면 위에 검색 칸을 두어 초점을 줍니다. 검색 전에는 처음 8개와 이미 고른 항목, 검색하면 최대 50개, 나머지는 개수로 알립니다. 전부 렌더링하지 않습니다. 이 수치는 서버 한도와 별개이며 프로젝트가 조절합니다. 하나 고르기는 고른 뒤 닫고 끝에 체크, 여럿 고르기는 열린 채 앞에 상자를 둡니다. 휴대폰의 동작·선택·시간 목록은 최소 48px 행에 15px 글자·20px 아이콘을 씁니다. 보조 정보는 13px입니다. 메뉴를 시트로 옮기는 것만으로 데스크톱 밀도가 자동으로 바뀐다고 가정하지 않습니다.
Expand All @@ -44,6 +46,6 @@

## 본문 칸

제목·본문 양식의 본문은 프로젝트가 채택한 리치에디터를 테두리와 서식 도구 막대 없이 종이처럼 둡니다. 서식은 에디터의 단축키·입력 규칙·선택 도구로 합니다. 본문 글자는 화면 글자(13px)보다 한 단계 큰 본문(15px, `--text-large-size`)이고 제목은 `--text-heading-2-size`입니다. 쓰는 칸과 그 글을 읽는 상세 본문은 같은 크기로 둡니다. 키트의 Tiptap 예시는 자리를 보이기 위한 것이며 특정 에디터를 기본 의존성으로 복사하지 않습니다.
제목·본문 양식의 본문은 프로젝트가 채택한 리치에디터를 테두리와 서식 도구 막대 없이 종이처럼 둡니다. 서식은 에디터의 단축키·입력 규칙·선택 도구로 합니다. 앱 화면의 본문 토큰(13px, 15px `--text-large-size`)은 목록·도구가 빽빽한 화면용으로 남기고, 긴 글을 읽는 본문 칸과 상세 본문 글자는 데스크톱·휴대폰 모두 16px, 줄 높이 1.7이며 제목은 `--text-heading-2-size`입니다. 테마가 문단에 글자 크기와 줄 높이를 직접 주므로 문단·목록·인용이 크기와 줄 높이를 함께 상속하게 합니다. 쓰는 칸과 그 글을 읽는 상세 본문은 같은 크기로 둡니다. 키트의 Tiptap 예시는 자리를 보이기 위한 것이며 특정 에디터를 기본 의존성으로 복사하지 않습니다.

구현이 없는 조합을 메뉴에 빈 항목으로 만들지 않습니다. 제품 고유 조합(아이콘 표시 고르기, 전역 검색 팔레트, 프로필 패널)은 프로젝트가 소유합니다.
Loading
Loading