feat: add generating image page, artwork, and map translation
* 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 --------- Co-authored-by: 이지은 <ijieun@ijieun-ui-MacBookPro.local> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
6
src/lib/assets/artwork/1.create1.svg
Normal file
6
src/lib/assets/artwork/1.create1.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="328" height="443" viewBox="0 0 328 443" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 515 B |
24
src/lib/assets/artwork/2.create2.svg
Normal file
24
src/lib/assets/artwork/2.create2.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="328" height="443" viewBox="0 0 328 443" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M158 316.5C176.167 299.333 208 252.7 190 203.5C167.5 142 206.5 90 221.5 88" stroke="#7D7D7D" stroke-width="6"/>
|
||||
<path d="M158 316.5C176.167 299.333 208 252.7 190 203.5C167.5 142 206.5 90 221.5 88" stroke="url(#paint0_linear_391_391)" stroke-width="6"/>
|
||||
<foreignObject x="137" y="68" width="111" height="91"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_0_391_391_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="147" y="78" width="91" height="71" fill="#F1C130"/>
|
||||
<foreignObject x="116.507" y="33.507" width="60.9719" height="49.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_1_391_391_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="122" y="39" width="49.9859" height="39" fill="#EDBA54"/>
|
||||
<foreignObject x="128.304" y="59.3043" width="80.3913" height="80.3913"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(9.35px);clip-path:url(#bgblur_2_391_391_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="18.6957" x="147" y="78" width="43" height="43" fill="#F19430"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
<defs>
|
||||
<clipPath id="bgblur_0_391_391_clip_path" transform="translate(-137 -68)"><rect x="147" y="78" width="91" height="71"/>
|
||||
</clipPath><clipPath id="bgblur_1_391_391_clip_path" transform="translate(-116.507 -33.507)"><rect x="122" y="39" width="49.9859" height="39"/>
|
||||
</clipPath><clipPath id="bgblur_2_391_391_clip_path" transform="translate(-128.304 -59.3043)"><rect x="147" y="78" width="43" height="43"/>
|
||||
</clipPath><linearGradient id="paint0_linear_391_391" x1="234.304" y1="63" x2="234.304" y2="316.5" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.355769" stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
34
src/lib/assets/artwork/3.upload1.svg
Normal file
34
src/lib/assets/artwork/3.upload1.svg
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg width="328" height="443" viewBox="0 0 328 443" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M151.5 369.5C133.333 352.333 125 274.2 143 225C165.5 163.5 103 143 88 141" stroke="#7D7D7D" stroke-width="6"/>
|
||||
<path d="M151.5 369.5C133.333 352.333 125 274.2 143 225C165.5 163.5 103 143 88 141" stroke="url(#paint0_linear_391_397)" stroke-width="6"/>
|
||||
<path d="M161 349.5C179.167 332.333 211 285.7 193 236.5C170.5 175 209.5 123 224.5 121" stroke="#7D7D7D" stroke-width="6"/>
|
||||
<path d="M161 349.5C179.167 332.333 211 285.7 193 236.5C170.5 175 209.5 123 224.5 121" stroke="url(#paint1_linear_391_397)" stroke-width="6"/>
|
||||
<rect x="186.535" y="85.1172" width="53.4653" height="50.6139" fill="#D9D9D9"/>
|
||||
<rect x="168" y="73" width="28.5149" height="12.1188" fill="#D9D9D9"/>
|
||||
<rect x="224.317" y="135.734" width="15.6832" height="12.1188" fill="#D9D9D9"/>
|
||||
<rect x="208.634" y="85.1172" width="31.3663" height="33.505" fill="#BABABA"/>
|
||||
<foreignObject x="70" y="129" width="111" height="91"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_0_391_397_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="80" y="139" width="91" height="71" fill="#F1C130"/>
|
||||
<foreignObject x="49.507" y="94.507" width="60.9718" height="49.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_1_391_397_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="55" y="100" width="49.9859" height="39" fill="#EDBA54"/>
|
||||
<foreignObject x="61.3043" y="120.304" width="80.3913" height="80.3913"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(9.35px);clip-path:url(#bgblur_2_391_397_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="18.6957" x="80" y="139" width="43" height="43" fill="#F19430"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
<defs>
|
||||
<clipPath id="bgblur_0_391_397_clip_path" transform="translate(-70 -129)"><rect x="80" y="139" width="91" height="71"/>
|
||||
</clipPath><clipPath id="bgblur_1_391_397_clip_path" transform="translate(-49.507 -94.507)"><rect x="55" y="100" width="49.9859" height="39"/>
|
||||
</clipPath><clipPath id="bgblur_2_391_397_clip_path" transform="translate(-61.3043 -120.304)"><rect x="80" y="139" width="43" height="43"/>
|
||||
</clipPath><linearGradient id="paint0_linear_391_397" x1="75.1957" y1="116" x2="75.1957" y2="369.5" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.355769" stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_391_397" x1="237.304" y1="96" x2="237.304" y2="349.5" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.355769" stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
39
src/lib/assets/artwork/4.upload2.svg
Normal file
39
src/lib/assets/artwork/4.upload2.svg
Normal file
@@ -0,0 +1,39 @@
|
||||
<svg width="328" height="443" viewBox="0 0 328 443" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
<path d="M187.866 226L77 271L187.866 301L266.5 264L187.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M169.14 433L121 424.25L141.632 405L179.8 394L219 423.5L169.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M140 326.5C158.167 309.333 196 278 184.5 227C170.095 163.117 192 152 208.5 136.5C219.529 126.139 272 124.5 261 74" stroke="#7D7D7D" stroke-width="6"/>
|
||||
<path d="M140 326.5C158.167 309.333 196 278 184.5 227C170.095 163.117 192 152 208.5 136.5C219.529 126.139 272 124.5 261 74" stroke="url(#paint0_linear_391_403)" stroke-width="6"/>
|
||||
<path d="M174.049 387C161.203 369.8 147.608 316.064 160.336 266.77C176.247 205.152 137.549 195.5 111.549 189.5C101.031 187.073 68.4078 176.597 76.1864 126" stroke="url(#paint1_linear_391_403)" stroke-width="6"/>
|
||||
<rect x="225" y="27" width="75" height="71" fill="#D9D9D9"/>
|
||||
<rect x="199" y="10" width="40" height="17" fill="#D9D9D9"/>
|
||||
<rect x="278" y="98" width="22" height="17" fill="#D9D9D9"/>
|
||||
<rect x="256" y="27" width="44" height="47" fill="#BABABA"/>
|
||||
<foreignObject x="118" y="99" width="111" height="91"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_0_391_403_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="128" y="109" width="91" height="71" fill="#F1C130"/>
|
||||
<foreignObject x="197.507" y="174.507" width="42.9859" height="33.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_1_391_403_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="203" y="180" width="32" height="23" fill="#FF8400"/>
|
||||
<foreignObject x="38" y="68" width="80" height="80"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_2_391_403_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="48" y="78" width="60" height="60" fill="#D14B16"/>
|
||||
<foreignObject x="95" y="22" width="43" height="43"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_3_391_403_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="105" y="32" width="23" height="23" fill="#7C1401"/>
|
||||
<foreignObject x="109.304" y="90.3043" width="80.3913" height="80.3913"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(9.35px);clip-path:url(#bgblur_4_391_403_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="18.6957" x="128" y="109" width="43" height="43" fill="#F19430"/>
|
||||
<foreignObject x="59" y="45" width="59" height="43"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_5_391_403_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="69" y="78" width="23" height="39" transform="rotate(-90 69 78)" fill="#ED6454"/>
|
||||
<path d="M198 288.566L77 271L134.932 360.865V372.576L121.295 424L176.523 402.87V372.576L198 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M137 307.86L266.5 264L203.383 360.612V371.263L218.362 423L180.915 412.095L137 307.86Z" fill="#D9D9D9"/>
|
||||
<defs>
|
||||
<clipPath id="bgblur_0_391_403_clip_path" transform="translate(-118 -99)"><rect x="128" y="109" width="91" height="71"/>
|
||||
</clipPath><clipPath id="bgblur_1_391_403_clip_path" transform="translate(-197.507 -174.507)"><rect x="203" y="180" width="32" height="23"/>
|
||||
</clipPath><clipPath id="bgblur_2_391_403_clip_path" transform="translate(-38 -68)"><rect x="48" y="78" width="60" height="60"/>
|
||||
</clipPath><clipPath id="bgblur_3_391_403_clip_path" transform="translate(-95 -22)"><rect x="105" y="32" width="23" height="23"/>
|
||||
</clipPath><clipPath id="bgblur_4_391_403_clip_path" transform="translate(-109.304 -90.3043)"><rect x="128" y="109" width="43" height="43"/>
|
||||
</clipPath><clipPath id="bgblur_5_391_403_clip_path" transform="translate(-59 -45)"><rect x="69" y="78" width="23" height="39" transform="rotate(-90 69 78)"/>
|
||||
</clipPath><linearGradient id="paint0_linear_391_403" x1="216.304" y1="73" x2="216.304" y2="326.5" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.355769" stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_391_403" x1="129.007" y1="126" x2="129.007" y2="379.987" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
50
src/lib/assets/artwork/5.message1.svg
Normal file
50
src/lib/assets/artwork/5.message1.svg
Normal file
@@ -0,0 +1,50 @@
|
||||
<svg width="328" height="443" viewBox="0 0 328 443" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M159 326.5C177.167 309.333 215 278 203.5 227C189.095 163.117 211 152 227.5 136.5C238.529 126.139 291 124.5 280 74" stroke="#7D7D7D" stroke-width="6"/>
|
||||
<path d="M159 326.5C177.167 309.333 215 278 203.5 227C189.095 163.117 211 152 227.5 136.5C238.529 126.139 291 124.5 280 74" stroke="url(#paint0_linear_391_351)" stroke-width="6"/>
|
||||
<path d="M193 386.5C174.833 369.333 155.608 315.7 173.608 266.5C196.108 205 122.108 190.5 107.108 188.5C92.1079 186.5 43.6079 176.5 54.6079 126" stroke="url(#paint1_linear_391_351)" stroke-width="6"/>
|
||||
<rect x="244" y="27" width="75" height="71" fill="#D9D9D9"/>
|
||||
<rect x="218" y="10" width="40" height="17" fill="#D9D9D9"/>
|
||||
<rect x="297" y="98" width="22" height="17" fill="#D9D9D9"/>
|
||||
<rect x="275" y="27" width="44" height="47" fill="#BABABA"/>
|
||||
<foreignObject x="137" y="109" width="111" height="91"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_0_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="147" y="119" width="91" height="71" fill="#F1C130"/>
|
||||
<foreignObject x="216.507" y="184.507" width="42.9859" height="33.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_1_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="222" y="190" width="32" height="23" fill="#FF8400"/>
|
||||
<foreignObject x="-1" y="79" width="80" height="80"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_2_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="9" y="89" width="60" height="60" fill="#D14B16"/>
|
||||
<foreignObject x="56" y="33" width="43" height="43"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_3_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="66" y="43" width="23" height="23" fill="#7C1401"/>
|
||||
<foreignObject x="128.304" y="100.304" width="80.3913" height="80.3913"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(9.35px);clip-path:url(#bgblur_4_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="18.6957" x="147" y="119" width="43" height="43" fill="#F19430"/>
|
||||
<foreignObject x="20" y="56" width="59" height="43"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_5_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="30" y="89" width="23" height="39" transform="rotate(-90 30 89)" fill="#ED6454"/>
|
||||
<path d="M197.028 348C163.862 314.667 100.428 253 118.028 199C140.68 129.5 128.862 85 122.528 71" stroke="url(#paint2_linear_391_351)" stroke-width="6"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
<foreignObject x="100.507" y="49.507" width="69.9859" height="49.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_6_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="106" y="55" width="59" height="39" fill="#FFB4DD"/>
|
||||
<foreignObject x="133.507" y="49.507" width="36.9859" height="25.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_7_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="139" y="55" width="26" height="15" fill="#FF5EB7"/>
|
||||
<foreignObject x="139.507" y="88.507" width="41.9859" height="23.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_8_391_351_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="145" y="94" width="31" height="13" fill="#C3438A"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
<defs>
|
||||
<clipPath id="bgblur_0_391_351_clip_path" transform="translate(-137 -109)"><rect x="147" y="119" width="91" height="71"/>
|
||||
</clipPath><clipPath id="bgblur_1_391_351_clip_path" transform="translate(-216.507 -184.507)"><rect x="222" y="190" width="32" height="23"/>
|
||||
</clipPath><clipPath id="bgblur_2_391_351_clip_path" transform="translate(1 -79)"><rect x="9" y="89" width="60" height="60"/>
|
||||
</clipPath><clipPath id="bgblur_3_391_351_clip_path" transform="translate(-56 -33)"><rect x="66" y="43" width="23" height="23"/>
|
||||
</clipPath><clipPath id="bgblur_4_391_351_clip_path" transform="translate(-128.304 -100.304)"><rect x="147" y="119" width="43" height="43"/>
|
||||
</clipPath><clipPath id="bgblur_5_391_351_clip_path" transform="translate(-20 -56)"><rect x="30" y="89" width="23" height="39" transform="rotate(-90 30 89)"/>
|
||||
</clipPath><clipPath id="bgblur_6_391_351_clip_path" transform="translate(-100.507 -49.507)"><rect x="106" y="55" width="59" height="39"/>
|
||||
</clipPath><clipPath id="bgblur_7_391_351_clip_path" transform="translate(-133.507 -49.507)"><rect x="139" y="55" width="26" height="15"/>
|
||||
</clipPath><clipPath id="bgblur_8_391_351_clip_path" transform="translate(-139.507 -88.507)"><rect x="145" y="94" width="31" height="13"/>
|
||||
</clipPath><linearGradient id="paint0_linear_391_351" x1="235.304" y1="73" x2="235.304" y2="326.5" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.355769" stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_391_351" x1="129.304" y1="126" x2="129.304" y2="379.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_391_351" x1="156.735" y1="71" x2="156.735" y2="348" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
50
src/lib/assets/artwork/6.generated.svg
Normal file
50
src/lib/assets/artwork/6.generated.svg
Normal file
@@ -0,0 +1,50 @@
|
||||
<svg width="328" height="443" viewBox="0 0 328 443" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M186.866 226L76 271L186.866 301L265.5 264L186.866 226Z" fill="#7D7D7D"/>
|
||||
<path d="M168.14 433L120 424.25L140.632 405L178.8 394L218 423.5L168.14 433Z" fill="#7D7D7D"/>
|
||||
<path d="M159 326.5C177.167 309.333 215 278 203.5 227C189.095 163.117 211 152 227.5 136.5C238.529 126.139 291 124.5 280 74" stroke="#7D7D7D" stroke-width="6"/>
|
||||
<path d="M159 326.5C177.167 309.333 215 278 203.5 227C189.095 163.117 211 152 227.5 136.5C238.529 126.139 291 124.5 280 74" stroke="url(#paint0_linear_391_409)" stroke-width="6"/>
|
||||
<path d="M193 386.5C174.833 369.333 155.608 315.7 173.608 266.5C196.108 205 122.108 190.5 107.108 188.5C92.1079 186.5 43.6079 176.5 54.6079 126" stroke="url(#paint1_linear_391_409)" stroke-width="6"/>
|
||||
<rect x="244" y="27" width="75" height="71" fill="#D9D9D9"/>
|
||||
<rect x="218" y="10" width="40" height="17" fill="#D9D9D9"/>
|
||||
<rect x="297" y="98" width="22" height="17" fill="#D9D9D9"/>
|
||||
<rect x="275" y="27" width="44" height="47" fill="#BABABA"/>
|
||||
<foreignObject x="137" y="109" width="111" height="91"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_0_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="147" y="119" width="91" height="71" fill="#F1C130"/>
|
||||
<foreignObject x="216.507" y="184.507" width="42.9859" height="33.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_1_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="222" y="190" width="32" height="23" fill="#FF8400"/>
|
||||
<foreignObject x="-1" y="79" width="80" height="80"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_2_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="9" y="89" width="60" height="60" fill="#D14B16"/>
|
||||
<foreignObject x="56" y="33" width="43" height="43"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_3_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="66" y="43" width="23" height="23" fill="#7C1401"/>
|
||||
<foreignObject x="128.304" y="100.304" width="80.3913" height="80.3913"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(9.35px);clip-path:url(#bgblur_4_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="18.6957" x="147" y="119" width="43" height="43" fill="#F19430"/>
|
||||
<foreignObject x="20" y="56" width="59" height="43"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(5px);clip-path:url(#bgblur_5_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="10" x="30" y="89" width="23" height="39" transform="rotate(-90 30 89)" fill="#ED6454"/>
|
||||
<path d="M197.028 348C163.862 314.667 100.428 253 118.028 199C140.68 129.5 128.862 85 122.528 71" stroke="url(#paint2_linear_391_409)" stroke-width="6"/>
|
||||
<path d="M197 288.566L76 271L133.932 360.865V372.576L120.295 424L175.523 402.87V372.576L197 288.566Z" fill="#BBBBBB"/>
|
||||
<path d="M136 307.86L265.5 264L202.383 360.612V371.263L217.362 423L179.915 412.095L136 307.86Z" fill="#D9D9D9"/>
|
||||
<foreignObject x="100.507" y="49.507" width="69.9859" height="49.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_6_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="106" y="55" width="59" height="39" fill="#FFB4DD"/>
|
||||
<foreignObject x="133.507" y="49.507" width="36.9859" height="25.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_7_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="139" y="55" width="26" height="15" fill="#FF5EB7"/>
|
||||
<foreignObject x="139.507" y="88.507" width="41.9859" height="23.9859"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.75px);clip-path:url(#bgblur_8_391_409_clip_path);height:100%;width:100%"></div></foreignObject><rect data-figma-bg-blur-radius="5.49296" x="145" y="94" width="31" height="13" fill="#C3438A"/>
|
||||
<path d="M134 373V360.5H202.5V371.5L134 373Z" fill="#7D7D7D"/>
|
||||
<path d="M157 357L177.5 355.5L176 376L157 373V357Z" fill="#7D7D7D"/>
|
||||
<path d="M123 394.5L171 363L173.5 370L134 402.5H127L123 394.5Z" fill="#7D7D7D"/>
|
||||
<path d="M198.5 382.5L163.5 361L161 368L187.5 390.5H194.5L198.5 382.5Z" fill="#7D7D7D"/>
|
||||
<defs>
|
||||
<clipPath id="bgblur_0_391_409_clip_path" transform="translate(-137 -109)"><rect x="147" y="119" width="91" height="71"/>
|
||||
</clipPath><clipPath id="bgblur_1_391_409_clip_path" transform="translate(-216.507 -184.507)"><rect x="222" y="190" width="32" height="23"/>
|
||||
</clipPath><clipPath id="bgblur_2_391_409_clip_path" transform="translate(1 -79)"><rect x="9" y="89" width="60" height="60"/>
|
||||
</clipPath><clipPath id="bgblur_3_391_409_clip_path" transform="translate(-56 -33)"><rect x="66" y="43" width="23" height="23"/>
|
||||
</clipPath><clipPath id="bgblur_4_391_409_clip_path" transform="translate(-128.304 -100.304)"><rect x="147" y="119" width="43" height="43"/>
|
||||
</clipPath><clipPath id="bgblur_5_391_409_clip_path" transform="translate(-20 -56)"><rect x="30" y="89" width="23" height="39" transform="rotate(-90 30 89)"/>
|
||||
</clipPath><clipPath id="bgblur_6_391_409_clip_path" transform="translate(-100.507 -49.507)"><rect x="106" y="55" width="59" height="39"/>
|
||||
</clipPath><clipPath id="bgblur_7_391_409_clip_path" transform="translate(-133.507 -49.507)"><rect x="139" y="55" width="26" height="15"/>
|
||||
</clipPath><clipPath id="bgblur_8_391_409_clip_path" transform="translate(-139.507 -88.507)"><rect x="145" y="94" width="31" height="13"/>
|
||||
</clipPath><linearGradient id="paint0_linear_391_409" x1="235.304" y1="73" x2="235.304" y2="326.5" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.355769" stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_391_409" x1="129.304" y1="126" x2="129.304" y2="379.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_391_409" x1="156.735" y1="71" x2="156.735" y2="348" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#523E03"/>
|
||||
<stop offset="1" stop-color="#08B816"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
@@ -2,33 +2,50 @@
|
||||
// The exhibited artwork — always shown on the left, acting like a step indicator.
|
||||
import Vase from './Vase.svelte';
|
||||
import DescriptionCard from './DescriptionCard.svelte';
|
||||
import ComingSoonTape from './ComingSoonTape.svelte';
|
||||
|
||||
let {
|
||||
title = 'Title',
|
||||
description = 'Description Description Description',
|
||||
/** options Continue 이후 확정된 꽃다발만 전달 (그 전에는 null → Vase) */
|
||||
imageSrc = null
|
||||
/** @type {import('./artworkVariants.js').ArtworkVariant} */
|
||||
variant = 'create1',
|
||||
/** edit Continue 이후 확정된 꽃다발만 전달 (그 전에는 null → Vase) */
|
||||
imageSrc = null,
|
||||
/** generating 단계: 작품 중앙 Coming Soon 밴드 */
|
||||
comingSoon = false
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="flex w-full shrink-0 flex-col border-b border-line lg:min-h-0 lg:w-[44%] lg:shrink-0 lg:overflow-y-auto lg:border-r lg:border-b-0"
|
||||
class="relative flex w-full shrink-0 flex-col border-b border-line lg:min-h-0 lg:h-full lg:w-[44%] lg:shrink-0 lg:overflow-y-auto lg:border-r lg:border-b-0"
|
||||
>
|
||||
<!-- mobile: compact row · desktop: centered column -->
|
||||
<!--
|
||||
mobile: row · desktop: 꽃 슬롯 높이 고정 → 설명 카드 길이와 무관하게 Y·크기 유지
|
||||
-->
|
||||
<div
|
||||
class="mx-auto flex w-full max-w-100 flex-row items-center gap-12 px-6 py-5 lg:flex-1 lg:flex-col lg:items-center lg:justify-center lg:gap-10 lg:px-6 lg:py-12"
|
||||
class="mx-auto flex min-h-0 w-full max-w-100 flex-1 flex-row items-start gap-8 px-6 py-6 lg:flex-col lg:items-center lg:justify-start lg:gap-4 lg:px-6 lg:pb-8 lg:pt-[calc(50%-5rem)]"
|
||||
>
|
||||
{#if imageSrc}
|
||||
<div class="mx-auto w-full max-w-24 shrink-0 overflow-hidden sm:max-w-28 lg:max-w-75">
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt="Selected bouquet"
|
||||
class="aspect-[3/4] h-auto w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Vase />
|
||||
{/if}
|
||||
<DescriptionCard {title} {description} />
|
||||
<div
|
||||
class="flex h-[11rem] shrink-0 items-end justify-center sm:h-[13rem] lg:h-[min(24rem,36vh)] lg:w-full"
|
||||
>
|
||||
{#if imageSrc}
|
||||
<div class="mx-auto w-full max-w-24 shrink-0 overflow-hidden sm:max-w-28 lg:max-w-75">
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt="Selected bouquet"
|
||||
class="aspect-[3/4] h-auto w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Vase {variant} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 shrink-0 lg:w-full lg:flex lg:justify-center">
|
||||
<DescriptionCard {title} {description} />
|
||||
</div>
|
||||
</div>
|
||||
{#if comingSoon}
|
||||
<ComingSoonTape />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
11
src/lib/components/ui/Artwork/ComingSoonTape.svelte
Normal file
11
src/lib/components/ui/Artwork/ComingSoonTape.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<!-- generating: 왼쪽 섹션 전체 가로 + blur, vase 높이 중앙 -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 top-[calc(50%-1.25rem)] z-30 -translate-y-1/2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="w-full border-y border-subtle/30 bg-muted/45 py-2.5 text-center backdrop-blur-md lg:py-3"
|
||||
>
|
||||
<p class="text-sm font-light tracking-wide text-ink lg:text-base">Coming Soon</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,16 @@
|
||||
<script>
|
||||
import vaseIllustration from '$lib/assets/vase-illustration.svg';
|
||||
import { getArtworkSrc } from './artworkVariants.js';
|
||||
|
||||
/** @type {import('./artworkVariants.js').ArtworkVariant} */
|
||||
let { variant = 'create1' } = $props();
|
||||
|
||||
const src = $derived(getArtworkSrc(variant));
|
||||
</script>
|
||||
|
||||
<img
|
||||
src={vaseIllustration}
|
||||
{src}
|
||||
alt=""
|
||||
class="mx-auto h-auto w-full max-w-24 shrink-0 sm:max-w-28 lg:max-w-75"
|
||||
width="320"
|
||||
height="452"
|
||||
width="328"
|
||||
height="443"
|
||||
/>
|
||||
|
||||
32
src/lib/components/ui/Artwork/artworkVariants.js
Normal file
32
src/lib/components/ui/Artwork/artworkVariants.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import create1 from '$lib/assets/artwork/1.create1.svg';
|
||||
import create2 from '$lib/assets/artwork/2.create2.svg';
|
||||
import upload1 from '$lib/assets/artwork/3.upload1.svg';
|
||||
import upload2 from '$lib/assets/artwork/4.upload2.svg';
|
||||
import message1 from '$lib/assets/artwork/5.message1.svg';
|
||||
import generated from '$lib/assets/artwork/6.generated.svg';
|
||||
|
||||
/** @typedef {'create1' | 'create2' | 'upload1' | 'upload2' | 'message1' | 'generated'} ArtworkVariant */
|
||||
|
||||
/** @type {Record<ArtworkVariant, string>} */
|
||||
export const ARTWORK_SRC = {
|
||||
create1,
|
||||
create2,
|
||||
upload1,
|
||||
upload2,
|
||||
message1,
|
||||
generated
|
||||
};
|
||||
|
||||
/** generating 페이지 순환 프레임 */
|
||||
export const GENERATING_ARTWORK_CYCLE = /** @type {const} */ ([
|
||||
'create2',
|
||||
'upload1',
|
||||
'upload2',
|
||||
'message1',
|
||||
'generated'
|
||||
]);
|
||||
|
||||
/** @param {ArtworkVariant} [variant='create1'] */
|
||||
export function getArtworkSrc(variant = 'create1') {
|
||||
return ARTWORK_SRC[variant] ?? ARTWORK_SRC.create1;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<script>
|
||||
import GenerationStepItem from './GenerationStepItem.svelte';
|
||||
import { GENERATION_STEPS, GENERATION_STEP_COUNT } from './generationSteps.js';
|
||||
|
||||
let {
|
||||
/** 현재 active 단계 (0–6). GENERATION_STEP_COUNT이면 전부 완료 */
|
||||
activeStepIndex = 0,
|
||||
error = '',
|
||||
retryLabel = '',
|
||||
canRetry = false,
|
||||
onRetry = () => {},
|
||||
onBack = () => {}
|
||||
} = $props();
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
* @returns {'completed' | 'active' | 'pending'}
|
||||
*/
|
||||
function stepStatus(index) {
|
||||
if (activeStepIndex >= GENERATION_STEP_COUNT) return 'completed';
|
||||
if (index < activeStepIndex) return 'completed';
|
||||
if (index === activeStepIndex) return 'active';
|
||||
return 'pending';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-1 flex-col justify-center px-6 py-10 md:px-12 lg:px-16 lg:py-16">
|
||||
<header class="mb-10 space-y-3 lg:mb-14">
|
||||
<h1 class="text-3xl leading-relaxed font-light text-muted md:text-4xl lg:text-[2.75rem]">
|
||||
Creating your bouquet...
|
||||
</h1>
|
||||
{#if retryLabel}
|
||||
<p class="text-sm text-muted">{retryLabel}</p>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<ol class="space-y-4 lg:space-y-5" aria-label="Bouquet creation progress">
|
||||
{#each GENERATION_STEPS as label, index (label)}
|
||||
<GenerationStepItem {label} status={stepStatus(index)} />
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
{#if error}
|
||||
<div class="mt-10 space-y-4">
|
||||
<p class="rounded bg-surface/95 px-3 py-2 text-sm text-red-600 ring-1 ring-black/5">
|
||||
{error}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{#if canRetry}
|
||||
<button type="button" class="bg-pill px-4 py-2 text-sm text-surface" onclick={onRetry}>
|
||||
Try again
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="border border-pill px-4 py-2 text-sm text-ink"
|
||||
onclick={onBack}
|
||||
>
|
||||
Back to message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
41
src/lib/components/ui/generating/GenerationStepItem.svelte
Normal file
41
src/lib/components/ui/generating/GenerationStepItem.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script>
|
||||
/** @type {'completed' | 'active' | 'pending'} */
|
||||
let { label, status = 'pending' } = $props();
|
||||
</script>
|
||||
|
||||
<li
|
||||
class={[
|
||||
'flex items-start gap-3 text-lg tracking-wide transition-all duration-500 md:text-xs',
|
||||
status === 'completed' && 'step-completed text-ink opacity-100',
|
||||
status === 'active' && 'text-ink',
|
||||
status === 'pending' && 'text-muted/50'
|
||||
]}
|
||||
>
|
||||
<span class="mt-0.5 w-5 shrink-0 text-center leading-none" aria-hidden="true">
|
||||
{#if status === 'completed'}
|
||||
<span class="inline-block">✓</span>
|
||||
{:else if status === 'active'}
|
||||
<span class="inline-block animate-pulse">●</span>
|
||||
{:else}
|
||||
<span class="inline-block">○</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="leading-snug">{label}</span>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.step-completed {
|
||||
animation: stepFadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes stepFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
12
src/lib/components/ui/generating/generationSteps.js
Normal file
12
src/lib/components/ui/generating/generationSteps.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** AI activity feed 단계 라벨 (generating 페이지) */
|
||||
export const GENERATION_STEPS = [
|
||||
'Understanding who this bouquet is for',
|
||||
'Discovering their mood and aesthetic',
|
||||
'Finding flowers that match their personality',
|
||||
'Exploring meaningful flower symbolism',
|
||||
'Designing the bouquet composition',
|
||||
'Choosing the wrapping style',
|
||||
'Adding the finishing ribbon'
|
||||
];
|
||||
|
||||
export const GENERATION_STEP_COUNT = GENERATION_STEPS.length;
|
||||
@@ -1,20 +1,49 @@
|
||||
<script>
|
||||
/** @typedef {{ text: string, highlight: boolean }} OrderMessageSegment */
|
||||
|
||||
let {
|
||||
plainText = '',
|
||||
segments = /** @type {OrderMessageSegment[]} */ ([])
|
||||
enPlainText = '',
|
||||
koPlainText = ''
|
||||
} = $props();
|
||||
|
||||
/** @type {'ko' | 'en'} */
|
||||
let activeLang = $state('ko');
|
||||
let textEn = $state('');
|
||||
let textKo = $state('');
|
||||
let seeded = $state(false);
|
||||
let copied = $state(false);
|
||||
|
||||
const hasMessage = $derived(Boolean(plainText?.trim()));
|
||||
$effect(() => {
|
||||
if (!seeded && (enPlainText || koPlainText)) {
|
||||
textEn = enPlainText;
|
||||
textKo = koPlainText;
|
||||
seeded = true;
|
||||
}
|
||||
});
|
||||
|
||||
const activeText = $derived(activeLang === 'ko' ? textKo : textEn);
|
||||
const hasMessage = $derived(
|
||||
Boolean(activeText?.trim()) || Boolean(textEn?.trim()) || Boolean(textKo?.trim())
|
||||
);
|
||||
|
||||
/** @param {Event & { currentTarget: HTMLTextAreaElement }} event */
|
||||
function handleInput(event) {
|
||||
const value = event.currentTarget.value;
|
||||
if (activeLang === 'ko') {
|
||||
textKo = value;
|
||||
} else {
|
||||
textEn = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {'ko' | 'en'} lang */
|
||||
function setLanguage(lang) {
|
||||
activeLang = lang;
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
if (!hasMessage) return;
|
||||
if (!activeText.trim()) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(plainText);
|
||||
await navigator.clipboard.writeText(activeText);
|
||||
copied = true;
|
||||
setTimeout(() => {
|
||||
copied = false;
|
||||
@@ -25,29 +54,50 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
{#if hasMessage}
|
||||
<p class="min-w-0 flex-1 text-sm leading-relaxed text-muted">
|
||||
{#each segments as segment, index (index)}
|
||||
{#if segment.highlight}
|
||||
<span class="text-pill">{'{'}</span><span class="font-medium text-ink"
|
||||
>{segment.text}</span
|
||||
><span class="text-pill">{'}'}</span>
|
||||
{:else}
|
||||
{segment.text}
|
||||
{/if}
|
||||
{/each}
|
||||
</p>
|
||||
<textarea
|
||||
class="min-h-[5.5rem] min-w-0 flex-1 resize-y rounded border border-line bg-transparent px-3 py-2 text-sm leading-relaxed text-muted focus:border-line-strong focus:outline-none"
|
||||
rows={4}
|
||||
value={activeText}
|
||||
oninput={handleInput}
|
||||
aria-label={activeLang === 'ko' ? '꽃집 주문 멘트 (한국어)' : 'Florist order message (English)'}
|
||||
></textarea>
|
||||
{:else}
|
||||
<p class="text-sm text-muted">Complete the flow to generate your order message.</p>
|
||||
<p class="min-w-0 flex-1 text-sm text-muted">Complete the flow to generate your order message.</p>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hasMessage}
|
||||
onclick={handleCopy}
|
||||
class="shrink-0 rounded bg-pill px-3 py-1.5 text-xs text-surface disabled:opacity-40"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
<div class="flex shrink-0 flex-col items-stretch gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hasMessage}
|
||||
onclick={handleCopy}
|
||||
class="rounded bg-pill px-3 py-1.5 text-xs text-surface disabled:opacity-40"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hasMessage}
|
||||
onclick={() => setLanguage('ko')}
|
||||
class="flex-1 rounded border px-2 py-1.5 text-xs disabled:opacity-40 {activeLang === 'ko'
|
||||
? 'border-pill bg-pill text-surface'
|
||||
: 'border-line text-muted hover:border-line-strong'}"
|
||||
>
|
||||
Kor
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hasMessage}
|
||||
onclick={() => setLanguage('en')}
|
||||
class="flex-1 rounded border px-2 py-1.5 text-xs disabled:opacity-40 {activeLang === 'en'
|
||||
? 'border-pill bg-pill text-surface'
|
||||
: 'border-line text-muted hover:border-line-strong'}"
|
||||
>
|
||||
Eng
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
mock = false,
|
||||
fitBounds = false,
|
||||
orderPlainText = '',
|
||||
orderSegments = [],
|
||||
orderKoPlainText = '',
|
||||
onrefresh
|
||||
} = $props();
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</header>
|
||||
|
||||
<div class="shrink-0 px-6 pb-4 md:px-10 lg:px-12">
|
||||
<FloristOrderMessage plainText={orderPlainText} segments={orderSegments} />
|
||||
<FloristOrderMessage enPlainText={orderPlainText} koPlainText={orderKoPlainText} />
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { hydrateDevUpload } from '$lib/dev/hydrateUpload.js';
|
||||
import { getFlowObject, isDevSeeded } from '$lib/flowerFlow/session.js';
|
||||
|
||||
let { primaryFile = $bindable(null), caption = 'build their moodboard!' } = $props();
|
||||
let { primaryFile = $bindable(null), caption = 'build their moodboard!', filledCount = $bindable(0), allFilled = $bindable(false) } = $props();
|
||||
|
||||
let colorFile = $state(null);
|
||||
let seasonFile = $state(null);
|
||||
@@ -16,6 +16,12 @@
|
||||
if (primaryFile !== next) primaryFile = next;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const count = [colorFile, seasonFile, characterFile, locationFile].filter(Boolean).length;
|
||||
filledCount = count;
|
||||
allFilled = count === 4;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
const devUpload = getFlowObject('devUpload');
|
||||
if (!isDevSeeded() || !devUpload?.active) return;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { hydrateDevUpload } from '$lib/dev/hydrateUpload.js';
|
||||
import { getFlowObject, isDevSeeded } from '$lib/flowerFlow/session.js';
|
||||
|
||||
let { primaryFile = $bindable(null), caption = 'upload their feed!' } = $props();
|
||||
let { primaryFile = $bindable(null), caption = 'upload their feed!', filledCount = $bindable(0), allFilled = $bindable(false) } = $props();
|
||||
|
||||
let firstFile = $state(null);
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
if (primaryFile !== next) primaryFile = next;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
filledCount = firstFile ? 1 : 0;
|
||||
allFilled = Boolean(firstFile);
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
const devUpload = getFlowObject('devUpload');
|
||||
if (!isDevSeeded() || !devUpload?.active) return;
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
|
||||
/** @typedef {{ text: string, highlight: boolean }} OrderMessageSegment */
|
||||
|
||||
/** @typedef {{ plainText: string, segments: OrderMessageSegment[] }} FloristOrderMessageResult */
|
||||
/** @typedef {{ plainText: string, segments: OrderMessageSegment[] }} OrderMessageLocale */
|
||||
|
||||
/** @typedef {OrderMessageLocale & { ko: OrderMessageLocale }} FloristOrderMessageResult */
|
||||
|
||||
const EMPTY_LOCALE = /** @type {OrderMessageLocale} */ ({ plainText: '', segments: [] });
|
||||
|
||||
/**
|
||||
* @param {string[]} [items]
|
||||
@@ -26,7 +30,7 @@ export function buildFloristOrderMessage(input) {
|
||||
const { userInput, moodAnalysis, recipe } = input;
|
||||
|
||||
if (!recipe && !userInput?.relationship && !userInput?.occasion) {
|
||||
return { plainText: '', segments: [] };
|
||||
return { ...EMPTY_LOCALE, ko: { ...EMPTY_LOCALE } };
|
||||
}
|
||||
|
||||
const relationship = userInput?.relationship ?? 'someone special';
|
||||
@@ -69,5 +73,29 @@ export function buildFloristOrderMessage(input) {
|
||||
{ text: ' tones. Would a reservation be possible?', highlight: false }
|
||||
];
|
||||
|
||||
return { plainText, segments };
|
||||
const koPlainText =
|
||||
`안녕하세요, 꽃 주문 문의드립니다. ` +
|
||||
`${relationship}에게 ${occasion} 꽃다발을 준비하고 싶습니다. 예산은 약 ${budget}입니다. ` +
|
||||
`${moodFeel}한 분위기로, ${colorTone} 톤으로 선물하고 싶습니다. ` +
|
||||
`예약 가능할까요?`;
|
||||
|
||||
const koSegments = [
|
||||
{ text: '안녕하세요, 꽃 주문 문의드립니다. ', highlight: false },
|
||||
{ text: relationship, highlight: true },
|
||||
{ text: '에게 ', highlight: false },
|
||||
{ text: occasion, highlight: true },
|
||||
{ text: ' 꽃다발을 준비하고 싶습니다. 예산은 약 ', highlight: false },
|
||||
{ text: budget, highlight: true },
|
||||
{ text: '입니다. ', highlight: false },
|
||||
{ text: moodFeel, highlight: true },
|
||||
{ text: '한 분위기로, ', highlight: false },
|
||||
{ text: colorTone, highlight: true },
|
||||
{ text: ' 톤으로 선물하고 싶습니다. 예약 가능할까요?', highlight: false }
|
||||
];
|
||||
|
||||
return {
|
||||
plainText,
|
||||
segments,
|
||||
ko: { plainText: koPlainText, segments: koSegments }
|
||||
};
|
||||
}
|
||||
|
||||
44
src/lib/flowerFlow/generatingArtworkCycle.js
Normal file
44
src/lib/flowerFlow/generatingArtworkCycle.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { GENERATING_ARTWORK_CYCLE } from '$lib/components/ui/Artwork/artworkVariants.js';
|
||||
|
||||
const FRAME_MS = 700;
|
||||
|
||||
/**
|
||||
* generating 페이지 artwork 순환 (create2 → … → generated)
|
||||
* @param {(variant: import('$lib/components/ui/Artwork/artworkVariants.js').ArtworkVariant) => void} onVariantChange
|
||||
*/
|
||||
export function createGeneratingArtworkCycle(onVariantChange) {
|
||||
let frameIndex = 0;
|
||||
/** @type {ReturnType<typeof setInterval> | null} */
|
||||
let timer = null;
|
||||
let disposed = false;
|
||||
|
||||
function emitCurrent() {
|
||||
onVariantChange(GENERATING_ARTWORK_CYCLE[frameIndex]);
|
||||
}
|
||||
|
||||
function start() {
|
||||
stop();
|
||||
disposed = false;
|
||||
frameIndex = 0;
|
||||
emitCurrent();
|
||||
timer = setInterval(() => {
|
||||
if (disposed) return;
|
||||
frameIndex = (frameIndex + 1) % GENERATING_ARTWORK_CYCLE.length;
|
||||
emitCurrent();
|
||||
}, FRAME_MS);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
disposed = true;
|
||||
stop();
|
||||
}
|
||||
|
||||
return { start, stop, dispose };
|
||||
}
|
||||
127
src/lib/flowerFlow/generationProgress.js
Normal file
127
src/lib/flowerFlow/generationProgress.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import { GENERATION_STEP_COUNT } from '$lib/components/ui/generating/generationSteps.js';
|
||||
|
||||
/** 실제 API 기준 예상 총 소요 시간 (ms) — 7단계 균등 분배 */
|
||||
export const DEFAULT_ESTIMATED_MS = 40_000;
|
||||
|
||||
/** mock/dev: 7단계가 눈에 보이도록 짧게 */
|
||||
export const MOCK_ESTIMATED_MS = 6_000;
|
||||
|
||||
/** stepInterval 하한 */
|
||||
export const MIN_STEP_MS = 500;
|
||||
|
||||
/** API 조기 완료 시 남은 단계 catch-up 간격 */
|
||||
export const CATCHUP_STEP_MS = 250;
|
||||
|
||||
/** @param {number} ms */
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* generating UI 7단계 시간 기반 진행 컨트롤러.
|
||||
* activeStepIndex: 0–6 = 해당 단계 active, GENERATION_STEP_COUNT = 전부 완료.
|
||||
*
|
||||
* @param {(index: number) => void} onStepChange
|
||||
*/
|
||||
export function createGenerationProgress(onStepChange) {
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
let stepTimer = null;
|
||||
let disposed = false;
|
||||
let currentIndex = 0;
|
||||
let capped = false;
|
||||
|
||||
function emit(index) {
|
||||
currentIndex = index;
|
||||
onStepChange(index);
|
||||
}
|
||||
|
||||
function clearStepTimer() {
|
||||
if (stepTimer) {
|
||||
clearTimeout(stepTimer);
|
||||
stepTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
disposed = true;
|
||||
clearStepTimer();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
clearStepTimer();
|
||||
disposed = false;
|
||||
capped = false;
|
||||
currentIndex = 0;
|
||||
emit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} estimatedMs
|
||||
* @returns {number}
|
||||
*/
|
||||
function stepIntervalMs(estimatedMs) {
|
||||
return Math.max(MIN_STEP_MS, Math.floor(estimatedMs / GENERATION_STEP_COUNT));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} estimatedMs
|
||||
*/
|
||||
function scheduleNextStep(estimatedMs) {
|
||||
clearStepTimer();
|
||||
if (disposed || capped) return;
|
||||
|
||||
stepTimer = setTimeout(() => {
|
||||
if (disposed || capped) return;
|
||||
|
||||
if (currentIndex < GENERATION_STEP_COUNT - 1) {
|
||||
emit(currentIndex + 1);
|
||||
|
||||
if (currentIndex >= GENERATION_STEP_COUNT - 1) {
|
||||
capped = true;
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleNextStep(estimatedMs);
|
||||
}
|
||||
}, stepIntervalMs(estimatedMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ estimatedMs?: number }} [options]
|
||||
*/
|
||||
function begin(options = {}) {
|
||||
const estimatedMs = options.estimatedMs ?? DEFAULT_ESTIMATED_MS;
|
||||
reset();
|
||||
emit(0);
|
||||
scheduleNextStep(estimatedMs);
|
||||
}
|
||||
|
||||
/** 전 단계 완료 */
|
||||
function completeAll() {
|
||||
clearStepTimer();
|
||||
capped = true;
|
||||
emit(GENERATION_STEP_COUNT);
|
||||
}
|
||||
|
||||
/** API 완료 시 — 남은 단계 catch-up 후 completeAll */
|
||||
async function finishWhenReady() {
|
||||
clearStepTimer();
|
||||
|
||||
while (!disposed && currentIndex < GENERATION_STEP_COUNT - 1) {
|
||||
emit(currentIndex + 1);
|
||||
await wait(CATCHUP_STEP_MS);
|
||||
}
|
||||
|
||||
if (!disposed) {
|
||||
completeAll();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
begin,
|
||||
completeAll,
|
||||
finishWhenReady,
|
||||
reset,
|
||||
dispose
|
||||
};
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
return `${occasion} ${who ?? '...'}`;
|
||||
});
|
||||
|
||||
const artworkVariant = $derived(hasAnySelection ? 'create2' : 'create1');
|
||||
|
||||
const artworkDescription = $derived(
|
||||
hasAnySelection
|
||||
? `${style ?? '—'} style · ₩${budget.toLocaleString('ko-KR')} budget`
|
||||
@@ -82,7 +84,7 @@
|
||||
<Header step={1} total={7} />
|
||||
|
||||
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||
<Artwork title={artworkTitle} description={artworkDescription} />
|
||||
<Artwork variant={artworkVariant} title={artworkTitle} description={artworkDescription} />
|
||||
|
||||
<section class="relative flex min-h-0 flex-1 flex-col lg:overflow-y-auto">
|
||||
<ContextForm bind:who bind:whatFor bind:style bind:budget />
|
||||
|
||||
@@ -3,25 +3,62 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import Header from '$lib/components/ui/Header.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork/Artwork.svelte';
|
||||
import GenerationActivityFeed from '$lib/components/ui/generating/GenerationActivityFeed.svelte';
|
||||
import { buildRecipe, generateImages } from '$lib/flowerFlow/api.js';
|
||||
import { clearFlow, getFlowObject, loadFlow, saveFlow } from '$lib/flowerFlow/session.js';
|
||||
import { createGenerationProgress, DEFAULT_ESTIMATED_MS, MOCK_ESTIMATED_MS } from '$lib/flowerFlow/generationProgress.js';
|
||||
import { createGeneratingArtworkCycle } from '$lib/flowerFlow/generatingArtworkCycle.js';
|
||||
import {
|
||||
clearFlow,
|
||||
getFlowObject,
|
||||
getFlowString,
|
||||
getFlowUserInput,
|
||||
loadFlow,
|
||||
saveFlow
|
||||
} from '$lib/flowerFlow/session.js';
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const userInput = getFlowUserInput();
|
||||
const cardMessage = getFlowString('cardMessage');
|
||||
|
||||
let status = $state('Preparing bouquet recipe...');
|
||||
const artworkTitle = $derived.by(() => {
|
||||
const who = typeof userInput.relationship === 'string' ? userInput.relationship : null;
|
||||
const whatFor = typeof userInput.occasion === 'string' ? userInput.occasion : null;
|
||||
if (!who && !whatFor) return 'Your bouquet';
|
||||
const occasion = whatFor ? `A ${whatFor} bouquet for` : 'A bouquet for';
|
||||
return `${occasion} ${who ?? '...'}`;
|
||||
});
|
||||
|
||||
const artworkDescription = $derived(cardMessage || '잠시 관리중 ~');
|
||||
|
||||
/** @type {import('$lib/components/ui/Artwork/artworkVariants.js').ArtworkVariant} */
|
||||
let artworkVariant = $state('create2');
|
||||
|
||||
let activeStepIndex = $state(0);
|
||||
let retryLabel = $state('');
|
||||
let error = $state('');
|
||||
let canRetry = $state(false);
|
||||
|
||||
let active = true;
|
||||
/** @type {ReturnType<typeof createGenerationProgress> | null} */
|
||||
let progress = null;
|
||||
/** @type {ReturnType<typeof createGeneratingArtworkCycle> | null} */
|
||||
let artworkCycle = null;
|
||||
|
||||
function startArtworkCycle() {
|
||||
artworkCycle?.dispose();
|
||||
artworkCycle = createGeneratingArtworkCycle((variant) => {
|
||||
artworkVariant = variant;
|
||||
});
|
||||
artworkCycle.start();
|
||||
}
|
||||
|
||||
/** @param {number} ms */
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise((resolveWait) => setTimeout(resolveWait, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the structured fields the server now sends. Falls back to message
|
||||
* sniffing only if an older/unstructured error slips through.
|
||||
* @param {any} err
|
||||
*/
|
||||
function classify(err) {
|
||||
@@ -42,9 +79,6 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a task with a finite, classified retry policy: permanent errors stop
|
||||
* immediately, transient ones retry up to MAX_RETRIES respecting the
|
||||
* server-provided delay, and the real error is surfaced either way.
|
||||
* @template T
|
||||
* @param {string} label
|
||||
* @param {() => Promise<T>} task
|
||||
@@ -55,8 +89,8 @@
|
||||
|
||||
while (active) {
|
||||
try {
|
||||
status =
|
||||
attempt === 0 ? label : `Retrying ${label.toLowerCase()} (${attempt}/${MAX_RETRIES})...`;
|
||||
retryLabel =
|
||||
attempt === 0 ? '' : `Retrying ${label.toLowerCase()} (${attempt}/${MAX_RETRIES})…`;
|
||||
error = '';
|
||||
return await task();
|
||||
} catch (err) {
|
||||
@@ -68,7 +102,7 @@
|
||||
|
||||
attempt += 1;
|
||||
const seconds = Math.round(retryAfterMs / 1000);
|
||||
status = `AI provider is busy. Retrying in ${seconds}s (${attempt}/${MAX_RETRIES})...`;
|
||||
retryLabel = `AI provider is busy. Retrying in ${seconds}s (${attempt}/${MAX_RETRIES})…`;
|
||||
await wait(retryAfterMs);
|
||||
}
|
||||
}
|
||||
@@ -77,10 +111,15 @@
|
||||
}
|
||||
|
||||
async function runGeneration() {
|
||||
if (!progress) return;
|
||||
|
||||
canRetry = false;
|
||||
error = '';
|
||||
retryLabel = '';
|
||||
|
||||
const flow = loadFlow();
|
||||
const jobId = typeof flow.jobId === 'string' ? flow.jobId : '';
|
||||
const userInput = getFlowObject('userInput') ?? {};
|
||||
const sessionUserInput = getFlowObject('userInput') ?? {};
|
||||
|
||||
if (!jobId) {
|
||||
await goto(resolve('/create'));
|
||||
@@ -88,21 +127,27 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const estimatedMs = flow.mock ? MOCK_ESTIMATED_MS : DEFAULT_ESTIMATED_MS;
|
||||
progress.begin({ estimatedMs });
|
||||
|
||||
const existingRecipe = getFlowObject('recipe');
|
||||
if (!existingRecipe) {
|
||||
const recipeResult = await runWithRetry('Building bouquet recipe...', () =>
|
||||
buildRecipe(jobId, userInput)
|
||||
const recipeResult = await runWithRetry('Building bouquet recipe', () =>
|
||||
buildRecipe(jobId, sessionUserInput)
|
||||
);
|
||||
saveFlow({ recipe: recipeResult.recipe });
|
||||
}
|
||||
|
||||
const imageResult = await runWithRetry('Generating bouquet image...', () =>
|
||||
const imageResult = await runWithRetry('Generating bouquet image', () =>
|
||||
generateImages(jobId)
|
||||
);
|
||||
// Do NOT persist the multi-MB base64 images in sessionStorage — Safari caps
|
||||
// it at ~5MB and throws "QuotaExceededError: The quota has been exceeded."
|
||||
// The images already live in Supabase Storage via the job; the options
|
||||
// The images already live in Supabase Storage via the job; the edit
|
||||
// and result pages fetch them by jobId. We only keep lightweight metadata here.
|
||||
|
||||
await progress.finishWhenReady();
|
||||
|
||||
saveFlow({
|
||||
imagesJobId: jobId,
|
||||
imagePrompt: imageResult.imagePrompt,
|
||||
@@ -119,61 +164,65 @@
|
||||
const stale =
|
||||
code === 'job_not_found' || (err && typeof err === 'object' && err.status === 404);
|
||||
if (stale) {
|
||||
// Keep the user's entered context (relationship/occasion/etc.), drop the
|
||||
// dead job, and re-upload to mint a fresh one.
|
||||
const userInput = getFlowObject('userInput');
|
||||
const preservedInput = getFlowObject('userInput');
|
||||
clearFlow();
|
||||
if (userInput) saveFlow({ userInput });
|
||||
error = '';
|
||||
status = 'This session expired. Starting over...';
|
||||
if (preservedInput) saveFlow({ userInput: preservedInput });
|
||||
retryLabel = '';
|
||||
await goto(resolve('/upload'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { permanent } = classify(err);
|
||||
error = err instanceof Error ? err.message : 'Generation failed';
|
||||
status = permanent ? 'Generation is blocked.' : 'Still failing after several retries.';
|
||||
retryLabel = permanent ? 'Generation is blocked.' : 'Still failing after several retries.';
|
||||
canRetry = true;
|
||||
progress?.reset();
|
||||
}
|
||||
}
|
||||
|
||||
function retry() {
|
||||
if (!active) return;
|
||||
startArtworkCycle();
|
||||
runGeneration();
|
||||
}
|
||||
|
||||
function backToMessage() {
|
||||
goto(resolve('/message'));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
active = true;
|
||||
progress = createGenerationProgress((index) => {
|
||||
activeStepIndex = index;
|
||||
});
|
||||
startArtworkCycle();
|
||||
runGeneration();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
progress?.dispose();
|
||||
artworkCycle?.dispose();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-dvh bg-surface text-ink">
|
||||
<div
|
||||
class="flex h-dvh flex-col overflow-x-hidden bg-surface text-ink lg:h-screen lg:overflow-hidden"
|
||||
>
|
||||
<Header step={4} total={7} />
|
||||
|
||||
<main class="mx-auto flex max-w-xl flex-col items-start px-6 py-16">
|
||||
<h1 class="mb-3 text-2xl">Generating</h1>
|
||||
<p class="text-sm text-muted">{status}</p>
|
||||
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||
<Artwork comingSoon variant={artworkVariant} title={artworkTitle} description={artworkDescription} />
|
||||
|
||||
{#if error}
|
||||
<p class="mt-6 text-sm text-red-600">{error}</p>
|
||||
<div class="mt-4 flex gap-3">
|
||||
{#if canRetry}
|
||||
<button type="button" class="bg-pill px-4 py-2 text-sm text-surface" onclick={retry}>
|
||||
Try again
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="border border-pill px-4 py-2 text-sm text-ink"
|
||||
onclick={() => goto(resolve('/message'))}
|
||||
>
|
||||
Back to message
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<section class="relative flex min-h-0 flex-1 flex-col lg:overflow-y-auto">
|
||||
<GenerationActivityFeed
|
||||
{activeStepIndex}
|
||||
{error}
|
||||
{retryLabel}
|
||||
{canRetry}
|
||||
onRetry={retry}
|
||||
onBack={backToMessage}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
let floristNote = $state('');
|
||||
let fitMapBounds = $state(true);
|
||||
let orderPlainText = $state('');
|
||||
let orderSegments = $state([]);
|
||||
let orderKoPlainText = $state('');
|
||||
let selectedImage = $state(null);
|
||||
|
||||
const sessionUserInput = getFlowObject('userInput') ?? {};
|
||||
@@ -83,7 +83,7 @@
|
||||
recipe: job.recipe
|
||||
});
|
||||
orderPlainText = order.plainText;
|
||||
orderSegments = order.segments;
|
||||
orderKoPlainText = order.ko.plainText;
|
||||
} catch {
|
||||
// job 없어도 지도·꽃집 검색은 계속
|
||||
}
|
||||
@@ -108,7 +108,7 @@
|
||||
{error}
|
||||
{mock}
|
||||
{orderPlainText}
|
||||
{orderSegments}
|
||||
{orderKoPlainText}
|
||||
fitBounds={fitMapBounds}
|
||||
onrefresh={(lat, lng) => loadShops(lat, lng, { fitBounds: false })}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
let error = $state('');
|
||||
let skipping = $state(false);
|
||||
|
||||
const artworkVariant = $derived(message.trim() ? 'message1' : 'upload2');
|
||||
|
||||
const artworkTitle = $derived(message ? 'Your message' : 'Title');
|
||||
|
||||
const artworkDescription = $derived(message || 'Description Description Description');
|
||||
@@ -102,7 +104,7 @@
|
||||
<Header step={3} total={7} />
|
||||
|
||||
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||
<Artwork title={artworkTitle} description={artworkDescription} />
|
||||
<Artwork variant={artworkVariant} title={artworkTitle} description={artworkDescription} />
|
||||
|
||||
<section class="relative flex min-h-0 flex-1 flex-col lg:overflow-y-auto">
|
||||
<MessageForm bind:message />
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
: 'moodboard'
|
||||
);
|
||||
let primaryFile = $state(null);
|
||||
let filledCount = $state(0);
|
||||
let allFilled = $state(false);
|
||||
let loading = $state(false);
|
||||
let error = $state('');
|
||||
|
||||
@@ -34,6 +36,37 @@
|
||||
return 'their';
|
||||
});
|
||||
|
||||
const hasUserContext = $derived(
|
||||
Boolean(userInput.relationship || userInput.occasion || userInput.style)
|
||||
);
|
||||
|
||||
const artworkTitle = $derived.by(() => {
|
||||
const who = userInput.relationship;
|
||||
const whatFor = userInput.occasion;
|
||||
if (!hasUserContext) return 'Title';
|
||||
const occasion = whatFor ? `A ${whatFor} bouquet for` : 'A bouquet for';
|
||||
return `${occasion} ${who ?? '...'}`;
|
||||
});
|
||||
|
||||
const artworkDescription = $derived(
|
||||
hasUserContext
|
||||
? `${userInput.style ?? '—'} style · ₩${Number(userInput.budget ?? 50_000).toLocaleString('ko-KR')} budget`
|
||||
: 'Description Description Description'
|
||||
);
|
||||
|
||||
/** create2(시작) → upload1(1장+) → upload2(전체 채움) */
|
||||
const artworkVariant = $derived.by(() => {
|
||||
if (allFilled) return 'upload2';
|
||||
if (filledCount > 0) return 'upload1';
|
||||
return 'create2';
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
void mode;
|
||||
filledCount = 0;
|
||||
allFilled = false;
|
||||
});
|
||||
|
||||
async function continueToMessage() {
|
||||
error = '';
|
||||
|
||||
@@ -84,16 +117,26 @@
|
||||
>
|
||||
<Header step={2} total={7} />
|
||||
|
||||
<main class="flex min-h-0 flex-1 flex-col pt-6 lg:flex-row lg:pt-8">
|
||||
<Artwork />
|
||||
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||
<Artwork variant={artworkVariant} title={artworkTitle} description={artworkDescription} />
|
||||
|
||||
<section
|
||||
class="relative flex min-h-0 flex-1 flex-col pb-[4.75rem] lg:grid lg:grid-rows-[minmax(0,1fr)_auto] lg:overflow-hidden lg:pb-8"
|
||||
class="relative flex min-h-0 flex-1 flex-col pt-6 pb-[4.75rem] lg:grid lg:grid-rows-[minmax(0,1fr)_auto] lg:overflow-hidden lg:pt-8 lg:pb-8"
|
||||
>
|
||||
{#if mode === 'moodboard'}
|
||||
<MoodboardGrid bind:primaryFile caption={`build ${recipientPronoun} moodboard!`} />
|
||||
<MoodboardGrid
|
||||
bind:primaryFile
|
||||
bind:filledCount
|
||||
bind:allFilled
|
||||
caption={`build ${recipientPronoun} moodboard!`}
|
||||
/>
|
||||
{:else}
|
||||
<SnsFeedUpload bind:primaryFile caption={`upload ${recipientPronoun} feed!`} />
|
||||
<SnsFeedUpload
|
||||
bind:primaryFile
|
||||
bind:filledCount
|
||||
bind:allFilled
|
||||
caption={`upload ${recipientPronoun} feed!`}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user