

ํ๋ก ํธ์๋ ๊ฐ๋ฐ์ ํ๋ค ๋ณด๋ฉด ๋์์ด๋์์ ํ์ ์ ํ์์ ๋๋ค. ํ์ง๋ง ์ด ํ์ ๊ณผ์ ์์ ๋ฐ๋ณต์ ์ด๊ณ ๋นํจ์จ์ ์ธ ์์ ๋ค์ด ์กด์ฌํ ์ ์๋๋ฐ์.
๋ํ ํนํ ์คํํธ์ ์ฒ๋ผ ๋ฆฌ์์ค๊ฐ ํ์ ๋ ํ๊ฒฝ์์๋ "์ด๋ป๊ฒ ๋ ์ ๋ง๋ค๊น?" ๋ฟ๋ง ์๋๋ผ "์ด๋ป๊ฒ ๋ ์ ์ผํ ์ ์์๊น?" ๋ ์ค์ํ ๊ณ ๋ฏผ์ ๋๋ค. ๊ทธ๋ฐ ๋ฌธ์ ์์์์ ์ถ๋ฐํด, ์ด ๊ธ์์ ์ฒ๋ผ ์ฌ๋ฌ ๋ฐฉ๋ฒ๋ค์ ์๋ํด๋ณด๊ธฐ๋ ํ๋๋ฐ์.
์ด๋ฒ์ ์กฐ๊ธ ๋ ๋์์ด๋์์ ํ์ ์ ์ง์คํด์ ํ์ ๊ณผ์ ์์์ ๋นํจ์จ/๋ณ๋ชฉ์ ์ค์ด๊ธฐ ์ํ Figma ํ๋ฌ๊ทธ์ธ์ ๋ง๋ค์๊ณ ๊ทธ ๊ฒฝํ์ ๊ณต์ ํ๋ฉด ์๋์ ๊ฐ์ต๋๋ค.
์์ด์ฝ ์ปดํฌ๋ํธ ์ถ๊ฐ ํ๋ก์ธ์ค
๋์์ด๋์ ๋ฐ๋ณต ์์
๐ ๊ธฐ๋ฅ ๊ฐ์
๋์์ด๋๊ฐ Figma์์ SVG ์์ด์ฝ์ ์ ํํด GitHub์ ์๋์ผ๋ก PR์ ์์ฑํ๋ ํ๋ฌ๊ทธ์ธ
๐ ์ํฌํ๋ก์ฐ
SVG to React Component ๋ณํ
// SVG Export ๋ฐ ์ ๋ฆฌ
const svgBytes = await exportNode.exportAsync({
format: 'SVG',
svgOutlineText: true,
svgIdAttribute: false,
svgSimplifyStroke: true,
});
// ๋ถํ์ํ ์์ ์ ๊ฑฐ
const cleanSvgString = svgString
.replace(/<g[^>]*>/g, '')
.replace(/<\/g>/g, '')
.replace(/<defs>[\s\S]*?<\/defs>/g, '')
.replace(/clip-path="[^"]*"/g, '');
// React ์ปดํฌ๋ํธ ํ
ํ๋ฆฟ ์์ฑ
const reactComponent = `
import { Colors } from '../../colors';
import { Svg } from '../../utils/IconBase';
import { IconProps } from '../../utils/icon';
const ${componentName} = ({ width = 24, height = 24, fill = Colors.gray900, ...props }: IconProps) => {
return (
<Svg width={width} height={height} viewBox="${viewBox}" fill="none" {...props}>
<path d="${pathData}" fill={fill} />
</Svg>
);
};
export default ${componentName};
`;GitHub API๋ฅผ ํตํ ์๋ PR ์์ฑ
// 1. ๋ธ๋์น ์์ฑ
const createBranchRes = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/refs`,
{
method: 'POST',
headers: {
Authorization: `token ${githubToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha: baseSha }),
}
);
// 2. ํ์ผ ์
๋ก๋ ๋ฐ ์ปค๋ฐ
const treeRes = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/trees`,
{
method: 'POST',
body: JSON.stringify({
base_tree: baseSha,
tree: [
{
path: componentPath,
mode: '100644',
type: 'blob',
sha: compBlob.sha,
},
{ path: indexPath, mode: '100644', type: 'blob', sha: idxBlob.sha },
],
}),
}
);
// 3. PR ์์ฑ
const prRes = await fetch(
`https://api.github.com/repos/${owner}/${repo}/pulls`,
{
method: 'POST',
body: JSON.stringify({
title: `[๐ค] feat: add icon component ${pascalName}`,
head: branchName,
base: baseBranch,
body: `Add new icon component: ${pascalName}`,
}),
}
);ํผ๊ทธ๋ง์ ์งํ ์ํ ์ ๋ฐ์ดํธ
function notifyUI(msg: string) {
figma.ui.postMessage({ type: 'github-upload-status', message: msg });
}
// ์งํ ๋จ๊ณ๋ณ ์ํ ์
๋ฐ์ดํธ
const steps = [
'๋ธ๋์น ์ ๋ณด ์กฐํ ์ค...',
'์ ๋ธ๋์น ์์ฑ ์ค...',
'index.tsx ์ฝ๊ธฐ ์ค...',
'tree ์์ฑ ์ค...',
'์ปค๋ฐ ์์ฑ ์ค...',
'๋ธ๋์น(ref) ์
๋ฐ์ดํธ ์ค...',
];
steps.forEach((step, index) => {
notifyUI(`${index + 1}/${steps.length}: ${step}`);
});๐ ๊ธฐ๋ฅ ๊ฐ์
์ ํํ ํ ์คํธ๋ฅผ OpenAI API๋ฅผ ํ์ฉํด ๊ฒํ ํ์ฌ UX writing ๊ฐ์ ์ฌํญ์ ์ ์ํ๋ ํ๋ฌ๊ทธ์ธ
๐ ์ํฌํ๋ก์ฐ
(Figma)ํ ์คํธ ๋ ธ๋ ์ถ์ถ
function getAllTextNodes(node: SceneNode | PageNode): TextNode[] {
let result: TextNode[] = [];
// ์จ๊ฒจ์ง ์์ ์ ์ธ
if ('visible' in node && node.visible === false) return result;
if (node.type === 'TEXT') {
result.push(node);
} else if ('children' in node) {
for (const child of node.children) {
result = result.concat(getAllTextNodes(child));
}
}
return result;
}(Figma)ํ ์คํธ ๊ฒ์ฆ์ ์ํด API์ ์์ฒญ
// UI์์ ๊ฒ์ฆ ์์ฒญ (ui.tsx)
async function checkSpelling(text: string, promptString?: string): Promise<SpellCheckResponse> {
return new Promise((resolve) => {
function handler(event: MessageEvent) {
const msg = event.data.pluginMessage;
if (msg?.type === 'SPELL_CHECK_RESULT') {
window.removeEventListener('message', handler);
resolve(msg.result);
}
}
window.addEventListener('message', handler);
parent.postMessage({
pluginMessage: { type: 'PROXY_SPELL_CHECK', text, promptString }
}, '*');
});
}
// API ํธ์ถ (code.ts)
if (msg.type === 'PROXY_SPELL_CHECK') {
const { text, promptString } = msg;
try {
const res = await fetch(
API_URL,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cors-token': process.env.REACT_APP_CORS_TOKEN,
},
body: JSON.stringify({
text,
prompt: promptString || '',
}),
}
);
const result = await res.json();
figma.ui.postMessage({ type: 'SPELL_CHECK_RESULT', result });
} catch (e) {
figma.ui.postMessage({
type: 'SPELL_CHECK_RESULT',
result: { error: true, message: e?.message || String(e) },
});
}
}(API)๋ณด์ ๋ฐ ์ ๊ทผ ์ ์ด
// Figma ์ ์ฉ ์ ๊ทผ ์ ํ
function isFigmaRequest(req: NextApiRequest): boolean {
const userAgent = req.headers['user-agent'] || '';
return userAgent.includes('Figma');
}
// ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ
const allowedToken = process.env.FIGMA_CORS_TOKEN;
const requestToken =
req.headers['x-cors-token'] || req.headers['authorization'];
if (!requestToken || requestToken !== allowedToken) {
return res.status(401).json({
error: {
message: 'Unauthorized. Valid token required.',
type: 'unauthorized',
},
});
}(API) CORS ์ ์ด
[์ฐธ๊ณ ์ฌํญ]
function setCorsHeaders(req: NextApiRequest, res: NextApiResponse) {
const requestToken =
req.headers['x-cors-token'] || req.headers['authorization'];
// ํ ํฐ์ด ์ผ์นํ ๋๋ง CORS ํ์ฉ
if (
req.method === 'OPTIONS' ||
(requestToken && allowedToken && requestToken === allowedToken)
) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, OPTIONS'
);
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, x-cors-token'
);
}
}(API)OpenAI API ํตํด ๊ฒ์ฆ
async function checkTexts(texts: string, customPrompt: string | undefined, openaiApiKey: string): Promise<string> {
const defaultPrompt = `๋ค์ ํ๊ตญ์ด ํ
์คํธ๋ค์ ๋ง์ถค๋ฒ๊ณผ ๋ฌธ๋ฒ์ ๊ฒํ ํ๊ณ ์์ ํด์ฃผ์ธ์.`;
const prompt = customPrompt || defaultPrompt;
const openaiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${openaiApiKey}`,
},
body: JSON.stringify({
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: prompt },
{ role: 'user', content: `๋ค์ ๋ฌธ์ฅ๋ค์ ๋ค์ ์์ฑํด ์ฃผ์ธ์:\n${texts}` },
],
temperature: 0.1, // ์ผ๊ด์ฑ์ ์ํ ๋ฎ์ ์จ๋ ์ค์
max_tokens: 4096,
}),
});
const openaiData = await openaiResponse.json();
return openaiData.choices[0]?.message?.content;
}1.์์ด์ฝ ์ปดํฌ๋ํธ PR์์ฑ