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

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 (32) hide show
  1. package/package.json +1 -1
  2. package/template/src/pages/b2c/products/B2cProductCreatePage.vue +9 -9
  3. package/template/src/pages/c2c/sales/SalesPerformancePage.vue +1 -1
  4. package/template/src/pages/components/ComponentsPage.vue +22 -24
  5. package/template/src/pages/content/CategoriesPage.vue +1 -1
  6. package/template/src/pages/content/ContentListPage.vue +2 -3
  7. package/template/src/pages/content/ExposurePage.vue +2 -2
  8. package/template/src/pages/content/ReviewPage.vue +1 -1
  9. package/template/src/pages/content/TagsPage.vue +1 -1
  10. package/template/src/pages/coupons/CouponPolicyPage.vue +9 -9
  11. package/template/src/pages/cs/AdminMemoDrawer.vue +4 -3
  12. package/template/src/pages/cs/ConsultationDetailDrawer.vue +3 -1
  13. package/template/src/pages/cs/ConsultationsPage.vue +5 -1
  14. package/template/src/pages/cs/CsHistoryDetailDrawer.vue +6 -2
  15. package/template/src/pages/cs/ProductInquiryDetailDrawer.vue +6 -2
  16. package/template/src/pages/delivery/DeliveryListPage.vue +1 -1
  17. package/template/src/pages/inventory/InventoryAlertsConfigPage.vue +13 -9
  18. package/template/src/pages/inventory/StockMovementManagementPage.vue +8 -4
  19. package/template/src/pages/logs/ApiLogDetailDrawer.vue +83 -0
  20. package/template/src/pages/logs/ErrorGroupDetailDrawer.vue +74 -0
  21. package/template/src/pages/logs/SecurityLogDetailDrawer.vue +116 -0
  22. package/template/src/pages/logs/SecurityLogPage.module.css +2 -0
  23. package/template/src/pages/logs/SecurityLogPage.vue +224 -0
  24. package/template/src/pages/logs/SystemLogPage.module.css +130 -0
  25. package/template/src/pages/logs/SystemLogPage.vue +346 -0
  26. package/template/src/pages/logs/securityLogData.ts +39 -0
  27. package/template/src/pages/logs/systemLogData.ts +160 -0
  28. package/template/src/pages/notifications/NotificationDispatchPage.vue +6 -5
  29. package/template/src/pages/orders/RefundPage.vue +6 -5
  30. package/template/src/pages/points/PointPolicyPage.vue +8 -8
  31. package/template/src/pages/promotions/PromotionStackSettingPage.vue +2 -2
  32. package/template/src/pages/stats/useStatisticsPeriod.ts +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m2m-bo-template-vue",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Scaffolding CLI for the M2M back-office vue template.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, h, onMounted, onUnmounted, reactive, ref, watch, type VNodeChild } from 'vue';
2
+ import { computed, h, onUnmounted, reactive, ref, watch, type VNodeChild } from 'vue';
3
3
  import { useRouter } from 'vue-router';
4
4
  import { CheckCircle2, ChevronDown, ChevronUp, Circle, CircleAlert, ImagePlus, Plus, X, XCircle } from 'lucide-vue-next';
5
5
  import styles from './B2cProductCreatePage.module.css';
