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:
Chaewon Lee
2026-06-14 15:05:00 +09:00
committed by GitHub
parent 80b84bd2ed
commit b50f57a6d6
99 changed files with 253 additions and 1 deletions

View File

@@ -8,7 +8,11 @@ GEMINI_TEXT_MODEL=gemini-2.5-flash-lite
IMAGE_PROVIDER=openai
OPENAI_API_KEY=your_openai_api_key_here
OPENAI_IMAGE_MODEL=gpt-image-1
# Bouquet preview (generating flow)
OPENAI_IMAGE_SIZE=1024x1024
# Flower catalog batch (scripts/generate-flower-catalog.js) — portrait cards
OPENAI_IMAGE_CATALOG_SIZE=1024x1536
OPENAI_IMAGE_CATALOG_QUALITY=low
GEMINI_IMAGE_MODEL=gemini-3.1-flash-image
# Kakao REST API (shop search for /map)
@@ -25,3 +29,9 @@ SUPABASE_STORAGE_BUCKET=flower-bouquets
# Dev seed button: shown only when `npm run dev` (production build hides it).
# To mute during local dev, set DEV_SEED_MUTED = true in DevSeedButton.svelte.
# Replace static/dev/bouquet-{s,m,l}.jpg with real photos for richer UI previews.
# Flower catalog (result cards) — one-time batch, not per user request:
# npm run generate:flowers -- --dry-run
# npm run generate:flowers -- --missing-only
# npm run generate:flowers -- --ids 7,14
# Output: static/flowers/{flowerDB.id}.png

View File

@@ -9,7 +9,8 @@
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
"format": "prettier --write .",
"generate:flowers": "node scripts/generate-flower-catalog.js"
},
"devDependencies": {
"@eslint/compat": "^2.0.4",

View 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);
});

View File

@@ -0,0 +1,49 @@
/** OpenAI flower card batch — 프롬프트 이름·템플릿 (flowerDB 레코드는 수정하지 않음) */
/** @type {Record<number, string>} id → 프롬프트에 넣을 영문 꽃 이름 */
export const PROMPT_NAME_OVERRIDES = {
33: 'red spider lily',
36: 'bird of paradise flower',
40: 'wax flower',
41: 'caspia statice',
47: 'craspedia billy balls',
49: "queen anne's lace",
50: 'nigella love-in-a-mist',
60: 'statice limonium',
62: 'strawflower helichrysum',
64: 'chinese lantern physalis',
65: 'globe amaranth gomphrena',
74: 'pussy willow branch',
80: 'foxtail millet stem',
83: 'silver grass miscanthus'
};
/**
* DB display name → 프롬프트용 이름 (괄호 앞, lowercase)
* @param {string} name
*/
export function normalizeFlowerPromptName(name) {
const primary = name.split('(')[0].trim();
return primary.toLowerCase();
}
/**
* @param {{ id: number, name: string }} flower
*/
export function getPromptNameForFlower(flower) {
return PROMPT_NAME_OVERRIDES[flower.id] ?? normalizeFlowerPromptName(flower.name);
}
/**
* @param {string} flowerName — getPromptNameForFlower 결과
*/
export function buildFlowerCardPrompt(flowerName) {
return (
`A single ${flowerName} flower stem, isolated object, transparent background, ` +
`realistic botanical style, front-facing, centered composition, ` +
`full stem visible from base to bloom, flower head in upper third of frame, ` +
`stem centered vertically, consistent catalog framing for all species, ` +
`no vase, no bouquet, no hand, no text, soft natural lighting, consistent scale, ` +
`PNG asset for UI card`
);
}

View File

@@ -0,0 +1,13 @@
/** flowerDB id → result 꽃 카드용 정적 이미지 경로 (런타임 AI 생성 없음) */
export const FLOWER_IMAGE_BASE = '/flowers';
export const FLOWER_IMAGE_PLACEHOLDER = `${FLOWER_IMAGE_BASE}/placeholder.svg`;
/** @param {number} id flowerDB id (193) */
export function getFlowerImageSrc(id) {
if (!Number.isFinite(id) || id < 1) {
return FLOWER_IMAGE_PLACEHOLDER;
}
return `${FLOWER_IMAGE_BASE}/${id}.png`;
}

BIN
static/flowers/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
static/flowers/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
static/flowers/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
static/flowers/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
static/flowers/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/17.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
static/flowers/18.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
static/flowers/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/21.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/22.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/23.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
static/flowers/24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/25.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
static/flowers/27.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/28.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
static/flowers/29.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
static/flowers/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/30.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/31.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/33.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
static/flowers/34.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/35.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
static/flowers/36.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/37.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/38.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/39.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
static/flowers/40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/41.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/42.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/43.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/44.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/45.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/46.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/47.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/49.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/50.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/51.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/52.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/53.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
static/flowers/54.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/55.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
static/flowers/56.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/57.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/58.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/59.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
static/flowers/60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/61.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/62.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/63.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/65.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/66.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/67.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/68.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
static/flowers/69.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/70.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/71.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
static/flowers/72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/73.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/74.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/75.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/76.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/77.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/78.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/79.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/80.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/81.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/82.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/83.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/84.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
static/flowers/85.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/86.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/87.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/88.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/89.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
static/flowers/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
static/flowers/90.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
static/flowers/91.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/92.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
static/flowers/93.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="640" viewBox="0 0 512 640" fill="none">
<rect width="512" height="640" fill="#E8E8E8"/>
<circle cx="256" cy="200" r="56" fill="#CFCFCF"/>
<rect x="248" y="248" width="16" height="280" rx="8" fill="#B8B8B8"/>
<ellipse cx="220" cy="420" rx="28" ry="14" fill="#CFCFCF"/>
<ellipse cx="292" cy="460" rx="28" ry="14" fill="#CFCFCF"/>
</svg>

After

Width:  |  Height:  |  Size: 405 B