엑셀·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

[팀블로그] 개편이야기-2

어드민 화면 제작 경험에서 고려한점 셀렉트어드민을 통해 사용자는 관리자 화면을 제공함 * 관리자 화면의 관리자 화면(편집,설정)이 존재하는 상황 * 어드민 목록, 어드민 화면, 어드민 설정 사이의 흐름이 불편함 (고객 혼란, 주소 공유 어려움등 발생) * 해결하기 위해 레이아웃 일치 (선택 메뉴, 어드민 화면, 어드민 설정) 여러가지 디자인 요소가 섞여있어서 정리가

By LEE JINHYUK

[팀블로그] 개편이야기-1

개편이야기-1 [왜 Front로 다시 구상했는지?] 셀렉트 어드민은 한주도 멈추지 않고 약 200주 연속으로 점진적 개선을 이어옴 * 셀렉트 어드민 기존 서비스는 2021년 가을부터 운영중 * 2022년 유료화 이후 많은 개선 * 2023년 어드민 넘어서 대시보드, 파트너센터까지 확장 * 2024년 대기업, 중견기업 요구사항 충족하면서 고도화 셀렉트 어드민은 확장해왔지만 기본 사용법은 그대로 머물러있다고 생각 * 편집 환경의

By LEE JINHYUK
부족하게 만들 용기 - 개발자의 딜레마

부족하게 만들 용기 - 개발자의 딜레마

새로운 기능과 제품을 만들때 고민되는 지점이 있다. * 누가 쓸지, 어떤 문제를 겪고 있는지 * 어떤 기능과 결과물이 필수인지 * 얼마나 완성도있게 만들지 * 언제까지 만들지 * 어떻게 사용을 편하게 할지, 사용법이 쉬운지 그러나 기획 의도와 방향을 정하고 프로젝트를 진행하면 늘 변수가 생긴다. 코딩, 개발 시간은 예측하기 어렵고 수 많은 제품 디자인적 의사결정이 필요하고 복잡도

By LEE JINHYUK