엑셀·CSV 업로드 운영 툴이 필요할 때

엑셀·CSV 데이터를 API로 업로드하고 미리보기까지 구현하는 방법. 서버 언어와 상관없이, UI는 셀렉트 어드민이 자동으로 구성합니다.

엑셀·CSV 업로드 운영 툴이 필요할 때

엑셀이나 CSV 파일로 받은 데이터를 빠르게 업로드하고 저장하는 기능.
운영툴을 만들다 보면 한 번은 꼭 필요해집니다.

이번 글에서는 셀렉트 어드민(Select Admin)으로
엑셀·CSV 업로드 → 변환 → 저장 → 미리보기까지 구현하는 방법을 소개합니다.

서버 언어는 상관없습니다.
Node.js, Python, Java 등 어떤 환경이든 API만 제공하면,
셀렉트 어드민이 UI를 자동으로 구성합니다.


왜 필요한 기능인가요?

운영팀은 종종 엑셀이나 CSV 데이터를 받아 임시로 시스템에 반입해야 합니다.
예를 들어 다음과 같은 경우입니다.

  • 재고 데이터를 엑셀로 받아 업로드해야 할 때
  • 파트너사에서 전달한 주문 데이터를 검증해야 할 때
  • CSV를 JSON 형태로 가공해 내부 API로 전달해야 할 때

이 예제는 그런 상황을 위한 “업로드 → 변환 → 저장 → 미리보기” 흐름 템플릿입니다.


작동 방식 한눈에 보기

  1. 엑셀 또는 CSV 파일을 업로드
  2. 지정된 컬럼(예: 상품명, 수량)으로 데이터 변환
  3. 지정한 API(/local/import/save-json)로 전송 및 저장
  4. 업로드된 파일 목록을 테이블로 표시
  5. 파일명을 클릭하면 상세 데이터를 모달로 미리보기

예제 코드 (셀렉트 어드민 YAML)

아래 YAML을 그대로 붙여넣으면,
파일 업로드 → 저장 → 목록 조회 → 상세 보기까지 자동으로 구성됩니다.

