[신규 템플릿 안내] 주문 대시보드

안녕하세요. 셀렉트어드민에 템플릿이 추가되어 안내드립니다.

주문 조회와 상황 파악, 환불 등을 운영 업무가 가능한 주문 대시보드 예제입니다.

아래와 같은 주문 대시보드를 통해

  • 조건에 따라 상태를 분류해 우선순위와 상황을 빠르게 파악할 수 있습니다.
  • 타임라인 뷰를 통해 히스토리를 쉽게 파악할 수 있습니다.
  • 지난주와 현재 상태를 쉽게 비교할 수 있습니다.

해당 템플릿에 활용한 주요 기능

  • pages layout
  • tableOptions
  • sortOptions
  • formOptions
  • display: chartjs + displayFn
  • updateParams
  • timelineOptions

템플릿 페이지 바로가기


menus:
- path: order-refund-center
  name: 주문 환불 센터

pages:
- path: order-refund-center
  class: container
  containerStyle: >
    padding: 40px
  layout:
    div:
      - name: top-container
        style:
          display: flex
          marginBottom: 20px
        div:
        - name: left-container
          style:
            width: calc(50% - 10px)
            padding: 10px
        - name: right-container
          style: 
            width: calc(50% - 10px)
            padding: 10px
      - name: bottom-container
  
  blocks:    
    - name: 처리 필요
      layout: left-container
      type: query
      resource: mysql.qa
      sqlType: select
      sql: >
        SELECT 
            COUNT(CASE WHEN re.status = 'REQUESTED' THEN 1 END) as refund_pending,
            COUNT(CASE WHEN o.order_status = 'SHIPPING' AND o.updated_at < DATE_SUB(NOW(), INTERVAL 3 DAY) THEN 1 END) as shipping_delayed,
            COUNT(CASE WHEN re.status IN ('REQUESTED', 'PROCESSING') AND re.created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN 1 END) as urgent_refund_exchange
        FROM orders0821 o
        LEFT JOIN refund_exchange0821 re ON o.order_id = re.order_id AND re.status = 'REQUESTED'
        WHERE o.ordered_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);
      showDownload: false
      tableOptions:
        sortable: false
      columns:
        refund_pending:
          label: 🟡 환불승인 대기
          style: { color: '#faad14', fontWeight: 'bold', fontSize: '18px' }
        shipping_delayed:
          label: ⚠️ 배송지연 의심
          style: { color: '#fa8c16', fontWeight: 'bold', fontSize: '18px' }
        urgent_refund_exchange:
          label: 🔴 24시간 초과
          style: { color: '#fa8c16', fontWeight: 'bold', fontSize: '18px' }          
      style:
        backgroundColor: '#f6ffed'
        border: '2px solid #b7eb8f'
        borderRadius: '8px'
        padding: '8px'
    
    - name: 주문내역 조회
      layout: bottom-container
      type: query
      resource: mysql.qa
      sqlType: select
      columns:
        ' ':
          prepend: true
          buttons:
            - label: 보기
              openModal: order-:order_id
        priority_badge:
          label: 우선순위
        order_id:
          label: 주문ID
        order_number:
          label: 주문번호
        ordered_at:
          label: 주문일
        customer_name: 
          label: 고객명
        customer_phone:
          label: 고객전화번호
        products_detail:
          label: 주문상품상세
        final_payment_amount:
          label: 최종결제금액
        order_status:
          label: 주문상태
          valueAs:
            ORDER_RECEIVED: '📋 주문접수'
            PREPARING: '📦 준비중'
            SHIPPING: '🚛 배송중'
            DELIVERED: '✅ 배송완료'
            CANCELLED: '❌ 취소완료'
            REFUNDED: '💰 환불완료'
        auto_process_status:
          label: 자동화상태
        updated_at:
          label: 수정일시
      showDownload: false
      resetButton: true
      sql: |
        SELECT 
            CASE 
                WHEN re.status = 'REQUESTED' THEN '🟡 환불대기'
                WHEN o.order_status = 'SHIPPING' AND o.updated_at < DATE_SUB(NOW(), INTERVAL 3 DAY) THEN '⚠️ 지연의심'
                ELSE '✅ 정상'
            END as priority_badge,
            o.order_id,
            o.order_number,
            DATE_FORMAT(o.ordered_at, '%Y-%m-%d %H:%i') as ordered_at,
            c.customer_name,
            c.customer_phone,
            (SELECT GROUP_CONCAT(
                CONCAT(oi.product_name, ' (', oi.quantity, '개)') 
                SEPARATOR ', '
            ) FROM order_items0821 oi WHERE oi.order_id = o.order_id) as products_detail,
            FORMAT(o.final_payment_amount, 0) as final_payment_amount,
            o.order_status,
            CASE 
                WHEN o.order_status IN ('ORDER_RECEIVED', 'PREPARING', 'SHIPPING', 'DELIVERED') THEN '🤖 자동처리'
                WHEN o.payment_status IN ('CANCELLED', 'REFUNDED') THEN '🔄 셀프처리'
                ELSE '⏸️ 대기중'
            END as auto_process_status,
            DATE_FORMAT(o.updated_at, '%Y-%m-%d %H:%i') as updated_at
        FROM orders0821 o
        JOIN customers0821 c ON o.customer_id = c.customer_id
        LEFT JOIN stores0821 s ON o.store_id = s.store_id
        LEFT JOIN refund_exchange0821 re ON o.order_id = re.order_id AND re.status = 'REQUESTED'
        WHERE 1=1        
          AND o.ordered_at > COALESCE(NULLIF(:date1, ''), DATE_SUB(NOW(), INTERVAL 8 DAY))
          AND o.ordered_at < COALESCE(NULLIF(:date2, ''), DATE_ADD(NOW(), INTERVAL 1 DAY))
          AND (COALESCE(NULLIF(:order_status, ''), '') = '' OR o.order_status = :order_status)
          AND (COALESCE(NULLIF(:order_number, ''), '') = '' OR o.order_number LIKE CONCAT('%', :order_number, '%'))
          AND (COALESCE(NULLIF(:customer_name, ''), '') = '' OR c.customer_name LIKE CONCAT('%', :customer_name, '%'))
          AND (COALESCE(NULLIF(:customer_phone, ''), '') = '' OR c.customer_phone LIKE CONCAT('%', :customer_phone, '%'))        

      requestFn: |
        const order_status = params.find(e => e.key == 'order_status')
        console.log(order_status)        
      multipleColumns: true
      sortOptions:
        enabled: true
        initialSortBy:
          - field: priority_badge
            type: desc
      formOptions:
        display: col
        firstLabelWidth: 180px
        labelWidth: 180px
      params:
        - key: date
          label: 주문일자
          style:
            width: 578px
          format: date
          range: true
          group: 1
          showButton: true
          shortcuts:
          - label: 최근 일주일
            from:
              offset: -7
              period: day
            to:
              offset: 0
              period: day
          - label: 이번달
            from:
              offset: 0
              startOf: month
            to:
              offset: 0
              endOf: month        
        - key: order_status
          label: 주문상태
          group: 1
          radioButtonGroup: true
          radio:     
            - value: ''
              label: 전체                
            - value: ORDER_RECEIVED
              label: 주문접수
            - value: PREPARING
              label: 준비중
            - value: SHIPPING
              label: 배송중
            - value: DELIVERED
              label: 배송완료
            - value: CANCELLED
              label: 취소
            - value: REFUNDED
              label: 환불
        - key: order_number
          label: 주문번호
          group: 2
        - key: customer_name
          label: 고객명
          group: 3
        - key: customer_phone
          label: 고객전화번호
          group: 3
      searchOptions:
        enabled: true
        placeholder: "주문번호, 고객명, 전화번호로 검색"
      paginationOptions:
        enabled: true
        perPage: 10
      modals:
        - path: order-:order_id
          mode: side
          header: false
          name: 주문 상세 정보
          blocks:
            - name: 📋 주문 기본 정보
              type: query
              resource: mysql.qa
              sqlType: select
              showDownload: false
              sql: >
                SELECT 
                    o.order_number,
                    o.order_status,
                    o.final_payment_amount,
                    o.payment_method,
                    o.payment_status,
                    c.customer_name,
                    c.customer_phone,
                    o.receiver_name,
                    o.shipping_address,
                    o.ordered_at,
                    o.admin_memo
                FROM orders0821 o
                JOIN customers0821 c ON o.customer_id = c.customer_id
                WHERE o.order_id = :order_id;
              columns:
                order_number:
                  label: 주문번호
                order_status:
                  label: 주문상태
                  valueAs:
                    ORDER_RECEIVED: '📋 주문접수'
                    PREPARING: '📦 준비중'
                    SHIPPING: '🚛 배송중'
                    DELIVERED: '✅ 배송완료'
                    CANCELLED: '❌ 취소완료'
                    REFUNDED: '💰 환불완료'                  
                final_payment_amount:
                  label: 최종결제금액                  
                  formatFn: number0
                payment_method:
                  label: 결제방법
                payment_status:
                  label: 결제상태
                customer_name:
                  label: 고객명
                customer_phone:
                  label: 고객전화번호
                receiver_name:
                  label: 수취인명
                shipping_address:
                  label: 배송주소
                ordered_at:
                  label: 주문일시
                admin_memo:
                  label: 관리자메모                  
              params:
                - key: order_id
                  valueFromRow: order_id
              display: col-1

            - name: 📦 주문 상품 상세
              type: query
              resource: mysql.qa
              sqlType: select
              showDownload: false
              tableOptions:
                sortable: false
              sql: >
                SELECT 
                    oi.product_name,
                    oi.product_option,
                    oi.quantity,
                    oi.unit_price,
                    oi.total_amount,
                    p.brand_name
                FROM order_items0821 oi
                JOIN products0821 p ON oi.product_id = p.product_id
                WHERE oi.order_id = :order_id
                ORDER BY oi.order_item_id;
              columns:
                product_name:
                  label: 상품명
                product_option:
                  label: 상품옵션
                quantity:
                  label: 수량
                unit_price:
                  label: 단가                  
                  formatFn: number0
                total_amount:
                  label: 총금액                  
                  formatFn: number0
                brand_name:
                  label: 브랜드명
              params:
                - key: order_id
                  valueFromRow: order_id

            - name: 💰 환불/교환 정보
              type: query
              resource: mysql.qa
              sqlType: select
              showDownload: false
              tableOptions:
                sortable: false              
              sql: >
                SELECT 
                    refund_exchange_id,
                    CASE re.type
                      WHEN 'REFUND' THEN '💰 환불'
                      WHEN 'EXCHANGE' THEN '🔄 교환'
                    END as type_display,
                    CASE re.status
                      WHEN 'REQUESTED' THEN '📝 신청됨'
                      WHEN 'APPROVED' THEN '✅ 승인됨'
                      WHEN 'REJECTED' THEN '❌ 거부됨'
                      WHEN 'COMPLETED' THEN '✅ 완료'
                    END as status_display,
                    re.reason_detail,
                    re.refund_amount,
                    re.created_at
                FROM refund_exchange0821 re
                WHERE re.order_id = :order_id
                ORDER BY re.created_at DESC;
              columns:
                refund_exchange_id:
                  hidden: true
                type_display:
                  label: 처리구분
                status_display:
                  label: 처리상태
                reason_detail:
                  label: 상세사유
                refund_amount:
                  label: 환불금액                  
                  formatFn: number0
                created_at:
                  label: 신청일시
              params:
                - key: order_id
                  valueFromRow: order_id
              selectOptions:
                enabled: true
              actions:
                - label: 환불승인
                  placement: right bottom
                  type: query
                  resource: mysql.qa
                  sqlType: select 1
                  params:
                    - key: refund_amount
                      label: 환불금액
                    - key: refund_exchange_id
                      valueFromSelectedRows: refund_exchange_id
                  confirm: |
                    환불을 진행하시겠습니까?
                    - 환불/교환ID: {{refund_exchange_id}}
                    - 환불금액: {{refund_amount}}

            - name: 📈 처리 히스토리
              style: >
                padding-top: 100px;
                margin-top: 100px;
              type: query
              resource: mysql.qa
              sqlType: select
              showDownload: false
              tableOptions:
                sortable: false
              sql: >
                SELECT 
                    osh.created_at,
                    CASE osh.previous_status
                      WHEN 'ORDER_RECEIVED' THEN '주문접수'
                      WHEN 'PREPARING' THEN '준비중'
                      WHEN 'SHIPPING' THEN '배송중'
                      WHEN 'DELIVERED' THEN '배송완료'
                      WHEN 'CANCELLED' THEN '취소완료'
                      WHEN 'REFUNDED' THEN '환불완료'
                      ELSE osh.previous_status
                    END as previous_status,
                    CASE osh.new_status
                      WHEN 'ORDER_RECEIVED' THEN '주문접수'
                      WHEN 'PREPARING' THEN '준비중'
                      WHEN 'SHIPPING' THEN '배송중'
                      WHEN 'DELIVERED' THEN '배송완료'
                      WHEN 'CANCELLED' THEN '취소완료'
                      WHEN 'REFUNDED' THEN '환불완료'
                      ELSE osh.new_status
                    END as new_status,
                    CASE 
                      WHEN osh.changed_by_admin_id IS NULL THEN '시스템'
                      WHEN osh.changed_by_admin_id = 0 THEN '고객'
                      ELSE CONCAT('관리자 ', osh.changed_by_admin_id)
                    END as changed_by_display,
                    osh.change_reason
                FROM order_status_history0821 osh
                WHERE osh.order_id = :order_id
                ORDER BY osh.created_at DESC;
              display: timeline
              timelineOptions:
                useColumn: created_at
                template: |
                  <b>{{changed_by_display}}</b>가 주문 상태를 변경했습니다.<br>
                  {{previous_status}} → <b>{{new_status}}</b><br>
                  <small>사유: {{change_reason}}</small>
              columns:
                created_at:
                  label: 처리일시
                previous_status:
                  label: 이전상태
                new_status:
                  label: 변경상태             
                changed_by_display:
                  label: 처리자
                change_reason:
                  label: 변경사유
              params:
                - key: order_id
                  valueFromRow: order_id

    - name: 🚨 처리 대기중인 환불/교환 목록
      layout: bottom-container
      type: query
      resource: mysql.qa
      sqlType: select
      showDownload: false
      sql: >
        SELECT 
            CASE re.type
              WHEN 'REFUND' THEN '💰'
              WHEN 'EXCHANGE' THEN '🔄'
            END as type_icon,
            re.refund_exchange_id,
            o.order_number,
            c.customer_name,
            c.customer_phone,
            FORMAT(o.final_payment_amount, 0) as final_payment_amount,
            FORMAT(re.refund_amount, 0) as refund_amount,
            re.reason_detail,
            CASE 
              WHEN re.created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN '🔴 24시간 초과'
              WHEN re.created_at < DATE_SUB(NOW(), INTERVAL 12 HOUR) THEN '🟡 12시간 초과'
              ELSE '🟢 정상'
            END as urgency_status,
            DATE_FORMAT(re.created_at, '%Y-%m-%d %H:%i') as created_at,
            DATE_FORMAT(o.ordered_at, '%Y-%m-%d') as ordered_at
        FROM refund_exchange0821 re
        JOIN orders0821 o ON re.order_id = o.order_id
        JOIN customers0821 c ON o.customer_id = c.customer_id
        WHERE re.status IN ('REQUESTED', 'PROCESSING')
        AND (LENGTH(:order_number)=0 OR order_number = :order_number)
        ORDER BY 
          CASE 
            WHEN re.created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN 1
            WHEN re.created_at < DATE_SUB(NOW(), INTERVAL 12 HOUR) THEN 2
            ELSE 3
          END ASC,
          re.created_at ASC;    
      columns:
        type_icon:
          label: 구분
        refund_exchange_id:
          label: 환불교환ID
        urgency_status:
          label: 긴급도
        order_number:
          label: 주문번호
          updateParams:
            order_number: "{{order_number}}"
        customer_name:
          label: 고객명
        customer_phone:
          label: 연락처
        final_payment_amount:
          label: 주문금액
        refund_amount:
          label: 환불요청금액
        reason_detail:
          label: 사유
        created_at:
          label: 신청일시
        ordered_at:
          label: 주문일
      paginationOptions:
        enabled: true
        perPage: 10

    - name: 오늘 지난주 주문 현황
      layout: right-container
      type: query
      resource: mysql.qa
      sqlType: select
      showDownload: false
      sql: >
        SELECT 
            CASE o.order_status
              WHEN 'ORDER_RECEIVED' THEN '📋 주문접수'
              WHEN 'PREPARING' THEN '📦 준비중'
              WHEN 'SHIPPING' THEN '🚛 배송중'
              WHEN 'DELIVERED' THEN '✅ 배송완료'
              WHEN 'CANCELLED' THEN '❌ 취소'
              WHEN 'REFUNDED' THEN '💰 환불'
              ELSE o.order_status
            END as order_status_display,
            SUM(CASE WHEN DATE(o.ordered_at) = CURDATE() AND TIME(o.ordered_at) <= CURTIME() THEN 1 ELSE 0 END) as today_count,
            SUM(CASE WHEN DATE(o.ordered_at) = CURDATE() AND TIME(o.ordered_at) <= CURTIME() THEN o.final_payment_amount ELSE 0 END) as today_amount,
            SUM(CASE WHEN DATE(o.ordered_at) = DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND TIME(o.ordered_at) <= CURTIME() THEN 1 ELSE 0 END) as last_week_count,
            SUM(CASE WHEN DATE(o.ordered_at) = DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND TIME(o.ordered_at) <= CURTIME() THEN o.final_payment_amount ELSE 0 END) as last_week_amount,
            CASE 
                WHEN SUM(CASE WHEN DATE(o.ordered_at) = DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND TIME(o.ordered_at) <= CURTIME() THEN 1 ELSE 0 END) > 0 
                THEN ROUND((SUM(CASE WHEN DATE(o.ordered_at) = CURDATE() AND TIME(o.ordered_at) <= CURTIME() THEN 1 ELSE 0 END) - SUM(CASE WHEN DATE(o.ordered_at) = DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND TIME(o.ordered_at) <= CURTIME() THEN 1 ELSE 0 END)) / SUM(CASE WHEN DATE(o.ordered_at) = DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND TIME(o.ordered_at) <= CURTIME() THEN 1 ELSE 0 END) * 100, 1)
                ELSE NULL 
            END as count_change_percent
        FROM orders0821 o
        WHERE (DATE(o.ordered_at) = CURDATE() AND TIME(o.ordered_at) <= CURTIME())
          OR (DATE(o.ordered_at) = DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND TIME(o.ordered_at) <= CURTIME())
        GROUP BY o.order_status
        HAVING today_count > 0 OR last_week_count > 0
        ORDER BY today_count DESC
      display: chartjs
      displayFn: |
        const labels = rows.map(row => row.order_status_display);
        const todayData = rows.map(row => row.today_count);
        const lastWeekData = rows.map(row => row.last_week_count);
        const changePercents = rows.map(row => row.count_change_percent);
        
        // 상태별 색상 정의
        const getStatusColor = (status, alpha = 0.8) => {
          const colorMap = {
            '📋 주문접수': `rgba(24, 144, 255, ${alpha})`,
            '📦 준비중': `rgba(255, 193, 7, ${alpha})`,
            '🚛 배송중': `rgba(82, 196, 26, ${alpha})`,
            '✅ 배송완료': `rgba(52, 196, 26, ${alpha})`,
            '❌ 취소': `rgba(245, 34, 45, ${alpha})`,
            '💰 환불': `rgba(245, 34, 45, ${alpha})`
          };
          return colorMap[status] || `rgba(217, 217, 217, ${alpha})`;
        };
        
        const todayColors = labels.map(label => getStatusColor(label, 0.8));
        const lastWeekColors = labels.map(label => getStatusColor(label, 0.4));
        
        const data = {
          labels: labels,
          datasets: [{
            label: '오늘 (현재시각까지)',
            data: todayData,
            backgroundColor: todayColors,
            borderColor: todayColors.map(color => color.replace('0.8', '1')),
            borderWidth: 1
          }, {
            label: '지난주 같은 요일 (같은 시각까지)',
            data: lastWeekData,
            backgroundColor: lastWeekColors,
            borderColor: lastWeekColors.map(color => color.replace('0.4', '1')),
            borderWidth: 1
          }]
        };
        
        const config = {
          type: 'bar',
          data: data,
          options: {
            indexAxis: 'y',
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
              title: { 
                display: true, 
                text: '🕐 오늘 vs 지난주 주문 현황',
                font: { size: 16 }
              },
              tooltip: {
                callbacks: {
                  afterLabel: function(context) {
                    const index = context.dataIndex;
                    const change = changePercents[index];
                    const todayAmount = rows[index].today_amount;
                    const lastWeekAmount = rows[index].last_week_amount;
                    
                    return [
                      `변화율: ${change ? (change > 0 ? '+' : '') + change + '%' : 'N/A'}`,
                      `오늘 금액: ${todayAmount.toLocaleString()}원`,
                      `지난주 금액: ${lastWeekAmount.toLocaleString()}원`
                    ];
                  }
                }
              },
              legend: {
                position: 'bottom'
              }
            },
            scales: {
              x: {
                title: { 
                  display: true, 
                  text: '주문 건수' 
                },
                beginAtZero: true
              },
              y: {
                title: { 
                  display: true, 
                  text: '주문 상태' 
                }
              }
            }
          }
        };
        
        const myChart = new Chart(ctx, config);

해당 템플릿에 대해 궁금한점이 있으시다면 문의주세요.

아울러 다른 해결하고 싶은 문제나 효율적인 업무를 위해 필요한 템플릿 주제가 있다면 편하게 말씀해주시기 바랍니다.
감사합니다.

1개의 좋아요

안녕하세요, @김학범님!

잇그린의 이주성입니다.

해당 템플릿을 보다가 첨부파일과 같이 변경로그 영역을 확인하게 되어서요.

변경로그 부분은 어떻게 구현된 건지 알 수 있을까요?

감사합니다.

2개의 좋아요

안녕하세요. 주성님

해당 부분은 display: timeline (timelineOptions) 기능으로 표현된 부분입니다!

관련 문서: menus: [] | SelectAdmin

1개의 좋아요