create-m2m-bo-template-vue 1.0.4 → 1.0.5

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 (223) hide show
  1. package/package.json +1 -1
  2. package/template/src/components/DataGrid/CellView.vue +11 -1
  3. package/template/src/components/DataGrid/DataGrid.module.css +41 -1
  4. package/template/src/components/DataGrid/DataGrid.vue +94 -10
  5. package/template/src/components/DataGrid/types.ts +6 -0
  6. package/template/src/components/common/ExcelDownloadButton.vue +1 -1
  7. package/template/src/components/common/PageSizeSelect.module.css +4 -10
  8. package/template/src/components/common/PageSizeSelect.vue +3 -2
  9. package/template/src/components/shell/GlobalSearchBar.module.css +13 -0
  10. package/template/src/components/shell/GlobalSearchBar.vue +64 -0
  11. package/template/src/components/shell/Header.module.css +31 -9
  12. package/template/src/components/shell/Header.vue +39 -30
  13. package/template/src/lib/globalSearch.ts +317 -0
  14. package/template/src/lib/nav.ts +8 -9
  15. package/template/src/lib/searchIndex.ts +429 -0
  16. package/template/src/pages/admin/AdminHistoryPage.vue +3 -1
  17. package/template/src/pages/admin/AdminRolesPage.vue +9 -8
  18. package/template/src/pages/admin/AdminsListPage.vue +1 -0
  19. package/template/src/pages/b2c/operations/ClaimPage.vue +2 -1
  20. package/template/src/pages/b2c/operations/DeliveryTablePage.vue +1 -0
  21. package/template/src/pages/b2c/products/B2cCategoryTreeRow.vue +75 -0
  22. package/template/src/pages/b2c/products/B2cProductBatchModal.module.css +168 -0
  23. package/template/src/pages/b2c/products/B2cProductBatchModal.vue +215 -0
  24. package/template/src/pages/b2c/products/B2cProductCreatePage.vue +34 -28
  25. package/template/src/pages/b2c/products/B2cProductHistoryDrawer.module.css +270 -0
  26. package/template/src/pages/b2c/products/B2cProductHistoryDrawer.vue +248 -0
  27. package/template/src/pages/b2c/products/B2cProductHistoryPage.vue +2 -1
  28. package/template/src/pages/b2c/products/B2cProductManagementPage.module.css +421 -0
  29. package/template/src/pages/b2c/products/B2cProductManagementPage.vue +780 -0
  30. package/template/src/pages/b2c/products/B2cStandardInfoPage.module.css +1200 -0
  31. package/template/src/pages/b2c/products/B2cStandardInfoPage.vue +996 -0
  32. package/template/src/pages/b2c/products/b2cProductsData.ts +362 -28
  33. package/template/src/pages/brands/BrandExposurePage.vue +2 -1
  34. package/template/src/pages/brands/BrandProductsPage.vue +2 -1
  35. package/template/src/pages/brands/BrandsListPage.vue +1 -1
  36. package/template/src/pages/brands/brandsData.ts +35 -2
  37. package/template/src/pages/c2c/chat/ChatAccessHistoryPage.vue +1 -0
  38. package/template/src/pages/c2c/chat/ChatListPage.vue +1 -1
  39. package/template/src/pages/c2c/proceeds/C2CSettlementManagementPage.vue +2 -1
  40. package/template/src/pages/c2c/proceeds/ProceedsLedgerPage.vue +1 -0
  41. package/template/src/pages/c2c/proceeds/ProceedsOverviewPage.vue +2 -1
  42. package/template/src/pages/c2c/proceeds/WithdrawalManagementPage.vue +2 -1
  43. package/template/src/pages/c2c/productPolicy/DetectionHistoryPage.vue +1 -0
  44. package/template/src/pages/c2c/productPolicy/ProductPolicyPage.vue +2 -1
  45. package/template/src/pages/c2c/products/UserProductModerationPage.vue +4 -3
  46. package/template/src/pages/c2c/products/UserProductReviewPage.vue +1 -1
  47. package/template/src/pages/c2c/purchases/PurchaseCancelPage.vue +1 -1
  48. package/template/src/pages/c2c/purchases/PurchaseDisputesPage.vue +1 -1
  49. package/template/src/pages/c2c/purchases/PurchaseHistoryPage.vue +1 -1
  50. package/template/src/pages/c2c/reports/ReportHistoryPage.vue +1 -0
  51. package/template/src/pages/c2c/reports/ReportProcessingPage.vue +1 -1
  52. package/template/src/pages/c2c/safety/TradeHoldManagementPage.vue +2 -1
  53. package/template/src/pages/c2c/safety/TradeRiskMonitoringPage.vue +2 -1
  54. package/template/src/pages/c2c/sales/SalesPerformancePage.vue +1 -0
  55. package/template/src/pages/c2c/sales/SalesStatusPage.vue +1 -1
  56. package/template/src/pages/c2c/sales/SalesTradesPage.vue +1 -1
  57. package/template/src/pages/c2c/sales/SellerListPage.vue +1 -1
  58. package/template/src/pages/c2c/sales/SellerProductsPage.vue +1 -1
  59. package/template/src/pages/c2c/sanctions/SanctionProcessingPage.vue +1 -1
  60. package/template/src/pages/c2c/verification/VerificationHistoryPage.vue +2 -1
  61. package/template/src/pages/c2c/verification/VerificationPolicyPage.vue +2 -1
  62. package/template/src/pages/c2c/verification/VerificationReviewPage.vue +2 -1
  63. package/template/src/pages/cancel/CancelApprovalPage.vue +1 -0
  64. package/template/src/pages/cancel/CancelCompletedPage.vue +1 -0
  65. package/template/src/pages/cancel/CancelHistoryPage.vue +1 -0
  66. package/template/src/pages/cancel/CancelPartialPage.vue +1 -0
  67. package/template/src/pages/cancel/CancelRejectedPage.vue +1 -0
  68. package/template/src/pages/cancel/CancelRequestsPage.vue +1 -0
  69. package/template/src/pages/cartconversion/CartConversionPage.vue +3 -2
  70. package/template/src/pages/common/ModulePlaceholderPage.vue +3 -1
  71. package/template/src/pages/components/ComponentsPage.vue +10 -8
  72. package/template/src/pages/content/CategoriesPage.vue +1 -1
  73. package/template/src/pages/content/ContentListPage.vue +1 -1
  74. package/template/src/pages/content/ReviewPage.vue +1 -1
  75. package/template/src/pages/content/TagsPage.vue +1 -1
  76. package/template/src/pages/contracts/ContractPeriodPage.vue +2 -1
  77. package/template/src/pages/contracts/ContractsPage.vue +2 -1
  78. package/template/src/pages/contracts/TradeTermsPage.vue +2 -1
  79. package/template/src/pages/contracts/UnitPricePage.vue +2 -1
  80. package/template/src/pages/coupons/AutomaticCouponsPage.vue +1 -0
  81. package/template/src/pages/coupons/CouponIssuesPage.vue +1 -1
  82. package/template/src/pages/coupons/CouponPolicyPage.vue +16 -15
  83. package/template/src/pages/coupons/CouponUsagePage.vue +2 -1
  84. package/template/src/pages/coupons/CouponsListPage.vue +1 -1
  85. package/template/src/pages/coupons/ExpiredCouponsPage.vue +1 -0
  86. package/template/src/pages/cs/ProductInquiriesListPage.vue +1 -0
  87. package/template/src/pages/delivery/DeliveryCarriersPage.vue +1 -0
  88. package/template/src/pages/delivery/DeliveryCompletePage.vue +1 -0
  89. package/template/src/pages/delivery/DeliveryFailedPage.vue +1 -0
  90. package/template/src/pages/delivery/DeliveryHistoryPage.vue +1 -0
  91. package/template/src/pages/delivery/DeliveryHoldPage.vue +1 -0
  92. package/template/src/pages/delivery/DeliveryInvoicesPage.vue +1 -0
  93. package/template/src/pages/delivery/DeliveryListPage.vue +1 -0
  94. package/template/src/pages/delivery/DeliveryPrepPage.vue +1 -0
  95. package/template/src/pages/delivery/DeliveryTrackingPage.vue +1 -0
  96. package/template/src/pages/delivery/InTransitPage.vue +1 -0
  97. package/template/src/pages/delivery/OutboundCompletePage.vue +1 -0
  98. package/template/src/pages/delivery/OutboundWaitingPage.vue +1 -0
  99. package/template/src/pages/delivery/deliveryShared.module.css +81 -0
  100. package/template/src/pages/deliverypolicy/RegionalShippingFeePage.vue +1 -0
  101. package/template/src/pages/deliverypolicy/ShippingBaseFeePage.vue +8 -7
  102. package/template/src/pages/exchanges/ExchangeCollectionPage.vue +1 -0
  103. package/template/src/pages/exchanges/ExchangeHistoryPage.vue +1 -0
  104. package/template/src/pages/exchanges/ExchangeHoldPage.vue +1 -0
  105. package/template/src/pages/exchanges/ExchangeRequestsPage.vue +1 -0
  106. package/template/src/pages/exchanges/ExchangeReshipPage.vue +1 -0
  107. package/template/src/pages/inventory/InboundManagementPage.vue +1 -1
  108. package/template/src/pages/inventory/InventoryAdjustmentPage.vue +1 -1
  109. package/template/src/pages/inventory/InventoryAlertsPage.vue +6 -4
  110. package/template/src/pages/inventory/InventoryStatusPage.vue +4 -3
  111. package/template/src/pages/inventory/SafetyStockPage.vue +6 -4
  112. package/template/src/pages/inventory/StockMovementHistoryPage.vue +5 -3
  113. package/template/src/pages/inventory/StockOutboundManagementPage.vue +1 -1
  114. package/template/src/pages/logs/SecurityLogPage.vue +2 -1
  115. package/template/src/pages/logs/SystemLogPage.vue +11 -8
  116. package/template/src/pages/members/MembersPage.vue +6 -5
  117. package/template/src/pages/notifications/NotificationDispatchPage.vue +2 -1
  118. package/template/src/pages/notifications/NotificationTemplatesPage.vue +1005 -0
  119. package/template/src/pages/ops/BannerDetailDrawer.vue +185 -0
  120. package/template/src/pages/ops/BannerEditorDrawer.vue +272 -0
  121. package/template/src/pages/ops/BannersPage.vue +582 -0
  122. package/template/src/pages/ops/EventDetailDrawer.vue +229 -0
  123. package/template/src/pages/ops/EventEditorPage.vue +464 -0
  124. package/template/src/pages/ops/EventPreviewDialog.vue +104 -0
  125. package/template/src/pages/ops/EventsPage.vue +644 -0
  126. package/template/src/pages/ops/FaqDetailDrawer.vue +188 -0
  127. package/template/src/pages/ops/FaqEditorDrawer.vue +263 -0
  128. package/template/src/pages/ops/FaqPage.vue +648 -0
  129. package/template/src/pages/ops/NoticeEditorDrawer.vue +217 -0
  130. package/template/src/pages/ops/NoticesPage.vue +498 -0
  131. package/template/src/pages/ops/PolicyDetailDrawer.vue +122 -0
  132. package/template/src/pages/ops/PolicyManagementPage.vue +337 -0
  133. package/template/src/pages/ops/PopupDetailDrawer.vue +205 -0
  134. package/template/src/pages/ops/PopupEditorDrawer.vue +342 -0
  135. package/template/src/pages/ops/PopupsPage.vue +554 -0
  136. package/template/src/pages/ops/TermsDetailDrawer.vue +143 -0
  137. package/template/src/pages/ops/TermsManagementPage.vue +400 -0
  138. package/template/src/pages/orders/OrderApprovalPage.vue +2 -1
  139. package/template/src/pages/orders/OrderCompletedPage.vue +2 -1
  140. package/template/src/pages/orders/OrderProcessingPage.vue +2 -1
  141. package/template/src/pages/orders/PurchaseOrdersPage.vue +2 -1
  142. package/template/src/pages/orders/RefundPage.vue +1 -1
  143. package/template/src/pages/partners/CompaniesPage.vue +2 -1
  144. package/template/src/pages/partners/ContactsPage.vue +2 -1
  145. package/template/src/pages/partners/CreditTermsPage.vue +2 -1
  146. package/template/src/pages/partners/TradeStatusPage.vue +2 -1
  147. package/template/src/pages/paymentmgmt/ExternalTransactionPage.vue +2 -1
  148. package/template/src/pages/paymentmgmt/PaymentAuditPage.vue +1 -1
  149. package/template/src/pages/paymentmgmt/PaymentListPage.vue +1 -1
  150. package/template/src/pages/payments/DepositConfirmPage.vue +2 -1
  151. package/template/src/pages/payments/PaymentRegisterDrawer.vue +13 -12
  152. package/template/src/pages/payments/PaymentsPage.vue +2 -1
  153. package/template/src/pages/payments/ReceivablesPage.vue +2 -1
  154. package/template/src/pages/payments/TaxInvoicesPage.vue +2 -1
  155. package/template/src/pages/points/PointExpiringPage.vue +5 -4
  156. package/template/src/pages/points/PointLedgerPage.vue +2 -1
  157. package/template/src/pages/points/PointPolicyPage.vue +32 -29
  158. package/template/src/pages/points/PointsBalancePage.vue +1 -1
  159. package/template/src/pages/policy/CancelPolicyPage.module.css +396 -0
  160. package/template/src/pages/policy/CancelPolicyPage.vue +1134 -0
  161. package/template/src/pages/policy/CancelReasonEditDialog.vue +53 -0
  162. package/template/src/pages/policy/FeePolicyDrawer.vue +405 -0
  163. package/template/src/pages/policy/FeePolicyPage.module.css +757 -0
  164. package/template/src/pages/policy/FeePolicyPage.vue +1270 -0
  165. package/template/src/pages/policy/OrderStatusDrawer.vue +259 -0
  166. package/template/src/pages/policy/OrderStatusPage.module.css +114 -0
  167. package/template/src/pages/policy/OrderStatusPage.vue +467 -0
  168. package/template/src/pages/policy/PaymentMethodEditDialog.vue +76 -0
  169. package/template/src/pages/policy/PaymentPolicyPage.module.css +355 -0
  170. package/template/src/pages/policy/PaymentPolicyPage.vue +1056 -0
  171. package/template/src/pages/policy/RefundPolicyPage.module.css +273 -0
  172. package/template/src/pages/policy/RefundPolicyPage.vue +921 -0
  173. package/template/src/pages/policy/RefundReasonEditDialog.vue +76 -0
  174. package/template/src/pages/policy/SettlementPolicyPage.module.css +296 -0
  175. package/template/src/pages/policy/SettlementPolicyPage.vue +983 -0
  176. package/template/src/pages/policy/TransitionEditDialog.vue +67 -0
  177. package/template/src/pages/policy/cancelPolicyData.ts +294 -0
  178. package/template/src/pages/policy/feePolicyData.ts +688 -0
  179. package/template/src/pages/policy/orderStatusData.ts +202 -0
  180. package/template/src/pages/policy/paymentPolicyData.ts +240 -0
  181. package/template/src/pages/policy/refundPolicyData.ts +294 -0
  182. package/template/src/pages/policy/settlementPolicyData.ts +296 -0
  183. package/template/src/pages/products/MinOrderQtyPage.vue +2 -1
  184. package/template/src/pages/products/PartnerPricingPage.vue +2 -1
  185. package/template/src/pages/products/ProductsListPage.vue +2 -1
  186. package/template/src/pages/products/SupplyPricePage.vue +2 -1
  187. package/template/src/pages/promotions/PromotionApplicationsPage.vue +2 -1
  188. package/template/src/pages/promotions/PromotionStackSettingPage.vue +12 -11
  189. package/template/src/pages/promotions/PromotionsListPage.vue +1 -1
  190. package/template/src/pages/quotes/QuoteApprovalPage.vue +2 -1
  191. package/template/src/pages/quotes/QuoteHistoryPage.vue +2 -1
  192. package/template/src/pages/quotes/QuoteRequestsPage.vue +2 -1
  193. package/template/src/pages/quotes/QuotesPage.vue +2 -1
  194. package/template/src/pages/records/RecordsPage.vue +7 -5
  195. package/template/src/pages/returns/ReturnHistoryPage.vue +1 -0
  196. package/template/src/pages/returns/ReturnListPage.vue +1 -0
  197. package/template/src/pages/reviews/ReviewAnalyticsPage.vue +2 -1
  198. package/template/src/pages/reviews/ReviewsListPage.vue +1 -1
  199. package/template/src/pages/search/GlobalSearchPage.module.css +706 -0
  200. package/template/src/pages/search/GlobalSearchPage.vue +632 -0
  201. package/template/src/pages/settlement/SettlementAdjustmentsPage.vue +2 -1
  202. package/template/src/pages/settlement/SettlementPage.vue +2 -1
  203. package/template/src/pages/settlement/SettlementTransactionsPage.vue +2 -1
  204. package/template/src/pages/stats/CancelRateStatsPage.vue +4 -2
  205. package/template/src/pages/stats/CustomerPurchaseAnalysisPage.vue +4 -2
  206. package/template/src/pages/stats/DisputeRateStatsPage.vue +4 -2
  207. package/template/src/pages/stats/ProductRegistrationStatsPage.vue +3 -1
  208. package/template/src/pages/stats/ReportRateStatsPage.vue +4 -2
  209. package/template/src/pages/stats/SalesAnalysisPage.vue +9 -7
  210. package/template/src/pages/stats/SellerProceedsStatsPage.vue +4 -2
  211. package/template/src/pages/stats/StatisticsFilterToolbar.vue +2 -1
  212. package/template/src/pages/stats/TrafficStatsPage.vue +2 -1
  213. package/template/src/pages/system/CommonCodesPage.vue +1 -1
  214. package/template/src/pages/system/IntegrationManagementPage.vue +1 -1
  215. package/template/src/pages/system/JobManagementPage.vue +1 -1
  216. package/template/src/pages/system/ServiceSettingsPage.vue +1 -1
  217. package/template/src/router/routes.b2cProducts.ts +10 -12
  218. package/template/src/router/routes.brands.ts +3 -4
  219. package/template/src/router/routes.notifications.ts +16 -0
  220. package/template/src/router/routes.ops.ts +21 -0
  221. package/template/src/router/routes.policy.ts +17 -0
  222. package/template/src/router/routes.search.ts +6 -0
  223. package/template/src/router/routes.ts +8 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m2m-bo-template-vue",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Scaffolding CLI for the M2M back-office vue template.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,10 +1,13 @@
