From 3e0ff5df7071f07f05904a103c0d94b1e6e392aa Mon Sep 17 00:00:00 2001 From: Chaewon Lee Date: Tue, 16 Jun 2026 10:27:27 +0900 Subject: [PATCH] refine: ui, prompt, and desc card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ui improved * prompt:realisic+noHuman * prompt:editRefinement * fix: map DescriptionCard truncation and truncateAt typo Prevent result/map card overflow with character limits and line-clamp; fix buildMapOrderDescription calling undefined truncateAt. Co-authored-by: Cursor --------- Co-authored-by: 이지은 Co-authored-by: Cursor --- src/lib/artwork/artworkSlotLayout.js | 25 ++ src/lib/artwork/drawMuseumFrame.js | 86 +++++ src/lib/artwork/museumFrameGeometry.js | 29 ++ src/lib/assets/artwork/2.create2.svg | 11 +- src/lib/components/dev/DevSeedButton.svelte | 47 ++- src/lib/components/ui/Artwork/Artwork.svelte | 59 ++-- .../ui/Artwork/DescriptionCard.svelte | 2 +- .../components/ui/Artwork/MuseumFrame.svelte | 94 +++++ src/lib/components/ui/Artwork/Vase.svelte | 3 +- .../components/ui/create/OptionGroup.svelte | 2 +- .../components/ui/landing/LandingHero.svelte | 33 +- src/lib/components/ui/map/KakaoMap.svelte | 8 +- src/lib/components/ui/map/MapPanel.svelte | 7 +- src/lib/components/ui/map/ShopList.svelte | 16 +- .../components/ui/upload/MoodboardGrid.svelte | 28 +- .../components/ui/upload/SnsFeedUpload.svelte | 6 +- .../components/ui/upload/UploadTile.svelte | 12 +- src/lib/flowerFlow/areaEditIntent.js | 178 ++++++++++ src/lib/flowerFlow/bouquetImageFormat.js | 57 +-- .../flowerFlow/buildFloristOrderMessage.js | 329 ++++++++++++++---- src/lib/flowerFlow/resolveRecipeFlowers.js | 94 ++--- src/lib/server/flowerFlow/refinedAreaMask.js | 195 +++++++++++ src/lib/server/flowerFlow/selectionMask.js | 74 +++- .../api/flower-flow/edit-images/+server.js | 55 ++- src/routes/edit/+page.svelte | 43 +-- src/routes/map/+page.svelte | 56 ++- src/routes/result/+page.svelte | 34 +- src/routes/upload/+page.svelte | 50 +-- 28 files changed, 1321 insertions(+), 312 deletions(-) create mode 100644 src/lib/artwork/artworkSlotLayout.js create mode 100644 src/lib/artwork/drawMuseumFrame.js create mode 100644 src/lib/artwork/museumFrameGeometry.js create mode 100644 src/lib/components/ui/Artwork/MuseumFrame.svelte create mode 100644 src/lib/flowerFlow/areaEditIntent.js create mode 100644 src/lib/server/flowerFlow/refinedAreaMask.js diff --git a/src/lib/artwork/artworkSlotLayout.js b/src/lib/artwork/artworkSlotLayout.js new file mode 100644 index 0000000..1c1916c --- /dev/null +++ b/src/lib/artwork/artworkSlotLayout.js @@ -0,0 +1,25 @@ +/** + * 2분할 좌측 액자 슬롯 — 페이지 전환 시 동일 위치·크기 유지. + * DescriptionCard가 커져도 액자 슬롯(flex-none)은 위치·크기 고정, 카드만 아래로 늘어남. + */ + +/** + * 액자 전용 슬롯 — 프레임 높이만큼만 차지 (데스크톱에서 불필요한 빈 높이 제거) + * 모바일 row 레이아웃용 최소 높이만 유지 + */ +export const ARTWORK_SLOT_FLOWER = + 'flex h-[11rem] w-full shrink-0 flex-none items-start justify-center sm:h-[13rem] lg:h-auto lg:min-h-0'; + +/** + * 액자 + DescriptionCard 래퍼 + * lg:pt-16 — 우측 ContextForm 헤더(lg:py-16)와 상단 정렬 (레퍼런스 기준) + * 세로 중앙 배치(calc(50%...)) 없음 → 카드 길이와 무관하게 액자 Y 고정 + */ +export const ARTWORK_SLOT_WRAPPER = + 'mx-auto flex min-h-0 w-full max-w-100 flex-1 flex-row items-start gap-8 px-6 pt-6 pb-8 lg:flex-col lg:items-center lg:justify-start lg:gap-5 lg:px-10 lg:pt-20 lg:pb-10'; + +/** 액자 외곽 최대 너비 */ +export const ARTWORK_FRAME_MAX_W = 'w-full max-w-24 sm:max-w-28 lg:max-w-75'; + +/** DescriptionCard — 액자 바로 아래, flex-none으로 카드만 세로 확장 */ +export const ARTWORK_SLOT_CARD = 'w-full min-w-0 shrink-0 flex-none lg:flex lg:justify-center'; diff --git a/src/lib/artwork/drawMuseumFrame.js b/src/lib/artwork/drawMuseumFrame.js new file mode 100644 index 0000000..11c87fa --- /dev/null +++ b/src/lib/artwork/drawMuseumFrame.js @@ -0,0 +1,86 @@ +import { + FRAME_FILL, + FRAME_STROKE, + INNER_H, + INNER_W, + MAT_X, + MAT_Y, + OUTER_H, + OUTER_W +} from './museumFrameGeometry.js'; + +/** + * p5 캔버스에 미술관 화이트 액자 링을 그립니다. + * 중앙 개구부는 투명 — 아래 HTML 슬롯이 보입니다. + * @param {import('p5')} p + * @param {number} canvasW + * @param {number} canvasH + */ +export function drawMuseumFrame(p, canvasW, canvasH) { + p.clear(); + + const scale = Math.min(canvasW / OUTER_W, canvasH / OUTER_H); + const drawW = OUTER_W * scale; + const drawH = OUTER_H * scale; + const offsetX = (canvasW - drawW) / 2; + const offsetY = (canvasH - drawH) / 2; + + const matX = MAT_X * scale; + const matY = MAT_Y * scale; + const innerW = INNER_W * scale; + const innerH = INNER_H * scale; + const innerX = offsetX + matX; + const innerY = offsetY + matY; + const strokeW = Math.max(1, scale); + + p.push(); + p.fill(FRAME_FILL); + p.noStroke(); + + // mat 링 (상·하·좌·우 4조각) + p.rect(offsetX, offsetY, drawW, matY); + p.rect(offsetX, offsetY + matY + innerH, drawW, matY); + p.rect(offsetX, offsetY + matY, matX, innerH); + p.rect(offsetX + matX + innerW, offsetY + matY, matX, innerH); + + // 외곽·내곽 테두리 + p.noFill(); + p.stroke(FRAME_STROKE); + p.strokeWeight(strokeW); + p.rect(offsetX, offsetY, drawW, drawH); + p.rect(innerX, innerY, innerW, innerH); + + p.pop(); +} + +/** + * p5 instance mode 스케치 팩토리. + * @param {HTMLElement} container + * @returns {Promise} + */ +export function createMuseumFrameSketch(container) { + return new Promise((resolve) => { + import('p5').then(({ default: p5 }) => { + const sketch = (/** @type {import('p5')} */ p) => { + p.setup = () => { + const w = container.clientWidth || 1; + const h = container.clientHeight || 1; + const canvas = p.createCanvas(w, h); + canvas.parent(container); + canvas.elt.style.pointerEvents = 'none'; + p.pixelDensity(Math.min(window.devicePixelRatio || 1, 2)); + drawMuseumFrame(p, w, h); + }; + + p.windowResized = () => { + const w = container.clientWidth || 1; + const h = container.clientHeight || 1; + p.resizeCanvas(w, h); + drawMuseumFrame(p, w, h); + }; + }; + + resolve(new p5(sketch, container)); + }); + }); +} diff --git a/src/lib/artwork/museumFrameGeometry.js b/src/lib/artwork/museumFrameGeometry.js new file mode 100644 index 0000000..917eb86 --- /dev/null +++ b/src/lib/artwork/museumFrameGeometry.js @@ -0,0 +1,29 @@ +/** + * white frame.svg 비율을 768×1024 내부 개구부에 맞춘 논리 좌표. + * SVG 외곽 206×280, 내부 179.29×253.17, mat ~13.3px. + */ + +/** 꽃다발 이미지 출력 크기 (3:4) */ +export const INNER_W = 768; +export const INNER_H = 1024; + +/** inner 대비 mat 두께 (SVG 비율 환산) */ +export const MAT_X = Math.round((13.3574 / 179.2866) * INNER_W); +export const MAT_Y = Math.round((13.2979 / 253.1691) * INNER_H); + +export const OUTER_W = INNER_W + MAT_X * 2; +export const OUTER_H = INNER_H + MAT_Y * 2; + +export const FRAME_STROKE = '#D5D5D5'; +export const FRAME_FILL = '#FFFFFF'; + +/** ref/frame ref.png 시각 기준 — 개구부 안 아트워크 너비 비율 (액자 크기는 그대로) */ +export const ARTWORK_INNER_SCALE = 0.85; + +/** CSS % 배치용 (0–100) */ +export const APERTURE_LEFT_PCT = (MAT_X / OUTER_W) * 100; +export const APERTURE_TOP_PCT = (MAT_Y / OUTER_H) * 100; +export const APERTURE_WIDTH_PCT = (INNER_W / OUTER_W) * 100; +export const APERTURE_HEIGHT_PCT = (INNER_H / OUTER_H) * 100; + +export const OUTER_ASPECT_RATIO = `${OUTER_W} / ${OUTER_H}`; diff --git a/src/lib/assets/artwork/2.create2.svg b/src/lib/assets/artwork/2.create2.svg index 4dffee0..d298623 100644 --- a/src/lib/assets/artwork/2.create2.svg +++ b/src/lib/assets/artwork/2.create2.svg @@ -7,16 +7,13 @@ -
-
-
+ + + - - - - + diff --git a/src/lib/components/dev/DevSeedButton.svelte b/src/lib/components/dev/DevSeedButton.svelte index 7aad45a..334a134 100644 --- a/src/lib/components/dev/DevSeedButton.svelte +++ b/src/lib/components/dev/DevSeedButton.svelte @@ -1,5 +1,7 @@ {#if dev && !DEV_SEED_MUTED}
- +
+ + +
{#if message && message !== 'Filled'}

{message} diff --git a/src/lib/components/ui/Artwork/Artwork.svelte b/src/lib/components/ui/Artwork/Artwork.svelte index 6ed83a2..ab725b4 100644 --- a/src/lib/components/ui/Artwork/Artwork.svelte +++ b/src/lib/components/ui/Artwork/Artwork.svelte @@ -1,9 +1,14 @@ + +

+
+ +
+ {#if mode === 'bouquet'} + {#if loading} +
+ {:else if imageSrc} + {imageAlt} + {:else} +
+ {/if} + {:else} + {#key variant} + + {/key} + {/if} +
+ + + +
+
diff --git a/src/lib/components/ui/Artwork/Vase.svelte b/src/lib/components/ui/Artwork/Vase.svelte index 97da57c..0191156 100644 --- a/src/lib/components/ui/Artwork/Vase.svelte +++ b/src/lib/components/ui/Artwork/Vase.svelte @@ -7,10 +7,11 @@ const src = $derived(getArtworkSrc(variant)); + diff --git a/src/lib/components/ui/create/OptionGroup.svelte b/src/lib/components/ui/create/OptionGroup.svelte index 5781c43..1816d31 100644 --- a/src/lib/components/ui/create/OptionGroup.svelte +++ b/src/lib/components/ui/create/OptionGroup.svelte @@ -10,7 +10,7 @@ type="button" onclick={() => onchange(option)} class={[ - 'text-xl tracking-wide transition-colors', + 'text-lg tracking-wide transition-colors', selected === option ? 'text-ink' : 'text-muted hover:text-ink' ]} > diff --git a/src/lib/components/ui/landing/LandingHero.svelte b/src/lib/components/ui/landing/LandingHero.svelte index 88ff389..0a08962 100644 --- a/src/lib/components/ui/landing/LandingHero.svelte +++ b/src/lib/components/ui/landing/LandingHero.svelte @@ -1,7 +1,7 @@
- +
+ -
-
- +

+ Every Bouquet Starts with a Muse +

+
-

- Every Bouquet Starts with a Muse -

-
+ +
+

+ Fleumuse +

-
-

AI Florist

-

- Fleumuse -

+
+
diff --git a/src/lib/components/ui/map/KakaoMap.svelte b/src/lib/components/ui/map/KakaoMap.svelte index 5cf7702..65084e7 100644 --- a/src/lib/components/ui/map/KakaoMap.svelte +++ b/src/lib/components/ui/map/KakaoMap.svelte @@ -52,12 +52,12 @@ ? `

${escapeHtml(shop.distance)}

` : ''; const phone = shop.phone - ? `

${escapeHtml(shop.phone)}

` + ? `

${escapeHtml(shop.phone)}

` : ''; - return `
-

${escapeHtml(shop.name)}

-

${escapeHtml(shop.address)}

+ return `
+

${escapeHtml(shop.name)}

+

${escapeHtml(shop.address)}

${distance} ${phone}
`; diff --git a/src/lib/components/ui/map/MapPanel.svelte b/src/lib/components/ui/map/MapPanel.svelte index cbd9855..9347573 100644 --- a/src/lib/components/ui/map/MapPanel.svelte +++ b/src/lib/components/ui/map/MapPanel.svelte @@ -48,8 +48,8 @@
-
-

+
+

Find a nearby florist

Move the map, then refresh to search this area.

@@ -59,7 +59,6 @@ {#if mock}

Showing sample shops (no Kakao API key).

{/if} -
@@ -94,7 +93,7 @@
-
+
{#if loading && shops.length === 0}

Searching for flower shops...

{:else} diff --git a/src/lib/components/ui/map/ShopList.svelte b/src/lib/components/ui/map/ShopList.svelte index 0a50fdf..72e0d7e 100644 --- a/src/lib/components/ui/map/ShopList.svelte +++ b/src/lib/components/ui/map/ShopList.svelte @@ -2,25 +2,29 @@ let { shops = [], selectedId = $bindable(null), onselect } = $props(); -
+
{#each shops as shop (shop.id)} {:else} diff --git a/src/lib/components/ui/upload/MoodboardGrid.svelte b/src/lib/components/ui/upload/MoodboardGrid.svelte index 334630e..c564286 100644 --- a/src/lib/components/ui/upload/MoodboardGrid.svelte +++ b/src/lib/components/ui/upload/MoodboardGrid.svelte @@ -65,10 +65,30 @@
- - - - + + + +