@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,465 @@
1
+ "use client";
2
+
3
+ /**
4
+ * `DetailPageHost` — 편집기가 바깥 세상과 만나는 **유일한 자리**.
5
+ *
6
+ * 소싱 서버 호출, 브랜드 저장소, react-query 캐시 키, 알림. 편집기가 앱에 물릴 수 있는
7
+ * 것 전부가 이 인터페이스 하나로 들어온다. 소비자가 자기 base URL·토큰·캐시 키를 꽂으면
8
+ * 되므로, "프론트 라이브러리만 설치하고 백엔드는 카페24 소싱 서버를 계속 쓴다"가 성립한다.
9
+ *
10
+ * ## 앱 안에 살 때와 무엇이 달라졌나
11
+ *
12
+ * 프론트 안에 있을 때 이 파일은 앱의 소싱 API 모듈을 이름공간 타입으로 들여오는 문
13
+ * 열두 줄로 계약을 적었다. 이름만 고르고 시그니처는 앱에서 끌어오면 드리프트가 0이라
14
+ * 그게 옳았다 — **앱 안에 사는 동안에는**. 패키지는 그 모듈에 닿을 수 없으므로 이제
15
+ * 시그니처를 직접 적는다.
16
+ *
17
+ * 그렇다고 드리프트가 열린 것은 아니다. 앱이 자기 진짜 구현으로 이 인터페이스를 채울 때
18
+ * (`leviosa-frontend` 의 `src/lib/detail-page-host.ts`) tsc 가 대입 가능성을 잰다.
19
+ * 앱 쪽 시그니처가 바뀌면 그 자리에서 죽는다. 검사가 사라진 게 아니라 **자리를 옮겼다**.
20
+ *
21
+ * ## 세 종류가 아니라 한 종류
22
+ *
23
+ * 여기 오는 것은 전부 **데이터 접근과 알림**이다. 편집기가 쓰는 부품(색 입력·툴팁·폰트
24
+ * 목록)은 패키지가 동봉하고, 앱 셸의 크롬(크레딧·언어·요금제)은 노드로 꽂는다. 그 둘은
25
+ * 이 파일에 없다 — 섞이면 소비자가 무엇을 구현해야 하는지가 흐려진다.
26
+ */
27
+
28
+ import {
29
+ createContext,
30
+ useContext,
31
+ type ComponentType,
32
+ type ReactNode,
33
+ } from "react";
34
+
35
+ import type {
36
+ DesignReferenceInput,
37
+ DetailPageDesignBriefResult,
38
+ DetailPageEditQuotaDetail,
39
+ DetailPageEditUsageResult,
40
+ DetailPageGroupEditItem,
41
+ DetailPageGroupPromptEditResult,
42
+ DetailPageImageEditResult,
43
+ DetailPageInsufficientCreditsDetail,
44
+ DetailPagePersonalShapeSaveResult,
45
+ DetailPagePromptEditResult,
46
+ DetailPageReferenceImageUploadUrlResponse,
47
+ DetailPageSectionReauthorResult,
48
+ DetailPageShapeLibraryItem,
49
+ DetailPageSvgPromptEditResult,
50
+ DetailPageBrandReferenceItem,
51
+ } from "../../types/detail-page-api";
52
+ import type {
53
+ Cafe24ProductDetailPayload,
54
+ Cafe24ProductListPage,
55
+ Cafe24ProductListParams,
56
+ Cafe24ProductSummary,
57
+ Cafe24RefreshParams,
58
+ CoupangListingDetail,
59
+ CoupangListingListPage,
60
+ CoupangListListingsParams,
61
+ NaverProductDetailPayload,
62
+ NaverProductListPage,
63
+ NaverProductListParams,
64
+ } from "../../types/commerce";
65
+
66
+ export type * from "../../types/detail-page-api";
67
+ export type * from "../../types/commerce";
68
+
69
+ /**
70
+ * 소싱 서버.
71
+ *
72
+ * 이름 하나하나가 서버가 지켜야 하는 표면이다. base URL·인증·재시도·오류 변환은
73
+ * 구현이 안다 — 편집기는 부르고 결과를 받을 뿐이다.
74
+ */
75
+ export interface DetailPageHostApi {
76
+ // ── 생성·재저작 ──
77
+ reauthorDetailPageSection: (
78
+ generatedId: string,
79
+ payload: {
80
+ label: string;
81
+ instruction?: string;
82
+ annotated_image?: string;
83
+ reference_images?: (string | DesignReferenceInput)[];
84
+ current_height?: number;
85
+ template_id?: string;
86
+ },
87
+ signal?: AbortSignal,
88
+ ) => Promise<DetailPageSectionReauthorResult>;
89
+ encodeDetailPageAnimation: (
90
+ frames: Blob[],
91
+ options: { fps: number; format: "webp" | "gif" },
92
+ signal?: AbortSignal,
93
+ ) => Promise<Blob>;
94
+
95
+ // ── 프롬프트 편집(카피·이미지·SVG·그룹) ──
96
+ promptEditDetailPageCopy: (
97
+ generatedId: string,
98
+ payload: {
99
+ slot_role: string;
100
+ current_text: string;
101
+ instruction: string;
102
+ max_length?: number;
103
+ render_kind?: string;
104
+ },
105
+ signal?: AbortSignal,
106
+ ) => Promise<DetailPagePromptEditResult>;
107
+ promptEditDetailPageImage: (
108
+ generatedId: string,
109
+ payload: {
110
+ slot_role?: string;
111
+ current_image_url?: string;
112
+ current_image_base64?: string;
113
+ instruction: string;
114
+ annotated_image?: string;
115
+ tier?: string;
116
+ brand_id?: string;
117
+ },
118
+ signal?: AbortSignal,
119
+ ) => Promise<DetailPageImageEditResult>;
120
+ svgPromptEditDetailPage: (
121
+ generatedId: string,
122
+ payload: { slot_role?: string; current_svg: string; instruction: string },
123
+ signal?: AbortSignal,
124
+ ) => Promise<DetailPageSvgPromptEditResult>;
125
+ groupPromptEditDetailPage: (
126
+ generatedId: string,
127
+ payload: { instruction: string; items: DetailPageGroupEditItem[] },
128
+ signal?: AbortSignal,
129
+ ) => Promise<DetailPageGroupPromptEditResult>;
130
+
131
+ // ── 레퍼런스·저작물 ──
132
+ analyzeDetailPageDesignReferences: (
133
+ payload: {
134
+ references: (string | DesignReferenceInput)[];
135
+ instruction?: string;
136
+ },
137
+ signal?: AbortSignal,
138
+ ) => Promise<DetailPageDesignBriefResult>;
139
+ createDetailPageReferenceImageUploadUrl: (
140
+ payload: Record<string, unknown>,
141
+ signal?: AbortSignal,
142
+ ) => Promise<DetailPageReferenceImageUploadUrlResponse>;
143
+ listDetailPageBrandReferences: (
144
+ params: { brand_id: string; limit?: number; cursor?: string },
145
+ signal?: AbortSignal,
146
+ ) => Promise<{
147
+ items: DetailPageBrandReferenceItem[];
148
+ next_cursor: string | null;
149
+ }>;
150
+ saveDetailPageAsBrandReference: (
151
+ generatedId: string,
152
+ payload: { brand_id: string; display_name?: string },
153
+ signal?: AbortSignal,
154
+ ) => Promise<{
155
+ generated_id: string;
156
+ brand_id: string;
157
+ reference_group: string;
158
+ display_name: string;
159
+ assets: Record<string, unknown>[];
160
+ }>;
161
+
162
+ // ── 도형 라이브러리 ──
163
+ listDetailPageShapeLibrary: (
164
+ signal?: AbortSignal,
165
+ ) => Promise<DetailPageShapeLibraryItem[]>;
166
+ savePersonalDetailPageShape: (
167
+ payload: { svg?: string; svg_base64?: string; origin?: string },
168
+ signal?: AbortSignal,
169
+ ) => Promise<DetailPagePersonalShapeSaveResult>;
170
+
171
+ // ── 사용량·과금 오류 판별 ──
172
+ //
173
+ // 402/429 본문을 읽어 무엇이 부족한지 가리는 것은 **호스트의 일**이다. 편집기는
174
+ // "부족하다더라" 를 화면에 옮길 뿐이라, 판별 함수를 주입받는다.
175
+ getDetailPageEditUsage: (
176
+ generatedId: string,
177
+ signal?: AbortSignal,
178
+ ) => Promise<DetailPageEditUsageResult>;
179
+ asEditQuotaError: (err: unknown) => DetailPageEditQuotaDetail | null;
180
+ asInsufficientCreditsError: (
181
+ err: unknown,
182
+ ) => DetailPageInsufficientCreditsDetail | null;
183
+ /** 오류의 정체를 가릴 때 쓴다(`err instanceof host.api.SourcingApiError`). */
184
+ SourcingApiError: new (
185
+ message: string,
186
+ status: number,
187
+ detail?: unknown,
188
+ ) => Error & { status: number; detail?: unknown };
189
+
190
+ // ── 업로드 3루트 프리필(수동·카페24·네이버) ──
191
+ fetchCafe24Products: (
192
+ params?: Cafe24ProductListParams,
193
+ signal?: AbortSignal,
194
+ ) => Promise<Cafe24ProductListPage>;
195
+ fetchCafe24ProductDetail: (
196
+ productNo: number,
197
+ signal?: AbortSignal,
198
+ params?: Cafe24RefreshParams,
199
+ ) => Promise<Cafe24ProductDetailPayload>;
200
+ fetchCafe24ProductOptions: (
201
+ productNo: number,
202
+ shopNo?: number,
203
+ signal?: AbortSignal,
204
+ params?: Cafe24RefreshParams,
205
+ ) => Promise<unknown>;
206
+ fetchCafe24ProductCategories: (
207
+ productNo: number,
208
+ shopNo?: number,
209
+ signal?: AbortSignal,
210
+ params?: Cafe24RefreshParams,
211
+ ) => Promise<unknown>;
212
+ fetchNaverProducts: (
213
+ params?: NaverProductListParams,
214
+ signal?: AbortSignal,
215
+ ) => Promise<NaverProductListPage>;
216
+ fetchNaverProductDetail: (
217
+ originProductNo: number,
218
+ signal?: AbortSignal,
219
+ ) => Promise<NaverProductDetailPayload>;
220
+ }
221
+
222
+ // ── 브랜드 저장소 ──────────────────────────────────────────────
223
+
224
+ export type BrandAssetKind =
225
+ | "logo"
226
+ | "font"
227
+ | "image"
228
+ | "product_image"
229
+ | "model_image"
230
+ | "document"
231
+ | "svg"
232
+ | "gif"
233
+ | "other"
234
+ | "shape";
235
+
236
+ export type BrandAssetGifKind = "shape" | "text" | "image_effect" | "image_prompt";
237
+
238
+ /** 목록 필터: GIF만 / GIF를 뺀 나머지만. */
239
+ export type BrandAssetMedia = "gif" | "image";
240
+
241
+ export interface BrandWorkspaceBrand {
242
+ id: string;
243
+ name: string;
244
+ ownedCompanyId: string;
245
+ revision: number;
246
+ }
247
+
248
+ export interface BrandAsset {
249
+ id: string;
250
+ brand_id: string;
251
+ asset_type: BrandAssetKind;
252
+ status:
253
+ | "pending"
254
+ | "completing"
255
+ | "active"
256
+ | "deleting"
257
+ | "deleted"
258
+ | "delete_failed";
259
+ filename: string;
260
+ display_name: string | null;
261
+ content_type: string;
262
+ size_bytes: number | null;
263
+ s3_key: string;
264
+ s3_etag: string | null;
265
+ s3_version_id: string | null;
266
+ download_url: string | null;
267
+ /**
268
+ * 만료 없는 서명 읽기 경로(API 베이스 상대). 편집기 문서처럼 오래 남는 곳에는
269
+ * presigned URL(``download_url``) 대신 이걸 저장해야 나중에 403이 안 난다.
270
+ */
271
+ stable_path: string;
272
+ gif_kind: BrandAssetGifKind | null;
273
+ revision: number;
274
+ metadata: Record<string, unknown>;
275
+ created_by: string;
276
+ created_at: string;
277
+ updated_at: string;
278
+ deleted_at: string | null;
279
+ }
280
+
281
+ /**
282
+ * 무드보드와 브랜드킷은 **편집기가 안을 안 본다**. 불러서 킷으로 바꾸고 브랜드킷
283
+ * 슬롯(호스트가 꽂는 노드)에 그대로 넘길 뿐이다.
284
+ *
285
+ * 그래서 모양을 선언하지 않는다. 선언하면 오히려 깨진다 — `deriveBrandKit` 의 인자는
286
+ * 반변이라, 우리가 `unknown` 으로 적으면 앱이 자기 `Moodboard` 를 받는 함수를 못 꽂고,
287
+ * 좁게 적으면 이번엔 우리 호출부가 못 넘긴다. 나르기만 하는 값에 양방향으로 열린 타입을
288
+ * 주는 것이 정직하다.
289
+ */
290
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
291
+ export type BrandMoodboard = any;
292
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
293
+ export type BrandKit = any;
294
+
295
+ export interface DetailPageHostBrand {
296
+ listBrandAssets: (
297
+ brandId: string,
298
+ signal?: AbortSignal,
299
+ media?: BrandAssetMedia,
300
+ ) => Promise<BrandAsset[]>;
301
+ uploadBrandAsset: (
302
+ brandId: string,
303
+ file: File,
304
+ assetType: BrandAssetKind,
305
+ options?: {
306
+ displayName?: string;
307
+ metadata?: Record<string, unknown>;
308
+ signal?: AbortSignal;
309
+ },
310
+ ) => Promise<BrandAsset>;
311
+ deleteBrandAsset: (
312
+ asset: Pick<BrandAsset, "id" | "brand_id" | "revision">,
313
+ signal?: AbortSignal,
314
+ ) => Promise<BrandAsset>;
315
+ brandAssetDocumentSrc: (
316
+ asset: Pick<BrandAsset, "stable_path" | "download_url">,
317
+ ) => string;
318
+
319
+ /** 어느 브랜드로 작업 중인가. 훅이라 컴포넌트 안에서만 부른다. */
320
+ useBrandWorkspace: () => {
321
+ brands: BrandWorkspaceBrand[];
322
+ activeBrand: BrandWorkspaceBrand | null;
323
+ activeBrandId: string | null;
324
+ setActiveBrandId: (brandId: string) => void;
325
+ isLoading: boolean;
326
+ error: Error | null;
327
+ };
328
+ /** 훅을 못 쓰는 자리(모듈 함수)에서 활성 브랜드를 알아낼 때. */
329
+ getStoredActiveBrandId: () => string | null;
330
+
331
+ loadBrandMoodboard: (
332
+ brand: BrandWorkspaceBrand,
333
+ assets: BrandAsset[],
334
+ signal?: AbortSignal,
335
+ ) => Promise<BrandMoodboard>;
336
+ deriveBrandKit: (moodboard: BrandMoodboard) => BrandKit;
337
+ useBrandPrimaryColor: () => string;
338
+ }
339
+
340
+ // ── 상품 어댑터 ────────────────────────────────────────────────
341
+
342
+ /**
343
+ * 프리필이 커머스 응답을 읽을 때만 쓴다.
344
+ *
345
+ * 정규화 함수를 주입받는 이유: 카페24 옵션·카테고리 응답은 계정마다 모양이 달라
346
+ * 앱이 이미 방어적 정규화를 갖고 있다. 편집기가 그걸 다시 쓰면 두 벌이 갈라진다.
347
+ */
348
+ export interface DetailPageHostProduct {
349
+ getCafe24ProductNo: (product: Cafe24ProductSummary) => number;
350
+ extractCafe24ProductCategories: (payload: unknown) => unknown[];
351
+ extractCafe24ProductOptions: (payload: unknown) => unknown[];
352
+ /** 쿠팡만 소싱 서버가 아니라 자체 클라이언트다. 편집기가 부르는 둘만 요구한다. */
353
+ coupangApi: {
354
+ listListings: (
355
+ params: CoupangListListingsParams,
356
+ ) => Promise<CoupangListingListPage>;
357
+ getListing: (
358
+ productNo: string,
359
+ options?: { signal?: AbortSignal },
360
+ ) => Promise<CoupangListingDetail>;
361
+ };
362
+ }
363
+
364
+ // ── 알림 ──────────────────────────────────────────────────────
365
+
366
+ /**
367
+ * 이건 부품이 아니라 **호스트의 것**이다. 토스트는 모듈 수준 싱글턴 저장소 + 화면
368
+ * 구석의 오버레이 한 벌로 굴러가므로, 패키지가 자기 것을 동봉하면 소비자 앱에는
369
+ * 토스터가 두 개 뜬다 — 편집기 알림만 다른 자리에 다른 모양으로 쌓인다.
370
+ *
371
+ * 반환값을 `void` 로 둔 것은 편집기가 토스트 id 를 안 쓰기 때문이다. 앱이 id 를
372
+ * 돌려주는 구현을 꽂아도 그대로 들어온다.
373
+ */
374
+ export interface DetailPageHostToast {
375
+ success: (message: string) => void;
376
+ error: (message: string) => void;
377
+ info: (message: string) => void;
378
+ }
379
+
380
+ // ── 캐시 키 ────────────────────────────────────────────────────
381
+
382
+ /**
383
+ * 소비자가 자기 키 공간을 준다 — 안 그러면 같은 앱 안에서 편집기와 호스트가 서로의
384
+ * 캐시를 무효화한다.
385
+ */
386
+ export interface DetailPageHostQueryKeys {
387
+ branding: {
388
+ brandAssets: (
389
+ brandId: string | null,
390
+ media?: BrandAssetMedia,
391
+ ) => readonly unknown[];
392
+ brandMoodboard: (brandId: string | null) => readonly unknown[];
393
+ detailPageShapeLibrary: () => readonly unknown[];
394
+ detailPageStockPhotos: (query: string) => readonly unknown[];
395
+ detailPageIcons: (
396
+ query: string,
397
+ group: string,
398
+ style?: string,
399
+ ) => readonly unknown[];
400
+ };
401
+ }
402
+
403
+ // ── 앱 크롬 슬롯 ──────────────────────────────────────────────
404
+
405
+ /**
406
+ * 편집기 **안에서 열리지만 편집기 것이 아닌** 화면.
407
+ *
408
+ * 요금제 모달은 크레딧이 모자랄 때 편집기가 열지만, 무엇을 얼마에 파는지는 앱이 안다.
409
+ * 브랜드킷 패널도 마찬가지로 브랜드 도메인 화면이다. 그래서 컴포넌트를 받는다.
410
+ *
411
+ * 전부 옵셔널이다. 안 꽂으면 그 자리는 안 그려진다 — 요금제가 없는 소비자도 편집기를
412
+ * 띄울 수 있어야 하고, "요금제 모달이 없다"가 편집기가 못 뜨는 이유가 되면 안 된다.
413
+ */
414
+ export interface DetailPageHostSlots {
415
+ PricingModal?: ComponentType<{
416
+ open: boolean;
417
+ onOpenChange: (open: boolean) => void;
418
+ }>;
419
+ BrandKitPanel?: ComponentType<{
420
+ kit: BrandKit;
421
+ store: never;
422
+ className?: string;
423
+ }>;
424
+ }
425
+
426
+ export interface DetailPageHost {
427
+ api: DetailPageHostApi;
428
+ toast: DetailPageHostToast;
429
+ brand: DetailPageHostBrand;
430
+ product: DetailPageHostProduct;
431
+ queryKeys: DetailPageHostQueryKeys;
432
+ slots?: DetailPageHostSlots;
433
+ }
434
+
435
+ const DetailPageHostContext = createContext<DetailPageHost | null>(null);
436
+
437
+ export function DetailPageHostProvider({
438
+ host,
439
+ children,
440
+ }: {
441
+ host: DetailPageHost;
442
+ children: ReactNode;
443
+ }) {
444
+ return (
445
+ <DetailPageHostContext.Provider value={host}>
446
+ {children}
447
+ </DetailPageHostContext.Provider>
448
+ );
449
+ }
450
+
451
+ /**
452
+ * 편집기 안에서 바깥을 부를 때 쓴다.
453
+ *
454
+ * 없으면 던진다. 조용히 폴백을 만들면 "호스트를 안 꽂았다"는 사실이 런타임 한참
455
+ * 뒤에야, 그것도 엉뚱한 증상으로 나타난다.
456
+ */
457
+ export function useDetailPageHost(): DetailPageHost {
458
+ const host = useContext(DetailPageHostContext);
459
+ if (!host) {
460
+ throw new Error(
461
+ "DetailPageHostProvider 안에서만 쓸 수 있습니다. 편집기를 띄우는 화면에서 host를 꽂아 주세요.",
462
+ );
463
+ }
464
+ return host;
465
+ }