Browse Source

+ 메뉴 업데이트, 정산관리 업데이트

송용우 4 months ago
parent
commit
2f5706ff03

+ 1 - 1
assets/scss/default.scss

@@ -311,7 +311,7 @@
               width: 100%;
               height: 100%;
               background-color: rgba(0,0,0,0.2);
-              border-radius: inherit;
+              border-radius: 18px;
               display: flex;
               align-items: center;
               justify-content: center;

+ 1 - 0
assets/scss/mode-w-m.scss

@@ -132,6 +132,7 @@
             /* width: calc(1vw * (180 / 19.2)); */
             /* height:calc(1vh * (90 / 10.8)); */
             /* min-height:90px; */
+            position: relative;
             padding-left: 2rem;
             width: 100%;
             text-align: left;

+ 24 - 3
backend/app/Controllers/Deli.php

@@ -341,8 +341,16 @@ class Deli extends ResourceController
         if (in_array('DELIVERY_STATUS', $columns)) {
             $builder->where('IOL.DELIVERY_STATUS', 'DELIVERED');
         } else {
-            // DELIVERY_STATUS 컬럼이 없으면 일단 빈 결과 반환
-            $builder->where('1', '0');
+            // DELIVERY_STATUS 컬럼이 없으면 배송업체와 송장번호가 있는 것을 배송완료로 간주
+            $builder->where('IOL.DELI_COMP !=', '')
+                    ->where('IOL.DELI_NUMB !=', '')
+                    ->where('IOL.DELI_COMP IS NOT NULL')
+                    ->where('IOL.DELI_NUMB IS NOT NULL');
+        }
+
+        // 정산완료되지 않은 주문만 표시 (배송완료 페이지용)
+        if (in_array('SETTLEMENT_STATUS', $columns)) {
+            $builder->where('(IOL.SETTLEMENT_STATUS IS NULL OR IOL.SETTLEMENT_STATUS != "COMPLETED")');
         }
 
         // 사용자 타입에 따른 필터링
@@ -352,9 +360,22 @@ class Deli extends ResourceController
             $builder->where('IOL.INF_SEQ', $infSeq);
         }
 
-        $builder->orderBy('IOL.DELIVERED_DATE', 'DESC');
+        // 정렬을 안전하게 처리
+        if (in_array('DELIVERED_DATE', $columns)) {
+            $builder->orderBy('IOL.DELIVERED_DATE', 'DESC');
+        } else {
+            $builder->orderBy('IOL.REG_DATE', 'DESC');
+        }
+        
         $lists = $builder->get()->getResultArray();
 
+        // 디버깅 로그 추가
+        error_log("getDeliveredList - memberType: " . ($memberType ?? 'null') . ", companyNumber: " . ($companyNumber ?? 'null') . ", infSeq: " . ($infSeq ?? 'null'));
+        error_log("getDeliveredList - result count: " . count($lists));
+        if (count($lists) > 0) {
+            error_log("getDeliveredList - sample data: " . json_encode($lists[0]));
+        }
+
         return $this->respond($lists, 200);
     }
 

+ 98 - 84
components/common/header.vue

@@ -16,38 +16,45 @@
         </div>
       </div>
       <div class="pro--info inf">{{ memberTypeText }}</div>
-      
+
       <!-- 알림 센터 추가 -->
       <NotificationCenter />
     </div>
     <nav class="gnb">
       <ul class="depth1">
-        <li v-for="(menu, index) in arrMenuInfo" :key="index" 
-            :class="{ 'has-submenu': menu.subMenus && menu.subMenus.length > 0 }"
-            @mouseenter="showSubmenu(menu.menuId)"
-            @mouseleave="hideSubmenu(menu.menuId)">
-          <button
-            @click="menuAction(menu.menuId, menu.menuName, menu.linkType)"
-            :class="{ actv: isMenuActive(menu) }"
+        <template v-for="(menu, index) in arrMenuInfo" :key="index">
+          <li :class="{ 'has-submenu': menu.subMenus && menu.subMenus.length > 0 }">
+            <button @click="handleMenuClick(menu)" :class="{ actv: isMenuActive(menu) }">
+              {{ menu.menuName }}
+              <i
+                v-if="menu.subMenus && menu.subMenus.length > 0"
+                class="ico-arrow"
+                :class="{ rotate: activeSubmenu === menu.menuId }"
+                >▼</i
+              >
+            </button>
+          </li>
+
+          <!-- 하위 메뉴들을 별도 li로 삽입 -->
+          <template
+            v-if="
+              menu.subMenus && menu.subMenus.length > 0 && activeSubmenu === menu.menuId
+            "
           >