@@ -379,7 +379,7 @@ function onFileInputChange(event: Event) {
379
379
  size="sm"
380
380
  :class="styles.fieldAside"
381
381
  :model-value="form.autoCode"
382
- @update:model-value="(checked: boolean) => { form.autoCode = checked; if (checked) form.code = createInitialForm().code; }"
382
+ @update:model-value="(checked: boolean | string[]) => { const c = checked as boolean; form.autoCode = c; if (c) form.code = createInitialForm().code; }"
383
383
  >
384
384
  자동 생성
385
385
  </Checkbox>
@@ -391,7 +391,7 @@ function onFileInputChange(event: Event) {
391
391
  placeholder="브랜드명 검색"
392
392
  :options="BRAND_OPTIONS"
393
393
  :model-value="form.brand"
394
- @update:model-value="(v: string) => update('brand', v)"
394
+ @update:model-value="(v: string | string[]) => update('brand', v as string)"
395
395
  />
396
396
  <span :class="styles.helperText">등록된 브랜드만 선택할 수 있습니다.</span>
397
397
  </label>
@@ -405,7 +405,7 @@ function onFileInputChange(event: Event) {
405
405
  placeholder="대분류 선택"
406
406
  :options="B2C_CATEGORIES.map((n) => ({ value: n.id, label: n.name }))"
407
407
  :model-value="form.cat1"
408
- @update:model-value="(v: string) => { form.cat1 = v; form.cat2 = ''; form.cat3 = ''; }"
408
+ @update:model-value="(v: string | string[]) => { form.cat1 = v as string; form.cat2 = ''; form.cat3 = ''; }"
409
409
  />
410
410
  <Select
411
411
  aria-label="중분류"
@@ -413,7 +413,7 @@ function onFileInputChange(event: Event) {
413
413
  :disabled="level2Options.length === 0"
414
414
  :options="level2Options.map((n) => ({ value: n.id, label: n.name }))"
415
415
  :model-value="form.cat2"
416
- @update:model-value="(v: string) => { form.cat2 = v; form.cat3 = ''; }"
416
+ @update:model-value="(v: string | string[]) => { form.cat2 = v as string; form.cat3 = ''; }"
417
417
  />
418
418
  <Select
419
419
  aria-label="소분류"
@@ -421,7 +421,7 @@ function onFileInputChange(event: Event) {
421
421
  :disabled="level3Options.length === 0"
422
422
  :options="level3Options.map((n) => ({ value: n.id, label: n.name }))"
423
423
  :model-value="form.cat3"
424
- @update:model-value="(v: string) => update('cat3', v)"
424
+ @update:model-value="(v: string | string[]) => update('cat3', v as string)"
425
425
  />
426
426
  </div>
427
427
  <span v-if="path.length === 3" :class="styles.helperText">{{ path.join(' > ') }}</span>
@@ -525,7 +525,7 @@ function onFileInputChange(event: Event) {
525
525
  :items="form.extraImages"
526
526
  @change="(next: typeof form.extraImages) => update('extraImages', next)"
527
527
  >
528
- <template #item="{ item: image, index }">
528
+ <template #default="{ item: image, index }">
529
529
  <div :class="styles.imageTile">
530
530
  <div :class="styles.tileTop">
531
531
  <span :class="styles.tileNum">{{ index + 1 }}</span>
@@ -663,7 +663,7 @@ function onFileInputChange(event: Event) {
663
663
 
664
664
  <div :class="styles.stockFooter">
665
665
  <span :class="styles.muted">재고 수량은 [상품 관리 &gt; 재고 관리]에서 관리합니다. 등록 편의를 위한 초기 재고만 여기서 입력할 수 있습니다.</span>
666
- <Checkbox size="sm" :model-value="form.withStock" @update:model-value="(v: boolean) => update('withStock', v)">초기 재고 함께 입력</Checkbox>
666
+ <Checkbox size="sm" :model-value="form.withStock" @update:model-value="(v: boolean | string[]) => update('withStock', v as boolean)">초기 재고 함께 입력</Checkbox>
667
667
  </div>
668
668
  </div>
669
669
  </Card>
@@ -716,7 +716,7 @@ function onFileInputChange(event: Event) {
716
716
  <div :class="styles.stack">
717
717
  <label :class="styles.fieldLabel">
718
718
  배송 정책 <span class="req">*</span>
719
- <Select :options="SHIPPING_POLICIES.map(({ value, label }) => ({ value, label }))" :model-value="form.shippingPolicy" placeholder="배송 정책 선택" @update:model-value="(v: string) => update('shippingPolicy', v)" />
719
+ <Select :options="SHIPPING_POLICIES.map(({ value, label }) => ({ value, label }))" :model-value="form.shippingPolicy" placeholder="배송 정책 선택" @update:model-value="(v: string | string[]) => update('shippingPolicy', v as string)" />
720
720
  </label>
721
721
  <Notice v-if="policy" type="neutral" :description="policy.summary" action-text="정책 관리로 이동" @action="router.push('/delivery-policy/base-fee')" />
722
722
  <div :class="styles.inlineFields">
@@ -100,7 +100,7 @@ function downloadPerformance() {
100
100
  <div :class="styles.donutArea">
101
101
  <StatisticsDonutChart
102
102
  :data="categoryTotals.map((item) => ({ label: item.category, value: item.value }))"
103
- :center-value="SALE_PRODUCTS.length"
103
+ :center-value="`${SALE_PRODUCTS.length}`"
104
104
  center-label="상품"
105
105
  :max-legend-items="4"
106
106
  aria-label="카테고리 거래 구성"
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from 'vue';
3
- import { Plus, CheckCircle, ArrowUpRight } from 'lucide-vue-next';
3
+ import { Plus, CheckCircle } from 'lucide-vue-next';
4
4
  import { Button } from 'm2m-uiux-vue/Button';
5
5
  import { ButtonGroup } from 'm2m-uiux-vue/ButtonGroup';
6
6
  import { Segmented } from 'm2m-uiux-vue/Segmented';
@@ -12,7 +12,7 @@ import { RadioGroup } from 'm2m-uiux-vue/RadioGroup';
12
12
  import { Switch } from 'm2m-uiux-vue/Switch';
13
13
  import { Textarea } from 'm2m-uiux-vue/Textarea';
14
14
  import { Divider } from 'm2m-uiux-vue/Divider';
15
- import { Badge } from 'm2m-uiux-vue/Badge';
15
+ import { Badge, BADGE_TYPE } from 'm2m-uiux-vue/Badge';
16
16
  import { Header as M2MHeader } from 'm2m-uiux-vue/Header';
17
17
  import { Form } from 'm2m-uiux-vue/Form';
18
18
  import { Table, type TableColumn } from 'm2m-uiux-vue/Table';
@@ -111,11 +111,11 @@ const switchOn = ref(true);
111
111
 
112
112
  const tableSel = ref<Array<string | number>>([]);
113
113
  const tabValue = ref('all');
114
- const segmentTab = ref('b2c');
114
+ const segmentTab = ref('B2C');
115
115
  const stepIndex = ref(1);
116
116
  const progress = ref(62);
117
117
 
118
- const dateSingle = ref<string | undefined>(undefined);
118
+ const dateSingle = ref<Date | null>(null);
119
119
  const timeValue = ref('');
120
120
  const legacyDate = ref('');
121
121
 
@@ -123,10 +123,10 @@ const gridSearch = ref('');
123
123
  const chartHeight = ref(220);
124
124
  const previewNavOpen = ref(true);
125
125
 
126
- const tableColumns: TableColumn<DemoRow>[] = [
127
- { key: 'name', title: '이름', dataIndex: 'name' },
128
- { key: 'status', title: '상태', dataIndex: 'status' },
129
- { key: 'amount', title: '금액', dataIndex: 'amount', align: 'right' },
126
+ const tableColumns: TableColumn<Record<string,any>>[] = [
127
+ { key: 'name', label: '이름' },
128
+ { key: 'status', label: '상태' },
129
+ { key: 'amount', label: '금액', align: 'right' },
130
130
  ];
131
131
 
132
132
  const gridColumns: GridColumn[] = [{ label: '이름' }, { label: '상태' }, { label: '금액', align: 'right' }];
@@ -214,7 +214,7 @@ const presetItems = [
214
214
  <div :class="[styles.demoBody, styles.col]">
215
215
  <StatGrid :columns="4">
216
216
  <StatCard label="당월 총 매출" value="₩128,450,000" value-color="#18181b" note="+14.2% 전월 대비 상승" tone="up">
217
- <template #badge><Badge type="success" size="sm">성장</Badge></template>
217
+ <template #badge><Badge :type="BADGE_TYPE['SUCCESS']" size="sm">성장</Badge></template>
218
218
  </StatCard>
219
219
  <StatCard label="신규 반품 요청" value="23건" value-color="#dc2626" note="-3건 전주 대비 감소" tone="down" dot="#ef4444" />
220
220
  <StatCard label="미처리 정산 건" value="12건" note="마감 D-2 예정" tone="warn" dot="#f59e0b" />
@@ -503,12 +503,11 @@ const presetItems = [
503
503
  <div :class="styles.demo">
504
504
  <div :class="styles.demoHead"><span :class="styles.demoName">Badge</span><span :class="styles.demoDesc">운영 화면에서 사용하는 상태 색상</span></div>
505
505
  <div :class="styles.demoBody">
506
- <Badge type="success">정상</Badge>
507
- <Badge type="info">진행중</Badge>
508
- <Badge type="primary">예약</Badge>
509
- <Badge type="warning">주의</Badge>
510
- <Badge type="error">실패</Badge>
511
- <Badge type="neutral">중지</Badge>
506
+ <Badge :type="BADGE_TYPE['SUCCESS']">정상</Badge>
507
+ <Badge :type="BADGE_TYPE['INFO']">진행중</Badge>
508
+ <Badge :type="BADGE_TYPE['PRIMARY']">예약</Badge>
509
+ <Badge :type="BADGE_TYPE['WARNING']">주의</Badge>
510
+ <Badge :type="BADGE_TYPE['ERROR']">실패</Badge>
512
511
  </div>
513
512
  </div>
514
513
  <div :class="styles.demo">
@@ -520,7 +519,7 @@ const presetItems = [
520
519
  <div :class="styles.demo">
521
520
  <div :class="styles.demoHead"><span :class="styles.demoName">Form / FormField</span></div>
522
521
  <div :class="[styles.demoBody, styles.col]">
523
- <Form layout="2column">
522
+ <Form>
524
523
  <Form.Field label="담당자" required>
525
524
  <Input placeholder="이름" />
526
525
  </Form.Field>
@@ -611,15 +610,14 @@ const presetItems = [
611
610
  </div>
612
611
  <div :class="[styles.demoBody, styles.col]">
613
612
  <div style="display: flex; flex-direction: column; gap: 14px">
614
- <div style="display: flex; flex-direction: column; gap: 6px">
613
+ <div style="display: inline-flex; flex-direction: column; gap: 6px; width: min-content;">
615
614
  <span style="font-size: 0.71875rem; font-weight: 700; color: var(--common-text-muted)">세그먼트형 (type="segmented") — 유형 전환 / 토글 탭</span>
616
- <Tabs
617
- type="segmented"
615
+ <Segmented
618
616
  v-model="segmentTab"
619
- :items="[
620
- { key: 'b2c', label: 'B2C' },
621
- { key: 'c2c', label: 'C2C' },
622
- { key: 'b2b', label: 'B2B' },
617
+ :options="[
618
+ 'B2C',
619
+ 'C2C',
620
+ 'B2B',
623
621
  ]"
624
622
  />
625
623
  </div>
@@ -732,7 +730,7 @@ const presetItems = [
732
730
  { label: '도서/음반', value: 310000 },
733
731
  { label: '기타 잡화', value: 150000 },
734
732
  ]"
735
- :center-value="186"
733
+ :center-value="'186'"
736
734
  center-label="상품"
737
735
  :max-legend-items="4"
738
736
  aria-label="카테고리 구성비 차트 예제"
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, onUnmounted, ref } from 'vue';
2
+ import { computed, onUnmounted, ref } from 'vue';
3
3
  import { useRouter } from 'vue-router';
4
4
  import sh from './contentShared.module.css';
5
5
  import styles from './CategoriesPage.module.css';
@@ -1,8 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
2
+ import { computed, onMounted, onUnmounted, ref } from 'vue';
3
3
  import { useRoute } from 'vue-router';
4
4
  import { PageSizeSelect } from '../../components/common';
5
- import ExcelDownloadButton from '../../components/common/ExcelDownloadButton.vue';
6
5
  import DatePicker from '../../components/forms/DatePicker.vue';
7
6
  import { SearchField } from '../../components/SearchField';
8
7
  import { ChevronDown, ChevronUp, RotateCcw, SlidersHorizontal } from 'lucide-vue-next';
@@ -107,7 +106,7 @@ const filtered = computed(() => {
107
106
  const query = q.value.trim().toLowerCase();
108
107
  let list = modeData.value.filter((r) => {
109
108
  if (query) {
110
- const hay = field === '콘텐츠 ID' ? r.id : field === '제목' ? r.title : field === '작성자' ? r.author : r.id + r.title + r.author;
109
+ const hay = r.id + r.title + r.author;
111
110
  if (!hay.toLowerCase().includes(query)) return false;
112
111
  }
113
112
  if (status.value !== '전체' && r.status !== status.value) return false;
@@ -116,7 +116,7 @@ function runNavDialog(b: NavDialogButton) {
116
116
  if (!navDialog.value) return;
117
117
  if (b.act === 'close') { navDialog.value = null; return; }
118
118
  if (b.act === 'gonosave') { changes.value = 0; area.value = navDialog.value.toId; selRowId.value = null; rowDraft.value = null; navDialog.value = null; }
119
- if (b.act === 'gosave') { changes.value = 0; area.value = navDialog.value.toId; selRowId.value = null; rowDraft.value = null; navDialog.value = null; toast.value = '노출 설정이 저장되었습니다.'; }
119
+ if (b.act === 'gosave') { changes.value = 0; area.value = navDialog.value?.toId as string; selRowId.value = null; rowDraft.value = null; navDialog.value = null; toast.value = '노출 설정이 저장되었습니다.'; }
120
120
  }
121
121
 
122
122
  function openRow(entry: ExposureEntry) {
@@ -443,7 +443,7 @@ function toggleAreaOpen(id: string) {
443
443
  <div :class="sh.metaRow">
444
444
  <div :class="sh.metaCell">
445
445
  <div :class="sh.metaCellLabel">현재 순서</div>
446
- <div :class="sh.metaCellValue">{{ curList.findIndex((x) => x.id === selEntry.id) + 1 }}</div>
446
+ <div :class="sh.metaCellValue">{{ curList.findIndex((x) => x.id === selEntry?.id) + 1 }}</div>
447
447
  </div>
448
448
  <div :class="sh.metaCell">
449
449
  <div :class="sh.metaCellLabel">등록 관리자</div>
@@ -64,7 +64,7 @@ const list = computed(() => {
64
64
  let out = modeItems.value.filter((it) => {
65
65
  if (tab.value !== '전체' && it.status !== tab.value) return false;
66
66
  if (query) {
67
- const hay = field === '콘텐츠 ID' ? it.ctid : field === '제목' ? it.title : field === '요청자' ? it.requester : it.ctid + it.title + it.requester;
67
+ const hay = it.ctid + it.title + it.requester;
68
68
  if (!hay.toLowerCase().includes(query)) return false;
69
69
  }
70
70
  if (reqTypeF.value !== '전체' && it.reqType !== reqTypeF.value) return false;
@@ -97,7 +97,7 @@ const list = computed(() => {
97
97
  let out = items.value.filter((it) => {
98
98
  if (it.scope !== scope.value) return false;
99
99
  if (query) {
100
- const hay = field === '태그명' ? it.name : field === '태그 코드' ? it.code : it.name + it.code;
100
+ const hay = it.name + it.code;
101
101
  if (!hay.toLowerCase().includes(query)) return false;
102
102
  }
103
103
  if (useF.value !== '전체' && (useF.value === '사용' ? !it.use : it.use)) return false;
@@ -294,9 +294,9 @@ const digest = computed<[string, string][]>(() => [
294
294
  </div>
295
295
 
296
296
  <div :class="styles.checkGrid">
297
- <Checkbox size="md" label="동일 쿠폰 여러 장" :model-value="p.allowSameCouponMultiple" @update:model-value="(checked: boolean) => set('allowSameCouponMultiple', checked)" />
298
- <Checkbox size="md" label="프로모션과 중복" :model-value="p.promotionStackDefault" @update:model-value="(checked: boolean) => set('promotionStackDefault', checked)" />
299
- <Checkbox size="md" label="포인트와 중복" :model-value="p.pointStackAllowed" @update:model-value="(checked: boolean) => set('pointStackAllowed', checked)" />
297
+ <Checkbox size="md" label="동일 쿠폰 여러 장" :model-value="p.allowSameCouponMultiple" @update:model-value="(checked) => set('allowSameCouponMultiple', checked as boolean)" />
298
+ <Checkbox size="md" label="프로모션과 중복" :model-value="p.promotionStackDefault" @update:model-value="(checked) => set('promotionStackDefault', checked as boolean)" />
299
+ <Checkbox size="md" label="포인트와 중복" :model-value="p.pointStackAllowed" @update:model-value="(checked) => set('pointStackAllowed', checked as boolean)" />
300
300
  </div>
301
301
  </div>
302
302
  </div>
@@ -322,7 +322,7 @@ const digest = computed<[string, string][]>(() => [
322
322
  <div>
323
323
  <div :class="styles.fieldLabel">포함 항목</div>
324
324
  <div :class="styles.checkRow">
325
- <Checkbox size="md" label="배송비 포함" :model-value="p.includeShippingInMin" @update:model-value="(checked: boolean) => set('includeShippingInMin', checked)" />
325
+ <Checkbox size="md" label="배송비 포함" :model-value="p.includeShippingInMin" @update:model-value="(checked) => set('includeShippingInMin', checked as boolean)" />
326
326
  </div>
327
327
  </div>
328
328
  </div>
@@ -348,23 +348,23 @@ const digest = computed<[string, string][]>(() => [
348
348
  </div>
349
349
  <div :class="styles.tableRow">
350
350
  <span :class="styles.tableRowLabel">전체 취소</span>
351
- <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="전체 취소 시 쿠폰 복원" :model-value="p.fullCancelRestore" @update:model-value="(checked: boolean) => set('fullCancelRestore', checked)" /></span>
351
+ <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="전체 취소 시 쿠폰 복원" :model-value="p.fullCancelRestore" @update:model-value="(checked) => set('fullCancelRestore', checked as boolean)" /></span>
352
352
  <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="전체 취소 시 잔여 재계산" :model-value="false" disabled /></span>
353
353
  </div>
354
354
  <div :class="styles.tableRow">
355
355
  <span :class="styles.tableRowLabel">전체 반품</span>
356
- <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="전체 반품 시 쿠폰 복원" :model-value="p.fullRefundRestore" @update:model-value="(checked: boolean) => set('fullRefundRestore', checked)" /></span>
356
+ <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="전체 반품 시 쿠폰 복원" :model-value="p.fullRefundRestore" @update:model-value="(checked) => set('fullRefundRestore', checked as boolean)" /></span>
357
357
  <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="전체 반품 시 잔여 재계산" :model-value="false" disabled /></span>
358
358
  </div>
359
359
  <div :class="styles.tableRow">
360
360
  <span :class="styles.tableRowLabel">부분 취소</span>
361
361
  <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="부분 취소 시 쿠폰 복원" :model-value="false" disabled /></span>
362
- <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="부분 취소 시 잔여 재계산" :model-value="p.partialCancelRecalculate" @update:model-value="(checked: boolean) => set('partialCancelRecalculate', checked)" /></span>
362
+ <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="부분 취소 시 잔여 재계산" :model-value="p.partialCancelRecalculate" @update:model-value="(checked) => set('partialCancelRecalculate', checked as boolean)" /></span>
363
363
  </div>
364
364
  <div :class="styles.tableRow">
365
365
  <span :class="styles.tableRowLabel">부분 반품</span>
366
366
  <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="부분 반품 시 쿠폰 복원" :model-value="false" disabled /></span>
367
- <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="부분 반품 시 잔여 재계산" :model-value="p.partialRefundRecalculate" @update:model-value="(checked: boolean) => set('partialRefundRecalculate', checked)" /></span>
367
+ <span :class="styles.tableCellCenter"><Checkbox size="sm" aria-label="부분 반품 시 잔여 재계산" :model-value="p.partialRefundRecalculate" @update:model-value="(checked) => set('partialRefundRecalculate', checked as boolean)" /></span>
368
368
  </div>
369
369
  </div>
370
370
  </div>
@@ -410,7 +410,7 @@ const digest = computed<[string, string][]>(() => [
410
410
  <div>
411
411
  <div :class="styles.fieldLabel">총 한도 초과 시</div>
412
412
  <div :class="styles.checkRow">
413
- <Checkbox size="md" label="발급 차단" :model-value="p.blockOnLimitExceeded" @update:model-value="(checked: boolean) => set('blockOnLimitExceeded', checked)" />
413
+ <Checkbox size="md" label="발급 차단" :model-value="p.blockOnLimitExceeded" @update:model-value="(checked) => set('blockOnLimitExceeded', checked as boolean)" />
414
414
  </div>
415
415
  </div>
416
416
  </div>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { DatePicker } from '../../components/forms';
3
+ import { useRouter } from 'vue-router';
3
4
  import { computed, ref } from 'vue';
4
5
  import drawer from '../ops/opsDrawerShared.module.css';
5
6
  import styles from './AdminMemosPage.module.css';
@@ -7,7 +8,7 @@ import { useOutsideClose } from '../../lib/useOutsideClose';
7
8
  import { MEMO_ADMINS, MEMO_TARGET_TYPES, MEMO_TYPES, followUpState, type AdminMemoEntry, type MemoTargetType, type MemoType, type PinScope } from './adminMemosData';
8
9
 
9
10
  type Tab = 'memo' | 'context' | 'followup' | 'version' | 'history';
10
-
11
+ const router = useRouter()
11
12
  const props = defineProps<{ initial: AdminMemoEntry; isNew: boolean; startEditing?: boolean }>();
12
13
  const emit = defineEmits<{
13
14
  close: [];
@@ -148,8 +149,8 @@ function handleFollowUpDateChange(e: Event) {
148
149
  </div>
149
150
  <section :class="styles.formSection">
150
151
  <h3>연결 업무</h3>
151
- <div v-if="draft.relatedInquiryId" :class="styles.contextRow"><span>1:1 문의</span><strong>{{ draft.relatedInquiryId }}</strong><em>연결됨</em><button type="button" @click="() => (window.location.href = '/cs/inquiries')">보기 ↗</button></div>
152
- <div v-if="draft.relatedConsultationId" :class="styles.contextRow"><span>상담</span><strong>{{ draft.relatedConsultationId }}</strong><em>연결됨</em><button type="button" @click="() => (window.location.href = '/cs/consultations')">보기 ↗</button></div>
152
+ <div v-if="draft.relatedInquiryId" :class="styles.contextRow"><span>1:1 문의</span><strong>{{ draft.relatedInquiryId }}</strong><em>연결됨</em><button type="button" @click="() => router.push('/cs/inquiries')">보기 ↗</button></div>
153
+ <div v-if="draft.relatedConsultationId" :class="styles.contextRow"><span>상담</span><strong>{{ draft.relatedConsultationId }}</strong><em>연결됨</em><button type="button" @click="() => router.push('/cs/consultations')">보기 ↗</button></div>
153
154
  <div v-if="draft.relatedOrderId" :class="styles.contextRow"><span>주문</span><strong>{{ draft.relatedOrderId }}</strong><em>연결됨</em><button type="button">보기 ↗</button></div>
154
155
  <div v-if="!draft.relatedInquiryId && !draft.relatedConsultationId && !draft.relatedOrderId" :class="drawer.emptyInline">추가로 연결된 업무가 없습니다.</div>
155
156
  </section>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref } from 'vue';
3
+ import { useRouter } from 'vue-router';
3
4
  import drawer from '../ops/opsDrawerShared.module.css';
4
5
  import styles from './ConsultationsPage.module.css';
5
6
  import { useOutsideClose } from '../../lib/useOutsideClose';
@@ -7,6 +8,7 @@ import { durationLabel, type ConsultationEntry } from './consultationsData';
7
8
 
8
9
  type Tab = 'summary' | 'conversation' | 'related' | 'result' | 'memo' | 'history';
9
10
 
11
+ const router = useRouter()
10
12
  const props = defineProps<{ item: ConsultationEntry }>();
11
13
  const emit = defineEmits<{
12
14
  close: [];
@@ -59,7 +61,7 @@ function styleKey(prefix: string, key: string) {
59
61
  </div>
60
62
  <div :class="drawer.actionRow">
61
63
  <button type="button" :class="drawer.actionLink">고객 보기 ↗</button>
62
- <button v-if="item.inquiryId" type="button" :class="drawer.actionLink" @click="() => (window.location.href = '/cs/inquiries')">원 문의 보기 ↗</button>
64
+ <button v-if="item.inquiryId" type="button" :class="drawer.actionLink" @click="() => (router.push('/cs/inquiries'))">원 문의 보기 ↗</button>
63
65
  <button type="button" :class="drawer.actionLink" @click="emit('correct')">상담 기록 정정</button>
64
66
  <span :class="drawer.spacer" />
65
67
  <button type="button" :class="drawer.actionLink" @click="emit('newInquiry')">새 문의 등록</button>
@@ -225,6 +225,10 @@ function register() {
225
225
  toastBriefly('상담 내역을 등록했습니다.');
226
226
  }
227
227
 
228
+ function goToInquiries() {
229
+ window.location.href = '/cs/inquiries';
230
+ }
231
+
228
232
  function reopenInquiry() {
229
233
  if (!detail.value) return;
230
234
  const current = detail.value;
@@ -473,7 +477,7 @@ const rows = computed<GridRow[]>(() =>
473
477
  @close="detailId = null"
474
478
  @reopen="modal = 'reopen'"
475
479
  @correct="correctResult = detail!.result; modal = 'correct'"
476
- @new-inquiry="() => (window.location.href = '/cs/inquiries')"
480
+ @new-inquiry="goToInquiries"
477
481
  />
478
482
 
479
483
  <div v-if="modal === 'register'" :class="shared.dialogOverlay" @mousedown="(e) => { if (e.target === e.currentTarget) modal = null; }">
@@ -19,6 +19,10 @@ const adjacent = computed(() => findAdjacent(props.log, props.all));
19
19
 
20
20
  const asideRef = ref<HTMLElement | null>(null);
21
21
  useOutsideClose(asideRef, () => emit('close'));
22
+
23
+ function goTo(path: string) {
24
+ window.location.href = path;
25
+ }
22
26
  </script>
23
27
 
24
28
  <template>
@@ -37,8 +41,8 @@ useOutsideClose(asideRef, () => emit('close'));
37
41
  <button type="button" :class="drawer.closeBtn" @click="emit('close')">×</button>
38
42
  </div>
39
43
  <div v-if="log.relatedInquiryId || log.relatedConsultationId" :class="drawer.actionRow">
40
- <button v-if="log.relatedInquiryId" type="button" :class="drawer.actionLink" @click="() => (window.location.href = '/cs/inquiries')">관련 문의 보기 ↗</button>
41
- <button v-if="log.relatedConsultationId" type="button" :class="drawer.actionLink" @click="() => (window.location.href = '/cs/consultations')">관련 상담 보기 ↗</button>
44
+ <button v-if="log.relatedInquiryId" type="button" :class="drawer.actionLink" @click="goTo('/cs/inquiries')">관련 문의 보기 ↗</button>
45
+ <button v-if="log.relatedConsultationId" type="button" :class="drawer.actionLink" @click="goTo('/cs/consultations')">관련 상담 보기 ↗</button>
42
46
  </div>
43
47
  </div>
44
48
 
@@ -25,6 +25,10 @@ function notify(message: string) {
25
25
  showToast({ message, type: 'success' });
26
26
  }
27
27
 
28
+ function goTo(path: string) {
29
+ window.location.href = path;
30
+ }
31
+
28
32
  function update(patch: Partial<ProductInquiry>) {
29
33
  Object.assign(props.inquiry, patch);
30
34
  emit('change');
@@ -96,13 +100,13 @@ function toggleHidden() {
96
100
  <div :class="drawer.sectionTitleLoose">상품 정보</div>
97
101
  <div :class="drawer.linkedItem">
98
102
  <div><strong>{{ productName(inquiry.productCode) }}</strong><div :class="drawer.sub">{{ inquiry.productCode }}</div></div>
99
- <button type="button" :class="drawer.actionLink" @click="() => (window.location.href = '/products')">상품 상세 보기</button>
103
+ <button type="button" :class="drawer.actionLink" @click="goTo('/products')">상품 상세 보기</button>
100
104
  </div>
101
105
 
102
106
  <div :class="drawer.sectionTitleLoose">작성자 정보</div>
103
107
  <div :class="drawer.linkedItem">
104
108
  <strong>{{ inquiry.member }}</strong>
105
- <button type="button" :class="drawer.actionLink" @click="() => (window.location.href = '/members')">회원 상세 보기</button>
109
+ <button type="button" :class="drawer.actionLink" @click="goTo('/members')">회원 상세 보기</button>
106
110
  </div>
107
111
 
108
112
  <div :class="drawer.sectionTitleLoose">문의 내용</div>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, reactive, ref } from 'vue';
2
+ import { computed, ref } from 'vue';
3
3
  import { Button } from 'm2m-uiux-vue/Button';
4
4
  import { ExcelDownloadButton, PageSizeSelect, showToast } from '../../components/common';
5
5
  import DataGrid from '../../components/DataGrid/DataGrid.vue';
@@ -16,6 +16,10 @@ function setTab(next: string) {
16
16
  router.replace({ query: { ...route.query, tab: next } });
17
17
  }
18
18
 
19
+ function isTab(t: 'safety' | 'config' | 'history') {
20
+ return tab.value === t;
21
+ }
22
+
19
23
  const lowStockAlert = ref(true);
20
24
  const soldOutAlert = ref(true);
21
25
  const imminentAlert = ref(true);
@@ -44,9 +48,9 @@ function toggleChannel(ch: string) {
44
48
  <template #header-tabs>
45
49
  <div :class="styles.viewToggleRow" style="margin-top: 12px; margin-bottom: 4px">
46
50
  <div :class="styles.viewToggle">
47
- <button type="button" :class="[styles.viewToggleBtn, tab === 'safety' && styles.active]" @click="setTab('safety')">안전재고 기준</button>
48
- <button type="button" :class="[styles.viewToggleBtn, tab === 'config' && styles.active]" @click="setTab('config')">알림 설정</button>
49
- <button type="button" :class="[styles.viewToggleBtn, tab === 'history' && styles.active]" @click="setTab('history')">알림 내역</button>
51
+ <button type="button" :class="[styles.viewToggleBtn, isTab('safety') && styles.active]" @click="setTab('safety')">안전재고 기준</button>
52
+ <button type="button" :class="[styles.viewToggleBtn, isTab('config') && styles.active]" @click="setTab('config')">알림 설정</button>
53
+ <button type="button" :class="[styles.viewToggleBtn, isTab('history') && styles.active]" @click="setTab('history')">알림 내역</button>
50
54
  </div>
51
55
  </div>
52
56
  </template>
@@ -60,9 +64,9 @@ function toggleChannel(ch: string) {
60
64
  <template #header-tabs>
61
65
  <div :class="styles.viewToggleRow" style="margin-top: 12px; margin-bottom: 4px">
62
66
  <div :class="styles.viewToggle">
63
- <button type="button" :class="[styles.viewToggleBtn, tab === 'safety' && styles.active]" @click="setTab('safety')">안전재고 기준</button>
64
- <button type="button" :class="[styles.viewToggleBtn, tab === 'config' && styles.active]" @click="setTab('config')">알림 설정</button>
65
- <button type="button" :class="[styles.viewToggleBtn, tab === 'history' && styles.active]" @click="setTab('history')">알림 내역</button>
67
+ <button type="button" :class="[styles.viewToggleBtn, isTab('safety') && styles.active]" @click="setTab('safety')">안전재고 기준</button>
68
+ <button type="button" :class="[styles.viewToggleBtn, isTab('config') && styles.active]" @click="setTab('config')">알림 설정</button>
69
+ <button type="button" :class="[styles.viewToggleBtn, isTab('history') && styles.active]" @click="setTab('history')">알림 내역</button>
66
70
  </div>
67
71
  </div>
68
72
  </template>
@@ -78,9 +82,9 @@ function toggleChannel(ch: string) {
78
82
  </div>
79
83
  <div :class="styles.viewToggleRow" style="margin-top: 12px; margin-bottom: 4px">
80
84
  <div :class="styles.viewToggle">
81
- <button type="button" :class="[styles.viewToggleBtn, tab === 'safety' && styles.active]" @click="setTab('safety')">안전재고 기준</button>
82
- <button type="button" :class="[styles.viewToggleBtn, tab === 'config' && styles.active]" @click="setTab('config')">알림 설정</button>
83
- <button type="button" :class="[styles.viewToggleBtn, tab === 'history' && styles.active]" @click="setTab('history')">알림 내역</button>
85
+ <button type="button" :class="[styles.viewToggleBtn, isTab('safety') && styles.active]" @click="setTab('safety')">안전재고 기준</button>
86
+ <button type="button" :class="[styles.viewToggleBtn, isTab('config') && styles.active]" @click="setTab('config')">알림 설정</button>
87
+ <button type="button" :class="[styles.viewToggleBtn, isTab('history') && styles.active]" @click="setTab('history')">알림 내역</button>
84
88
  </div>
85
89
  </div>
86
90
  <div :class="styles.definitionStrip">
@@ -13,6 +13,10 @@ const tab = computed(() => (route.query.tab === 'outbound' ? 'outbound' : 'inbou
13
13
  function setTab(next: 'inbound' | 'outbound') {
14
14
  router.replace({ query: { ...route.query, tab: next } });
15
15
  }
16
+
17
+ function isTab(t: 'inbound' | 'outbound') {
18
+ return tab.value === t;
19
+ }
16
20
  </script>
17
21
 
18
22
  <template>
@@ -20,8 +24,8 @@ function setTab(next: 'inbound' | 'outbound') {
20
24
  <template #header-tabs>
21
25
  <div :class="styles.viewToggleRow" style="margin-top: 12px; margin-bottom: 4px">
22
26
  <div :class="styles.viewToggle">
23
- <button type="button" :class="[styles.viewToggleBtn, tab === 'inbound' && styles.active]" @click="setTab('inbound')">입고 관리</button>
24
- <button type="button" :class="[styles.viewToggleBtn, tab === 'outbound' && styles.active]" @click="setTab('outbound')">출고 관리</button>
27
+ <button type="button" :class="[styles.viewToggleBtn, isTab('inbound') && styles.active]" @click="setTab('inbound')">입고 관리</button>
28
+ <button type="button" :class="[styles.viewToggleBtn, isTab('outbound') && styles.active]" @click="setTab('outbound')">출고 관리</button>
25
29
  </div>
26
30
  </div>
27
31
  </template>
@@ -30,8 +34,8 @@ function setTab(next: 'inbound' | 'outbound') {
30
34
  <template #header-tabs>
31
35
  <div :class="styles.viewToggleRow" style="margin-top: 12px; margin-bottom: 4px">
32
36
  <div :class="styles.viewToggle">
33
- <button type="button" :class="[styles.viewToggleBtn, tab === 'inbound' && styles.active]" @click="setTab('inbound')">입고 관리</button>
34
- <button type="button" :class="[styles.viewToggleBtn, tab === 'outbound' && styles.active]" @click="setTab('outbound')">출고 관리</button>
37
+ <button type="button" :class="[styles.viewToggleBtn, isTab('inbound') && styles.active]" @click="setTab('inbound')">입고 관리</button>
38
+ <button type="button" :class="[styles.viewToggleBtn, isTab('outbound') && styles.active]" @click="setTab('outbound')">출고 관리</button>
35
39
  </div>
36
40
  </div>
37
41
  </template>