@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.
- package/components/cardnews/color-input.tsx +380 -0
- package/components/detail-page/ai-generate-panel.tsx +763 -0
- package/components/detail-page/annotation-canvas.tsx +860 -0
- package/components/detail-page/annotation-dialog.tsx +397 -0
- package/components/detail-page/bubble-tail-overlay.tsx +209 -0
- package/components/detail-page/canvas-context-menu.tsx +234 -0
- package/components/detail-page/canvas-observer.tsx +68 -0
- package/components/detail-page/canvas-overlay-host.tsx +44 -0
- package/components/detail-page/canvas-store-facade.ts +51 -0
- package/components/detail-page/chart-inspector.tsx +531 -0
- package/components/detail-page/design-reference-picker.tsx +383 -0
- package/components/detail-page/detail-page-basic-shapes.tsx +180 -0
- package/components/detail-page/detail-page-brand-gifs-panel.tsx +261 -0
- package/components/detail-page/detail-page-brand-kit-panel.tsx +77 -0
- package/components/detail-page/detail-page-brand-panel-header.tsx +44 -0
- package/components/detail-page/detail-page-charts-panel.tsx +104 -0
- package/components/detail-page/detail-page-decorations-panel.tsx +125 -0
- package/components/detail-page/detail-page-download-dialog.tsx +524 -0
- package/components/detail-page/detail-page-editor.tsx +974 -0
- package/components/detail-page/detail-page-elements-panel.tsx +117 -0
- package/components/detail-page/detail-page-font-picker.tsx +231 -0
- package/components/detail-page/detail-page-history-buttons.tsx +87 -0
- package/components/detail-page/detail-page-host-context.tsx +465 -0
- package/components/detail-page/detail-page-icons-panel.tsx +251 -0
- package/components/detail-page/detail-page-konva-json-preview.tsx +474 -0
- package/components/detail-page/detail-page-layers-panel.tsx +618 -0
- package/components/detail-page/detail-page-my-images-panel.tsx +179 -0
- package/components/detail-page/detail-page-my-shapes-panel.tsx +174 -0
- package/components/detail-page/detail-page-page-toolbar.tsx +171 -0
- package/components/detail-page/detail-page-pages-panel.tsx +299 -0
- package/components/detail-page/detail-page-pages-timeline.tsx +79 -0
- package/components/detail-page/detail-page-photos-panel.tsx +257 -0
- package/components/detail-page/detail-page-properties-panel.tsx +2680 -0
- package/components/detail-page/detail-page-qr-panel.tsx +137 -0
- package/components/detail-page/detail-page-references-panel.tsx +166 -0
- package/components/detail-page/detail-page-selection.ts +44 -0
- package/components/detail-page/detail-page-shapes-panel.tsx +16 -0
- package/components/detail-page/detail-page-sidebar-sections.tsx +324 -0
- package/components/detail-page/detail-page-tables-panel.tsx +106 -0
- package/components/detail-page/detail-page-text-panel.tsx +179 -0
- package/components/detail-page/detail-page-thumbnail-bus.ts +42 -0
- package/components/detail-page/edit-quota-ui.tsx +150 -0
- package/components/detail-page/editor-history.ts +56 -0
- package/components/detail-page/editor-hotkeys.tsx +165 -0
- package/components/detail-page/element-recents-strip.tsx +105 -0
- package/components/detail-page/element-rects.ts +82 -0
- package/components/detail-page/fill-control.tsx +261 -0
- package/components/detail-page/find-replace-panel.tsx +252 -0
- package/components/detail-page/generation-progress-fill.tsx +57 -0
- package/components/detail-page/gif-animator.tsx +184 -0
- package/components/detail-page/gif-effect-picker.tsx +136 -0
- package/components/detail-page/group-drill-in.tsx +219 -0
- package/components/detail-page/group-prompt-edit-panel.tsx +154 -0
- package/components/detail-page/hover-highlight-overlay.tsx +111 -0
- package/components/detail-page/hovered-layer.ts +34 -0
- package/components/detail-page/inspector-controls.tsx +175 -0
- package/components/detail-page/leviosa-canvas-workspace.tsx +313 -0
- package/components/detail-page/nested-selection-overlay.tsx +364 -0
- package/components/detail-page/panel-search-input.tsx +49 -0
- package/components/detail-page/prompt-edit-panel.tsx +146 -0
- package/components/detail-page/section-height-handle.tsx +276 -0
- package/components/detail-page/section-reauthor-controller.tsx +210 -0
- package/components/detail-page/spec-prompt-edit-panel.tsx +178 -0
- package/components/detail-page/spec-resize-absorber.tsx +149 -0
- package/components/detail-page/svg-prompt-edit-panel.tsx +139 -0
- package/components/detail-page/table-canvas-overlay.tsx +707 -0
- package/components/detail-page/table-inspector.tsx +586 -0
- package/components/ui/button.tsx +64 -0
- package/components/ui/command.tsx +175 -0
- package/components/ui/dialog.tsx +167 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/select.tsx +190 -0
- package/components/ui/slider.tsx +63 -0
- package/components/ui/switch.tsx +35 -0
- package/components/ui/tooltip.tsx +57 -0
- package/config/detail-page-bundle-font-tags.json +39 -0
- package/config/detail-page-fonts.json +373 -0
- package/index.ts +24 -0
- package/lib/cardnews/font-loader.ts +306 -0
- package/lib/detail-page/animation-sniff.ts +101 -0
- package/lib/detail-page/asset-bytes-url.ts +70 -0
- package/lib/detail-page/basic-shapes.ts +603 -0
- package/lib/detail-page/canvas-menu.ts +182 -0
- package/lib/detail-page/chart/defaults.ts +283 -0
- package/lib/detail-page/chart/format.ts +66 -0
- package/lib/detail-page/chart/normalize.ts +149 -0
- package/lib/detail-page/chart/palette.ts +85 -0
- package/lib/detail-page/chart/paste.ts +82 -0
- package/lib/detail-page/chart/preview.ts +78 -0
- package/lib/detail-page/chart/render.ts +188 -0
- package/lib/detail-page/chart/renderers/bar-h.ts +103 -0
- package/lib/detail-page/chart/renderers/bar-v.ts +213 -0
- package/lib/detail-page/chart/renderers/donut.ts +186 -0
- package/lib/detail-page/chart/renderers/gauge.ts +96 -0
- package/lib/detail-page/chart/renderers/line.ts +143 -0
- package/lib/detail-page/chart/renderers/shared.ts +263 -0
- package/lib/detail-page/chart/renderers/stack.ts +121 -0
- package/lib/detail-page/chart/sync.ts +92 -0
- package/lib/detail-page/chart/types.ts +145 -0
- package/lib/detail-page/css-color.ts +34 -0
- package/lib/detail-page/data-gif-payload.ts +167 -0
- package/lib/detail-page/design-reference.ts +244 -0
- package/lib/detail-page/distribute.ts +89 -0
- package/lib/detail-page/editor-routes.ts +10 -0
- package/lib/detail-page/element-recents.ts +167 -0
- package/lib/detail-page/find-replace.ts +120 -0
- package/lib/detail-page/format-painter.ts +159 -0
- package/lib/detail-page/gif-frames.ts +216 -0
- package/lib/detail-page/gif-timeline.ts +65 -0
- package/lib/detail-page/group-action.ts +62 -0
- package/lib/detail-page/hangul-roman.ts +58 -0
- package/lib/detail-page/icon-curated.ts +290 -0
- package/lib/detail-page/icon-fuzzy.ts +250 -0
- package/lib/detail-page/icon-keywords.generated.ts +3766 -0
- package/lib/detail-page/icon-keywords.ko.ts +524 -0
- package/lib/detail-page/icon-search.ts +257 -0
- package/lib/detail-page/icons.ts +67 -0
- package/lib/detail-page/image-credit.ts +121 -0
- package/lib/detail-page/image-data-uri.ts +29 -0
- package/lib/detail-page/insert-image.ts +75 -0
- package/lib/detail-page/insert-shape.ts +49 -0
- package/lib/detail-page/korean-dictionary.ts +187 -0
- package/lib/detail-page/layer-move.ts +257 -0
- package/lib/detail-page/new-product-draft-store.ts +105 -0
- package/lib/detail-page/new-product-draft.ts +283 -0
- package/lib/detail-page/personal-images-refresh.ts +30 -0
- package/lib/detail-page/product-source.ts +587 -0
- package/lib/detail-page/qr-code.ts +214 -0
- package/lib/detail-page/reference-image.ts +103 -0
- package/lib/detail-page/reference-upload.ts +181 -0
- package/lib/detail-page/replace-page.ts +35 -0
- package/lib/detail-page/replace-with-gif.ts +251 -0
- package/lib/detail-page/section-height.ts +131 -0
- package/lib/detail-page/section-reauthor-bus.ts +61 -0
- package/lib/detail-page/shape-to-image.ts +219 -0
- package/lib/detail-page/spec-group/clipboard.ts +35 -0
- package/lib/detail-page/spec-group/sync.ts +377 -0
- package/lib/detail-page/spec-group/text-metrics.ts +48 -0
- package/lib/detail-page/stock-photos.ts +88 -0
- package/lib/detail-page/svg-colors.ts +74 -0
- package/lib/detail-page/table/defaults.ts +344 -0
- package/lib/detail-page/table/edit.ts +312 -0
- package/lib/detail-page/table/harvest.ts +103 -0
- package/lib/detail-page/table/normalize.ts +125 -0
- package/lib/detail-page/table/paste.ts +49 -0
- package/lib/detail-page/table/preview.ts +66 -0
- package/lib/detail-page/table/render.ts +388 -0
- package/lib/detail-page/table/sync.ts +107 -0
- package/lib/detail-page/table/types.ts +102 -0
- package/lib/detail-page/text-gif-layout.ts +245 -0
- package/lib/detail-page/text-gif-payload.ts +67 -0
- package/lib/detail-page/text-presets.ts +230 -0
- package/lib/detail-page/use-fake-progress.ts +67 -0
- package/lib/detail-page/z-order.ts +82 -0
- package/lib/detail-page-canvas/bubble-path.ts +190 -0
- package/lib/detail-page-canvas/canvas-key.ts +22 -0
- package/lib/detail-page-canvas/clip-rect.ts +65 -0
- package/lib/detail-page-canvas/custom-props-adapter.ts +219 -0
- package/lib/detail-page-canvas/editor-fonts.ts +161 -0
- package/lib/detail-page-canvas/export/ai.ts +413 -0
- package/lib/detail-page-canvas/export/color.ts +35 -0
- package/lib/detail-page-canvas/export/document-model.ts +78 -0
- package/lib/detail-page-canvas/export/export-files.ts +290 -0
- package/lib/detail-page-canvas/export/font-subset.ts +264 -0
- package/lib/detail-page-canvas/export/gif-export.ts +310 -0
- package/lib/detail-page-canvas/export/gif-plan.ts +95 -0
- package/lib/detail-page-canvas/export/mp4-encode.ts +158 -0
- package/lib/detail-page-canvas/export/mp4-support.ts +22 -0
- package/lib/detail-page-canvas/export/pdf/geometry.ts +309 -0
- package/lib/detail-page-canvas/export/pdf/inline-svg.ts +399 -0
- package/lib/detail-page-canvas/export/pdf/resources.ts +443 -0
- package/lib/detail-page-canvas/export/pdf/text-ops.ts +81 -0
- package/lib/detail-page-canvas/export/pdf/writer.ts +109 -0
- package/lib/detail-page-canvas/export/psd.ts +354 -0
- package/lib/detail-page-canvas/export/raster.ts +255 -0
- package/lib/detail-page-canvas/export/svg.ts +479 -0
- package/lib/detail-page-canvas/export/text-layout.ts +142 -0
- package/lib/detail-page-canvas/font-catalog.ts +177 -0
- package/lib/detail-page-canvas/font-tags.ts +47 -0
- package/lib/detail-page-canvas/gif-web-fonts.ts +90 -0
- package/lib/detail-page-canvas/konva-fallback.ts +477 -0
- package/lib/detail-page-canvas/text-background-plan.ts +124 -0
- package/lib/detail-page-canvas/text-highlight-bands.ts +127 -0
- package/lib/utils.ts +10 -0
- package/package.json +68 -0
- package/types/commerce.ts +171 -0
- package/types/detail-page-api.ts +200 -0
- package/types/detail-page-canvas.ts +20 -0
- package/types/fontkit.d.ts +42 -0
- package/types/gifenc.d.ts +51 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { IconStyle } from "./icon-search";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 아이콘 격자의 **첫 화면**에 놓을 것들. 라우트(`/api/icons`)가 검색어 없이 불릴 때 쓴다.
|
|
5
|
+
*
|
|
6
|
+
* 라우트가 아니라 여기 있는 이유는 둘이다. 하나, 라우트 파일은 HTTP 메서드 말고 다른 것을
|
|
7
|
+
* 내보낼 수 없어서 목록만 따로 테스트할 수 없다. 둘, 이건 로직이 아니라 **우리가 고른 것**이라
|
|
8
|
+
* 손으로 늘어나는 자리다 — 늘릴 때 지켜야 할 규칙을 테스트가 지키게 두는 편이 낫다.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 우리가 쓰는 세트와 그 우선순위. 인터리브 순서가 이 순서다.
|
|
13
|
+
*
|
|
14
|
+
* 선(Lucide·Tabler)을 앞에 두는 것은 상세페이지 기본 축이 선이기 때문이다.
|
|
15
|
+
* **이 목록은 편의일 뿐 보안 경계가 아니다** — 실제 판정은 SPDX가 한다.
|
|
16
|
+
*/
|
|
17
|
+
export const ICON_PREFIXES = ["lucide", "tabler", "ph", "ri", "material-symbols"] as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 검색어가 없을 때 보여 줄 큐레이션. 상세페이지에서 손이 가장 자주 가는 것들이다.
|
|
21
|
+
* 빈 격자를 보여주지 않는다(`/api/stock-photos`가 `curated`로 하는 것과 같은 처리).
|
|
22
|
+
*
|
|
23
|
+
* **선/채움 두 벌을 따로 둔다.** 큐레이션을 한 벌만 두면 선/채움 토글이 검색어를 넣기
|
|
24
|
+
* 전까지 아무것도 안 바꾼다 — Lucide·Tabler는 세트 전체가 선이고 Phosphor `-fill`은
|
|
25
|
+
* 세트 전체가 채움이라, 같은 목록에 스타일 필터를 걸면 한쪽이 통째로 빈다.
|
|
26
|
+
*
|
|
27
|
+
* 갈래 순서가 곧 격자 순서다(세트 인터리브가 그 안에서 한 번 더 섞는다). 앞쪽 몇 줄이
|
|
28
|
+
* 검증·배송·결제인 것은 상세페이지에서 그 셋이 가장 먼저 필요하기 때문이다.
|
|
29
|
+
*
|
|
30
|
+
* **`conceptOf`가 같은 것을 하나로 접는다.** 그래서 두 벌 안에서 개념이 겹치면 조용히
|
|
31
|
+
* 사라진다 — `lucide:star`와 `tabler:star-filled`를 한 벌에 같이 두면 안 된다.
|
|
32
|
+
* 그리고 여기 적은 이름은 **전부 제공처에 실재하는 것으로 확인했다**. 오타는 404가
|
|
33
|
+
* 아니라 조용한 결번으로 나타나니(마크업 배치에서 빠질 뿐이다) 손으로 늘릴 때 주의.
|
|
34
|
+
*/
|
|
35
|
+
export const CURATED_STROKE: readonly string[] = [
|
|
36
|
+
// 신뢰 · 인증
|
|
37
|
+
"lucide:check", "lucide:circle-check", "lucide:square-check", "tabler:rosette-discount-check",
|
|
38
|
+
"lucide:shield-check", "tabler:certificate", "lucide:award", "lucide:medal",
|
|
39
|
+
"lucide:trophy", "lucide:crown", "lucide:star", "lucide:heart",
|
|
40
|
+
"lucide:thumbs-up", "lucide:thumbs-down", "lucide:bookmark", "lucide:stamp",
|
|
41
|
+
"tabler:ribbon-health", "lucide:diamond", "lucide:hand-heart", "tabler:hand-click",
|
|
42
|
+
|
|
43
|
+
// 배송 · 물류
|
|
44
|
+
"lucide:truck", "tabler:truck-delivery", "lucide:rocket", "lucide:package",
|
|
45
|
+
"lucide:package-2", "tabler:package-import", "lucide:plane", "lucide:ship",
|
|
46
|
+
"lucide:train", "lucide:forklift", "tabler:building-warehouse", "lucide:route",
|
|
47
|
+
"lucide:map", "lucide:map-pin", "lucide:compass", "lucide:navigation",
|
|
48
|
+
"tabler:package-export", "tabler:truck-return", "lucide:arrow-right-left", "lucide:radar",
|
|
49
|
+
"lucide:flag", "lucide:globe",
|
|
50
|
+
|
|
51
|
+
// 결제 · 가격
|
|
52
|
+
"lucide:credit-card", "tabler:cash", "tabler:coin", "lucide:wallet",
|
|
53
|
+
"lucide:receipt", "tabler:discount", "lucide:percent", "lucide:tag",
|
|
54
|
+
"lucide:ticket", "lucide:gift", "lucide:shopping-cart", "lucide:shopping-bag",
|
|
55
|
+
"tabler:shopping-cart-plus", "tabler:pig-money", "tabler:coin-bitcoin", "tabler:receipt-refund",
|
|
56
|
+
"tabler:gift-card", "lucide:calculator", "tabler:file-invoice", "tabler:arrows-transfer-up",
|
|
57
|
+
|
|
58
|
+
// 문의 · 응대
|
|
59
|
+
"lucide:phone", "lucide:mail", "lucide:message-circle", "lucide:headset",
|
|
60
|
+
"tabler:messages", "lucide:user", "lucide:users", "lucide:help-circle",
|
|
61
|
+
"lucide:info", "lucide:alert-circle", "lucide:megaphone", "tabler:speakerphone",
|
|
62
|
+
"lucide:bell", "tabler:message-2", "lucide:star-half", "tabler:hand-stop",
|
|
63
|
+
"lucide:clipboard-check", "lucide:phone-call",
|
|
64
|
+
|
|
65
|
+
// 치수 · 규격
|
|
66
|
+
"lucide:ruler", "tabler:dimensions", "lucide:scale", "lucide:weight",
|
|
67
|
+
"lucide:thermometer", "lucide:zoom-in", "lucide:zoom-out", "tabler:arrows-vertical",
|
|
68
|
+
"tabler:arrows-horizontal", "lucide:angle", "lucide:grid-3x3", "lucide:layers",
|
|
69
|
+
"tabler:align-center", "tabler:aspect-ratio", "tabler:cube",
|
|
70
|
+
|
|
71
|
+
// 소재 · 관리
|
|
72
|
+
"lucide:droplet", "tabler:wash-machine", "tabler:ironing", "lucide:wind",
|
|
73
|
+
"tabler:bleach", "tabler:hand-sanitizer", "lucide:sun", "lucide:snowflake",
|
|
74
|
+
"lucide:leaf", "lucide:recycle", "lucide:feather", "tabler:needle",
|
|
75
|
+
"lucide:scissors", "tabler:needle-thread", "tabler:texture", "lucide:flower",
|
|
76
|
+
"lucide:droplet-off", "tabler:air-balloon", "tabler:virus", "lucide:sparkles",
|
|
77
|
+
|
|
78
|
+
// 안전 · 보증
|
|
79
|
+
"lucide:shield", "lucide:lock", "lucide:key", "lucide:alert-triangle",
|
|
80
|
+
"lucide:ban", "lucide:flame", "lucide:fire-extinguisher", "tabler:helmet",
|
|
81
|
+
"lucide:hand-grab", "tabler:face-mask", "tabler:plant", "lucide:eye",
|
|
82
|
+
"lucide:fingerprint", "lucide:list-check",
|
|
83
|
+
|
|
84
|
+
// 의류 · 잡화
|
|
85
|
+
"lucide:shirt", "tabler:shoe", "tabler:sock", "lucide:briefcase",
|
|
86
|
+
"lucide:backpack", "tabler:zip", "lucide:circle-dot", "tabler:eyeglass",
|
|
87
|
+
"tabler:sunglasses", "lucide:clock", "lucide:watch", "tabler:diamonds",
|
|
88
|
+
"lucide:umbrella", "tabler:hanger-2",
|
|
89
|
+
|
|
90
|
+
// 뷰티 · 욕실
|
|
91
|
+
"lucide:sparkle", "tabler:spray", "tabler:perfume", "lucide:brush",
|
|
92
|
+
"lucide:mirror-rectangular", "tabler:face-id", "tabler:hand-finger", "tabler:manual-gearbox",
|
|
93
|
+
"tabler:bottle", "tabler:bowl", "tabler:sun-high", "tabler:massage",
|
|
94
|
+
"lucide:bath", "tabler:dental",
|
|
95
|
+
|
|
96
|
+
// 식음료
|
|
97
|
+
"lucide:coffee", "tabler:cup", "lucide:wine", "lucide:beer",
|
|
98
|
+
"lucide:bottle-wine", "lucide:apple", "lucide:carrot", "tabler:bread",
|
|
99
|
+
"lucide:egg", "lucide:fish", "tabler:meat", "lucide:cake",
|
|
100
|
+
"lucide:ice-cream", "lucide:cookie", "lucide:milk", "tabler:bubble-tea",
|
|
101
|
+
"lucide:salad", "lucide:pizza", "lucide:chef-hat", "tabler:tools-kitchen-2",
|
|
102
|
+
"tabler:bowl-spoon", "tabler:cooker", "tabler:cheese", "lucide:candy",
|
|
103
|
+
"tabler:grain", "lucide:cherry", "lucide:broccoli", "lucide:nut",
|
|
104
|
+
|
|
105
|
+
// 가전 · 디지털
|
|
106
|
+
"tabler:device-tv", "lucide:monitor", "tabler:device-laptop", "tabler:device-mobile",
|
|
107
|
+
"tabler:device-tablet", "lucide:headphones", "lucide:speaker", "lucide:camera",
|
|
108
|
+
"lucide:printer", "lucide:keyboard", "lucide:mouse", "lucide:plug",
|
|
109
|
+
"lucide:battery", "lucide:wifi", "lucide:bluetooth", "lucide:usb",
|
|
110
|
+
"lucide:battery-charging", "tabler:propeller", "tabler:air-conditioning", "tabler:vacuum-cleaner",
|
|
111
|
+
"tabler:bulb", "tabler:device-remote", "tabler:microphone", "tabler:device-gamepad",
|
|
112
|
+
"lucide:drone", "tabler:robot", "lucide:cpu", "lucide:database",
|
|
113
|
+
"lucide:power",
|
|
114
|
+
|
|
115
|
+
// 가구 · 생활
|
|
116
|
+
"lucide:sofa", "lucide:bed", "lucide:armchair", "tabler:desk",
|
|
117
|
+
"tabler:door", "tabler:window", "lucide:home", "lucide:building",
|
|
118
|
+
"tabler:building-store", "lucide:lamp", "tabler:photo", "lucide:trash",
|
|
119
|
+
"tabler:basket", "lucide:broom", "lucide:hammer", "tabler:tool",
|
|
120
|
+
"tabler:paint", "ri:brush-2-line", "tabler:ladder", "lucide:archive",
|
|
121
|
+
"lucide:blinds", "tabler:stairs",
|
|
122
|
+
|
|
123
|
+
// 반려 · 유아
|
|
124
|
+
"tabler:paw", "lucide:dog", "lucide:cat", "lucide:bone",
|
|
125
|
+
"lucide:bird", "tabler:fish-bone", "tabler:dog-bowl", "tabler:baby-carriage",
|
|
126
|
+
"tabler:baby-bottle", "tabler:diaper", "lucide:puzzle", "lucide:blocks",
|
|
127
|
+
"lucide:balloon",
|
|
128
|
+
|
|
129
|
+
// 운동 · 야외
|
|
130
|
+
"tabler:barbell", "lucide:bike", "tabler:run", "tabler:yoga",
|
|
131
|
+
"tabler:ball-football", "tabler:ball-basketball", "tabler:ball-tennis", "tabler:swimming",
|
|
132
|
+
"lucide:mountain", "lucide:tent", "tabler:trekking", "tabler:campfire",
|
|
133
|
+
"tabler:fish-hook", "tabler:ski-jumping", "tabler:golf", "tabler:heartbeat",
|
|
134
|
+
"ri:footprint-line",
|
|
135
|
+
|
|
136
|
+
// 문구 · 사무
|
|
137
|
+
"lucide:book", "lucide:notebook", "lucide:pen", "lucide:pencil",
|
|
138
|
+
"lucide:paperclip", "lucide:folder", "lucide:file", "lucide:calendar",
|
|
139
|
+
"lucide:clipboard", "tabler:note", "tabler:briefcase-2", "lucide:eraser",
|
|
140
|
+
"tabler:file-certificate", "tabler:news", "tabler:id", "tabler:mail-opened",
|
|
141
|
+
|
|
142
|
+
// 조작 · 표시
|
|
143
|
+
"lucide:arrow-right", "lucide:arrow-up", "lucide:plus", "lucide:minus",
|
|
144
|
+
"lucide:x", "tabler:refresh", "lucide:download", "lucide:upload",
|
|
145
|
+
"lucide:share", "lucide:link", "lucide:copy", "tabler:filter",
|
|
146
|
+
"lucide:search", "lucide:settings", "tabler:menu-2", "tabler:player-play",
|
|
147
|
+
"tabler:player-pause", "lucide:lock-open", "tabler:trash-x", "lucide:eye-off",
|
|
148
|
+
|
|
149
|
+
// 강조 · 지표
|
|
150
|
+
"lucide:bolt", "lucide:trending-up", "lucide:trending-down", "lucide:chart-bar",
|
|
151
|
+
"lucide:chart-pie", "lucide:chart-line", "lucide:target", "tabler:flag-2",
|
|
152
|
+
"tabler:confetti", "tabler:gift-off", "tabler:circle-number-1", "lucide:podium",
|
|
153
|
+
"lucide:hourglass", "tabler:stopwatch", "tabler:alarm", "tabler:calendar-event",
|
|
154
|
+
"lucide:infinity",
|
|
155
|
+
|
|
156
|
+
// 자연 · 계절
|
|
157
|
+
"lucide:moon", "lucide:cloud", "lucide:cloud-rain", "lucide:cloud-snow",
|
|
158
|
+
"tabler:windsock", "tabler:tree", "lucide:rose", "tabler:wave-sine",
|
|
159
|
+
"lucide:glass-water", "tabler:world-longitude", "tabler:snowman",
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/** 채움 큐레이션. 같은 개념을 채움 축에서 다시 고른 것이다. */
|
|
163
|
+
export const CURATED_FILL: readonly string[] = [
|
|
164
|
+
// 신뢰 · 인증
|
|
165
|
+
"ph:check-fill", "tabler:circle-check-filled", "tabler:square-check-filled", "tabler:rosette-discount-check-filled",
|
|
166
|
+
"ph:shield-check-fill", "ph:certificate-fill", "ri:award-fill", "ph:medal-fill",
|
|
167
|
+
"ph:trophy-fill", "ph:crown-fill", "ph:star-fill", "ph:heart-fill",
|
|
168
|
+
"ph:thumbs-up-fill", "ph:thumbs-down-fill", "ph:bookmark-fill", "ph:stamp-fill",
|
|
169
|
+
"ph:diamond-fill", "ph:hand-heart-fill", "ph:hands-clapping-fill",
|
|
170
|
+
|
|
171
|
+
// 배송 · 물류
|
|
172
|
+
"ph:truck-fill", "ph:rocket-fill", "ph:package-fill", "material-symbols:package-2",
|
|
173
|
+
"ri:plane-fill", "ri:ship-fill", "ph:train-fill", "material-symbols:forklift",
|
|
174
|
+
"ph:warehouse-fill", "ri:route-fill", "ri:map-fill", "ph:map-pin-fill",
|
|
175
|
+
"ph:compass-fill", "ri:navigation-fill", "ri:exchange-fill", "ri:radar-fill",
|
|
176
|
+
"ph:flag-fill", "ph:globe-fill",
|
|
177
|
+
|
|
178
|
+
// 결제 · 가격
|
|
179
|
+
"ph:credit-card-fill", "ri:cash-fill", "ph:coin-fill", "ph:wallet-fill",
|
|
180
|
+
"ph:receipt-fill", "tabler:discount-filled", "ph:percent-fill", "ph:tag-fill",
|
|
181
|
+
"ph:ticket-fill", "ph:gift-fill", "ph:shopping-cart-fill", "ph:shopping-bag-fill",
|
|
182
|
+
"ph:trolley-fill", "ph:piggy-bank-fill", "tabler:coin-bitcoin-filled", "ri:refund-fill",
|
|
183
|
+
"tabler:gift-card-filled", "ph:calculator-fill", "tabler:file-invoice-filled", "tabler:send-filled",
|
|
184
|
+
|
|
185
|
+
// 문의 · 응대
|
|
186
|
+
"ph:phone-fill", "ri:mail-fill", "tabler:message-circle-filled", "ph:headset-fill",
|
|
187
|
+
"tabler:messages-filled", "ph:user-fill", "ph:users-fill", "tabler:help-circle-filled",
|
|
188
|
+
"ph:info-fill", "tabler:alert-circle-filled", "ph:megaphone-fill", "ph:bell-fill",
|
|
189
|
+
"ri:message-2-fill", "ph:star-half-fill", "ph:hand-fill", "tabler:clipboard-check-filled",
|
|
190
|
+
"ph:phone-call-fill",
|
|
191
|
+
|
|
192
|
+
// 치수 · 규격
|
|
193
|
+
"ph:ruler-fill", "tabler:scale-filled", "ri:weight-fill", "ph:thermometer-fill",
|
|
194
|
+
"ri:zoom-in-fill", "ri:zoom-out-fill", "ph:arrows-vertical-fill", "ph:arrows-horizontal-fill",
|
|
195
|
+
"ph:angle-fill", "material-symbols:grid-3x3", "material-symbols:layers", "material-symbols:align-center",
|
|
196
|
+
"ri:aspect-ratio-fill", "ph:cube-fill",
|
|
197
|
+
|
|
198
|
+
// 소재 · 관리
|
|
199
|
+
"tabler:droplet-filled", "ph:washing-machine-fill", "tabler:ironing-filled", "ph:wind-fill",
|
|
200
|
+
"ph:flask-fill", "ri:hand-sanitizer-fill", "ph:sun-fill", "ph:snowflake-fill",
|
|
201
|
+
"ph:leaf-fill", "ph:recycle-fill", "ph:feather-fill", "ph:needle-fill",
|
|
202
|
+
"ph:scissors-fill", "material-symbols:texture", "ph:flower-fill", "tabler:air-balloon-filled",
|
|
203
|
+
"ph:virus-fill", "tabler:sparkles-filled",
|
|
204
|
+
|
|
205
|
+
// 안전 · 보증
|
|
206
|
+
"ph:shield-fill", "ph:lock-fill", "ph:key-fill", "tabler:alert-triangle-filled",
|
|
207
|
+
"ph:prohibit-fill", "ph:flame-fill", "ph:fire-extinguisher-fill", "ph:hard-hat-fill",
|
|
208
|
+
"ph:boxing-glove-fill", "ph:face-mask-fill", "ph:plant-fill", "ph:eye-fill",
|
|
209
|
+
"ph:fingerprint-fill", "tabler:list-check-filled",
|
|
210
|
+
|
|
211
|
+
// 의류 · 잡화
|
|
212
|
+
"ri:shirt-fill", "ph:hoodie-fill", "ph:pants-fill", "ph:dress-fill",
|
|
213
|
+
"ph:boot-fill", "ph:sneaker-fill", "ph:sock-fill", "ph:baseball-cap-fill",
|
|
214
|
+
"ph:briefcase-fill", "ph:backpack-fill", "tabler:circle-dot-filled", "tabler:eyeglass-filled",
|
|
215
|
+
"ph:sunglasses-fill", "ph:clock-fill", "ph:watch-fill", "tabler:diamonds-filled",
|
|
216
|
+
"ph:umbrella-fill", "ph:belt-fill", "tabler:hanger-2-filled",
|
|
217
|
+
|
|
218
|
+
// 뷰티 · 욕실
|
|
219
|
+
"ph:sparkle-fill", "ph:spray-bottle-fill", "ri:brush-fill", "ph:smiley-fill",
|
|
220
|
+
"ph:hand-palm-fill", "ph:hair-dryer-fill", "tabler:manual-gearbox-filled", "ph:hand-soap-fill",
|
|
221
|
+
"tabler:bottle-filled", "ph:jar-fill", "tabler:sun-high-filled", "material-symbols:massage",
|
|
222
|
+
"tabler:bath-filled", "ph:tooth-fill",
|
|
223
|
+
|
|
224
|
+
// 식음료
|
|
225
|
+
"ph:coffee-fill", "ri:cup-fill", "ph:wine-fill", "ri:beer-fill",
|
|
226
|
+
"ph:beer-bottle-fill", "ri:apple-fill", "ph:carrot-fill", "ph:bread-fill",
|
|
227
|
+
"ph:egg-fill", "ph:fish-fill", "ph:cake-fill", "ph:ice-cream-fill",
|
|
228
|
+
"ph:cookie-fill", "tabler:milk-filled", "tabler:salad-filled", "ph:pizza-fill",
|
|
229
|
+
"ph:chef-hat-fill", "tabler:tools-kitchen-2-filled", "tabler:bowl-spoon-filled", "ph:cooking-pot-fill",
|
|
230
|
+
"ph:cheese-fill", "material-symbols:grain", "tabler:cherry-filled", "ph:nut-fill",
|
|
231
|
+
|
|
232
|
+
// 가전 · 디지털
|
|
233
|
+
"tabler:device-tv-filled", "ph:monitor-fill", "ph:laptop-fill", "ph:device-mobile-fill",
|
|
234
|
+
"ph:device-tablet-fill", "ph:headphones-fill", "ri:speaker-fill", "ph:camera-fill",
|
|
235
|
+
"ph:printer-fill", "ph:keyboard-fill", "ph:mouse-fill", "ph:plug-fill",
|
|
236
|
+
"ri:battery-fill", "ri:wifi-fill", "ph:bluetooth-fill", "ph:usb-fill",
|
|
237
|
+
"ph:battery-charging-fill", "ph:fan-fill", "tabler:bulb-filled", "tabler:device-remote-filled",
|
|
238
|
+
"ph:microphone-fill", "tabler:device-gamepad-filled", "ph:drone-fill", "ph:robot-fill",
|
|
239
|
+
"ph:cpu-fill", "ph:database-fill", "ph:power-fill",
|
|
240
|
+
|
|
241
|
+
// 가구 · 생활
|
|
242
|
+
"ri:sofa-fill", "ph:bed-fill", "ph:armchair-fill", "ph:desk-fill",
|
|
243
|
+
"ph:door-fill", "ri:window-fill", "ri:home-fill", "ph:building-fill",
|
|
244
|
+
"ph:storefront-fill", "ph:lamp-fill", "ph:potted-plant-fill", "tabler:photo-filled",
|
|
245
|
+
"ph:trash-fill", "ph:basket-fill", "ph:broom-fill", "ph:hammer-fill",
|
|
246
|
+
"ph:wrench-fill", "ph:screwdriver-fill", "ri:paint-fill", "ri:brush-2-fill",
|
|
247
|
+
"ph:ladder-fill", "ph:archive-fill", "material-symbols:curtains", "ph:rug-fill",
|
|
248
|
+
"ph:stairs-fill",
|
|
249
|
+
|
|
250
|
+
// 반려 · 유아
|
|
251
|
+
"tabler:paw-filled", "ph:dog-fill", "ph:cat-fill", "ph:bone-fill",
|
|
252
|
+
"ph:bird-fill", "tabler:fish-bone-filled", "ph:baby-carriage-fill", "ri:puzzle-fill",
|
|
253
|
+
"ph:balloon-fill",
|
|
254
|
+
|
|
255
|
+
// 운동 · 야외
|
|
256
|
+
"ph:barbell-fill", "ri:bike-fill", "ri:run-fill", "ph:soccer-ball-fill",
|
|
257
|
+
"ph:basketball-fill", "ph:tennis-ball-fill", "ph:person-simple-swim-fill", "tabler:mountain-filled",
|
|
258
|
+
"ph:tent-fill", "material-symbols:hiking", "ph:campfire-fill", "ph:person-simple-ski-fill",
|
|
259
|
+
"ph:golf-fill", "ph:heartbeat-fill", "ri:footprint-fill",
|
|
260
|
+
|
|
261
|
+
// 문구 · 사무
|
|
262
|
+
"ph:book-fill", "ph:notebook-fill", "ph:pen-fill", "ph:pencil-fill",
|
|
263
|
+
"ph:paperclip-fill", "ph:folder-fill", "ph:file-fill", "ph:calendar-fill",
|
|
264
|
+
"ph:clipboard-fill", "ph:note-fill", "ri:briefcase-2-fill", "ph:eraser-fill",
|
|
265
|
+
"ri:file-check-fill", "ri:news-fill", "tabler:id-filled", "tabler:mail-opened-filled",
|
|
266
|
+
|
|
267
|
+
// 조작 · 표시
|
|
268
|
+
"ph:arrow-right-fill", "ph:arrow-up-fill", "ph:plus-fill", "ph:minus-fill",
|
|
269
|
+
"ph:x-fill", "ri:refresh-fill", "ph:download-fill", "ph:upload-fill",
|
|
270
|
+
"ph:share-fill", "ph:link-fill", "ph:copy-fill", "ri:filter-fill",
|
|
271
|
+
"ri:search-fill", "ri:settings-fill", "ri:menu-2-fill", "tabler:player-play-filled",
|
|
272
|
+
"tabler:player-pause-filled", "ph:lock-open-fill", "tabler:trash-x-filled", "ri:eye-off-fill",
|
|
273
|
+
|
|
274
|
+
// 강조 · 지표
|
|
275
|
+
"tabler:bolt-filled", "material-symbols:trending-up", "material-symbols:trending-down", "ph:chart-bar-fill",
|
|
276
|
+
"ph:chart-pie-fill", "ph:chart-line-fill", "ph:target-fill", "ri:flag-2-fill",
|
|
277
|
+
"ph:confetti-fill", "ri:gift-2-fill", "tabler:circle-number-1-filled", "ph:ranking-fill",
|
|
278
|
+
"ph:hourglass-fill", "ph:alarm-fill", "ri:calendar-event-fill", "ph:infinity-fill",
|
|
279
|
+
|
|
280
|
+
// 자연 · 계절
|
|
281
|
+
"ph:moon-fill", "ph:cloud-fill", "ph:cloud-rain-fill", "ph:cloud-snow-fill",
|
|
282
|
+
"tabler:windsock-filled", "ph:tree-fill", "ph:flower-lotus-fill", "ph:wave-sine-fill",
|
|
283
|
+
"ph:drop-half-fill", "ph:planet-fill",
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
export function curatedFor(style: IconStyle | undefined): readonly string[] {
|
|
287
|
+
if (style === "fill") return CURATED_FILL;
|
|
288
|
+
if (style === "stroke") return CURATED_STROKE;
|
|
289
|
+
return [...CURATED_STROKE, ...CURATED_FILL];
|
|
290
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "정확히 맞은 것 먼저, 비슷한 것은 그 아래."
|
|
3
|
+
*
|
|
4
|
+
* 아이콘 검색이 두 가지로 새고 있었다.
|
|
5
|
+
*
|
|
6
|
+
* 1. **오타 한 글자에 0건.** `aple`을 치면 제공처 검색이 아무것도 안 준다. 사람은
|
|
7
|
+
* 자기가 틀린 줄 모르고 "그런 아이콘이 없다"고 읽는다.
|
|
8
|
+
* 2. **외래어가 안 걸린다.** "애플"은 사전에 없으면 0건인데, 정작 `apple`로 치면 나온다.
|
|
9
|
+
* 한글로 치는 사람만 손해를 본다.
|
|
10
|
+
*
|
|
11
|
+
* 그래서 이름을 **자리표(skeleton)**로 뭉갠 뒤 견준다. 모음과 겹글자를 걷어내고 같은
|
|
12
|
+
* 소리를 내는 자음을 한 글자로 모으면, `apple`·`aple`·`애플`이 전부 `apl`로 만난다.
|
|
13
|
+
*
|
|
14
|
+
* 등급을 매기는 이유는 순서 때문이다. 비슷한 것을 정확한 것과 섞어 버리면 검색이
|
|
15
|
+
* 나빠진다 — 아래로 내려보내야 "덤"이 된다.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { hasHangul, romanizeHangul } from "./hangul-roman";
|
|
19
|
+
|
|
20
|
+
/** 얼마나 잘 맞았나. 큰 것이 위로 간다. */
|
|
21
|
+
export const MATCH_EXACT = 3;
|
|
22
|
+
export const MATCH_PARTIAL = 2;
|
|
23
|
+
export const MATCH_FUZZY = 1;
|
|
24
|
+
export const MATCH_NONE = 0;
|
|
25
|
+
|
|
26
|
+
export type MatchTier =
|
|
27
|
+
| typeof MATCH_EXACT
|
|
28
|
+
| typeof MATCH_PARTIAL
|
|
29
|
+
| typeof MATCH_FUZZY
|
|
30
|
+
| typeof MATCH_NONE;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 견줄 수 있는 한 가지 모양으로 뭉갠다.
|
|
34
|
+
*
|
|
35
|
+
* 규칙은 전부 "한글로 옮겨 적을 때 흔들리는 자리"다 — `c/k`, `ph/f`, `x/ks`, 겹자음,
|
|
36
|
+
* 끝의 묵음 `e`. 모음은 남긴다(모음까지 지우면 `bell`과 `ball`이 같아진다).
|
|
37
|
+
*/
|
|
38
|
+
export function skeleton(text: string): string {
|
|
39
|
+
let out = text.toLowerCase().replace(/[^a-z]/g, "");
|
|
40
|
+
if (!out) return "";
|
|
41
|
+
out = out
|
|
42
|
+
.replace(/ph/g, "f")
|
|
43
|
+
.replace(/ck/g, "k")
|
|
44
|
+
.replace(/c/g, "k")
|
|
45
|
+
.replace(/q/g, "k")
|
|
46
|
+
.replace(/x/g, "ks")
|
|
47
|
+
.replace(/z/g, "s")
|
|
48
|
+
.replace(/(.)\1+/g, "$1");
|
|
49
|
+
// 끝의 묵음 e — `apple`과 `appl`이 갈리면 안 된다.
|
|
50
|
+
if (out.length > 2 && out.endsWith("e")) out = out.slice(0, -1);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 한글 낱말의 자리표. 로마자로 옮긴 뒤 **한국어가 끼워 넣는 모음을 뺀다.**
|
|
56
|
+
*
|
|
57
|
+
* "플"의 `eu`(ㅡ)와 "트"의 `eu`는 영어에 없던 소리다 — 그걸 빼야 `aepeul`이 `apl`이
|
|
58
|
+
* 되어 `apple`과 만난다. `ae`(ㅐ)와 `eo`(ㅓ)도 영어의 `a`·`o` 자리라 되돌린다.
|
|
59
|
+
*/
|
|
60
|
+
export function koreanSkeleton(text: string): string {
|
|
61
|
+
const roman = romanizeHangul(text);
|
|
62
|
+
const folded = roman
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/eu/g, "")
|
|
65
|
+
.replace(/ae/g, "a")
|
|
66
|
+
.replace(/eo/g, "o")
|
|
67
|
+
.replace(/oe/g, "o");
|
|
68
|
+
return skeleton(folded);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 한글 자리표에서 **영어 쪽으로 갈릴 수 있는 자리**를 되돌린 후보들.
|
|
73
|
+
*
|
|
74
|
+
* 자리표를 더 뭉개는 길도 있었지만 그쪽은 못 쓴다. `f`와 `p`를 합치면 `folder`와
|
|
75
|
+
* `fold`가 같은 자리표가 되어 "폴더"가 `calendar-fold` 계열을 물어 온다(실측 415건).
|
|
76
|
+
* 뭉개기는 **모든 이름에** 걸리기 때문이다.
|
|
77
|
+
*
|
|
78
|
+
* 그래서 반대로 간다. 이름은 건드리지 않고 **질의 쪽에서만** 후보를 펼친 뒤
|
|
79
|
+
* *정확히* 같은 것을 찾는다. 흔들리는 자리는 넷뿐이다.
|
|
80
|
+
*
|
|
81
|
+
* - ㅍ은 `p`이자 `f`다 — "필터" ↔ `filter`
|
|
82
|
+
* - ㄹ은 `r`이자 `l`이다 — "링크" ↔ `link`
|
|
83
|
+
* - 종성 ㅇ은 늘 `ng`인데 영어는 그 자리가 `n`인 것이 많다 — `ringk` ↔ `link`
|
|
84
|
+
* - 영어의 약모음 꼬리를 한국어는 ㅓ/ㅡ로 받는다 — "폴더" ↔ `folder`
|
|
85
|
+
*
|
|
86
|
+
* 정확히 같은 것만 세므로 후보를 늘려도 잡음이 거의 안 는다. `fold`처럼 진짜로 겹치는
|
|
87
|
+
* 말만 남는데, 그것들은 어차피 맨 아래 등급이다.
|
|
88
|
+
*/
|
|
89
|
+
export function pronunciationVariants(base: string): string[] {
|
|
90
|
+
if (base.length < 3) return [];
|
|
91
|
+
|
|
92
|
+
let set = new Set([base]);
|
|
93
|
+
const branch = (fold: (word: string) => string) => {
|
|
94
|
+
const next = new Set(set);
|
|
95
|
+
for (const word of set) {
|
|
96
|
+
const folded = fold(word);
|
|
97
|
+
if (folded !== word) next.add(folded);
|
|
98
|
+
}
|
|
99
|
+
set = next;
|
|
100
|
+
};
|
|
101
|
+
branch((word) => word.replace(/p/g, "f"));
|
|
102
|
+
branch((word) => word.replace(/r/g, "l"));
|
|
103
|
+
branch((word) => word.replace(/ng/g, "n"));
|
|
104
|
+
|
|
105
|
+
const withTails = new Set(set);
|
|
106
|
+
for (const word of set) {
|
|
107
|
+
if (!/[ou]$/.test(word)) continue;
|
|
108
|
+
for (const tail of ["er", "or", "ar", "e", "a", ""]) {
|
|
109
|
+
withTails.add(word.slice(0, -1) + tail);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return [...withTails].filter((word) => word.length >= 3);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 질의 하나의 후보 집합. `matchTier`는 이름마다 불리므로(3.6만 번) 여기서 한 번만 편다.
|
|
117
|
+
*/
|
|
118
|
+
const variantMemo = new Map<string, ReadonlySet<string>>();
|
|
119
|
+
|
|
120
|
+
function variantsFor(term: string): ReadonlySet<string> {
|
|
121
|
+
const hit = variantMemo.get(term);
|
|
122
|
+
if (hit) return hit;
|
|
123
|
+
const made = new Set(pronunciationVariants(koreanSkeleton(term)));
|
|
124
|
+
// 질의는 몇 개 안 된다. 늘어나면 통째로 버린다.
|
|
125
|
+
if (variantMemo.size > 200) variantMemo.clear();
|
|
126
|
+
variantMemo.set(term, made);
|
|
127
|
+
return made;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 두 낱말 사이의 편집 거리. `max`를 넘으면 그 자리에서 그만둔다(빨라야 한다). */
|
|
131
|
+
export function editDistance(a: string, b: string, max = 3): number {
|
|
132
|
+
if (a === b) return 0;
|
|
133
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
134
|
+
if (!a.length || !b.length) return Math.max(a.length, b.length);
|
|
135
|
+
|
|
136
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
137
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
138
|
+
const row = [i];
|
|
139
|
+
let best = i;
|
|
140
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
141
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
142
|
+
const value = Math.min(row[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
143
|
+
row.push(value);
|
|
144
|
+
if (value < best) best = value;
|
|
145
|
+
}
|
|
146
|
+
// 이 줄에서 이미 전부 max를 넘었으면 더 볼 것이 없다.
|
|
147
|
+
if (best > max) return max + 1;
|
|
148
|
+
prev = row;
|
|
149
|
+
}
|
|
150
|
+
return prev[b.length];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 낱말 길이에 맞춘 오타 허용치. 짧은 말일수록 빡빡해야 한다.
|
|
155
|
+
*
|
|
156
|
+
* 네 글자까지 한 글자도 안 봐주는 것은 실측 때문이다. `kopi`(커피)에 한 글자를 허용하면
|
|
157
|
+
* `copy`와 `cookie`가 딸려 오고, `star`에 한 글자를 허용하면 `start`가 붙은 정렬
|
|
158
|
+
* 아이콘이 수백 개 들어온다. 짧은 말에서는 한 글자가 곧 다른 말이다.
|
|
159
|
+
*/
|
|
160
|
+
function tolerance(word: string): number {
|
|
161
|
+
if (word.length <= 4) return 0;
|
|
162
|
+
if (word.length <= 7) return 1;
|
|
163
|
+
return 2;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 아이콘 이름을 견줄 조각들로. `arrow-right-fill` → `["arrow","right","fill"]`. */
|
|
167
|
+
function words(name: string): string[] {
|
|
168
|
+
return name.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 이 이름이 이 낱말들에 얼마나 맞는가.
|
|
173
|
+
*
|
|
174
|
+
* 이름 전체가 그 말이면 최고점, 이름 안에 그 말이 조각으로 들어 있으면 그 다음,
|
|
175
|
+
* 자리표가 오타 한두 개 거리면 마지막 등급이다.
|
|
176
|
+
*/
|
|
177
|
+
export function matchTier(
|
|
178
|
+
name: string,
|
|
179
|
+
terms: readonly string[],
|
|
180
|
+
/**
|
|
181
|
+
* 오타·소리로 되짚어도 되는 말들. 기본은 `terms` 전부다.
|
|
182
|
+
*
|
|
183
|
+
* 라우트는 여기에 **사용자가 실제로 친 말만** 넣는다. 사전이 옮겨 준 영어까지
|
|
184
|
+
* 흐리게 견주면 얻는 것 없이 잃기만 한다 — "별점"의 `stars`가 `align-start`를
|
|
185
|
+
* 물어 오는 식이다(실측 79건). 사전 낱말은 이미 정확한 영어다.
|
|
186
|
+
*/
|
|
187
|
+
fuzzyTerms: readonly string[] = terms,
|
|
188
|
+
): MatchTier {
|
|
189
|
+
const flat = name.toLowerCase();
|
|
190
|
+
const parts = words(name);
|
|
191
|
+
const joined = parts.join("");
|
|
192
|
+
let best: MatchTier = MATCH_NONE;
|
|
193
|
+
|
|
194
|
+
for (const raw of terms) {
|
|
195
|
+
const term = raw.toLowerCase().trim();
|
|
196
|
+
if (!term) continue;
|
|
197
|
+
|
|
198
|
+
if (flat === term || joined === term.replace(/[^a-z0-9]/g, "")) return MATCH_EXACT;
|
|
199
|
+
// `apple`로 쳤을 때 `apple-fill`·`apple-logo`도 정확히 맞은 것으로 친다.
|
|
200
|
+
if (parts[0] === term) return MATCH_EXACT;
|
|
201
|
+
|
|
202
|
+
// 조각이 **통째로** 그 말일 때만 친다. 글자만 들어 있으면 되게 두면
|
|
203
|
+
// `star`가 `align-start`를 물어 온다(실측 207건).
|
|
204
|
+
if (parts.includes(term)) {
|
|
205
|
+
best = Math.max(best, MATCH_PARTIAL) as MatchTier;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!fuzzyTerms.includes(raw)) continue;
|
|
210
|
+
const korean = hasHangul(term);
|
|
211
|
+
const key = korean ? koreanSkeleton(term) : skeleton(term);
|
|
212
|
+
if (key.length < 3) continue;
|
|
213
|
+
// 소리 후보는 한글에만 편다. 영어로 친 말은 이미 영어라 펼 것이 없다.
|
|
214
|
+
const sounds = korean ? variantsFor(term) : null;
|
|
215
|
+
const limit = tolerance(key);
|
|
216
|
+
for (const part of [joined, ...parts]) {
|
|
217
|
+
const other = skeleton(part);
|
|
218
|
+
if (other.length < 3) continue;
|
|
219
|
+
if (sounds?.has(other) || editDistance(key, other, limit) <= limit) {
|
|
220
|
+
best = Math.max(best, MATCH_FUZZY) as MatchTier;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return best;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 이름 목록을 등급별로 나눈다. 위 등급부터 내보내면 "정확한 것 먼저"가 된다.
|
|
230
|
+
*
|
|
231
|
+
* `ids`는 `"prefix:name"`이다 — 등급은 이름만 보고 매긴다.
|
|
232
|
+
*/
|
|
233
|
+
export function tierIds(
|
|
234
|
+
ids: readonly string[],
|
|
235
|
+
terms: readonly string[],
|
|
236
|
+
fuzzyTerms: readonly string[] = terms,
|
|
237
|
+
): { exact: string[]; partial: string[]; fuzzy: string[] } {
|
|
238
|
+
const exact: string[] = [];
|
|
239
|
+
const partial: string[] = [];
|
|
240
|
+
const fuzzy: string[] = [];
|
|
241
|
+
for (const id of ids) {
|
|
242
|
+
const colon = id.indexOf(":");
|
|
243
|
+
const name = colon > 0 ? id.slice(colon + 1) : id;
|
|
244
|
+
const tier = matchTier(name, terms, fuzzyTerms);
|
|
245
|
+
if (tier === MATCH_EXACT) exact.push(id);
|
|
246
|
+
else if (tier === MATCH_PARTIAL) partial.push(id);
|
|
247
|
+
else if (tier === MATCH_FUZZY) fuzzy.push(id);
|
|
248
|
+
}
|
|
249
|
+
return { exact, partial, fuzzy };
|
|
250
|
+
}
|