Skip to content
Open
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
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

.pnpm-store

58 changes: 58 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import eslintPluginPrettier from 'eslint-plugin-prettier';
import eslintConfigPrettier from 'eslint-config-prettier';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
prettier: eslintPluginPrettier,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
},
// Prettier 설정
{
plugins: {
prettier: eslintPluginPrettier, // Prettier 플러그인 로드
},
rules: {
...eslintConfigPrettier.rules,
'prettier/prettier': [
'error',
{
useTabs: false,
printWidth: 120,
tabWidth: 2,
singleQuote: true,
semi: true,
bracketSpacing: true,
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxBracketSameLine: false,
jsxSingleQuote: true,
quoteProps: 'as-needed',
requirePragma: false,
trailingComma: 'all',
arrowParens: 'always',
proseWrap: 'never',
endOfLine: 'auto',
},
], // Prettier 규칙을 ESLint 에러로 처리
},
},
);
36 changes: 18 additions & 18 deletions code.js → origin-code/code.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ figma.showUI(__html__, { width: 400, height: 400 });
function syncSelectionToUI() {
const selection = figma.currentPage.selection;
const count = selection.length;
const icCount = selection.filter(node => node.name.toLowerCase().includes('ic')).length;
const icCount = selection.filter((node) => node.name.toLowerCase().includes('ic')).length;
figma.ui.postMessage({ type: 'selection-changed', count, icCount });
}

Expand All @@ -21,36 +21,36 @@ async function exportSelectedNodesToSvg() {
if (selectedNodes.length === 0) {
figma.ui.postMessage({
type: 'error',
message: '내보내려는 아이콘을 선택해주세요.'
message: '내보내려는 아이콘을 선택해주세요.',
});
return;
}

const exports = [];

for (const node of selectedNodes) {
try {
const svg = await node.exportAsync({
format: 'SVG'
format: 'SVG',
});

const svgString = String.fromCharCode.apply(null, svg);

exports.push({
name: node.name,
svg: svgString
svg: svgString,
});
} catch (error) {
figma.ui.postMessage({
type: 'error',
message: `"${node.name}" 내보내기 실패: ${error.message}`
message: `"${node.name}" 내보내기 실패: ${error.message}`,
});
}
}

figma.ui.postMessage({
type: 'svg-exports',
data: exports
data: exports,
});
}

Expand All @@ -59,23 +59,23 @@ async function runCommand(command, cwd) {
try {
// 명령어 실행
const result = await figma.execCommand(command, cwd);

// 성공 응답 전송
figma.ui.postMessage({
type: 'command-result',
data: {
success: true,
result
}
result,
},
});
} catch (error) {
// 실패 응답 전송
figma.ui.postMessage({
type: 'command-result',
data: {
success: false,
error: error.message
}
error: error.message,
},
});
}
}
Expand All @@ -96,7 +96,7 @@ figma.ui.onmessage = async (msg) => {
const svgString = String.fromCharCode.apply(null, svg);
exports.push({
name: node.name,
data: svgString
data: svgString,
});
} catch (error) {
figma.notify(`${node.name} 내보내기 실패: ${error.message}`);
Expand All @@ -112,7 +112,7 @@ figma.ui.onmessage = async (msg) => {
const token = await figma.clientStorage.getAsync('githubToken');
figma.ui.postMessage({
type: 'token-loaded',
token: token
token: token,
});
} else if (msg.type === 'set-token') {
await figma.clientStorage.setAsync('githubToken', msg.token);
Expand All @@ -124,7 +124,7 @@ figma.ui.onmessage = async (msg) => {
figma.notify('선택된 아이콘이 없습니다.');
return;
}
const names = selection.map(node => node.name);
const names = selection.map((node) => node.name);
figma.ui.postMessage({ type: 'delete-names', data: names });
}
};
};
File renamed without changes.
44 changes: 44 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "figma-icon-automation",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"test": "pnpm tsc && pnpm build",
"format": "prettier --write .",
"tsc": "pnpm tsc:main && pnpm tsc:ui",
"tsc:main": "tsc --noEmit -p plugin-src",
"tsc:ui": "tsc --noEmit -p ui-src",
"tsc:watch": "concurrently -n widget,iframe \"pnpm tsc:main --watch --preserveWatchOutput\" \"pnpm tsc:ui --watch --preserveWatchOutput\"",
"build": "pnpm build:ui && pnpm build:main && cp ./manifest.json ./dist/manifest.json",
"build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js --minify",
"build:ui": "npx vite build --minify esbuild --emptyOutDir=false",
"build:watch": "concurrently -n widget,iframe \"pnpm build:main --watch\" \"pnpm build:ui --watch\"",
"dev": "concurrently -n tsc,build,vite 'pnpm:tsc:watch' 'pnpm:build:watch' 'vite'"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@figma/plugin-typings": "^1.113.0",
"@types/node": "^22.15.26",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"concurrently": "^9.1.2",
"esbuild": "^0.25.5",
"eslint": "^9.25.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5",
"vite-plugin-copy": "^0.1.6",
"vite-plugin-singlefile": "^2.2.0"
}
}
130 changes: 130 additions & 0 deletions plugin-src/code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
figma.showUI(__html__, { width: 400, height: 400 });

