feat: add flower db images
* feat: add options/map flow, dev seed, and artwork fixes Options page, Kakao map with florist order message, dev tooling, and create/message dummy gating — without secrets in .env.example. Co-authored-by: Cursor <cursoragent@cursor.com> * with generating page + art work * with flower images --------- Co-authored-by: 이지은 <ijieun@ijieun-ui-MacBookPro.local> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
172
scripts/generate-flower-catalog.js
Normal file
172
scripts/generate-flower-catalog.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* flowerDB 카탈로그 이미지 batch 생성 (1회 실행 → static/flowers/{id}.png)
|
||||
*
|
||||
* 사용:
|
||||
* npm run generate:flowers -- --dry-run
|
||||
* npm run generate:flowers -- --missing-only
|
||||
* npm run generate:flowers -- --ids 7,14,18
|
||||
* npm run generate:flowers -- --force --ids 14
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import OpenAI from 'openai';
|
||||
import { flowerDB } from '../src/lib/server/flowerFlow/flowerDB.js';
|
||||
import {
|
||||
buildFlowerCardPrompt,
|
||||
getPromptNameForFlower
|
||||
} from '../src/lib/flowerFlow/flowerCatalogPrompt.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..');
|
||||
const OUT_DIR = join(ROOT, 'static', 'flowers');
|
||||
|
||||
/** @param {string} flag */
|
||||
function hasFlag(flag) {
|
||||
return process.argv.includes(flag);
|
||||
}
|
||||
|
||||
/** @param {string} flag */
|
||||
function readFlagValue(flag) {
|
||||
const index = process.argv.indexOf(flag);
|
||||
if (index === -1) return null;
|
||||
return process.argv[index + 1] ?? null;
|
||||
}
|
||||
|
||||
function loadEnvFile() {
|
||||
const envPath = join(ROOT, '.env');
|
||||
if (!existsSync(envPath)) return;
|
||||
|
||||
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
|
||||
const separator = trimmed.indexOf('=');
|
||||
if (separator === -1) continue;
|
||||
|
||||
const key = trimmed.slice(0, separator).trim();
|
||||
const value = trimmed.slice(separator + 1).trim();
|
||||
// .env 값을 항상 우선 (터미널에 남은 옛 OPENAI_API_KEY 덮어씀)
|
||||
if (key) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} value */
|
||||
function parseIdList(value) {
|
||||
return value
|
||||
.split(',')
|
||||
.map((part) => Number(part.trim()))
|
||||
.filter((id) => Number.isInteger(id) && id > 0);
|
||||
}
|
||||
|
||||
/** @param {number} ms */
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} prompt
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async function generateFlowerPng(prompt) {
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY is not configured (.env)');
|
||||
}
|
||||
|
||||
const size = process.env.OPENAI_IMAGE_CATALOG_SIZE || '1024x1536';
|
||||
const quality = process.env.OPENAI_IMAGE_CATALOG_QUALITY || 'low';
|
||||
|
||||
const client = new OpenAI({ apiKey });
|
||||
const response = await client.images.generate({
|
||||
model: process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1',
|
||||
prompt,
|
||||
size,
|
||||
quality,
|
||||
n: 1
|
||||
});
|
||||
|
||||
const image = response.data?.[0];
|
||||
if (image?.b64_json) {
|
||||
return Buffer.from(image.b64_json, 'base64');
|
||||
}
|
||||
|
||||
if (image?.url) {
|
||||
const imageResponse = await fetch(image.url);
|
||||
return Buffer.from(await imageResponse.arrayBuffer());
|
||||
}
|
||||
|
||||
throw new Error('OpenAI image model did not return image data');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
loadEnvFile();
|
||||
|
||||
const dryRun = hasFlag('--dry-run');
|
||||
const force = hasFlag('--force');
|
||||
const missingOnly = hasFlag('--missing-only');
|
||||
const delayMs = Number(readFlagValue('--delay') ?? 2000);
|
||||
const idsArg = readFlagValue('--ids');
|
||||
|
||||
/** @type {typeof flowerDB} */
|
||||
let targets = [...flowerDB];
|
||||
|
||||
if (idsArg) {
|
||||
const ids = new Set(parseIdList(idsArg));
|
||||
targets = targets.filter((flower) => ids.has(flower.id));
|
||||
}
|
||||
|
||||
if (missingOnly) {
|
||||
targets = targets.filter((flower) => !existsSync(join(OUT_DIR, `${flower.id}.png`)));
|
||||
}
|
||||
|
||||
if (targets.length === 0) {
|
||||
console.log('생성할 꽃이 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
const size = process.env.OPENAI_IMAGE_CATALOG_SIZE || '1024x1536';
|
||||
const quality = process.env.OPENAI_IMAGE_CATALOG_QUALITY || 'low';
|
||||
console.log(`대상: ${targets.length}종 · ${size} · quality=${quality}${dryRun ? ' (dry-run)' : ''}`);
|
||||
|
||||
for (const flower of targets) {
|
||||
const outPath = join(OUT_DIR, `${flower.id}.png`);
|
||||
const promptName = getPromptNameForFlower(flower);
|
||||
const prompt = buildFlowerCardPrompt(promptName);
|
||||
|
||||
if (existsSync(outPath) && !force) {
|
||||
console.log(`skip id=${flower.id} ${flower.name} (already exists)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`\nid=${flower.id} ${flower.name}`);
|
||||
console.log(`promptName: ${promptName}`);
|
||||
console.log(`prompt: ${prompt}`);
|
||||
|
||||
if (dryRun) continue;
|
||||
|
||||
try {
|
||||
const bytes = await generateFlowerPng(prompt);
|
||||
writeFileSync(outPath, bytes);
|
||||
console.log(`saved → static/flowers/${flower.id}.png`);
|
||||
} catch (err) {
|
||||
console.error(`failed id=${flower.id}:`, err instanceof Error ? err.message : err);
|
||||
}
|
||||
|
||||
if (delayMs > 0) {
|
||||
await wait(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n완료.');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user