Ver código fonte

공동구매 완료

DESKTOP-T61HUSC\user 4 meses atrás
pai
commit
b1bebe800d

+ 56 - 49
assets/scss/default.scss

@@ -5,8 +5,10 @@
 }
 
 .container{
+  padding-left: 340px;
   height:100%;
   .content{
+    width: 100%;
     height:100%;    
     display: flex;
 
@@ -39,63 +41,68 @@
       padding: 20px;
       width:calc(100%);     
 
-      .data--list--wrap{
-        width:100%;
-        
-        .btn--actions--wrap{
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          padding-bottom:25px;
+      .btn--actions--wrap{
+        &.pb--rem{
+          padding-bottom: 0.75rem;
+          align-items: flex-end;
         }
-        .left--sections{
-          display: flex;
-          gap: 1rem;
-        }
-        .right--sections{
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding-bottom:25px;
+      }
+      .left--sections{
+        display: flex;
+        gap: 1rem;
+      }
+      .right--sections{
+        display: flex;
+        gap: 1rem;
+        .caption--wrap{
           display: flex;
-          gap: 1rem;
-          .caption--wrap{
-            display: flex;
-            align-items: center;
+          align-items: center;
+          position: relative;
+          .ico{
+            font-size: 1rem;
+            width: 2rem;
+            height: 2rem;
+            text-align: center;
+            cursor: pointer;
+            line-height: 2rem;
+            border-radius: 50%;
+            // background-color: #F74F78;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: #fff;
+            display: inline-block;
             position: relative;
-            .ico{
-              font-size: 1rem;
-              width: 2rem;
-              height: 2rem;
-              text-align: center;
-              cursor: pointer;
-              line-height: 2rem;
-              border-radius: 50%;
-              background-color: #F74F78;
-              color: #fff;
-              display: inline-block;
-              position: relative;
-              font-style: normal;
+            font-style: normal;
 
-            }
+          }
+          .caption--box{
+            position: absolute;
+            font-size: 0.875rem;
+            bottom: 100%;
+            border: 2px solid #DFE7EF;
+            background-color: #fff;
+            border-radius: 10px;
+            right:0;
+            line-height: 1.4;
+            padding: 15px 20px;
+            white-space: nowrap;
+            color: #9DA9B6;
+            z-index: 10;
+            display: none;
+          }
+          &:hover{
             .caption--box{
-              position: absolute;
-              font-size: 0.875rem;
-              bottom: 100%;
-              border: 2px solid #DFE7EF;
-              background-color: #fff;
-              border-radius: 10px;
-              right:0;
-              line-height: 1.4;
-              padding: 15px 20px;
-              white-space: nowrap;
-              color: #9DA9B6;
-              z-index: 10;
-              display: none;
-            }
-            &:hover{
-              .caption--box{
-                display: block;
-              }
+              display: block;
             }
           }
         }
+      }
+      .data--list--wrap{
+        width:100%;
+        
         .item--section{
           border: 1px solid #ccc;
           padding: 2rem;

+ 37 - 13
assets/scss/mode-w-m.scss

@@ -724,8 +724,11 @@
     align-items: center;
     flex-direction: column;
     flex-shrink: 0;
-    position: relative;
+    position: fixed;
+    left: 0;
+    top: 0;
     width: 340px;
+    height: 100vh;
     padding: 20px;
     z-index: 22;
     .pro--wrap{
@@ -7377,20 +7380,41 @@ z
   }
 }
 .tbl-wrapper{
-  .tbl-wrap .ag-checkbox-input-wrapper{
-    width: 20px;
-    height: 20px;
-    background-color: #ffffff;
-    border: 1px solid #b0b0b0;
-    &.ag-checked{
-      &::after{
-        display: block;
-        width: 20px;
-        height: 20px;
-        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M10 3L4.5 8.5L2 6' stroke='%230094FF' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
-        background-position: center;
+  .tbl-wrap{
+    width: 100%;
+    
+    .ag-checkbox-input-wrapper{
+      width: 20px;
+      height: 20px;
+      background-color: #ffffff;
+      border: 1px solid #b0b0b0;
+      &.ag-checked{
+        &::after{
+          display: block;
+          width: 20px;
+          height: 20px;
+          background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M10 3L4.5 8.5L2 6' stroke='%230094FF' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+          background-position: center;
+        }
       }
     }
+
+    // .order--table{
+    //   .ag-header-row{
+    //     display: flex;
+    //     .ag-header-cell{
+    //       position: static;
+    //       width: calc(100% / 6)!important;
+    //     }
+    //   }
+    //   .ag-row{
+    //     display: flex;
+    //     .ag-cell-value{
+    //       position: static;
+    //       width: calc(100% / 6)!important;
+    //     }
+    //   }
+    // }
   }
   .tbl-wrap .ag-checkbox-input-wrapper:before,
   .tbl-wrap .ag-checkbox-input-wrapper:after{

+ 12 - 1
assets/scss/style.scss

@@ -1298,7 +1298,7 @@ p.success-txt {
 .custom-btn.v-btn.v-btn--density-default {
   width: 100%;
   height: 3.63rem;
-  border-radius: 0;
+  border-radius: 8px;
   box-shadow: none;
   padding: 0 0.63rem;
 
@@ -1326,6 +1326,17 @@ p.success-txt {
     }
   }
 
+  &.btn-purple{
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    &.v-btn--disabled {
+      background: #C5CDD4 !important;
+    }
+
+    .v-btn__content {
+      color: #fff;
+    }
+  }
+
   &.btn-blue-bor {
     background: transparent;
     border: 0.06rem solid rgba(3, 78, 162, 0.5);

+ 2 - 1
backend/app/Config/Routes.php

@@ -56,10 +56,11 @@ $routes->post('item/search', 'Item::itemSearch');
 $routes->get('item/download/(:segment)', 'Item::file/$1');
 
 // 제품 주문 라우트
+$routes->get('deli/orderList/(:segment)', 'Deli::orderList');
+$routes->post('deli/reg', 'Deli::orderRegister');
 $routes->post('deli/search', 'Deli::deliSearch');
 $routes->post('deli/itemlist', 'Deli::itemlist');
 $routes->post('deli/list', 'Deli::delilist');
-$routes->post('deli/reg', 'Deli::deliRegister');
 
 // 고객센터 라우트
 $routes->post('cs/list', 'Cs::csList');

+ 285 - 1
backend/app/Controllers/Deli.php

@@ -6,6 +6,291 @@ use CodeIgniter\RESTful\ResourceController;
 
 class Deli extends ResourceController
 {
+    // New: 공동구매 주문 내역 리스트
+    public function orderList()
+    {
+        $db = \Config\Database::connect();
+        $request = $this->request->getJSON(true);
+
+        $memberType = isset($request['MEMBER_TYPE']) ? $request['MEMBER_TYPE'] : null;
+        $companyNumber = isset($request['COMPANY_NUMBER']) ? $request['COMPANY_NUMBER'] : null;
+        $infSeq = isset($request['INF_SEQ']) ? $request['INF_SEQ'] : null;
+        $itemType = isset($request['TYPE']) ? $request['TYPE'] : null;
+
+        // ITEM_ORDER_LIST와 ITEM_LIST를 JOIN해서 주문 목록 조회
+        $builder = $db->table('ITEM_ORDER_LIST IOL')
+            ->select('IOL.*, IL.NAME as ITEM_NAME, IL.PRICE1, IL.PRICE2, IL.TYPE as ITEM_TYPE, IL.THUMB_FILE')
+            ->join('ITEM_LIST IL', 'IOL.ITEM_SEQ = IL.SEQ', 'inner')
+            ->where('IL.DEL_YN', 'N'); // 삭제되지 않은 아이템만
+
+        // 아이템 타입 필터링
+        if (!empty($itemType)) {
+            $builder->where('IL.TYPE', $itemType);
+        }
+
+        // 사용자 타입에 따른 필터링
+        if ($memberType === 'VENDOR' && !empty($companyNumber)) {
+            // 벤더사: 자사 아이템의 주문만
+            $builder->where('IL.COMPANY_NUMBER', $companyNumber);
+        } elseif ($memberType === 'INFLUENCER' && !empty($infSeq)) {
+            // 인플루언서: 자신이 담당하는 주문만
+            $builder->where('IOL.INF_SEQ', $infSeq);
+        } elseif ($memberType === 'BRAND' && !empty($infSeq)) {
+            // 브랜드사: 자사가 담당하는 아이템의 주문만
+            $builder->where('IL.CONTACT_BRD', $infSeq);
+        }
+
+        // 주문일 기준으로 최신순 정렬
+        $builder->orderBy('IOL.ORDER_DATE', 'DESC');
+
+        $lists = $builder->get()->getResultArray();
+
+        return $this->respond($lists, 200);
+    }
+
+
+    // New: 공동구매 주문 내역 등록/업데이트
+    public function orderRegister()
+    {
+        // 한국 시간으로 설정
+        date_default_timezone_set('Asia/Seoul');
+        
+        $db = \Config\Database::connect();
+        $request = $this->request->getJSON(true);
+
+        $itemSeq = isset($request['item_seq']) ? $request['item_seq'] : null;
+        $orderList = $request['orderList'] ?? [];
+
+        if (!$itemSeq) {
+            return $this->fail('필수 파라미터가 누락되었습니다.', 400);
+        }
+
+        // orderList가 비어있으면 해당 item_seq의 모든 데이터 삭제만 수행
+        if (empty($orderList)) {
+            $db->transBegin();
+            try {
+                // 기존 데이터 모두 삭제
+                $deletedCount = $db->table('ITEM_ORDER_LIST')
+                    ->where('ITEM_SEQ', $itemSeq)
+                    ->delete();
+                
+                $db->transCommit();
+                return $this->respond([
+                    'message' => '주문 내역이 성공적으로 처리되었습니다.',
+                    'updated_count' => 0,
+                    'new_count' => 0,
+                    'deleted_count' => $deletedCount,
+                    'errors' => []
+                ], 200);
+            } catch (\Exception $e) {
+                $db->transRollback();
+                return $this->fail('주문 내역 처리 중 오류가 발생했습니다: ' . $e->getMessage(), 500);
+            }
+        }
+
+        // 빈 데이터나 변경사항 없는 경우 먼저 체크
+        $hasValidData = false;
+        foreach ($orderList as $order) {
+            if (!empty($order['ORDER_NUMB']) && !empty($order['BUYER_NAME'])) {
+                $hasValidData = true;
+                break;
+            }
+        }
+        
+        // 유효한 데이터가 없으면 기존 데이터와 비교해서 변경사항 확인
+        if (!$hasValidData) {
+            $existingCount = $db->table('ITEM_ORDER_LIST')
+                ->where('ITEM_SEQ', $itemSeq)
+                ->countAllResults();
+            
+            // 기존 데이터도 없고 새로운 유효 데이터도 없으면 변경사항 없음
+            if ($existingCount == 0) {
+                return $this->respond([
+                    'message' => '저장할 데이터가 없습니다.',
+                    'updated_count' => 0,
+                    'new_count' => 0,
+                    'deleted_count' => 0,
+                    'errors' => []
+                ], 200);
+            }
+        }
+
+        // 유효성 검사 (유효한 데이터가 있을 때만)
+        foreach ($orderList as $index => $order) {
+            // 빈 행은 건너뛰기
+            if (empty($order['ORDER_NUMB']) && empty($order['BUYER_NAME'])) {
+                continue;
+            }
+            
+            $requiredFields = ['ORDER_NUMB', 'BUYER_NAME'];
+            foreach ($requiredFields as $field) {
+                if (!isset($order[$field]) || $order[$field] === '') {
+                    return $this->fail("orderList[$index] 항목의 '{$field}' 값이 누락되었습니다.", 400);
+                }
+            }
+        }
+
+        $db->transBegin();
+        $updatedCount = 0;
+        $newCount = 0;
+        $deletedCount = 0;
+        $errors = [];
+
+        try {
+            // 1. 먼저 해당 item_seq의 기존 데이터를 모두 조회
+            $existingOrders = $db->table('ITEM_ORDER_LIST')
+                ->where('ITEM_SEQ', $itemSeq)
+                ->get()->getResultArray();
+            
+            // 2. 현재 전송된 데이터 목록에서 ORDER_NUMB + BUYER_NAME 조합 추출
+            $currentOrderKeys = [];
+            foreach ($orderList as $order) {
+                if (!empty($order['ORDER_NUMB']) && !empty($order['BUYER_NAME'])) {
+                    $currentOrderKeys[] = $order['ORDER_NUMB'] . '_' . $order['BUYER_NAME'];
+                }
+            }
+            
+            // 3. 기존 데이터 중 현재 목록에 없는 항목들 삭제
+            foreach ($existingOrders as $existingOrder) {
+                $existingKey = $existingOrder['ORDER_NUMB'] . '_' . $existingOrder['BUYER_NAME'];
+                if (!in_array($existingKey, $currentOrderKeys)) {
+                    $deleteResult = $db->table('ITEM_ORDER_LIST')
+                        ->where('SEQ', $existingOrder['SEQ'])
+                        ->delete();
+                    
+                    if ($deleteResult) {
+                        $deletedCount++;
+                    }
+                }
+            }
+            
+            // 4. 업데이트 또는 신규 추가 처리
+            foreach ($orderList as $order) {
+                $metadata = $order['_metadata'] ?? [];
+                $isUpdated = $metadata['isUpdated'] ?? false;
+                $isNew = $metadata['isNew'] ?? false;
+
+                // 메타데이터가 없는 경우 DB에서 기존 데이터 확인
+                if (!$isUpdated && !$isNew) {
+                    $existingRecord = $db->table('ITEM_ORDER_LIST')
+                        ->where('ITEM_SEQ', $itemSeq)
+                        ->where('ORDER_NUMB', $order['ORDER_NUMB'])
+                        ->where('BUYER_NAME', $order['BUYER_NAME'])
+                        ->get()->getRowArray();
+                    
+                    if ($existingRecord) {
+                        $isUpdated = true;
+                    } else {
+                        // 주문번호+구매자명이 정확히 일치하지 않으면 신규 추가
+                        $isNew = true;
+                    }
+                }
+
+                if ($isUpdated) {
+                    // 기존 데이터 조회하여 변경사항 확인
+                    $existingRecord = $db->table('ITEM_ORDER_LIST')
+                        ->where('ITEM_SEQ', $itemSeq)
+                        ->where('ORDER_NUMB', $order['ORDER_NUMB'])
+                        ->where('BUYER_NAME', $order['BUYER_NAME'])
+                        ->get()->getRowArray();
+                    
+                    if ($existingRecord) {
+                        $updateData = [];
+                        $hasChanges = false;
+                        
+                        // 각 필드별로 변경사항 확인
+                        if (($existingRecord['PHONE'] ?? '') !== ($order['PHONE'] ?? '')) {
+                            $updateData['PHONE'] = $order['PHONE'] ?? '';
+                            $hasChanges = true;
+                        }
+                        if (($existingRecord['QTY'] ?? 0) != ($order['QTY'] ?? 0)) {
+                            $updateData['QTY'] = $order['QTY'] ?? 0;
+                            $hasChanges = true;
+                        }
+                        if (($existingRecord['DELI_COMP'] ?? '') !== ($order['DELI_COMP'] ?? '')) {
+                            $updateData['DELI_COMP'] = $order['DELI_COMP'] ?? '';
+                            $hasChanges = true;
+                        }
+                        if (($existingRecord['DELI_NUMB'] ?? '') !== ($order['DELI_NUMB'] ?? '')) {
+                            $updateData['DELI_NUMB'] = $order['DELI_NUMB'] ?? '';
+                            $hasChanges = true;
+                        }
+                        
+                        // 실제 변경사항이 있을 때만 UPDATE_DATE 갱신
+                        if ($hasChanges) {
+                            $updateData['UPDATE_DATE'] = date('Y-m-d H:i:s');
+                            
+                            $result = $db->table('ITEM_ORDER_LIST')
+                                ->where('ITEM_SEQ', $itemSeq)
+                                ->where('ORDER_NUMB', $order['ORDER_NUMB'])
+                                ->where('BUYER_NAME', $order['BUYER_NAME'])
+                                ->update($updateData);
+
+                            if ($result) {
+                                $updatedCount++;
+                            } else {
+                                $errors[] = "업데이트 실패: {$order['ORDER_NUMB']} - {$order['BUYER_NAME']}";
+                            }
+                        }
+                        // 변경사항이 없으면 업데이트하지 않음
+                    } else {
+                        // 기존 데이터를 찾을 수 없으면 신규로 처리
+                        $isNew = true;
+                    }
+
+                }
+                
+                if ($isNew) {
+                    // 신규 데이터 추가
+                    $insertData = [
+                        'ITEM_SEQ' => $itemSeq,
+                        'ORDER_NUMB' => $order['ORDER_NUMB'],
+                        'BUYER_NAME' => $order['BUYER_NAME'],
+                        'PHONE' => $order['PHONE'] ?? '',
+                        'QTY' => $order['QTY'] ?? 0,
+                        'DELI_COMP' => $order['DELI_COMP'] ?? '',
+                        'DELI_NUMB' => $order['DELI_NUMB'] ?? '',
+                        'REG_DATE' => $metadata['originalCreatedAt'] ?? date('Y-m-d H:i:s'),
+                        'UPDATE_DATE' => $metadata['lastModifiedAt'] ?? date('Y-m-d H:i:s'),
+                    ];
+
+                    $result = $db->table('ITEM_ORDER_LIST')->insert($insertData);
+
+                    if ($result) {
+                        $newCount++;
+                    } else {
+                        $errors[] = "삽입 실패: {$order['ORDER_NUMB']} - {$order['BUYER_NAME']}";
+                    }
+                }
+            }
+
+            // 에러가 있으면 실패로 처리
+            if (count($errors) > 0) {
+                $db->transRollback();
+                return $this->fail(implode(' | ', $errors), 400);
+            }
+            
+            if ($updatedCount > 0 || $newCount > 0 || $deletedCount > 0) {
+                $db->transCommit();
+                return $this->respond([
+                    'message' => '주문 내역이 성공적으로 처리되었습니다.',
+                    'updated_count' => $updatedCount,
+                    'new_count' => $newCount,
+                    'deleted_count' => $deletedCount,
+                    'errors' => $errors
+                ], 200);
+            } else {
+                $db->transRollback();
+                return $this->fail('처리할 수 있는 데이터가 없습니다.', 400);
+            }
+
+        } catch (\Exception $e) {
+            $db->transRollback();
+            return $this->fail('주문 내역 처리 중 오류가 발생했습니다: ' . $e->getMessage(), 500);
+        }
+    }
+
+
     //아이템 리스트
     public function itemlist()
     {
@@ -666,5 +951,4 @@ class Deli extends ResourceController
 
         return $this->respond($lists, 200);
     }
-
 }

+ 6 - 0
backend/app/Controllers/Item.php

@@ -146,6 +146,9 @@ class Item extends ResourceController
     //아이템 등록
     public function itemRegister()
     {
+        // 한국 시간으로 설정
+        date_default_timezone_set('Asia/Seoul');
+        
         $db = \Config\Database::connect();
         $request = \Config\Services::request();
         $regdate = date('Y-m-d H:i:s');
@@ -273,6 +276,9 @@ class Item extends ResourceController
     //아이템 수정
     public function ItemUpdate($seq)
     {
+        // 한국 시간으로 설정
+        date_default_timezone_set('Asia/Seoul');
+        
         $db = \Config\Database::connect();
         $request = $this->request;
         $upddate = date('Y-m-d H:i:s');

+ 6 - 0
components/common/header.vue

@@ -191,6 +191,12 @@
           menuName: "공동구매",
           linkType: "/view/common/item",
         },
+        {
+          menuId: "menu03",
+          parentMenuId: "menu03",
+          menuName: "마감된 공동구매",
+          linkType: "/view/common/item/closed",
+        },
         // {
         //   menuId: "menu02",
         //   parentMenuId: "menu02",

+ 0 - 4
pages/index.vue

@@ -555,10 +555,6 @@
         event: "FN_LOGIN",
         param: "",
       },
-      no: {
-        text: "취소",
-        isProc: false,
-      },
     };
     $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
   };

+ 328 - 0
pages/view/common/item/closed.vue

@@ -0,0 +1,328 @@
+<template>
+  <div>
+    <div class="inner--headers">
+      <h2>{{ pageId }}</h2>
+      <div class="bread--crumbs--wrap">
+        <span>홈</span>
+        <span>{{ pageId }}</span>
+      </div>
+    </div>
+
+    <div class="data--list--wrap">
+      <div class="item--list--wrap">
+        <div class="cs--list--wrap">
+          <div class="cs--header">
+            <h3>나의 공동구매</h3>
+          </div>
+          
+          <!-- 검색 영역 -->
+          <div class="cs--search-area">
+            <div class="search-controls">
+              <div class="search-filter">
+                <v-select
+                  v-model="filter"
+                  :items="filderArr"
+                  variant="outlined"
+                  density="compact"
+                  hide-details
+                ></v-select>
+              </div>
+              <div class="search-input">
+                <v-text-field
+                  v-model="searchModel"
+                  placeholder="검색어를 입력하세요"
+                  variant="outlined"
+                  density="compact"
+                  hide-details
+                  prepend-inner-icon="mdi-magnify"
+                  @keyup.enter="fnSearch(searchModel, filter)"
+                ></v-text-field>
+              </div>
+              <div class="search-actions">
+                <v-btn
+                  class="custom-btn mini btn--pp"
+                  color="primary"
+                  @click="fnSearch(searchModel, filter)"
+                >
+                  검색
+                </v-btn>
+                <v-btn
+                  class="custom-btn mini btn-white"
+                  color="secondary"
+                  variant="outlined"
+                  @click="resetSearch"
+                >
+                  초기화
+                </v-btn>
+              </div>
+            </div>
+          </div>
+          <div class="cs--list" v-if="itemList.length > 0">
+            <div v-for="(items, index) in paginatedItems" :key="index" class="">
+              <!-- <div v-if="itemType == 'E'" @click="toItemDetail(items.SEQ)" class="item-content">
+                <div class="item--img"><img v-if="items.THUMB_FILE" :src="`https://shopdeli.mycafe24.com/writable/uploads/item/thumb/${items.THUMB_FILE}`"></div>
+                <h3>{{ items.NAME }}</h3>
+                <p>공급가: {{ Number(items.PRICE1).toLocaleString() }}<br>판매가: {{ Number(items.PRICE2).toLocaleString() }}</p>
+                <span>등록일: {{ items.REGDATE.slice(0, 10) }}</span>
+                <span>업데이트 날짜: {{ items.UDPDATE.slice(0, 10) }}</span>
+                <div
+                  v-if="items.STATUS == 1 || isRecentUpdate(items.UDPDATE)"
+                  class="sold--out"
+                  :class="{ 'blue--type': isRecentUpdate(items.UDPDATE) && items.STATUS != 1 }"
+                >
+                  <span>
+                    {{ items.STATUS == 1 ? '품절' : '업데이트' }}
+                  </span>
+                </div>
+              </div> -->
+              <div v-if="itemType == 'G'" @click="toItemDetail(items.SEQ)" class="list">
+                <span class="list--seq">{{ items.SEQ }}</span>
+                <span class="list--writer">{{ items.COMPANY_NAME }}</span>
+                <h3 class="list--title">{{ items.NAME }}</h3>
+                <div class="list--circle list--ml" :class="[ items.STATUS == 0 ? 'status--on' : 'status--off' ]">
+                  {{ items.STATUS == 0 ? '진행중' : '마감' }}
+                </div>
+                <span class="list--date">{{ items.ORDER_START_DATE.slice(0, 10) }} ~ {{ items.ORDER_END_DATE.slice(0, 10) }}</span>
+              </div>
+            </div>
+          </div>
+          <div class="cs--list" v-else>
+            <div class="no-data">
+              <h4>등록된 제품이 없습니다</h4>
+            </div>
+          </div>
+        </div>
+        <div class="item--pagination" v-if="itemList.length > 0">
+          <v-pagination
+            v-model="currentPage"
+            :length="Math.ceil(itemList.length / itemsPerPage)"
+          ></v-pagination>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import "@vuepic/vue-datepicker/dist/main.css";
+import dayjs from 'dayjs';
+  /************************************************************************
+|    레이아웃
+************************************************************************/
+  definePageMeta({
+    layout: "default",
+  });
+  /************************************************************************
+|   PROPS
+ ************************************************************************/
+  const props = defineProps({
+    propsData: {
+      type: Object,
+      default: () => {},
+    },
+  });
+  /************************************************************************
+|    스토어
+ ************************************************************************/
+  const useDtStore = useDetailStore();
+  const useAtStore = useAuthStore();
+  /************************************************************************
+|    전역
+ ************************************************************************/
+  const itemType = ref("G");
+  const memberType = useAtStore.auth.memberType;
+  const searchModel = ref("");
+  const selectedRange = ref('all');
+  const searchStartDate = ref("");
+  const searchEndDate = ref("");
+  const dateOptions = [
+    { label: '오늘', value: 'today' },
+    { label: '7일', value: '7d' },
+    { label: '1개월', value: '1m' },
+    { label: '3개월', value: '3m' },
+    { label: '전체', value: 'all' },
+  ]
+  const datePickerFormat = "yyyy-MM-dd";
+  const filter = ref("");
+  const filderArr = ref([
+    { title: "전체", value: "" },
+    { title: "제품명", value: "name" },
+    { title: "회사명", value: "company" },
+  ]);
+  const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
+  const router = useRouter();
+  const pageId = computed(() => {
+    return '마감된 공동구매';
+    //return memberType === 'INFLUENCER' ? '제품 관리 (파트너십)' : '제품 관리 (자사)';
+  });
+  const itemList = ref([]);
+  const allItemList = ref([]); // 전체 데이터 저장용
+  const itemsPerPage = 10;
+  const currentPage = ref(1);
+
+  /* eslint-disable */
+  /* prettier-ignore */
+
+  /************************************************************************
+|    함수(METHODS)
+************************************************************************/
+
+  const paginatedItems = computed(() => {
+    const start = (currentPage.value - 1) * itemsPerPage;
+    return itemList.value.slice(start, start + itemsPerPage);
+  });
+
+  const setDateRange = (range) => {
+    const today = dayjs();
+
+    switch(range) {
+      case 'today' :
+        searchStartDate.value = today.format('YYYY-MM-DD');
+        searchEndDate.value = today.format('YYYY-MM-DD');
+        selectedRange.value = 'today';
+        break;
+      case '7d':
+        searchStartDate.value = today.subtract(7, 'day').format('YYYY-MM-DD');
+        searchEndDate.value = today.format('YYYY-MM-DD');
+        selectedRange.value = '7d';
+        break;
+      case '1m':
+        searchStartDate.value = today.subtract(1, 'month').format('YYYY-MM-DD');
+        searchEndDate.value = today.format('YYYY-MM-DD');
+        selectedRange.value = '1m';
+        break;
+      case '3m':
+        searchStartDate.value = today.subtract(3, 'month').format('YYYY-MM-DD');
+        searchEndDate.value = today.format('YYYY-MM-DD');
+        selectedRange.value = '3m';
+        break;
+      case 'all':
+        searchStartDate.value = "";
+        searchEndDate.value = today.format('YYYY-MM-DD');
+        selectedRange.value = 'all';
+        break
+    }
+  }
+
+  const addLocated = () => {
+    router.push({
+      path: "/view/common/item/add",
+    });
+    useDtStore.boardInfo.pageType = "I";
+    useDtStore.boardInfo.itemType = itemType.value;
+  };
+
+  const toItemDetail = (__EVENT) => {
+    router.push({
+      path: "/view/common/item/detail",
+    });
+    useDtStore.boardInfo.seq = __EVENT;
+    useDtStore.boardInfo.itemType = itemType;
+    useDtStore.boardInfo.pageType = "D";
+  };
+
+  const itemListGet = async () => {
+    let _req = {
+      // Y : 노출, N : 비노출
+      SHOW_YN: "Y",
+      TYPE: itemType.value,
+      INF_SEQ: useAtStore.auth.seq,
+      MEMBER_TYPE: memberType,
+      MEMBER_SEQ: useAtStore.auth.seq,
+      STATUS: "1",
+    };
+
+    if(memberType !== "INFLUENCER"){
+      _req.COMPANY_NUMBER = useAtStore.auth.companyNumber || "1";
+    }
+
+    
+    await useAxios()
+    .post("/item/list", _req)
+    .then((res) => {
+        allItemList.value = res.data; // 전체 데이터 저장
+        itemList.value = res.data; // 초기 표시용
+      });
+  };
+
+  const fnSearch = (__KEYWORD, __FILTER) => {
+    let filteredItems = [...allItemList.value];
+    
+    // 키워드 검색
+    if (__KEYWORD && __KEYWORD.trim() !== '') {
+      const keyword = __KEYWORD.toLowerCase();
+      filteredItems = filteredItems.filter(item => {
+        if (__FILTER === 'name') {
+          return item.NAME && item.NAME.toLowerCase().includes(keyword);
+        } else if (__FILTER === 'company') {
+          return item.COMPANY_NAME && item.COMPANY_NAME.toLowerCase().includes(keyword);
+        } else {
+          // 전체 검색 (이름 또는 회사명)
+          return (item.NAME && item.NAME.toLowerCase().includes(keyword)) ||
+                 (item.COMPANY_NAME && item.COMPANY_NAME.toLowerCase().includes(keyword));
+        }
+      });
+    }
+    
+    // 날짜 필터링 (마감일 기준)
+    // if (searchStartDate.value && searchEndDate.value) {
+    //   const startDate = dayjs(searchStartDate.value).format('YYYY-MM-DD');
+    //   const endDate = dayjs(searchEndDate.value).format('YYYY-MM-DD');
+    //   filteredItems = filteredItems.filter(item => {
+    //     const orderEndDate = dayjs(item.ORDER_END_DATE).format('YYYY-MM-DD');
+    //     return orderEndDate >= startDate && orderEndDate <= endDate;
+    //   });
+    // }
+    
+    itemList.value = filteredItems;
+    currentPage.value = 1; // 검색 시 첫 페이지로 이동
+  };
+
+  // 검색 초기화
+  const resetSearch = () => {
+    searchModel.value = '';
+    searchStartDate.value = null;
+    searchEndDate.value = null;
+    selectedRange.value = '';
+    itemList.value = [...allItemList.value];
+    currentPage.value = 1;
+  };
+
+
+  const goToDeliveryDetail = (item) => {
+    // 제품 정보를 스토어에 저장
+    useDtStore.boardInfo.seq = item.SEQ;
+    useDtStore.boardInfo.pageType = "D";
+    useDtStore.boardInfo.itemType = itemType.value;
+    
+    // 배송 관리 페이지로 이동
+    router.push({
+      path: "/view/common/deli/detail",
+      query: {
+        itemId: item.SEQ,
+        itemName: item.NAME,
+        price1: item.PRICE1,
+        price2: item.PRICE2 || item.PRICE1,
+        thumbFile: item.THUMB_FILE || ''
+      }
+    });
+  };
+
+  /************************************************************************
+|    WATCH
+************************************************************************/
+
+  watch(itemType, () => {
+    itemListGet();
+  });
+
+  
+  onMounted(() => {
+    itemType.value = 'G'
+    itemListGet();
+
+    // 날짜 초기화
+    const today = dayjs();
+    searchEndDate.value = today.format('YYYY-MM-DD');
+  });
+</script>

+ 644 - 38
pages/view/common/item/detail.vue

@@ -48,20 +48,28 @@
               <div class="form-field">
                 <label class="field-label">인플루언서</label>
                 <div class="field-content">
-                  <div class="display-value date-range">
+                  <div v-if="form.contact_inf" class="display-value date-range">
                     <i class="mdi mdi-account-circle"></i>
                     {{ form.contact_inf_display }}
                   </div>
+                  <div v-else class="display-value date-range">
+                    <i class="mdi mdi-account-circle"></i>
+                    배정된 인플루언서가 없습니다.
+                  </div>
                 </div>
               </div>
               <!-- 브랜드사 (공동구매인 경우) -->
               <div v-if="memberType !== 'BRAND'" class="form-field">
                 <label class="field-label">브랜드사</label>
                 <div class="field-content">
-                  <div class="display-value date-range">
+                  <div v-if="form.contact_brd" class="display-value date-range">
                     <i class="mdi mdi-domain"></i>
                     {{ form.contact_brd_display }}
                   </div>
+                  <div v-else class="display-value date-range">
+                    <i class="mdi mdi-domain"></i>
+                    배정된 브랜드사가 없습니다.
+                  </div>
                 </div>
               </div>
             </div>
@@ -93,6 +101,70 @@
                 ></v-text-field>
               </div>
             </div>
+
+            <div class="form-field">
+              <div class="field-content">
+                <div class="btn--actions--wrap pb--rem">
+                  <div class="left--sections">
+                    <label class="mb--0 field-label">주문 내역</label>
+                  </div>
+                  <div class="right--sections">
+                    <div class="caption--wrap">
+                      <i class="ico">!</i>
+                      <div class="caption--box">
+                        - 주문 내역 입력 후 저장 버튼을 꼭 클릭해 주세요.<br />
+                        - 엑셀 파일은 최대 10MB까지 업로드 가능합니다.<br />
+                        - 엑셀 파일의 헤더명(주문번호, 구매자명)이 일치해야 정상적으로 업로드됩니다.
+                      </div>
+                    </div>
+                    <v-btn class="custom-btn btn-white mini" @click="addEmptyRow"
+                      ><i class="ico"></i>항목 추가</v-btn
+                    >
+                    <v-btn class="custom-btn btn-white mini" @click="deleteSelectedRows"
+                      ><i class="ico"></i>항목 삭제</v-btn
+                    >
+                    <input
+                      ref="excelFileInput"
+                      type="file"
+                      accept=".xlsx,.xls"
+                      @change="handleExcelUpload"
+                      style="display: none"
+                    />
+                    <v-btn class="custom-btn btn-excel" @click="$refs.excelFileInput.click()"
+                      ><i class="ico"></i>엑셀 업로드</v-btn
+                    >
+                    <v-btn class="custom-btn btn-excel" @click="downloadExcel"
+                      ><i class="ico"></i>엑셀 다운로드</v-btn
+                    >
+                    <v-btn class="custom-btn btn-purple mini" @click="fnRegEvt"
+                      ><i class="ico"></i>저장</v-btn
+                    >
+                  </div>
+                </div>
+                <div class="tbl-wrapper">
+                  <div class="tbl-wrap">
+                    <!-- ag grid -->
+                    <ag-grid-vue
+                      :style="{ width: '100%', height: gridHeight }"
+                      class="ag-theme-quartz order--table"
+                      :gridOptions="gridOptions"
+                      rowSelection="multiple"
+                      :rowData="tblItems"
+                      :paginationPageSize="pageObj.pageSize"
+                      :suppressPaginationPanel="true"
+                      @grid-ready="onGridReady"
+                      @cell-value-changed="onCellValueChanged"
+                    >
+                    </ag-grid-vue>
+
+                    <!-- 페이징 -->
+                    <!-- <div class="ag-grid-custom-pagenations">
+                      <pagination @chg_page="chgPage" :pageObj="pageObj"></pagination>
+                    </div> -->
+                  </div>
+                </div>
+              </div>
+            </div>
           </div>
         </v-form>
       </div>
@@ -115,7 +187,7 @@
             color="error"
             @click="fnDelEvt"
           >
-            <i class="mdi mdi-delete"></i>
+            <i class="mdi mdi-close-circle"></i>
             삭제
           </v-btn>
         </div>
@@ -138,6 +210,8 @@
 <script setup>
 import useAxios from "@/composables/useAxios";
 import "@vuepic/vue-datepicker/dist/main.css";
+import { AgGridVue } from "ag-grid-vue3";
+import * as XLSX from "xlsx";
 /************************************************************************
 |    레이아웃
 ************************************************************************/
@@ -159,21 +233,13 @@ const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
 const router = useRouter();
 const pageId = ref("");
 const itemType = useDtStore.boardInfo.itemType;
-const datePickerFormat = "yyyy-MM-dd";
-const sunEditorWrapper = ref(null); //에디터용 전역
-const updatedContent = ref(null); //에디터용 전역
-const editorContentReq = ref(); //에디터용 전역
+let pageObj = ref({
+  page: 1, // 현재 페이지
+  pageMaxNumSize: 10, // 페이지 숫자 최대 표현 개수
+  pageSize: 10, // 테이블 조회 데이터 개수
+  totalCnt: 0, // 전체 페이지
+});
 const addForm = ref(null);
-const index = ref(null);
-const imageIndex = ref(0);
-const items = ref([]);
-const quillEditor = ref(null);
-const imgTemp = ref("");
-const zipInfo = ref({
-  file_path: "",
-  original_name: ""
-})
-const rowId = ref();
 const form = ref({
   formValue1: "",
   formValue2: "",
@@ -201,25 +267,7 @@ const form = ref({
   order_start_date: "",
   order_end_date: "",
 });
-
-// 인플루언서 관련 변수
-const influencerModal = ref(false);
-const influencerList = ref([]);
-const filteredInfluencerList = ref([]);
-const selectedInfluencer = ref(null);
-const influencerSearchQuery = ref("");
-const isSearching = ref(false);
-
-// 브랜드사 관련 변수
-const brandModal = ref(false);
-const brandList = ref([]);
-const filteredBrandList = ref([]);
-const selectedBrand = ref(null);
-const brandSearchQuery = ref("");
-const isBrandSearching = ref(false);
-
 const apiUrl = ref("");
-
 apiUrl.value = import.meta.env.VITE_APP_API_URL;
 const objProc = ref({
   validErrorMessage: "",
@@ -227,9 +275,420 @@ const objProc = ref({
 
 const pageType = ref("");
 
+// ag-grid 관련 변수
+const tblItems = ref([]);
+const gridApi = ref(null);
+const gridOptions = ref({
+  columnDefs: [
+    { checkboxSelection: true, headerCheckboxSelection: true, width: 50, sortable: false, filter: false,},
+    {
+      headerName: "No",
+      valueGetter: (params) => params.api.getDisplayedRowCount() - params.node.rowIndex,
+      sortable: false,
+      filter: false,
+      width: 80,
+    },
+    {
+      headerName: "주문번호",
+      field: "ORDER_NUMB",
+      cellStyle: { textAlign: 'center' },
+      editable: true,
+    },
+    {
+      headerName: "구매자명",
+      field: "BUYER_NAME",
+      editable: true,
+    },
+    {
+      headerName: "연락처",
+      field: "PHONE",
+      editable: true,
+    },
+    // {
+    //   headerName: "배송주소",
+    //   field: "ADDRESS",
+    // },
+    {
+      headerName: "수량",
+      field: "QTY",
+      editable: true,
+      valueFormatter: (params) => {
+        return params.value ? Number(params.value).toLocaleString() : '0';
+      }
+    },
+    {
+      headerName: "배송업체",
+      field: "DELI_COMP",
+      editable: true,
+    },
+    {
+      headerName: "송장번호",
+      field: "DELI_NUMB",
+      editable: true,
+    },
+  ],
+  autoSizeStrategy: {
+    type: "fitGridWidth", // width맞춤
+  },
+  suppressHorizontalScroll: true, // 가로 스크롤 제거
+  defaultColDef: {
+    sortable: true,
+    filter: true,
+    resizable: false,
+  },
+  suppressMovableColumns: true,
+  suppressPaginationPanel: true, // 하단 default 페이징 컨트롤 숨김
+  rowMultiSelectWithClick: true,
+  rowSelection: {
+    checkboxes: true,
+    headerCheckbox: true,
+    enableClickSelection: false,
+    mode: "multiRow",
+  },
+  localeText: {
+    noRowsToShow: '주문 내역이 없습니다.'
+  }
+});
+
 /************************************************************************
-|    함수(METHODS)
-************************************************************************/
+ |    함수(METHODS)
+ ************************************************************************/
+
+// 동적 높이 계산
+const gridHeight = computed(() => {
+  const rowCount = tblItems.value.length;
+  const minRows = 3; // 최소 5줄 높이
+  const maxRows = 10; // 최대 15줄 높이 (스크롤 시작점)
+  const rowHeight = 2.94; // rem 단위
+  
+  if (rowCount <= minRows) {
+    return `calc(${minRows} * ${rowHeight}rem)`;
+  } else if (rowCount > maxRows) {
+    return `calc(${maxRows} * ${rowHeight}rem)`;
+  } else {
+    return `calc(${rowCount} * ${rowHeight}rem)`;
+  }
+});
+
+const addEmptyRow = () => {
+  const newRow = {
+    BUYER_NAME: "",
+    ADDRESS: "",
+    PHONE: "",
+    EMAIL: "",
+    QTY: "",
+    TOTAL: "",
+    DELI_COMP: "",
+    DELI_NUMB: "",
+    ORDER_DATE: "",
+  };
+
+  // 맨 앞에 추가 (unshift 사용)
+  tblItems.value.unshift(newRow);
+  pageObj.value.totalCnt = tblItems.value.length;
+
+  // ag-grid 데이터 갱신
+  if (gridApi.value) {
+    gridApi.value.setGridOption("rowData", tblItems.value);
+  }
+
+  $toast.success("새 항목이 추가되었습니다.");
+};
+
+const deleteSelectedRows = () => {
+  if (!gridApi.value) return;
+
+  const selectedRows = gridApi.value.getSelectedRows();
+  if (selectedRows.length === 0) {
+    $toast.warning("삭제할 항목을 선택해주세요.");
+    return;
+  }
+
+  let param = {
+    id: pageId,
+    title: pageId,
+    content: `선택된 ${selectedRows.length}개 항목을 삭제하시겠습니까?`,
+    yes: {
+      text: "삭제",
+      isProc: true,
+      event: "FN_DELETE_SELECTED",
+      param: selectedRows,
+    },
+    no: {
+      text: "취소",
+      isProc: false,
+    },
+  };
+  $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
+};
+
+const fnDeleteSelected = (selectedRows) => {
+  // 선택된 행들을 tblItems에서 제거
+  selectedRows.forEach((selectedRow) => {
+    const index = tblItems.value.findIndex(
+      (item) =>
+        item.BUYER_NAME === selectedRow.BUYER_NAME &&
+        item.ADDRESS === selectedRow.ADDRESS &&
+        item.PHONE === selectedRow.PHONE &&
+        item.EMAIL === selectedRow.EMAIL
+    );
+    if (index > -1) {
+      tblItems.value.splice(index, 1);
+    }
+  });
+
+  pageObj.value.totalCnt = tblItems.value.length;
+
+  // ag-grid 데이터 갱신
+  if (gridApi.value) {
+    gridApi.value.setGridOption("rowData", tblItems.value);
+  }
+
+  $toast.success(`${selectedRows.length}개 항목이 삭제되었습니다.`);
+};
+
+const handleExcelUpload = async (event) => {
+  const file = event.target.files[0];
+  if (!file) return;
+
+  const errorHandler = useErrorHandler();
+
+  // 파일 크기 검증 (10MB)
+  const maxSize = 10 * 1024 * 1024;
+  if (file.size > maxSize) {
+    const sizeError = new Error("파일 크기 초과");
+    sizeError.name = "FileSizeError";
+    errorHandler.handleFileError(sizeError, file.name);
+    event.target.value = "";
+    return;
+  }
+
+  // 파일 형식 검증
+  const allowedTypes = [
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    "application/vnd.ms-excel",
+  ];
+  if (!allowedTypes.includes(file.type)) {
+    const typeError = new Error("지원하지 않는 파일 형식");
+    typeError.name = "FileTypeError";
+    errorHandler.handleFileError(typeError, file.name);
+    event.target.value = "";
+    return;
+  }
+
+  const reader = new FileReader();
+  reader.onload = async (e) => {
+    try {
+      const data = new Uint8Array(e.target.result);
+      const workbook = XLSX.read(data, { type: "array", cellDates: true });
+      const sheetName = workbook.SheetNames[0];
+      const worksheet = workbook.Sheets[sheetName];
+      const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, raw: false });
+
+      if (jsonData.length < 2) {
+        $toast.error("엑셀 파일에 데이터가 없습니다. 헤더와 최소 1개 이상의 데이터 행이 필요합니다.");
+        return;
+      }
+
+      const headers = jsonData[0];
+      const rows = jsonData.slice(1);
+
+      // 헤더 매핑 (다양한 형태의 헤더명 지원)
+      const headerMapping = {
+        "주문번호": "ORDER_NUMB",
+        "구매자명": "BUYER_NAME",
+        "구매자이름": "BUYER_NAME", 
+        "구매자성명": "BUYER_NAME",
+        "연락처": "PHONE",
+        "수량": "QTY",
+        "배송업체": "DELI_COMP",
+        "택배사": "DELI_COMP",
+        "송장번호": "DELI_NUMB"
+      };
+
+      // 필수 헤더 검증 (like 검색으로 변경)
+      const requiredHeaders = ["주문번호", "구매자명"];
+      const foundHeaders = headers.filter(header => 
+        requiredHeaders.some(required => 
+          (required === "주문번호" && header.includes("주문번호")) ||
+          (required === "구매자명" && (
+            header === "구매자" || header === "수취인" ||
+            (header.includes("구매자") && (header.includes("이름") || header.includes("성함") || header.includes("명"))) ||
+            (header.includes("수취인") && (header.includes("이름") || header.includes("성함") || header.includes("명")))
+          ))
+        )
+      );
+      
+      if (foundHeaders.length < requiredHeaders.length) {
+        $toast.error(`필수 헤더가 누락되었습니다. 필요한 헤더: ${requiredHeaders.join(", ")}`);
+        return;
+      }
+
+      // 데이터 변환
+      const mappedData = rows
+        .map((row, rowIndex) => {
+          const mappedRow = {};
+          let hasValidData = false;
+
+          headers.forEach((header, index) => {
+            let fieldName = null;
+            
+            // 헤더 매핑 로직
+            if (header.includes("주문번호")) fieldName = "ORDER_NUMB";
+            else if (header === "구매자" || header === "수취인" ||
+                    (header.includes("구매자") && (header.includes("이름") || header.includes("성함") || header.includes("명"))) ||
+                    (header.includes("수취인") && (header.includes("이름") || header.includes("성함") || header.includes("명")))) {
+              fieldName = "BUYER_NAME";
+            }
+            else if (header.includes("연락처")) fieldName = "PHONE";
+            else if (header.includes("수량")) fieldName = "QTY";
+            else if (header.includes("배송업체") || header.includes("택배사")) fieldName = "DELI_COMP";
+            else if (header.includes("송장")) fieldName = "DELI_NUMB";
+            
+            if (fieldName && row[index] !== undefined && row[index] !== "") {
+              mappedRow[fieldName] = row[index].toString().trim();
+              hasValidData = true;
+            }
+          });
+
+          // 필수 필드 검증
+          if (hasValidData) {
+            const missingFields = [];
+            if (!mappedRow.ORDER_NUMB) missingFields.push("주문번호");
+            if (!mappedRow.BUYER_NAME) missingFields.push("구매자명");
+
+            if (missingFields.length > 0) {
+              console.warn(`${rowIndex + 2}행: 필수 필드 누락 - ${missingFields.join(", ")}`);
+              return null;
+            }
+          }
+
+          return hasValidData ? mappedRow : null;
+        })
+        .filter((row) => row !== null);
+
+      if (mappedData.length === 0) {
+        $toast.error("매핑 가능한 데이터가 없습니다. 엑셀 헤더명과 데이터를 확인해주세요.");
+        return;
+      }
+
+      // 기존 데이터 업데이트 및 신규 데이터 추가 처리
+      let updatedCount = 0;
+      let newCount = 0;
+      
+      mappedData.forEach(newItem => {
+        // 기존 데이터에서 주문번호 + 구매자명이 일치하는 항목 찾기
+        const existingIndex = tblItems.value.findIndex(existingItem => 
+          existingItem.ORDER_NUMB === newItem.ORDER_NUMB && 
+          existingItem.BUYER_NAME === newItem.BUYER_NAME
+        );
+        
+        if (existingIndex !== -1) {
+          // 업데이트 메타데이터 추가
+          newItem._metadata = {
+            isUpdated: true,
+            isNew: false,
+            originalCreatedAt: tblItems.value[existingIndex].created_at || tblItems.value[existingIndex]._metadata?.originalCreatedAt,
+            lastModifiedAt: new Date().toISOString()
+          };
+          // 기존 데이터 업데이트
+          tblItems.value[existingIndex] = { ...tblItems.value[existingIndex], ...newItem };
+          updatedCount++;
+        } else {
+          // 신규 메타데이터 추가
+          newItem._metadata = {
+            isUpdated: false,
+            isNew: true,
+            originalCreatedAt: new Date().toISOString(),
+            lastModifiedAt: new Date().toISOString()
+          };
+          // 신규 데이터 추가
+          tblItems.value.push(newItem);
+          newCount++;
+        }
+      });
+      
+      pageObj.value.totalCnt = tblItems.value.length;
+
+      // ag-grid 데이터 갱신
+      if (gridApi.value) {
+        gridApi.value.setGridOption("rowData", tblItems.value);
+      }
+
+      // 결과 메시지 표시
+      if (updatedCount > 0 && newCount > 0) {
+        $toast.success(`총 ${mappedData.length}건 처리완료 (업데이트: ${updatedCount}건, 신규추가: ${newCount}건)`);
+      } else if (updatedCount > 0) {
+        $toast.success(`${updatedCount}건의 기존 주문이 업데이트되었습니다.`);
+      } else {
+        $toast.success(`${newCount}건의 주문 내역이 추가되었습니다.`);
+      }
+
+      // 파일 입력 초기화
+      event.target.value = "";
+
+    } catch (error) {
+      console.error("엑셀 파일 처리 중 오류:", error);
+      $toast.error("엑셀 파일을 읽는 중 오류가 발생했습니다. 파일 형식을 확인해주세요.");
+    }
+  };
+
+  reader.onerror = () => {
+    $toast.error("파일을 읽는 중 오류가 발생했습니다.");
+  };
+
+  reader.readAsArrayBuffer(file);
+}
+
+const downloadExcel = () => {
+  if (!tblItems.value || tblItems.value.length === 0) {
+    $toast.warning("다운로드할 데이터가 없습니다.");
+    return;
+  }
+
+  // 한글 헤더명 배열
+  const headers = [
+    "주문번호",
+    "구매자명",
+    "연락처",
+    "구매수량",
+    "배송업체",
+    "송장번호",
+  ];
+
+  // 데이터를 엑셀 형식으로 변환
+  const excelData = tblItems.value.map((item) => [
+    item.ORDER_NUMB || "",
+    item.BUYER_NAME || "",
+    item.PHONE || "",
+    item.QTY || "",
+    item.DELI_COMP || "",
+    item.DELI_NUMB || "",
+  ]);
+
+  // 헤더를 첫 번째 행에 추가
+  const worksheetData = [headers, ...excelData];
+
+  // 워크시트 생성
+  const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
+
+  // 워크북 생성
+  const workbook = XLSX.utils.book_new();
+  XLSX.utils.book_append_sheet(workbook, worksheet, "배송관리");
+
+  // 파일명 생성 (현재 날짜 포함)
+  const today = new Date();
+  const dateString =
+    today.getFullYear() +
+    String(today.getMonth() + 1).padStart(2, "0") +
+    String(today.getDate()).padStart(2, "0");
+  const fileName = `배송관리_${dateString}.xlsx`;
+
+  // 엑셀 파일 다운로드
+  XLSX.writeFile(workbook, fileName);
+
+  $toast.success("엑셀 파일이 다운로드되었습니다.");
+};
 
 const listLocated = () => {
   router.push({
@@ -297,7 +756,7 @@ const fnDelEvt = () => {
   let param = {
     id: pageId,
     title: pageId,
-    content: "삭제하시겠습니까?",
+    content: "공동구매를 종료하시겠습니까?",
     yes: {
       text: "확인",
       isProc: true,
@@ -363,6 +822,62 @@ const fnDelete = () => {
     });
 };
 
+const getOrderList = () => {
+  let req = {
+    MEMBER_TYPE: memberType,
+    COMPANY_NUMBER: useAtStore.auth.companyNumber || "1",
+    INF_SEQ: useAtStore.auth.seq,
+    TYPE: itemType,
+    ITEM_SEQ: useDtStore.boardInfo.seq // 특정 아이템의 주문만 조회하려면 추가
+  };
+  
+  useAxios()
+    .get(`/deli/orderList/${req.ITEM_SEQ}`, req)
+    .then(async (res) => {
+      // 특정 아이템의 주문만 필터링
+      const filteredData = res.data.filter(item => item.ITEM_SEQ == useDtStore.boardInfo.seq);
+      tblItems.value = filteredData;
+      pageObj.value.totalCnt = filteredData.length;
+    })
+    .catch((error) => {
+    })
+    .finally(() => {
+    });
+};
+
+// ag-grid 관련 함수들
+const onGridReady = (params) => {
+  gridApi.value = params.api;
+};
+
+const onCellValueChanged = (params) => {
+  console.log('셀 값 변경:', params);
+};
+
+const chgPage = (page) => {
+  pageObj.value.page = page;
+  // 페이징이 필요한 경우 여기에 추가 로직 구현
+};
+
+const fnRegEvt = () => {
+  let param = {
+      id: pageId,
+      title: pageId,
+      content: "주문 내역을 저장하시겠습니까?",
+      yes: {
+        text: "확인",
+        isProc: true,
+        event: "FN_INSERT",
+        param: "",
+      },
+      no: {
+        text: "취소",
+        isProc: false,
+      },
+    };
+    $eventBus.emit("OPEN_CONFIRM_POP_UP", param);
+};
+
 const fnDetail = () => {
   let req = {
     seq: useDtStore.boardInfo.seq,
@@ -395,6 +910,7 @@ const fnDetail = () => {
           form.value.contact_brd_display = brdData.NAME;
         }
       }
+      getOrderList();
     })
     .catch((error) => {
     })
@@ -402,6 +918,86 @@ const fnDetail = () => {
     });
 };
 
+const fnInsert = async () => {
+  try {
+    const req = {
+      item_seq: useDtStore.boardInfo.seq,
+      orderList: tblItems.value || []
+    };
+
+    const response = await useAxios().post('/deli/reg', req);
+    
+    console.log('응답 상태:', response.status);
+    console.log('전체 응답:', response);
+    
+    // 응답 상태가 400대 에러면 에러로 처리
+    if (response.status >= 400) {
+      throw new Error(response.data?.message || '서버 에러가 발생했습니다.');
+    }
+    
+    if (response.data) {
+      const { message, updated_count, new_count, deleted_count, errors } = response.data;
+      
+      console.log('백엔드 응답:', response.data);
+      
+      // 백엔드에서 "저장할 데이터가 없습니다" 메시지가 온 경우
+      if (message === '저장할 데이터가 없습니다.') {
+        console.log('저장할 데이터 없음 감지');
+        $toast.warning("저장할 데이터가 없습니다.");
+        return; // 새로고침하지 않음
+      }
+      
+      // 결과 메시지 표시
+      const totalProcessed = updated_count + new_count + deleted_count;
+      
+      if (totalProcessed === 0) {
+        $toast.success("주문 내역이 저장되었습니다.");
+      } else {
+        let message = `주문 내역이 저장되었습니다.`;
+        
+        $toast.success(message);
+      }
+
+      // 에러가 있으면 콘솔에 출력하고 토스트로 표시
+      if (errors && errors.length > 0) {
+        console.warn('저장 중 일부 오류 발생:', errors);
+        errors.forEach(error => {
+          $toast.error(error);
+        });
+      }
+
+      // 저장 후 페이지 새로고침
+      window.location.reload();
+    }
+  } catch (error) {
+    console.error('주문 내역 저장 중 오류:', error);
+    
+    // 백엔드 에러 응답 확인
+    if (error.response?.data) {
+      const errorData = error.response.data;
+      console.log('에러 응답:', errorData);
+      
+      // "처리할 수 있는 데이터가 없습니다" 에러를 저장할 데이터 없음으로 처리
+      if (errorData.messages?.error === "처리할 수 있는 데이터가 없습니다." || 
+          errorData.message === "처리할 수 있는 데이터가 없습니다.") {
+        $toast.warning("저장할 데이터가 없습니다.");
+        return; // 새로고침하지 않음
+      }
+      
+      // 다른 에러 메시지
+      if (errorData.message) {
+        $toast.error(errorData.message);
+      } else if (errorData.messages?.error) {
+        $toast.error(errorData.messages.error);
+      } else {
+        $toast.error('주문번호, 구매자명은 필수로 입력해야 합니다.');
+      }
+    } else {
+      $toast.error('주문번호, 구매자명은 필수로 입력해야 합니다.');
+    }
+  }
+};
+
 /************************************************************************
 |    팝업 이벤트버스 정의
 ************************************************************************/
@@ -416,6 +1012,16 @@ $eventBus.on("FN_CLOSE", () => {
   fnClose();
 });
 
+$eventBus.off("FN_INSERT");
+$eventBus.on("FN_INSERT", () => {
+  fnInsert();
+});
+
+$eventBus.off("FN_DELETE_SELECTED");
+$eventBus.on("FN_DELETE_SELECTED", (selectedRows) => {
+  fnDeleteSelected(selectedRows);
+});
+
 /************************************************************************
 |    라이프사이클
 ************************************************************************/

+ 1 - 0
pages/view/common/item/index.vue

@@ -248,6 +248,7 @@ import dayjs from 'dayjs';
       INF_SEQ: useAtStore.auth.seq,
       MEMBER_TYPE: memberType,
       MEMBER_SEQ: useAtStore.auth.seq,
+      STATUS: 0,
     };
 
     if(memberType !== "INFLUENCER"){