-            {{ menu.menuName }}
-            <i v-if="menu.subMenus && menu.subMenus.length > 0" class="ico-arrow">▼</i>
-          </button>
-          
-          <!-- 하위 메뉴 -->
-          <ul v-if="menu.subMenus && menu.subMenus.length > 0" 
-              class="depth2" 
-              :class="{ show: activeSubmenu === menu.menuId }">
-            <li v-for="(subMenu, subIndex) in menu.subMenus" :key="subIndex">
+            <li
+              v-for="(subMenu, subIndex) in menu.subMenus"
+              :key="`${index}-sub-${subIndex}`"
+              class="submenu-item"
+            >
               <button
-                @click="menuAction(subMenu.menuId, subMenu.menuName, subMenu.linkType)"
+                @click="handleSubMenuClick(subMenu)"
                 :class="{ actv: subMenu.linkType === $route.path }"
               >
                 {{ subMenu.menuName }}
               </button>
             </li>
-          </ul>
-        </li>
+          </template>
+        </template>
       </ul>
     </nav>
   </header>
@@ -131,19 +138,19 @@
             {
               menuId: "menu02-1",
               menuName: "배송 관리",
-              linkType: "/view/common/deli"
+              linkType: "/view/common/deli",
             },
             {
-              menuId: "menu02-2", 
+              menuId: "menu02-2",
               menuName: "배송중",
-              linkType: "/view/common/deli/shipping"
+              linkType: "/view/common/deli/shipping",
             },
             {
               menuId: "menu02-3",
-              menuName: "배송완료", 
-              linkType: "/view/common/deli/delivered"
-            }
-          ]
+              menuName: "배송완료",
+              linkType: "/view/common/deli/delivered",
+            },
+          ],
         },
         {
           menuId: "menu03",
@@ -189,19 +196,19 @@
             {
               menuId: "menu02-1",
               menuName: "배송 관리",
-              linkType: "/view/common/deli"
+              linkType: "/view/common/deli",
             },
             {
-              menuId: "menu02-2", 
+              menuId: "menu02-2",
               menuName: "배송중",
-              linkType: "/view/common/deli/shipping"
+              linkType: "/view/common/deli/shipping",
             },
             {
               menuId: "menu02-3",
-              menuName: "배송완료", 
-              linkType: "/view/common/deli/delivered"
-            }
-          ]
+              menuName: "배송완료",
+              linkType: "/view/common/deli/delivered",
+            },
+          ],
         },
         {
           menuId: "menu03",
@@ -228,24 +235,51 @@
     $log.debug("[header][fnSetMenu][success] - MEMBER_TYPE:", memberType);
   };
 
