@leviosa-ai/detail-page-editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/components/cardnews/color-input.tsx +380 -0
  2. package/components/detail-page/ai-generate-panel.tsx +763 -0
  3. package/components/detail-page/annotation-canvas.tsx +860 -0
  4. package/components/detail-page/annotation-dialog.tsx +397 -0
  5. package/components/detail-page/bubble-tail-overlay.tsx +209 -0
  6. package/components/detail-page/canvas-context-menu.tsx +234 -0
  7. package/components/detail-page/canvas-observer.tsx +68 -0
  8. package/components/detail-page/canvas-overlay-host.tsx +44 -0
  9. package/components/detail-page/canvas-store-facade.ts +51 -0
  10. package/components/detail-page/chart-inspector.tsx +531 -0
  11. package/components/detail-page/design-reference-picker.tsx +383 -0
  12. package/components/detail-page/detail-page-basic-shapes.tsx +180 -0
  13. package/components/detail-page/detail-page-brand-gifs-panel.tsx +261 -0
  14. package/components/detail-page/detail-page-brand-kit-panel.tsx +77 -0
  15. package/components/detail-page/detail-page-brand-panel-header.tsx +44 -0
  16. package/components/detail-page/detail-page-charts-panel.tsx +104 -0
  17. package/components/detail-page/detail-page-decorations-panel.tsx +125 -0
  18. package/components/detail-page/detail-page-download-dialog.tsx +524 -0
  19. package/components/detail-page/detail-page-editor.tsx +974 -0
  20. package/components/detail-page/detail-page-elements-panel.tsx +117 -0
  21. package/components/detail-page/detail-page-font-picker.tsx +231 -0
  22. package/components/detail-page/detail-page-history-buttons.tsx +87 -0
  23. package/components/detail-page/detail-page-host-context.tsx +465 -0
  24. package/components/detail-page/detail-page-icons-panel.tsx +251 -0
  25. package/components/detail-page/detail-page-konva-json-preview.tsx +474 -0
  26. package/components/detail-page/detail-page-layers-panel.tsx +618 -0
  27. package/components/detail-page/detail-page-my-images-panel.tsx +179 -0
  28. package/components/detail-page/detail-page-my-shapes-panel.tsx +174 -0
  29. package/components/detail-page/detail-page-page-toolbar.tsx +171 -0
  30. package/components/detail-page/detail-page-pages-panel.tsx +299 -0
  31. package/components/detail-page/detail-page-pages-timeline.tsx +79 -0
  32. package/components/detail-page/detail-page-photos-panel.tsx +257 -0
  33. package/components/detail-page/detail-page-properties-panel.tsx +2680 -0
  34. package/components/detail-page/detail-page-qr-panel.tsx +137 -0
  35. package/components/detail-page/detail-page-references-panel.tsx +166 -0
  36. package/components/detail-page/detail-page-selection.ts +44 -0
  37. package/components/detail-page/detail-page-shapes-panel.tsx +16 -0
  38. package/components/detail-page/detail-page-sidebar-sections.tsx +324 -0
  39. package/components/detail-page/detail-page-tables-panel.tsx +106 -0
  40. package/components/detail-page/detail-page-text-panel.tsx +179 -0
  41. package/components/detail-page/detail-page-thumbnail-bus.ts +42 -0
  42. package/components/detail-page/edit-quota-ui.tsx +150 -0
  43. package/components/detail-page/editor-history.ts +56 -0
  44. package/components/detail-page/editor-hotkeys.tsx +165 -0
  45. package/components/detail-page/element-recents-strip.tsx +105 -0
  46. package/components/detail-page/element-rects.ts +82 -0
  47. package/components/detail-page/fill-control.tsx +261 -0
  48. package/components/detail-page/find-replace-panel.tsx +252 -0
  49. package/components/detail-page/generation-progress-fill.tsx +57 -0
  50. package/components/detail-page/gif-animator.tsx +184 -0
  51. package/components/detail-page/gif-effect-picker.tsx +136 -0
  52. package/components/detail-page/group-drill-in.tsx +219 -0
  53. package/components/detail-page/group-prompt-edit-panel.tsx +154 -0
  54. package/components/detail-page/hover-highlight-overlay.tsx +111 -0
  55. package/components/detail-page/hovered-layer.ts +34 -0
  56. package/components/detail-page/inspector-controls.tsx +175 -0
  57. package/components/detail-page/leviosa-canvas-workspace.tsx +313 -0
  58. package/components/detail-page/nested-selection-overlay.tsx +364 -0
  59. package/components/detail-page/panel-search-input.tsx +49 -0
  60. package/components/detail-page/prompt-edit-panel.tsx +146 -0
  61. package/components/detail-page/section-height-handle.tsx +276 -0
  62. package/components/detail-page/section-reauthor-controller.tsx +210 -0
  63. package/components/detail-page/spec-prompt-edit-panel.tsx +178 -0
  64. package/components/detail-page/spec-resize-absorber.tsx +149 -0
  65. package/components/detail-page/svg-prompt-edit-panel.tsx +139 -0
  66. package/components/detail-page/table-canvas-overlay.tsx +707 -0
  67. package/components/detail-page/table-inspector.tsx +586 -0
  68. package/components/ui/button.tsx +64 -0
  69. package/components/ui/command.tsx +175 -0
  70. package/components/ui/dialog.tsx +167 -0
  71. package/components/ui/label.tsx +24 -0
  72. package/components/ui/select.tsx +190 -0
  73. package/components/ui/slider.tsx +63 -0
  74. package/components/ui/switch.tsx +35 -0
  75. package/components/ui/tooltip.tsx +57 -0
  76. package/config/detail-page-bundle-font-tags.json +39 -0
  77. package/config/detail-page-fonts.json +373 -0
  78. package/index.ts +24 -0
  79. package/lib/cardnews/font-loader.ts +306 -0
  80. package/lib/detail-page/animation-sniff.ts +101 -0
  81. package/lib/detail-page/asset-bytes-url.ts +70 -0
  82. package/lib/detail-page/basic-shapes.ts +603 -0
  83. package/lib/detail-page/canvas-menu.ts +182 -0
  84. package/lib/detail-page/chart/defaults.ts +283 -0
  85. package/lib/detail-page/chart/format.ts +66 -0
  86. package/lib/detail-page/chart/normalize.ts +149 -0
  87. package/lib/detail-page/chart/palette.ts +85 -0
  88. package/lib/detail-page/chart/paste.ts +82 -0
  89. package/lib/detail-page/chart/preview.ts +78 -0
  90. package/lib/detail-page/chart/render.ts +188 -0
  91. package/lib/detail-page/chart/renderers/bar-h.ts +103 -0
  92. package/lib/detail-page/chart/renderers/bar-v.ts +213 -0
  93. package/lib/detail-page/chart/renderers/donut.ts +186 -0
  94. package/lib/detail-page/chart/renderers/gauge.ts +96 -0
  95. package/lib/detail-page/chart/renderers/line.ts +143 -0
  96. package/lib/detail-page/chart/renderers/shared.ts +263 -0
  97. package/lib/detail-page/chart/renderers/stack.ts +121 -0
  98. package/lib/detail-page/chart/sync.ts +92 -0
  99. package/lib/detail-page/chart/types.ts +145 -0
  100. package/lib/detail-page/css-color.ts +34 -0
  101. package/lib/detail-page/data-gif-payload.ts +167 -0
  102. package/lib/detail-page/design-reference.ts +244 -0
  103. package/lib/detail-page/distribute.ts +89 -0
  104. package/lib/detail-page/editor-routes.ts +10 -0
  105. package/lib/detail-page/element-recents.ts +167 -0
  106. package/lib/detail-page/find-replace.ts +120 -0
  107. package/lib/detail-page/format-painter.ts +159 -0
  108. package/lib/detail-page/gif-frames.ts +216 -0
  109. package/lib/detail-page/gif-timeline.ts +65 -0
  110. package/lib/detail-page/group-action.ts +62 -0
  111. package/lib/detail-page/hangul-roman.ts +58 -0
  112. package/lib/detail-page/icon-curated.ts +290 -0
  113. package/lib/detail-page/icon-fuzzy.ts +250 -0
  114. package/lib/detail-page/icon-keywords.generated.ts +3766 -0
  115. package/lib/detail-page/icon-keywords.ko.ts +524 -0
  116. package/lib/detail-page/icon-search.ts +257 -0
  117. package/lib/detail-page/icons.ts +67 -0
  118. package/lib/detail-page/image-credit.ts +121 -0
  119. package/lib/detail-page/image-data-uri.ts +29 -0
  120. package/lib/detail-page/insert-image.ts +75 -0
  121. package/lib/detail-page/insert-shape.ts +49 -0
  122. package/lib/detail-page/korean-dictionary.ts +187 -0
  123. package/lib/detail-page/layer-move.ts +257 -0
  124. package/lib/detail-page/new-product-draft-store.ts +105 -0
  125. package/lib/detail-page/new-product-draft.ts +283 -0
  126. package/lib/detail-page/personal-images-refresh.ts +30 -0
  127. package/lib/detail-page/product-source.ts +587 -0
  128. package/lib/detail-page/qr-code.ts +214 -0
  129. package/lib/detail-page/reference-image.ts +103 -0
  130. package/lib/detail-page/reference-upload.ts +181 -0
  131. package/lib/detail-page/replace-page.ts +35 -0
  132. package/lib/detail-page/replace-with-gif.ts +251 -0
  133. package/lib/detail-page/section-height.ts +131 -0
  134. package/lib/detail-page/section-reauthor-bus.ts +61 -0
  135. package/lib/detail-page/shape-to-image.ts +219 -0
  136. package/lib/detail-page/spec-group/clipboard.ts +35 -0
  137. package/lib/detail-page/spec-group/sync.ts +377 -0
  138. package/lib/detail-page/spec-group/text-metrics.ts +48 -0
  139. package/lib/detail-page/stock-photos.ts +88 -0
  140. package/lib/detail-page/svg-colors.ts +74 -0
  141. package/lib/detail-page/table/defaults.ts +344 -0
  142. package/lib/detail-page/table/edit.ts +312 -0
  143. package/lib/detail-page/table/harvest.ts +103 -0
  144. package/lib/detail-page/table/normalize.ts +125 -0
  145. package/lib/detail-page/table/paste.ts +49 -0
  146. package/lib/detail-page/table/preview.ts +66 -0
  147. package/lib/detail-page/table/render.ts +388 -0
  148. package/lib/detail-page/table/sync.ts +107 -0
  149. package/lib/detail-page/table/types.ts +102 -0
  150. package/lib/detail-page/text-gif-layout.ts +245 -0
  151. package/lib/detail-page/text-gif-payload.ts +67 -0
  152. package/lib/detail-page/text-presets.ts +230 -0
  153. package/lib/detail-page/use-fake-progress.ts +67 -0
  154. package/lib/detail-page/z-order.ts +82 -0
  155. package/lib/detail-page-canvas/bubble-path.ts +190 -0
  156. package/lib/detail-page-canvas/canvas-key.ts +22 -0
  157. package/lib/detail-page-canvas/clip-rect.ts +65 -0
  158. package/lib/detail-page-canvas/custom-props-adapter.ts +219 -0
  159. package/lib/detail-page-canvas/editor-fonts.ts +161 -0
  160. package/lib/detail-page-canvas/export/ai.ts +413 -0
  161. package/lib/detail-page-canvas/export/color.ts +35 -0
  162. package/lib/detail-page-canvas/export/document-model.ts +78 -0
  163. package/lib/detail-page-canvas/export/export-files.ts +290 -0
  164. package/lib/detail-page-canvas/export/font-subset.ts +264 -0
  165. package/lib/detail-page-canvas/export/gif-export.ts +310 -0
  166. package/lib/detail-page-canvas/export/gif-plan.ts +95 -0
  167. package/lib/detail-page-canvas/export/mp4-encode.ts +158 -0
  168. package/lib/detail-page-canvas/export/mp4-support.ts +22 -0
  169. package/lib/detail-page-canvas/export/pdf/geometry.ts +309 -0
  170. package/lib/detail-page-canvas/export/pdf/inline-svg.ts +399 -0
  171. package/lib/detail-page-canvas/export/pdf/resources.ts +443 -0
  172. package/lib/detail-page-canvas/export/pdf/text-ops.ts +81 -0
  173. package/lib/detail-page-canvas/export/pdf/writer.ts +109 -0
  174. package/lib/detail-page-canvas/export/psd.ts +354 -0
  175. package/lib/detail-page-canvas/export/raster.ts +255 -0
  176. package/lib/detail-page-canvas/export/svg.ts +479 -0
  177. package/lib/detail-page-canvas/export/text-layout.ts +142 -0
  178. package/lib/detail-page-canvas/font-catalog.ts +177 -0
  179. package/lib/detail-page-canvas/font-tags.ts +47 -0
  180. package/lib/detail-page-canvas/gif-web-fonts.ts +90 -0
  181. package/lib/detail-page-canvas/konva-fallback.ts +477 -0
  182. package/lib/detail-page-canvas/text-background-plan.ts +124 -0
  183. package/lib/detail-page-canvas/text-highlight-bands.ts +127 -0
  184. package/lib/utils.ts +10 -0
  185. package/package.json +68 -0
  186. package/types/commerce.ts +171 -0
  187. package/types/detail-page-api.ts +200 -0
  188. package/types/detail-page-canvas.ts +20 -0
  189. package/types/fontkit.d.ts +42 -0
  190. package/types/gifenc.d.ts +51 -0
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ import { encodeSvgDataUri } from "../../lib/detail-page-canvas/export/svg";
7
+ import { insertShape } from "../../lib/detail-page/insert-shape";
8
+ import { rememberElement } from "../../lib/detail-page/element-recents";
9
+ import { ean13Svg, qrCodeSvg } from "../../lib/detail-page/qr-code";
10
+ import { ColorInput } from "../cardnews/color-input";
11
+
12
+ /**
13
+ * "요소 · QR" 그룹 — QR 코드와 EAN-13 바코드.
14
+ *
15
+ * **서버도 크레딧도 안 든다.** 전부 브라우저 안에서 SVG로 만들어 도형과 같은 길
16
+ * (`insertShape`)로 넣는다. 그래서 우측 색 컨트롤도 그대로 붙는다.
17
+ *
18
+ * 미리보기와 실제 삽입이 **같은 마크업**을 쓴다 — 눌러 보고 다른 것이 나오면 안 된다
19
+ * (기본 도형 패널이 세운 규칙과 같다).
20
+ */
21
+
22
+ type CodeKind = "qr" | "ean13";
23
+
24
+ export function DetailPageQrPanel({ store }: { store: unknown }) {
25
+ const { t } = useTranslation("branding");
26
+ const [kind, setKind] = useState<CodeKind>("qr");
27
+ const [value, setValue] = useState("");
28
+ const [foreground, setForeground] = useState("#111111");
29
+ const [background, setBackground] = useState("#ffffff");
30
+
31
+ const built = useMemo((): {
32
+ markup: string;
33
+ viewBox: string;
34
+ /** EAN-13일 때만 — 검증번호까지 채운 실제 값. */
35
+ value?: string;
36
+ } | null => {
37
+ const colors = { foreground, background };
38
+ return kind === "qr" ? qrCodeSvg(value, colors) : ean13Svg(value, colors);
39
+ }, [kind, value, foreground, background]);
40
+
41
+ const trimmed = value.trim();
42
+ const invalid = trimmed.length > 0 && built === null;
43
+
44
+ return (
45
+ <div className="flex h-full flex-col gap-3 overflow-y-auto p-3">
46
+ <div role="group" aria-label={t("detailPage.qr.kindLabel")} className="flex rounded-md border border-neutral-200 p-0.5">
47
+ {(["qr", "ean13"] as const).map((option) => {
48
+ const active = option === kind;
49
+ return (
50
+ <button
51
+ key={option}
52
+ type="button"
53
+ aria-pressed={active}
54
+ onClick={() => setKind(option)}
55
+ className={`flex-1 rounded px-2 py-1 text-[11px] font-medium transition-colors ${
56
+ active ? "bg-neutral-900 text-white" : "text-neutral-500 hover:bg-neutral-100"
57
+ }`}
58
+ >
59
+ {t(option === "qr" ? "detailPage.qr.kindQr" : "detailPage.qr.kindEan")}
60
+ </button>
61
+ );
62
+ })}
63
+ </div>
64
+
65
+ <label className="grid gap-1 text-[11px] font-medium text-neutral-500">
66
+ {t(kind === "qr" ? "detailPage.qr.valueLabel" : "detailPage.qr.eanLabel")}
67
+ <input
68
+ type="text"
69
+ value={value}
70
+ onChange={(event) => setValue(event.target.value)}
71
+ placeholder={t(
72
+ kind === "qr" ? "detailPage.qr.valuePlaceholder" : "detailPage.qr.eanPlaceholder",
73
+ )}
74
+ inputMode={kind === "ean13" ? "numeric" : "text"}
75
+ className="rounded-lg border border-neutral-200 px-2.5 py-2 text-sm placeholder:text-neutral-400 focus:border-neutral-400 focus:outline-none"
76
+ />
77
+ </label>
78
+
79
+ {invalid ? (
80
+ <p className="text-xs text-red-600">
81
+ {t(kind === "qr" ? "detailPage.qr.qrInvalid" : "detailPage.qr.eanInvalid")}
82
+ </p>
83
+ ) : null}
84
+
85
+ <div className="grid grid-cols-2 gap-2">
86
+ <label className="grid gap-1 text-[11px] font-medium text-neutral-500">
87
+ {t("detailPage.qr.foreground")}
88
+ <ColorInput value={foreground} onChange={setForeground} />
89
+ </label>
90
+ <label className="grid gap-1 text-[11px] font-medium text-neutral-500">
91
+ {t("detailPage.qr.background")}
92
+ <ColorInput value={background} onChange={setBackground} />
93
+ </label>
94
+ </div>
95
+
96
+ <div className="flex min-h-24 items-center justify-center rounded-lg border border-dashed border-neutral-200 p-3">
97
+ {built ? (
98
+ /* eslint-disable-next-line @next/next/no-img-element */
99
+ <img
100
+ src={encodeSvgDataUri(built.markup)}
101
+ alt={t("detailPage.qr.previewAlt")}
102
+ className="max-h-40 max-w-full object-contain"
103
+ />
104
+ ) : (
105
+ <p className="text-xs text-neutral-400">{t("detailPage.qr.previewEmpty")}</p>
106
+ )}
107
+ </div>
108
+
109
+ {/* EAN-13은 12자리를 넣으면 체크디짓을 채워 준다. 무엇이 들어갔는지 보여 준다. */}
110
+ {built?.value ? (
111
+ <p className="text-center text-xs tabular-nums text-neutral-500">{built.value}</p>
112
+ ) : null}
113
+
114
+ <button
115
+ type="button"
116
+ disabled={!built}
117
+ onClick={() => {
118
+ if (!built) return;
119
+ insertShape(store, built.markup, built.viewBox);
120
+ rememberElement({
121
+ key: `${kind}:${trimmed}`,
122
+ markup: built.markup,
123
+ viewBox: built.viewBox,
124
+ label: trimmed,
125
+ });
126
+ }}
127
+ className="rounded-lg bg-neutral-900 px-3 py-2 text-sm font-semibold text-white hover:bg-neutral-800 disabled:cursor-not-allowed disabled:bg-neutral-200 disabled:text-neutral-400"
128
+ >
129
+ {t("detailPage.qr.insert")}
130
+ </button>
131
+
132
+ <p className="mt-auto px-1 text-[11px] leading-relaxed text-neutral-500">
133
+ {t("detailPage.qr.hint")}
134
+ </p>
135
+ </div>
136
+ );
137
+ }
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { BookmarkPlus, ImageOff, Loader2 } from "lucide-react";
6
+
7
+ import { BrandPanelHeader } from "./detail-page-brand-panel-header";
8
+ import { useDetailPageHost } from "./detail-page-host-context";
9
+ import type { DetailPageBrandReferenceItem } from "./detail-page-host-context";
10
+
11
+ /**
12
+ * 내 레퍼런스 — 저작한 상세페이지를 브랜드 버킷에 넣고, 다음 상품에서 다시 꺼내 쓴다.
13
+ *
14
+ * 공들여 만든 페이지는 다음 상품에서 참고하고 싶은 **자기 레퍼런스**다. 그런데 편집기를
15
+ * 닫으면 그 화면은 그 인스턴스 안에만 남아서, 다음 상품을 만들 때 다시 찾아 열어야
16
+ * 보인다. 여기 저장해 두면 브랜드 자산으로 남아 어느 상세페이지에서든 꺼내 쓸 수 있다.
17
+ *
18
+ * ## 화면 캡쳐와 편집기 문서를 함께 저장한다
19
+ *
20
+ * 서버가 화면별 PNG 와 편집기 문서(JSON) 둘 다 넣는다. 캡쳐만 남기면 "이런 느낌"까지만
21
+ * 되돌릴 수 있고 좌표·슬롯·폰트는 사라진다 — 그건 템플릿화가 아니라 스크린샷 모으기다.
22
+ * 다만 **여기 목록에 거는 것은 캡쳐(role=screen)뿐이다.** 문서는 그림이 아니라서 걸면
23
+ * 깨진 썸네일이 된다.
24
+ *
25
+ * ## 왜 저장이 렌더를 요구하는가
26
+ *
27
+ * 서버는 화면 그림을 새로 굽지 않고 저장·렌더가 만들어 둔 것을 옮긴다 — 셀러가 마지막으로
28
+ * 본 화면과 같은 그림이어야 하기 때문이다. 그래서 한 번도 저장하지 않은 문서는 409 로
29
+ * 돌아온다. 그 경우 "먼저 저장하세요"라고 말해 준다.
30
+ */
31
+
32
+ const REFERENCES_QUERY_ROOT = "detail-page-brand-references";
33
+
34
+ export function DetailPageReferencesPanel({
35
+ generatedId,
36
+ }: {
37
+ /** 지금 편집 중인 상세페이지. 없으면 저장 버튼 없이 목록만 보여 준다. */
38
+ generatedId?: string;
39
+ }) {
40
+ const { api, brand, toast } = useDetailPageHost();
41
+ const queryClient = useQueryClient();
42
+ const { activeBrand, activeBrandId, isLoading: brandsLoading } = brand.useBrandWorkspace();
43
+ const [error, setError] = useState<string | null>(null);
44
+
45
+ const listQuery = useQuery({
46
+ queryKey: [REFERENCES_QUERY_ROOT, activeBrandId],
47
+ queryFn: ({ signal }) =>
48
+ api.listDetailPageBrandReferences({ brand_id: activeBrandId! }, signal),
49
+ enabled: Boolean(activeBrandId),
50
+ staleTime: 60_000,
51
+ });
52
+
53
+ /** 한 상세페이지에서 나온 캡쳐를 묶어 보여 준다 — 낱장으로 흩어 두면 무엇의 몇 번째 화면인지 모른다. */
54
+ const groups = useMemo(() => {
55
+ const screens = (listQuery.data?.items ?? []).filter(
56
+ (item) => item.role === "screen",
57
+ );
58
+ const byGroup = new Map<string, DetailPageBrandReferenceItem[]>();
59
+ for (const item of screens) {
60
+ const key = item.reference_group || item.asset_id;
61
+ byGroup.set(key, [...(byGroup.get(key) ?? []), item]);
62
+ }
63
+ return [...byGroup.entries()].map(([key, items]) => ({
64
+ key,
65
+ name: items[0]?.reference_name || "상세페이지",
66
+ items: [...items].sort((a, b) => a.screen_index - b.screen_index),
67
+ }));
68
+ }, [listQuery.data]);
69
+
70
+ const saveMutation = useMutation({
71
+ mutationFn: async () => {
72
+ if (!generatedId) throw new Error("no-instance");
73
+ if (!activeBrandId) throw new Error("brand-not-selected");
74
+ return api.saveDetailPageAsBrandReference(generatedId, { brand_id: activeBrandId });
75
+ },
76
+ onSuccess: async (result) => {
77
+ setError(null);
78
+ const screens = result.assets.filter((asset) => asset.role === "screen").length;
79
+ toast.success(`레퍼런스로 저장했어요 (화면 ${screens}장).`);
80
+ await queryClient.invalidateQueries({
81
+ queryKey: [REFERENCES_QUERY_ROOT, activeBrandId],
82
+ });
83
+ },
84
+ onError: (err) => {
85
+ setError(
86
+ err instanceof Error && err.message !== "no-instance"
87
+ ? err.message
88
+ : "레퍼런스로 저장하지 못했어요.",
89
+ );
90
+ },
91
+ });
92
+
93
+ return (
94
+ <div className="flex h-full flex-col p-4">
95
+ <BrandPanelHeader onRefresh={() => void listQuery.refetch()} />
96
+
97
+ {generatedId ? (
98
+ <button
99
+ type="button"
100
+ disabled={!activeBrandId || saveMutation.isPending}
101
+ onClick={() => saveMutation.mutate()}
102
+ className="mb-3 flex h-16 w-full flex-col items-center justify-center gap-1 rounded-md border border-dashed border-neutral-300 text-neutral-400 transition-colors hover:border-neutral-400 hover:text-neutral-600 disabled:opacity-40"
103
+ >
104
+ {saveMutation.isPending ? (
105
+ <Loader2 aria-hidden="true" className="animate-spin" size={18} />
106
+ ) : (
107
+ <BookmarkPlus aria-hidden="true" size={18} />
108
+ )}
109
+ <span className="text-[11px]">이 상세페이지를 레퍼런스로 저장</span>
110
+ </button>
111
+ ) : null}
112
+
113
+ {error ? (
114
+ <p className="mb-2 text-xs font-medium text-red-600">{error}</p>
115
+ ) : null}
116
+
117
+ {brandsLoading || listQuery.isLoading ? (
118
+ <div className="flex flex-1 items-center justify-center text-neutral-400">
119
+ <Loader2 aria-hidden="true" className="animate-spin" size={22} />
120
+ </div>
121
+ ) : !activeBrand ? (
122
+ <p className="text-xs text-neutral-400">브랜드를 먼저 골라 주세요.</p>
123
+ ) : listQuery.error ? (
124
+ <p className="text-xs font-medium text-red-600">
125
+ 레퍼런스를 불러오지 못했어요.
126
+ </p>
127
+ ) : groups.length === 0 ? (
128
+ <div className="flex flex-1 flex-col items-center justify-center gap-2 text-center text-neutral-400">
129
+ <ImageOff aria-hidden="true" size={22} />
130
+ <p className="text-xs">
131
+ 아직 저장한 레퍼런스가 없어요. 마음에 드는 상세페이지를 저장해 두면 다음
132
+ 상품에서 바로 참고할 수 있어요.
133
+ </p>
134
+ </div>
135
+ ) : (
136
+ <div className="space-y-4 overflow-y-auto">
137
+ {groups.map((group) => (
138
+ <section key={group.key}>
139
+ <h3 className="mb-1.5 truncate text-[11px] font-medium text-neutral-600">
140
+ {group.name}
141
+ <span className="ml-1 text-neutral-400">{group.items.length}장</span>
142
+ </h3>
143
+ <div className="grid grid-cols-2 gap-2">
144
+ {group.items.map((item) => (
145
+ <figure
146
+ key={item.asset_id}
147
+ className="overflow-hidden rounded-md border border-neutral-200"
148
+ title={item.screen_label || item.display_name}
149
+ >
150
+ {/* eslint-disable-next-line @next/next/no-img-element */}
151
+ <img
152
+ src={item.url}
153
+ crossOrigin="anonymous"
154
+ alt={item.display_name}
155
+ className="h-24 w-full object-cover object-top"
156
+ />
157
+ </figure>
158
+ ))}
159
+ </div>
160
+ </section>
161
+ ))}
162
+ </div>
163
+ )}
164
+ </div>
165
+ );
166
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Selection resolver that can see INSIDE groups.
3
+ *
4
+ * the stock editor's ``store.selectedElements`` getter only scans each page's *direct*
5
+ * children, so an id that belongs to a group child resolves to nothing — the
6
+ * element reads as "not selected" even though ``store.selectElements([id])``
7
+ * stored it. (The canvas reinforces this: a click resolves the hit node to
8
+ * ``el.top``, the outermost non-group ancestor, so the stock editor's selection only ever
9
+ * holds top-level ids.)
10
+ *
11
+ * ``store.getElementById`` is backed by ``_idsMap``, which IS built by walking
12
+ * the whole tree, groups included. Resolving ``selectedElementsIds`` through it
13
+ * therefore yields nested elements too — which is what lets the layers tree
14
+ * select a group child and the properties inspector edit it.
15
+ *
16
+ * Both reads stay mobx-reactive, so ``observer`` components re-render on
17
+ * selection change exactly as they did with the stock getter.
18
+ */
19
+
20
+ export type SelectableElement = {
21
+ id: string;
22
+ type: string;
23
+ set: (props: Record<string, unknown>) => void;
24
+ [key: string]: unknown;
25
+ };
26
+
27
+ type SelectionStore = {
28
+ selectedElementsIds?: string[];
29
+ selectedElements?: SelectableElement[];
30
+ getElementById?: (id: string) => SelectableElement | undefined;
31
+ };
32
+
33
+ export function selectedElementsDeep(store: SelectionStore): SelectableElement[] {
34
+ const ids = store.selectedElementsIds ?? [];
35
+ const byId = store.getElementById;
36
+ if (ids.length && byId) {
37
+ const resolved = ids
38
+ .map((id) => byId(id))
39
+ .filter((el): el is SelectableElement => !!el);
40
+ if (resolved.length) return resolved;
41
+ }
42
+ // Older/foreign stores without _idsMap fall back to the stock getter.
43
+ return store.selectedElements ?? [];
44
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import { DetailPageBasicShapes } from "./detail-page-basic-shapes";
4
+
5
+ /**
6
+ * "도형" 패널 — 기본 도형 카탈로그 하나만 있다.
7
+ *
8
+ * 예전에는 여기 아래에 공용 도형 라이브러리가 같이 붙어 있었다. 그 라이브러리는 성격이
9
+ * 셋으로 갈린다(아이콘 · 배지 · 선 장식). 아이콘은 이제 "아이콘" 그룹이 2만 개를 검색으로
10
+ * 덮으므로 여기 36개가 남아 있을 이유가 없고, 배지·선 장식은 **도형이 아니다** —
11
+ * 도형은 자리를 만드는 것이고 장식은 완성된 그림이다. 그래서 "장식" 그룹으로 나갔다.
12
+ */
13
+
14
+ export function DetailPageShapesPanel({ store }: { store: unknown }) {
15
+ return <DetailPageBasicShapes store={store} />;
16
+ }