1
1
  <script setup lang="ts">
2
+ import { ref } from 'vue';
2
3
  import { Badge } from 'm2m-uiux-vue/Badge';
3
4
  import styles from './DataGrid.module.css';
4
5
  import RowMenuView from './RowMenuView.vue';
5
6
  import type { Cell } from './types';
6
7
 
7
8
  defineProps<{ cell: Cell }>();
9
+
10
+ const thumbFailed = ref(false);
8
11
  </script>
9
12
 
10
13
  <template>
@@ -85,7 +88,14 @@ defineProps<{ cell: Cell }>();
85
88
  </span>
86
89
 
87
90
  <div v-else-if="cell.kind === 'thumbTitle'" :class="styles.thumbTitle">
88
- <div :class="styles.thumb" :style="{ background: `repeating-linear-gradient(135deg, ${cell.thumb} 0 6px, #fff 6px 12px)` }" />
91
+ <img
92
+ v-if="cell.imageSrc && !thumbFailed"
93
+ :class="[styles.thumb, styles.thumbImage]"
94
+ :src="cell.imageSrc"
95
+ alt=""
96
+ @error="thumbFailed = true"
97
+ />
98
+ <div v-else :class="styles.thumb" :style="{ background: `repeating-linear-gradient(135deg, ${cell.thumb} 0 6px, #fff 6px 12px)` }" aria-hidden="true" />
89
99
  <div style="min-width: 0">