blocks:
  # 1) 파일 업로드 & JSON 저장
  - type: http
    name: 엑셀 업로드 → JSON 저장
    method: POST
    display: form
    params:
      - key: sheet
        label: 엑셀/CSV 업로드
        width: 400px
        format: sheet
        accept: .csv,.xlsx
        preview: true
        sheetOptions:
          multiple: true
          # ✅ 엑셀 시리얼날짜 자동 변환 (YYYY-MM-DD 형태)
          convertDate:
            - 시작일
            - 종료일
    fetchFn: |
      try {
        if (!Array.isArray(sheet) || sheet.length === 0) {
          throw new Error('업로드된 시트에 데이터가 없습니다.')
        }

        const rows = sheet.map(r => ({
          name: String(r['상품명'] ?? ''),
          unit: Number(r['수량'] ?? 0),
          ...(r['시작일'] ? { startDate: r['시작일'] } : {}),
          ...(r['종료일'] ? { endDate: r['종료일'] } : {})
        }))

        const base = API || 'http://localhost:9500'
        const res = await fetch(`${base}/local/import/save-json`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ payload: { rows } })
        })

        if (!res.ok) throw new Error(`HTTP ${res.status}`)
        await res.json().catch(() => ({}))

        $toast(`저장 완료 (${rows.length}행)`)        
        return true
      } catch (err) {
        $toast(String(err?.message || err), { type: 'error' })
        return false
      }

  # 1-1) 업로드 양식 다운로드
  - type: http
    method: GET
    fetchFn: |
      return [
        {
          "id": "",
          "상품명": "",
          "수량": "",
          "시작일": "",
          "종료일": ""
        }
      ]
    showDownload: false
    tableOptions:
      hidden: true
    actions:
      - label: 업로드 양식 다운로드
        button:
          icon: mdi-download
        showDownload: csv xlsx
        single: true

  # 2) 업로드된 파일 목록
  - type: http
    name: 업로드 파일 목록
    method: GET
    display: table
    showDownload: false
    fetchFn: |
      try {
        const base = API || 'http://localhost:9500'
        const r = await fetch(`${base}/local/import/list`)
        if (!r.ok) throw new Error(`HTTP ${r.status}`)
        const data = await r.json().catch(() => ({}))

        return (data.files || []).map(f => ({
          file: f.file,
          size: f.size,
          mtime: new Date(f.mtime).toISOString()
        }))
      } catch (err) {
        $toast(`파일 목록을 불러오지 못했습니다: ${String(err?.message || err)}`, { type: 'error' })
        return []
      }
    columns:
      file:
        width: 520px
        openModal: file-detail-:file
      size:
        formatFn: |
          (value > 1024*1024)
            ? (value/1024/1024).toFixed(1) + ' MB'
            : (value/1024).toFixed(1) + ' KB'
      mtime:
        width: 220px

    # 3) 파일 상세 보기 모달
    modals:
      - path: file-detail-:file
        width: 800px
        title: 파일 내용 보기
        blocks:
          - type: http
            name: 파일 데이터
            method: GET
            fetchFn: |
              try {
                const base = API || 'http://localhost:9500'
                const url = new URL(`${base}/local/import/read`)
                url.searchParams.set('file', file)

                const r = await fetch(url)
                if (!r.ok) throw new Error(`HTTP ${r.status}`)
                const data = await r.json().catch(() => ({}))

                return data.rows || []
              } catch (err) {
                $toast(`파일을 불러오지 못했습니다: ${String(err?.message || err)}`, { type: 'error' })
                return []
              }
            columns:
              name: { width: 300px }
              unit: { width: 120px }
              startDate:
                width: 140px
                formatFn: date
              endDate:
                width: 140px
                formatFn: date
            params:
              - key: file
                valueFromRow: file
            showDownload: csv xlsx

이런 상황에 유용합니다

  • 파트너사 데이터를 업로드 후 바로 검증해야 할 때
  • 사내에서 CSV → JSON 변환 플로우를 빠르게 구성해야 할 때
  • 로컬 API만 있는 환경에서 UI를 즉시 띄워보고 싶을 때

서버 코드도 빠르게 만들고 싶다면

위 YAML을 기반으로 테스트용 서버 코드까지 빠르게 만들고 싶다면,
LLM에게 아래처럼 요청해보세요.

환경:
- Node.js 20
- "type": "module" (ESM)
- Express

요청:
아래 YAML 스펙에 맞는 API 서버 코드를 작성해주세요.
(프론트엔드가 이 YAML을 기반으로 API를 호출합니다)

<여기에 YAML 코드 붙여넣기>

마무리하며

이 예제는 엑셀·CSV 데이터를 업로드 → 저장 → 조회 → 미리보기까지
한 번에 연결하는 완결된 흐름을 보여줍니다.

서버는 단지 API만 제공하면 됩니다.
UI 구성은 셀렉트 어드민이 자동으로 처리합니다.

복잡한 설정 없이 바로 복사해 실행해 보세요.
운영툴의 첫 페이지를, 오늘 안에 완성할 수 있습니다.

Read more

주문 데이터 기반으로 티켓 관리 시스템 만들어보기

주문 데이터 기반으로 티켓 관리 시스템 만들어보기

고객을 응대할때 같은 질문을 반복하게 됩니다. 이 고객이 무엇을 샀는지, 지금 주문 상태는 어떤지, 이전에도 같은 이슈가 있었는지. 문의를 처리하는 기존 방법들부터, 주문 데이터를 기준으로 티켓을 정리하면 무엇이 달라지는지를 다룹니다. 복잡한 자동화가 아니라, 검색과 처리에 집중한 최소한의 시작 방법을 정리했습니다.

By Hakbeom Kim