-  const showSubmenu = (menuId) => {
-    activeSubmenu.value = menuId;
+  const handleMenuClick = (menu) => {
+    // 하위 메뉴가 있는 경우 아코디언 토글
+    if (menu.subMenus && menu.subMenus.length > 0) {
+      // 다른 메뉴가 열려있으면 닫고 현재 메뉴 열기
+      if (activeSubmenu.value === menu.menuId) {
+        activeSubmenu.value = ""; // 같은 메뉴면 닫기
+      } else {
+        activeSubmenu.value = menu.menuId; // 다른 메뉴면 현재 메뉴 열기
+      }
+    } else {
+      // 하위 메뉴가 없는 경우 모든 아코디언 닫고 페이지 이동
+      activeSubmenu.value = "";
+      menuAction(menu.menuId, menu.menuName, menu.linkType);
+    }
   };
 
-  const hideSubmenu = (menuId) => {
-    activeSubmenu.value = "";
+  const handleSubMenuClick = (subMenu) => {
+    // 하위 메뉴 클릭 시 페이지 이동만 (아코디언은 유지)
+    menuAction(subMenu.menuId, subMenu.menuName, subMenu.linkType);
   };
 
   const isMenuActive = (menu) => {
-    if (menu.linkType === route.path) {
+    // 아코디언이 열려있는 경우 해당 메뉴만 활성화
+    if (activeSubmenu.value === menu.menuId) {
       return true;
     }
-    
-    // 하위 메뉴 중 하나가 활성화되어 있는지 확인
-    if (menu.subMenus && menu.subMenus.length > 0) {
-      return menu.subMenus.some(subMenu => subMenu.linkType === route.path);
+
+    // 아코디언이 열려있지 않을 때만 페이지 기준으로 활성화 판단
+    if (!activeSubmenu.value) {
+      // 현재 페이지 경로와 일치하는 경우
+      if (menu.linkType === route.path) {
+        return true;
+      }
+
+      // 하위 메뉴 중 하나가 현재 페이지인 경우
+      if (menu.subMenus && menu.subMenus.length > 0) {
+        const hasActiveSubMenu = menu.subMenus.some(
+          (subMenu) => subMenu.linkType === route.path
+        );
+        if (hasActiveSubMenu) {
+          return true;
+        }
+      }
     }
-    
+
     return false;
   };
 
@@ -316,6 +350,9 @@
         $log.debug("[withdrawal][fnIdPwCheck][finished]");
       });
   };
+  // 페이지 변경 시 아코디언 상태 관리 (간단한 ref 기반)
+  const currentActivePage = ref(route.path);
+
   /************************************************************************
 |    라이프사이클 : onMounted
 ************************************************************************/
@@ -447,11 +484,11 @@
 
   .depth1 {
     display: flex;
+    flex-direction: column;
     width: 100%;
     list-style: none;
     margin: 0;
     padding: 0;
-    align-items: center;
   }
 
   .depth1 button {
@@ -501,73 +538,50 @@
     font-size: 10px;
     margin-left: 6px;
     transition: transform 0.2s ease;
+    font-style: normal;
+    position: absolute;
+    right: 24px;
+    top: 50%;
+    transform: translateY(-50%);
   }
 
-  .has-submenu:hover .ico-arrow {
+  .ico-arrow.rotate {
     transform: rotate(180deg);
   }
 
   /* 하위 메뉴 스타일 */
-  .depth2 {
-    position: absolute;
-    top: 100%;
-    left: 0;
-    background: white;
-    border: 1px solid #e5e7eb;
-    border-radius: 8px;
-    box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
-    z-index: 1000;
-    min-width: 180px;
-    list-style: none;
-    margin: 0;
-    padding: 8px 0;
-    opacity: 0;
-    visibility: hidden;
-    transform: translateY(-10px);
-    transition: all 0.2s ease;
-  }
-
-  .depth2.show {
-    opacity: 1;
-    visibility: visible;
-    transform: translateY(0);
+  .submenu-item {
+    background: #f8f9fa;
   }
 
-  .depth2 button {
+  .submenu-item button {
     width: 100%;
-    padding: 12px 20px !important;
+    padding: 12px 24px 12px 40px !important;
     border: none;
     background: none;
     font-size: 14px !important;
     font-weight: 400 !important;
-    color: #374151;
+    color: #555;
     cursor: pointer;
     transition: all 0.2s ease;
     text-align: left;
     border-bottom: none !important;
   }
 
-  .depth2 button:hover {
-    background: #f3f4f6;
+  .submenu-item button:hover {
+    background: #e9ecef;
     color: #667eea;
     transform: none !important;
   }
 
-  .depth2 button.actv {
-    background: #f0f7ff;
+  .submenu-item button.actv {
+    background: #e3f2fd;
     color: #667eea;
     font-weight: 500 !important;
     border-bottom: none !important;
   }
 
-  .depth2 button.actv::before {
+  .submenu-item button.actv::before {
     display: none;
   }
-
-  /* 호버 시 하위 메뉴 표시 */
-  .has-submenu:hover .depth2 {
-    opacity: 1;
-    visibility: visible;
-    transform: translateY(0);
-  }
 </style>

+ 158 - 48
pages/view/vendor/dashboard/index.vue

@@ -166,6 +166,12 @@
           <template v-slot:item.amount="{ item }">
             {{ formatCurrency(item.amount) }}
           </template>
+          <template v-slot:item.deliveryCompany="{ item }">
+            {{ item.deliveryInfo?.company || '-' }}
+          </template>
+          <template v-slot:item.trackingNumber="{ item }">
+            {{ item.deliveryInfo?.number || '-' }}
+          </template>
           <template v-slot:item.status="{ item }">
             <v-chip
               :color="getStatusColor(item.status)"
@@ -269,37 +275,14 @@
     { title: '인플루언서', key: 'influencer', sortable: true },
     { title: '상품명', key: 'productName', sortable: false },
     { title: '주문금액', key: 'amount', sortable: true },
+    { title: '배송업체', key: 'deliveryCompany', sortable: false },
+    { title: '송장번호', key: 'trackingNumber', sortable: false },
     { title: '주문일', key: 'orderDate', sortable: true },
     { title: '상태', key: 'status', sortable: true },
     { title: '액션', key: 'actions', sortable: false }
   ]);
 
-  const recentOrders = ref([
-    {
-      orderNo: 'ORD-2025001',
-      influencer: '김인플루',
-      productName: '프리미엄 화장품 세트',
-      amount: 150000,
-      orderDate: '2025-01-15',
-      status: 'completed'
-    },
-    {
-      orderNo: 'ORD-2025002',
-      influencer: '박유튜버',
-      productName: '건강기능식품',
-      amount: 85000,
-      orderDate: '2025-01-14',
-      status: 'shipping'
-    },
-    {
-      orderNo: 'ORD-2025003',
-      influencer: '이인스타',
-      productName: '패션 액세서리',
-      amount: 65000,
-      orderDate: '2025-01-13',
-      status: 'pending'
-    }
-  ]);
+  const recentOrders = ref([]);
 
   /************************************************************************
 |    함수(METHODS)
@@ -348,6 +331,7 @@
   const getStatusColor = (status) => {
     switch (status) {
       case 'completed': return 'success';
+      case 'delivered': return 'primary';
       case 'shipping': return 'info';
       case 'pending': return 'warning';
       case 'cancelled': return 'error';
@@ -358,7 +342,8 @@
   // 상태 텍스트
   const getStatusText = (status) => {
     switch (status) {
-      case 'completed': return '완료';
+      case 'completed': return '정산완료';
+      case 'delivered': return '배송완료';
       case 'shipping': return '배송중';
       case 'pending': return '대기';
       case 'cancelled': return '취소';
@@ -384,13 +369,18 @@
     if (!salesChart.value) return;
     
     const ctx = salesChart.value.getContext('2d');
+    // 실제 데이터가 있으면 사용, 없으면 빈 차트
+    const salesData = metrics.value.totalSales > 0 ? 
+      [0, 0, 0, 0, 0, metrics.value.totalSales] : 
+      [0, 0, 0, 0, 0, 0];
+    
     new ChartJS(ctx, {
       type: salesChartType.value,
       data: {
         labels: ['1월', '2월', '3월', '4월', '5월', '6월'],
         datasets: [{
           label: '매출',
-          data: [12000000, 15000000, 13000000, 18000000, 16000000, 20000000],
+          data: salesData,
           borderColor: '#3f51b5',
           backgroundColor: salesChartType.value === 'bar' ? '#3f51b5' : 'rgba(63, 81, 181, 0.1)',
           borderWidth: 2,
@@ -423,12 +413,19 @@
     if (!ordersChart.value) return;
     
     const ctx = ordersChart.value.getContext('2d');
+    // 실제 주문 데이터 계산
+    const totalOrders = metrics.value.totalOrders || 1;
+    const completedRate = metrics.value.settlementRate || 0;
+    const deliveredRate = Math.max(0, completedRate - 10); // 임시 계산
+    const shippingRate = Math.max(0, 100 - completedRate - deliveredRate - 10);
+    const pendingRate = Math.max(0, 100 - completedRate - deliveredRate - shippingRate);
+    
     new ChartJS(ctx, {
       type: 'doughnut',
       data: {
-        labels: ['신규 주문', '처리중', '배송중', '완료'],
+        labels: ['신규 주문', '배송중', '배송완료', '정산완료'],
         datasets: [{
-          data: [45, 25, 20, 10],
+          data: [pendingRate, shippingRate, deliveredRate, completedRate],
           backgroundColor: ['#4caf50', '#ff9800', '#2196f3', '#9c27b0'],
           borderWidth: 0
         }]
@@ -449,17 +446,20 @@
     if (!settlementChart.value) return;
     
     const ctx = settlementChart.value.getContext('2d');
+    const settlementRate = metrics.value.settlementRate || 0;
+    const pendingRate = 100 - settlementRate;
+    
     new ChartJS(ctx, {
       type: 'bar',
       data: {
         labels: ['1주', '2주', '3주', '4주'],
         datasets: [{
           label: '정산 완료',
-          data: [95, 92, 98, 94],
+          data: [settlementRate, settlementRate, settlementRate, settlementRate],
           backgroundColor: '#4caf50'
         }, {
           label: '정산 대기',
-          data: [5, 8, 2, 6],
+          data: [pendingRate, pendingRate, pendingRate, pendingRate],
           backgroundColor: '#ff9800'
         }]
       },
@@ -489,13 +489,15 @@
     if (!categoryChart.value) return;
     
     const ctx = categoryChart.value.getContext('2d');
+    // 실제 데이터가 없으므로 현재는 균등 분배로 표시
+    const totalSales = metrics.value.totalSales || 1;
     new ChartJS(ctx, {
       type: 'pie',
       data: {
-        labels: ['화장품', '패션', '건강식품', '전자제품', '기타'],
+        labels: ['전체 상품'],
         datasets: [{
-          data: [35, 25, 20, 15, 5],
-          backgroundColor: ['#e91e63', '#9c27b0', '#3f51b5', '#009688', '#ff5722']
+          data: [100],
+          backgroundColor: ['#3f51b5']
         }]
       },
       options: {
@@ -510,24 +512,129 @@
     });
   };
 
+  // 정산 데이터 가져오기
+  const fetchSettlementData = async () => {
+    try {
+      const userInfo = useAuthStore().getUser;
+      const _req = {
+        MEMBER_TYPE: 'VENDOR',
+        COMPANY_NUMBER: userInfo?.COMPANY_NUMBER
+      };
+
+      // 전체 정산 데이터 가져오기
+      const allSettlementResponse = await useAxios().post("/deli/settlement", _req);
+      const allSettlements = allSettlementResponse.data || [];
+
+      // 정산완료된 데이터만 가져오기
+      const completedSettlementResponse = await useAxios().post("/deli/settlement", {
+        ..._req,
+        SETTLEMENT_STATUS: 'COMPLETED'
+      });
+      const completedSettlements = completedSettlementResponse.data || [];
+
+      // 정산 메트릭 계산
+      const totalOrders = allSettlements.length;
+      const completedOrders = completedSettlements.length;
+      const settlementRate = totalOrders > 0 ? ((completedOrders / totalOrders) * 100).toFixed(1) : 0;
+      
+      const totalSales = completedSettlements.reduce((sum, item) => sum + (item.TOTAL || 0), 0);
+      const avgOrderValue = totalOrders > 0 ? (totalSales / totalOrders) : 0;
+
+      // 메트릭 업데이트 (변화율은 현재 데이터만으로는 계산 불가하므로 0으로 설정)
+      metrics.value = {
+        totalSales: totalSales,
+        totalOrders: totalOrders,
+        settlementRate: parseFloat(settlementRate),
+        avgOrderValue: avgOrderValue,
+        salesChange: 0,
+        ordersChange: 0,
+        settlementChange: 0,
+        avgOrderChange: 0
+      };
+
+    } catch (error) {
+      console.error('정산 데이터 조회 실패:', error);
+    }
+  };
+
+  // 최근 주문 데이터 가져오기
+  const fetchRecentOrders = async () => {
+    try {
+      const userInfo = useAuthStore().getUser;
+      const _req = {
+        MEMBER_TYPE: 'VENDOR',
+        COMPANY_NUMBER: userInfo?.COMPANY_NUMBER
+      };
+
+      // 배송중 데이터
+      const shippingResponse = await useAxios().post("/deli/shipping", _req);
+      const shippingOrders = shippingResponse.data || [];
+
+      // 배송완료 데이터
+      const deliveredResponse = await useAxios().post("/deli/delivered", _req);
+      const deliveredOrders = deliveredResponse.data || [];
+
+      // 정산완료 데이터
+      const settlementResponse = await useAxios().post("/deli/settlement", {
+        ..._req,
+        SETTLEMENT_STATUS: 'COMPLETED'
+      });
+      const settlementOrders = settlementResponse.data || [];
+
+      // 모든 주문 데이터 통합 및 정렬
+      const allOrders = [
+        ...shippingOrders.map(order => ({ ...order, status: 'shipping' })),
+        ...deliveredOrders.map(order => ({ ...order, status: 'delivered' })),
+        ...settlementOrders.map(order => ({ ...order, status: 'completed' }))
+      ];
+
+      // 날짜순 정렬 (최신순)
+      allOrders.sort((a, b) => {
+        const dateA = new Date(a.DELIVERED_DATE || a.SHIPPING_DATE || a.REG_DATE || a.ORDER_DATE);
+        const dateB = new Date(b.DELIVERED_DATE || b.SHIPPING_DATE || b.REG_DATE || b.ORDER_DATE);
+        return dateB - dateA;
+      });
+
+      // 테이블 형식으로 변환 (최근 20개만)
+      recentOrders.value = allOrders.slice(0, 20).map((order, index) => ({
+        orderNo: `ORD-${order.SEQ || (Date.now() + index)}`,
+        influencer: order.NICK_NAME || '알 수 없음',
+        productName: order.ITEM_NAME || '상품명 없음',
+        amount: order.TOTAL || 0,
+        deliveryCompany: order.DELI_COMP || '-',
+        trackingNumber: order.DELI_NUMB || '-',
+        orderDate: $dayjs(order.ORDER_DATE || order.REG_DATE).format('YYYY-MM-DD'),
+        status: order.status,
+        deliveryInfo: {
+          company: order.DELI_COMP,
+          number: order.DELI_NUMB
+        },
+        originalData: order
+      }));
+
+    } catch (error) {
+      console.error('최근 주문 데이터 조회 실패:', error);
+      $toast.error('최근 주문 데이터를 불러오는데 실패했습니다.');
+    }
+  };
+
   // 대시보드 데이터 가져오기
   const fetchDashboardData = async () => {
     loading.value = true;
     
     try {
-      const _req = {
-        compId: useAuthStore().getCompanyId,
-        startDate: dateRange.value[0],
-        endDate: dateRange.value[1]
-      };
-
-      await useAxios()
-        .post("/dashboard/metrics", _req)
-        .then((res) => {
-          if (res.data) {
-            metrics.value = res.data;
-          }
-        });
+      await Promise.all([
+        fetchSettlementData(),
+        fetchRecentOrders()
+      ]);
+      
+      // 데이터 로드 후 차트 업데이트
+      nextTick(() => {
+        createSalesChart();
+        createOrdersChart();
+        createSettlementChart();
+        createCategoryChart();
+      });
     } catch (error) {
       $toast.error('대시보드 데이터를 불러오는데 실패했습니다.');
     } finally {
@@ -551,6 +658,9 @@
     // 기본 1개월 기간 설정
     setQuickPeriod('month');
     
+    // 대시보드 데이터 로드
+    fetchDashboardData();
+    
     nextTick(() => {
       createSalesChart();
       createOrdersChart();