90
100
  <button type="button" :class="styles.thumbTitleBtn" :style="{ color: cell.titleFg }" @click="cell.onClick?.()">{{ cell.title }}</button>
91
101
  <span :class="styles.thumbTitleId">#{{ cell.id }}</span>
@@ -71,12 +71,49 @@
71
71
  position: sticky;
72
72
  }
73
73
  .headBtn {
74
+ display: inline-flex;
75
+ align-items: center;
76
+ gap: 3px;
74
77
  border: 0;
75
78
  background: transparent;
76
- padding: 0;
79
+ padding: 3px 6px;
80
+ margin: -3px -6px;
81
+ border-radius: 5px;
77
82
  font: inherit;
78
83
  color: inherit;
79
84
  cursor: pointer;
85
+ transition:
86
+ background-color 0.12s ease,
87
+ color 0.12s ease;
88
+ user-select: none;
89
+ max-width: 100%;
90
+ }
91
+ .headBtn:hover {
92
+ background: rgba(0, 0, 0, 0.05);
93
+ color: #18181b;
94
+ }
95
+
96
+ .sortIcon {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ font-size: 0.59375rem;
101
+ line-height: 1;
102
+ color: #a1a1aa;
103
+ opacity: 0.35;
104
+ transition:
105
+ opacity 0.12s ease,
106
+ color 0.12s ease;
107
+ flex: none;
108
+ }
109
+ .headBtn:hover .sortIcon {
110
+ opacity: 0.85;
111
+ color: #52525b;
112
+ }
113
+ .sortIconActive {
114
+ opacity: 1 !important;
115
+ color: var(--common-primary, #2563eb) !important;
116
+ font-weight: 700;
80
117
  }
81
118
 
82
119
  .row {
@@ -378,6 +415,9 @@
378
415
  border-radius: 6px;
379
416
  border: 1px solid rgba(0, 0, 0, 0.07);
380
417
  }
418
+ .thumbImage {
419
+ object-fit: cover;
420
+ }
381
421
  .thumbTitleBtn {
382
422
  display: block;
383
423
  max-width: 100%;
@@ -6,7 +6,10 @@ import CellView from './CellView.vue';
6
6
  import styles from './DataGrid.module.css';
7
7
  import type { Cell, DataGridProps, GridColumn, GridRow } from './types';
8
8
 
9
- const props = defineProps<DataGridProps>();
9
+ const props = withDefaults(defineProps<DataGridProps>(), {
10
+ sortable: true,
11
+ showPagination: true,
12
+ });
10
13
 
11
14
  const isInlineDetailLabel = (value?: string) => Boolean(value && /^상세(?:\s*보기)?$/.test(value.trim()));
12
15
 
@@ -62,6 +65,38 @@ function withoutGridTracks(template: string, removed: Set<number>, columnCount:
62
65
  return template;
63
66
  }
64
67
 
68
+ function parseSortableValue(val: unknown): { isNum: boolean; num: number; str: string } {
69
+ if (typeof val === 'number') return { isNum: !Number.isNaN(val), num: val, str: String(val) };
70
+ if (typeof val !== 'string') val = String(val ?? '');
71
+ const str = (val as string).trim();
72
+ if (!str) return { isNum: false, num: 0, str: '' };
73
+ const cleanStr = str.replace(/,/g, '').replace(/[원개건회명%₩$\s]/g, '');
74
+ if (cleanStr && !Number.isNaN(Number(cleanStr)) && !str.includes('/') && !str.includes(':')) {
75
+ const isNegativeOrSingle = !str.includes('-') || (str.startsWith('-') && str.indexOf('-', 1) === -1);
76
+ if (isNegativeOrSingle) return { isNum: true, num: Number(cleanStr), str };
77
+ }
78
+ return { isNum: false, num: 0, str };
79
+ }
80
+
81
+ function getCellRawValue(cell: Cell | undefined): unknown {
82
+ if (!cell) return '';
83
+ if (cell.kind === 'progress') return cell.pct;
84
+ if (cell.kind === 'custom') return cell.exportValue ?? '';
85
+ return cellExportValue(cell);
86
+ }
87
+
88
+ function compareCells(cellA: Cell | undefined, cellB: Cell | undefined, direction: 'asc' | 'desc'): number {
89
+ const parsedA = parseSortableValue(getCellRawValue(cellA));
90
+ const parsedB = parseSortableValue(getCellRawValue(cellB));
91
+ if (!parsedA.str && !parsedB.str) return 0;
92
+ if (!parsedA.str) return 1;
93
+ if (!parsedB.str) return -1;
94
+ let comparison = 0;
95
+ if (parsedA.isNum && parsedB.isNum) comparison = parsedA.num - parsedB.num;
96
+ else comparison = parsedA.str.localeCompare(parsedB.str, 'ko', { numeric: true, sensitivity: 'base' });
97
+ return direction === 'asc' ? comparison : -comparison;
98
+ }
99
+
65
100
  function cellExportValue(cell: Cell): string {
66
101
  switch (cell.kind) {
67
102
  case 'text':
@@ -136,10 +171,12 @@ function cellFgColor(cell: Cell): string | undefined {
136
171
  }
137
172
 
138
173
  const rootRef = ref<HTMLDivElement | null>(null);
139
- const autoSelectable = ref(false);
140
174
  const internalSelected = ref<Set<string | number>>(new Set());
141
175
  const pageSize = ref<number | null>(null);
142
176
  const currentPage = ref(1);
177
+ const sortColumnIndex = ref<number | null>(props.defaultSortColumn ?? null);
178
+ const sortDirection = ref<'asc' | 'desc' | null>(props.defaultSortDirection ?? null);
179
+ const effectiveSortable = computed(() => props.sortable ?? true);
143
180
 
144
181
  function handlePageSize(event: Event) {
145
182
  const size = (event as CustomEvent<{ pageSize?: number }>).detail?.pageSize;
@@ -150,9 +187,6 @@ function handlePageSize(event: Event) {
150
187
 
151
188
  onMounted(() => {
152
189
  rootRef.value?.addEventListener(DATA_GRID_PAGE_SIZE_EVENT, handlePageSize);
153
- if (props.selectable === undefined) {
154
- autoSelectable.value = Boolean(rootRef.value?.ownerDocument.querySelector('[data-excel-download]'));
155
- }
156
190
  });
157
191
  onUnmounted(() => {
158
192
  rootRef.value?.removeEventListener(DATA_GRID_PAGE_SIZE_EVENT, handlePageSize);
@@ -172,7 +206,7 @@ watch(
172
206
  }
173
207
  );
174
208
 
175
- const effectiveSelectable = computed(() => props.selectable ?? autoSelectable.value);
209
+ const effectiveSelectable = computed(() => props.selectable ?? false);
176
210
  const pageStart = computed(() => (pageSize.value ? (currentPage.value - 1) * pageSize.value : 0));
177
211
  const visibleSourceRows = computed(() => (pageSize.value ? props.rows.slice(pageStart.value, pageStart.value + pageSize.value) : props.rows));
178
212
 
@@ -226,7 +260,42 @@ const displayRows = computed(() =>
226
260
  }),
227
261
  }))
228
262
  );
229
- const pagedDisplayRows = computed(() => (pageSize.value ? displayRows.value.slice(pageStart.value, pageStart.value + pageSize.value) : displayRows.value));
263
+ const sortedDisplayRows = computed(() => {
264
+ if (sortColumnIndex.value === null || sortDirection.value === null) return displayRows.value;
265
+ const index = sortColumnIndex.value;
266
+ const direction = sortDirection.value;
267
+ return [...displayRows.value].sort((rowA, rowB) => compareCells(rowA.cells[index], rowB.cells[index], direction));
268
+ });
269
+ const pagedDisplayRows = computed(() => (pageSize.value ? sortedDisplayRows.value.slice(pageStart.value, pageStart.value + pageSize.value) : sortedDisplayRows.value));
270
+
271
+ function isSortableColumn(col: GridColumn) {
272
+ return effectiveSortable.value && (col.sortable ?? true) && !(typeof col.label === 'string' && ['관리', '상세', '설정', '메뉴'].includes(col.label.trim()));
273
+ }
274
+
275
+ watch(displayColumns, (columns) => {
276
+ if (sortColumnIndex.value !== null && sortColumnIndex.value >= columns.length) {
277
+ sortColumnIndex.value = null;
278
+ sortDirection.value = null;
279
+ }
280
+ });
281
+
282
+ function handleHeaderClick(columnIndex: number, col: GridColumn) {
283
+ col.onClick?.();
284
+ if (!isSortableColumn(col)) return;
285
+ let nextDirection: 'asc' | 'desc' | null = 'asc';
286
+ let nextColumn: number | null = columnIndex;
287
+ if (sortColumnIndex.value === columnIndex) {
288
+ if (sortDirection.value === 'asc') nextDirection = 'desc';
289
+ else if (sortDirection.value === 'desc') {
290
+ nextDirection = null;
291
+ nextColumn = null;
292
+ } else nextDirection = 'asc';
293
+ }
294
+ sortColumnIndex.value = nextColumn;
295
+ sortDirection.value = nextDirection;
296
+ currentPage.value = 1;
297
+ props.onSortChange?.(nextColumn, nextDirection);
298
+ }
230
299
 
231
300
  const temporalColumnIndexes = computed(() => new Set(displayColumns.value.flatMap((column, columnIndex) => (isTemporalColumnLabel(column.label) ? [columnIndex] : []))));
232
301
  const serialColumnIndexes = computed(() => new Set(displayColumns.value.flatMap((column, columnIndex) => (isSerialColumnLabel(column.label) ? [columnIndex] : []))));
@@ -315,9 +384,24 @@ function cellAlignOf(cell: Cell, index: number) {
315
384
  <input type="checkbox" :class="styles.checkbox" :checked="effectiveAllSelected" style="pointer-events: none" aria-label="현재 목록 전체 선택" @change="() => {}" />
316
385
  </div>
317
386
  <template v-for="(col, i) in displayColumns" :key="i">
318
- <button v-if="col.onClick" type="button" :class="styles.headBtn" :style="{ textAlign: centeredColumnIndexes.has(i) ? 'center' : col.align }" @click="col.onClick">
319
- <span data-datagrid-column>{{ col.label }}</span>
320
- </button>
387
+ <div
388
+ v-if="isSortableColumn(col) || col.onClick"
389
+ :style="{ display: 'flex', justifyContent: centeredColumnIndexes.has(i) ? 'center' : col.align === 'right' ? 'flex-end' : 'flex-start', minWidth: 0 }"
390
+ >
391
+ <button
392
+ type="button"
393
+ :class="styles.headBtn"
394
+ :style="{ justifyContent: centeredColumnIndexes.has(i) ? 'center' : col.align === 'right' ? 'flex-end' : 'flex-start' }"
395
+ :aria-sort="sortColumnIndex === i && sortDirection ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'"
396
+ :title="isSortableColumn(col) ? (sortColumnIndex !== i || !sortDirection ? '오름차순 정렬' : sortDirection === 'asc' ? '내림차순 정렬' : '정렬 해제') : undefined"
397
+ @click="handleHeaderClick(i, col)"
398
+ >
399
+ <span data-datagrid-column>{{ col.label }}</span>
400
+ <span v-if="isSortableColumn(col)" :class="[styles.sortIcon, sortColumnIndex === i && sortDirection && styles.sortIconActive]" aria-hidden="true">{{
401
+ sortColumnIndex === i && sortDirection === 'asc' ? '▲' : sortColumnIndex === i && sortDirection === 'desc' ? '▼' : '⇅'
402
+ }}</span>
403
+ </button>
404
+ </div>
321
405
  <span v-else :style="{ textAlign: centeredColumnIndexes.has(i) ? 'center' : col.align }" data-datagrid-column>{{ col.label }}</span>
322
406
  </template>
323
407
  </div>
@@ -100,6 +100,7 @@ export interface NoTagCell extends BaseCell {
100
100
  export interface ThumbTitleCell extends BaseCell {
101
101
  kind: 'thumbTitle';
102
102
  thumb: string;
103
+ imageSrc?: string;
103
104
  title: string;
104
105
  id: string;
105
106
  titleFg?: string;
@@ -161,6 +162,7 @@ export interface GridColumn {
161
162
  label: string;
162
163
  align?: Align;
163
164
  onClick?: () => void;
165
+ sortable?: boolean;
164
166
  }
165
167
 
166
168
  export interface PageBtn {
@@ -191,4 +193,8 @@ export interface DataGridProps {
191
193
  showTopBar?: boolean;
192
194
  totalLabel?: string;
193
195
  actions?: { label: string; onClick: () => void }[];
196
+ sortable?: boolean;
197
+ defaultSortColumn?: number;
198
+ defaultSortDirection?: 'asc' | 'desc';
199
+ onSortChange?: (columnIndex: number | null, direction: 'asc' | 'desc' | null) => void;
194
200
  }
@@ -15,7 +15,7 @@ const emit = defineEmits<{ click: [MouseEvent] }>();
15
15
  type="button"
16
16
  variant="secondary"
17
17
  size="sm"
18
- :class="[$style.button, className]"
18
+ :class-names="[$style.button, className ?? '']"
19
19
  data-excel-download
20
20
  data-grid-download
21
21
  @click="emit('click', $event)"
@@ -3,6 +3,10 @@
3
3
  display: inline-flex;
4
4
  align-items: center;
5
5
  vertical-align: middle;
6
+ /* 부모가 flex/grid일 때 늘어나면 화살표(absolute)가 박스 밖에 떠버린다 */
7
+ width: fit-content;
8
+ flex: none;
9
+ align-self: flex-start;
6
10
  }
7
11
 
8
12
  .select {
@@ -39,13 +43,3 @@
39
43
  cursor: not-allowed;
40
44
  background-color: var(--common-bg-disabled, #f4f4f5);
41
45
  }
42
-
43
- .arrowIcon {
44
- position: absolute;
45
- right: 9px;
46
- top: 50%;
47
- transform: translateY(-50%);
48
- pointer-events: none;
49
- color: var(--common-text-muted, #71717a);
50
- flex-shrink: 0;
51
- }
@@ -19,7 +19,6 @@ export const DATA_GRID_PAGE_SIZE_EVENT = 'data-grid-page-size-change';
19
19
 
20
20
  <script setup lang="ts">
21
21
  import { onMounted, onUnmounted, ref } from 'vue';
22
- import { ChevronDown } from 'lucide-vue-next';
23
22
 
24
23
  function pageSizeOf(value: string) {
25
24
  const parsed = Number.parseInt(value.replace(/[^0-9]/g, ''), 10);
@@ -50,6 +49,8 @@ function notifyNearestDataGrid(select: HTMLSelectElement) {
50
49
  grid?.dispatchEvent(new CustomEvent(DATA_GRID_PAGE_SIZE_EVENT, { detail: { pageSize } }));
51
50
  }
52
51
 
52
+ defineOptions({ inheritAttrs: false });
53
+
53
54
  const props = withDefaults(defineProps<PageSizeSelectProps>(), {
54
55
  options: () => DEFAULT_PAGE_SIZE_OPTIONS,
55
56
  defaultValue: '20개씩 보기',
@@ -81,6 +82,7 @@ function handleChange(event: Event) {
81
82
  <div :class="[$style.wrapper, wrapperClassName].filter(Boolean).join(' ')">
82
83
  <select
83
84
  ref="selectRef"
85
+ v-bind="$attrs"
84
86
  :aria-label="props['aria-label'] ?? '페이지당 표시 개수'"
85
87
  :value="value ?? defaultValue"
86
88
  :class="[$style.select, className].filter(Boolean).join(' ')"
@@ -92,7 +94,6 @@ function handleChange(event: Event) {
92
94
  </option>
93
95
  </slot>
94
96
  </select>
95
- <ChevronDown :size="14" :class="$style.arrowIcon" aria-hidden="true" />
96
97
  </div>
97
98
  </template>
98
99
 
@@ -0,0 +1,13 @@
1
+ .bar {
2
+ width: 100%;
3
+ }
4
+
5
+ /* 헤더 안에서는 각진 모서리로 통일 (공용 SearchField 기본값 덮어쓰기) */
6
+ .bar > * {
7
+ border-radius: 0;
8
+ }
9
+
10
+ .bar input,
11
+ .bar button {
12
+ border-radius: 0;
13
+ }
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, onUnmounted, ref } from 'vue';
3
+ import { useRouter } from 'vue-router';
4
+ import SearchField from '../SearchField/SearchField.vue';
5
+ import styles from './GlobalSearchBar.module.css';
6
+
7
+ withDefaults(defineProps<{ placeholder?: string; className?: string }>(), {
8
+ placeholder: '메뉴, 상품, 주문 검색',
9
+ className: '',
10
+ });
11
+
12
+ const router = useRouter();
13
+ const wrapRef = ref<HTMLElement | null>(null);
14
+ const keyword = ref('');
15
+
16
+ function focusInput() {
17
+ wrapRef.value?.querySelector('input')?.focus();
18
+ wrapRef.value?.querySelector('input')?.select();
19
+ }
20
+
21
+ function handler(e: KeyboardEvent) {
22
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
23
+ e.preventDefault();
24
+ focusInput();
25
+ }
26
+ }
27
+
28
+ onMounted(() => window.addEventListener('keydown', handler));
29
+ onUnmounted(() => window.removeEventListener('keydown', handler));
30
+
31
+ function runSearch(value: string) {
32
+ const query = value.trim();
33
+ if (!query) {
34
+ wrapRef.value?.querySelector('input')?.focus();
35
+ return;
36
+ }
37
+ router.push(`/search?q=${encodeURIComponent(query)}`);
38
+ }
39
+
40
+ function handleKeyDown(e: KeyboardEvent) {
41
+ if (e.key === 'Enter') {
42
+ e.preventDefault();
43
+ runSearch(keyword.value);
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <template>
49
+ <span ref="wrapRef">
50
+ <SearchField
51
+ size="sm"
52
+ clearable
53
+ full-width
54
+ :class-name="`${styles.bar} ${className}`"
55
+ :placeholder="placeholder"
56
+ shortcut-hint="Ctrl K"
57
+ :value="keyword"
58
+ :on-value-change="(v: string) => (keyword = v)"
59
+ :on-search="runSearch"
60
+ @keydown="handleKeyDown"
61
+ aria-label="통합 검색"
62
+ />
63
+ </span>
64
+ </template>
@@ -1,12 +1,15 @@
1
1
  .header {
2
2
  display: flex;
3
3
  align-items: center;
4
- gap: 14px;
4
+ gap: 12px;
5
5
  height: 52px;
6
- padding: 0 18px;
6
+ padding: 0 20px;
7
7
  background: var(--common-bg);
8
8
  border-bottom: 1px solid var(--common-border);
9
+ border-radius: 0;
9
10
  flex: none;
11
+ width: 100%;
12
+ box-sizing: border-box;
10
13
  }
11
14
 
12
15
  .crumb {
@@ -15,6 +18,9 @@
15
18
  gap: 7px;
16
19
  font-size: 0.78125rem;
17
20
  color: var(--common-text-muted);
21
+ white-space: nowrap;
22
+ flex-shrink: 0;
23
+ min-width: max-content;
18
24
  }
19
25
  .crumbSep {
20
26
  color: var(--common-border-strong);
@@ -22,10 +28,26 @@
22
28
  .crumbLeaf {
23
29
  color: var(--common-text);
24
30
  font-weight: 600;
31
+ white-space: nowrap;
25
32
  }
26
33
 
27
34
  .spacer {
28
35
  flex: 1;
36
+ min-width: 8px;
37
+ }
38
+
39
+ .searchWrapper {
40
+ flex: 0 1 340px;
41
+ min-width: 180px;
42
+ max-width: 400px;
43
+ }
44
+
45
+ .rightGroup {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 8px;
49
+ flex-shrink: 0;
50
+ white-space: nowrap;
29
51
  }
30
52
 
31
53
  .modeSwitcherWrapper {
@@ -37,7 +59,7 @@
37
59
  padding: 0 10px;
38
60
  border: 1px solid var(--common-border);
39
61
  background: var(--common-bg);
40
- border-radius: 8px;
62
+ border-radius: var(--common-radius-md);
41
63
  color: var(--common-text);
42
64
  font-size: 0.75rem;
43
65
  font-weight: 600;
@@ -57,7 +79,7 @@
57
79
  font-size: 0.65625rem;
58
80
  font-weight: 700;
59
81
  padding: 1px 6px;
60
- border-radius: 4px;
82
+ border-radius: var(--common-radius-sm);
61
83
  }
62
84
 
63
85
  .modeDropdown {
@@ -67,7 +89,7 @@
67
89
  min-width: 175px;
68
90
  background: var(--common-bg);
69
91
  border: 1px solid var(--common-border);
70
- border-radius: 10px;
92
+ border-radius: var(--common-radius-lg);
71
93
  box-shadow: var(--common-shadow-popover);
72
94
  padding: 6px;
73
95
  z-index: 1000;
@@ -81,7 +103,7 @@
81
103
  align-items: center;
82
104
  justify-content: space-between;
83
105
  padding: 7px 10px;
84
- border-radius: 6px;
106
+ border-radius: var(--common-radius-sm);
85
107
  border: none;
86
108
  background: none;
87
109
  font-size: 0.75rem;
@@ -115,7 +137,7 @@
115
137
  cursor: pointer;
116
138
  border: none;
117
139
  background: none;
118
- border-radius: 4px;
140
+ border-radius: var(--common-radius-sm);
119
141
  }
120
142
 
121
143
  .modeDropdownLink:hover {
@@ -127,7 +149,7 @@
127
149
  width: 32px;
128
150
  border: 1px solid var(--common-border);
129
151
  background: var(--common-bg);
130
- border-radius: 9px;
152
+ border-radius: var(--common-radius-md);
131
153
  color: var(--common-text-sub);
132
154
  cursor: pointer;
133
155
  display: flex;
@@ -154,7 +176,7 @@
154
176
  .accountAvatar {
155
177
  width: 26px;
156
178
  height: 26px;
157
- border-radius: 8px;
179
+ border-radius: var(--common-radius-md);
158
180
  background: var(--common-primary-light);
159
181
  color: var(--common-primary);
160
182
  display: flex;
@@ -4,6 +4,7 @@ import { Blocks, Settings, ChevronDown, Check } from 'lucide-vue-next';
4
4
  import { Header as M2MHeader } from 'm2m-uiux-vue/Header';
5
5
  import { useRoute, useRouter } from 'vue-router';
6
6
  import { activeKeyForPath, breadcrumbForKey } from '../../lib/nav';
7
+ import GlobalSearchBar from './GlobalSearchBar.vue';
7
8
  import {
8
9
  loadBusinessSettings,
9
10
  saveBusinessSettings,
@@ -86,43 +87,51 @@ function goSystemDisplay() {
86
87
 
87
88
  <div :class="styles.spacer" />
88
89
 
89
- <div ref="dropdownRef" :class="styles.modeSwitcherWrapper">
90
- <button type="button" :class="styles.modeSwitcherBtn" title="작업 비즈니스 모드 변경" @click="modeDropdownOpen = !modeDropdownOpen">
91
- <span :class="styles.modeTag" :style="{ backgroundColor: modeBadgeColor.bg, color: modeBadgeColor.fg }">{{ currentModeInfo.badge }}</span>
92
- <span>{{ currentModeInfo.title.split(' ')[0] }}</span>
93
- <ChevronDown :size="14" />
94
- </button>
90
+ <div :class="styles.searchWrapper">
91
+ <GlobalSearchBar />
92
+ </div>
93
+
94
+ <div :class="styles.spacer" />
95
95
 
96
- <div v-if="modeDropdownOpen" :class="styles.modeDropdown">
97
- <button
98
- v-for="card in BUSINESS_MODE_CARDS"
99
- :key="card.mode"
100
- type="button"
101
- :class="[styles.modeOption, businessSettings.activeMode === card.mode && styles.modeOptionActive]"
102
- :disabled="!isEnabled(card.mode)"
103
- :style="{ opacity: isEnabled(card.mode) ? 1 : 0.45 }"
104
- @click="isEnabled(card.mode) && handleSelectMode(card.mode)"
105
- >
106
- <span>{{ card.title }}</span>
107
- <Check v-if="businessSettings.activeMode === card.mode" :size="14" />
96
+ <div :class="styles.rightGroup">
97
+ <div ref="dropdownRef" :class="styles.modeSwitcherWrapper">
98
+ <button type="button" :class="styles.modeSwitcherBtn" title="작업 비즈니스 모드 변경" @click="modeDropdownOpen = !modeDropdownOpen">
99
+ <span :class="styles.modeTag" :style="{ backgroundColor: modeBadgeColor.bg, color: modeBadgeColor.fg }">{{ currentModeInfo.badge }}</span>
100
+ <span>{{ currentModeInfo.title.split(' ')[0] }}</span>
101
+ <ChevronDown :size="14" />
108
102
  </button>
109
103
 
110
- <div :class="styles.modeDropdownDivider" />
111
- <button type="button" :class="styles.modeDropdownLink" @click="goSystemDisplay">운영 설정 관리 →</button>
104
+ <div v-if="modeDropdownOpen" :class="styles.modeDropdown">
105
+ <button
106
+ v-for="card in BUSINESS_MODE_CARDS"
107
+ :key="card.mode"
108
+ type="button"
109
+ :class="[styles.modeOption, businessSettings.activeMode === card.mode && styles.modeOptionActive]"
110
+ :disabled="!isEnabled(card.mode)"
111
+ :style="{ opacity: isEnabled(card.mode) ? 1 : 0.45 }"
112
+ @click="isEnabled(card.mode) && handleSelectMode(card.mode)"
113
+ >
114
+ <span>{{ card.title }}</span>
115
+ <Check v-if="businessSettings.activeMode === card.mode" :size="14" />
116
+ </button>
117
+
118
+ <div :class="styles.modeDropdownDivider" />
119
+ <button type="button" :class="styles.modeDropdownLink" @click="goSystemDisplay">운영 설정 관리 →</button>
120
+ </div>
112
121
  </div>
113
- </div>
114
122
 
115
- <button type="button" :class="[styles.iconBtn, route.path === '/components' && styles.iconBtnActive]" title="컴포넌트" @click="goComponents">
116
- <Blocks :size="15" :stroke-width="1.8" />
117
- </button>
123
+ <button type="button" :class="[styles.iconBtn, route.path === '/components' && styles.iconBtnActive]" title="컴포넌트" @click="goComponents">
124
+ <Blocks :size="15" :stroke-width="1.8" />
125
+ </button>
118
126
 
119
- <button type="button" :class="[styles.iconBtn, route.path.startsWith('/system') && styles.iconBtnActive]" title="환경 설정" @click="goSystemDisplay">
120
- <Settings :size="15" :stroke-width="1.8" />
121
- </button>
127
+ <button type="button" :class="[styles.iconBtn, route.path.startsWith('/system') && styles.iconBtnActive]" title="환경 설정" @click="goSystemDisplay">
128
+ <Settings :size="15" :stroke-width="1.8" />
129
+ </button>
122
130
 
123
- <div :class="styles.account">
124
- <div :class="styles.accountAvatar">관</div>
125
- <span :class="styles.accountName">관리자</span>
131
+ <div :class="styles.account">
132
+ <div :class="styles.accountAvatar">관</div>
133
+ <span :class="styles.accountName">관리자</span>
134
+ </div>
126
135
  </div>
127
136
  </M2MHeader>
128
137
  </template>