// UI에 현재 선택 상태를 동기화하는 함수
function syncSelectionToUI() {
const selection = figma.currentPage.selection;
const count = selection.length;
const icCount = selection.filter((node) => node.name.toLowerCase().includes('ic')).length;
figma.ui.postMessage({ type: 'selection-changed', count, icCount });
}

// selectionchange 이벤트에서 UI로 선택 개수 전달
figma.on('selectionchange', syncSelectionToUI);

// 플러그인 실행 시에도 동기화
syncSelectionToUI();

// 선택된 노드를 SVG로 내보내기
async function exportSelectedNodesToSvg() {
const selectedNodes = figma.currentPage.selection;

if (selectedNodes.length === 0) {
figma.ui.postMessage({
type: 'error',
message: '내보내려는 아이콘을 선택해주세요.',
});
return;
}

const exports = [];

for (const node of selectedNodes) {
try {
const svg = await node.exportAsync({
format: 'SVG',
});

const svgString = String.fromCharCode.apply(null, svg);

exports.push({
name: node.name,
svg: svgString,
});
} catch (error) {
figma.ui.postMessage({
type: 'error',
message: `"${node.name}" 내보내기 실패: ${error.message}`,
});
}
}

figma.ui.postMessage({
type: 'svg-exports',
data: exports,
});
}

// 터미널 명령어 실행
async function runCommand(command, cwd) {
try {
// 명령어 실행
const result = await figma.execCommand(command, cwd);

// 성공 응답 전송
figma.ui.postMessage({
type: 'command-result',
data: {
success: true,
result,
},
});
} catch (error) {
// 실패 응답 전송
figma.ui.postMessage({
type: 'command-result',
data: {
success: false,
error: error.message,
},
});
}
}

// 메세지 핸들러
figma.ui.onmessage = async (msg) => {
if (msg.type === 'export-svg') {
const selection = figma.currentPage.selection;
if (selection.length === 0) {
figma.notify('선택된 아이콘이 없습니다.');
return;
}

const exports = [];
for (const node of selection) {
try {
const svg = await node.exportAsync({ format: 'SVG' });
const svgString = String.fromCharCode.apply(null, svg);
exports.push({
name: node.name,
data: svgString,
});
} catch (error) {
figma.notify(`${node.name} 내보내기 실패: ${error.message}`);
}
}

figma.ui.postMessage({ type: 'svg-exports', data: exports });
} else if (msg.type === 'run-command') {
await runCommand(msg.data.command, msg.data.cwd);
} else if (msg.type === 'close') {
figma.closePlugin();
} else if (msg.type === 'get-token') {
const token = await figma.clientStorage.getAsync('githubToken');
figma.ui.postMessage({
type: 'token-loaded',
token: token,
});
} else if (msg.type === 'set-token') {
await figma.clientStorage.setAsync('githubToken', msg.token);
} else if (msg.type === 'delete-token') {
await figma.clientStorage.deleteAsync('githubToken');
} else if (msg.type === 'delete-svg') {
const selection = figma.currentPage.selection;
if (selection.length === 0) {
figma.notify('선택된 아이콘이 없습니다.');
return;
}
const names = selection.map((node) => node.name);
figma.ui.postMessage({ type: 'delete-names', data: names });
}
};
8 changes: 8 additions & 0 deletions plugin-src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["es6"],
"strict": true,
"typeRoots": ["../node_modules/@figma"]
}
}
Loading