How this skill is triggered — by the user, by Claude, or both
Slash command
/moodboard-plugin:moodboard <키워드1> <키워드2> ... [장수 (기본값: 30)]<키워드1> <키워드2> ... [장수 (기본값: 30)]This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
`$ARGUMENTS`에서 키워드와 장수를 파싱해 Pinterest에서 이미지를 수집하고 `~/Downloads/moodboard_<키워드>/` 에 저장한 뒤 Finder로 엽니다.
$ARGUMENTS에서 키워드와 장수를 파싱해 Pinterest에서 이미지를 수집하고 ~/Downloads/moodboard_<키워드>/ 에 저장한 뒤 Finder로 엽니다.
$ARGUMENTS의 마지막 토큰이 숫자면 장수, 나머지는 모두 키워드입니다.
미니멀 카페 30 → 키워드=["미니멀","카페"], 장수=30빈티지 인테리어 → 키워드=["빈티지","인테리어"], 장수=30 (기본값)아래 Python 코드를 Bash로 실행해 키워드와 장수를 파싱하고 /tmp/moodboard_args.json에 저장합니다.
import json, sys, os
raw = "$ARGUMENTS".strip().split()
if raw and raw[-1].isdigit():
count = int(raw[-1])
keywords = raw[:-1]
else:
count = 30
keywords = raw
if not keywords:
print("ERROR: 키워드를 입력해주세요. 예) /moodboard 미니멀 카페 30")
sys.exit(1)
query = " ".join(keywords)
folder_name = "moodboard_" + "_".join(keywords)
save_dir = os.path.join(os.path.expanduser("~/Downloads"), folder_name)
data = {"keywords": keywords, "query": query, "count": count, "save_dir": save_dir, "folder_name": folder_name}
with open('/tmp/moodboard_args.json', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False)
print(f"키워드: {', '.join(keywords)}")
print(f"수집 장수: {count}장")
print(f"저장 위치: {save_dir}")
파싱 결과를 사용자에게 보여주고 진행 여부를 확인합니다.
/tmp/moodboard_scraper.mjs 파일을 Write 툴로 작성합니다.
파일 경로: /tmp/moodboard_scraper.mjs
내용:
import { chromium } from 'playwright';
import fs from 'fs';
import path from 'path';
import https from 'https';
import http from 'http';
const args = JSON.parse(fs.readFileSync('/tmp/moodboard_args.json', 'utf8'));
const { query, count, save_dir } = args;
if (!fs.existsSync(save_dir)) fs.mkdirSync(save_dir, { recursive: true });
const encoded = encodeURIComponent(query);
const SEARCH_URL = `https://kr.pinterest.com/search/pins/?q=${encoded}&rs=typed`;
function downloadImage(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
const protocol = url.startsWith('https') ? https : http;
protocol.get(url, { rejectUnauthorized: false }, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
file.close();
downloadImage(res.headers.location, dest).then(resolve).catch(reject);
return;
}
res.pipe(file);
file.on('finish', () => { file.close(); resolve(); });
}).on('error', (err) => { fs.unlink(dest, () => {}); reject(err); });
});
}
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
console.log(`검색: "${query}" — ${count}장 수집 시작`);
await page.goto(SEARCH_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(3000);
const collected = new Set();
const collectImages = async () => {
const urls = await page.evaluate(() => {
return Array.from(document.querySelectorAll('img[src*="pinimg.com"]'))
.map(img => img.src)
.filter(src =>
src.includes('pinimg.com') &&
!src.includes('75x75') &&
!src.includes('30x30') &&
!src.includes('avatar') &&
!src.includes('user')
)
.map(src => src.replace(/\/\d+x(\d+)?\//, '/736x/'));
});
urls.forEach(u => collected.add(u));
};
let scrollCount = 0;
while (collected.size < count && scrollCount < 25) {
await collectImages();
process.stdout.write(`\r스크롤 ${scrollCount + 1}회 — 수집: ${collected.size}개`);
await page.evaluate(() => window.scrollBy(0, 1800));
await page.waitForTimeout(1800);
scrollCount++;
}
await collectImages();
console.log(`\n총 ${Math.min(collected.size, count)}장 다운로드 시작 → ${save_dir}`);
const urls = [...collected].slice(0, count);
let downloaded = 0;
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
const ext = (url.split('.').pop() || 'jpg').split('?')[0];
const filename = `${String(i + 1).padStart(2, '0')}_${args.keywords.join('_')}.${ext}`;
const dest = path.join(save_dir, filename);
try {
await downloadImage(url, dest);
downloaded++;
console.log(`[${downloaded}/${urls.length}] ${filename}`);
} catch (e) {
console.log(`[오류] ${url}`);
}
}
await browser.close();
console.log(`\n완료: ${downloaded}장 저장 → ${save_dir}`);
process.exit(downloaded >= Math.floor(count * 0.7) ? 0 : 1);
})();
~/playwright-tests 에서 아래 명령을 실행합니다.
cp /tmp/moodboard_scraper.mjs ~/playwright-tests/moodboard_scraper.mjs && cd ~/playwright-tests && NODE_TLS_REJECT_UNAUTHORIZED=0 node moodboard_scraper.mjs
open "$(python3 -c "import json; print(json.load(open('/tmp/moodboard_args.json'))['save_dir'])")"
import json, os
with open('/tmp/moodboard_args.json', encoding='utf-8') as f:
args = json.load(f)
save_dir = args['save_dir']
files = [f for f in os.listdir(save_dir) if not f.startswith('.')] if os.path.exists(save_dir) else []
print(f"=== 무드보드 완료 ===")
print(f"키워드: {', '.join(args['keywords'])}")
print(f"저장 위치: {save_dir}")
print(f"저장된 이미지: {len(files)}장")
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub juhee815/test --plugin moodboard-plugin