100줄 YAML로 구성하는 QR 코드 생성기

UI는 셀렉트 어드민 YAML, 서버는 LLM 기반 스캐폴딩으로 빠르게 구축하는 패턴

100줄 YAML로 구성하는 QR 코드 생성기

내부용 유틸리티는 대개 단순합니다.
하지만 프론트엔드 화면·상태 관리·테이블·모달 구성 등에 시간을 쓰다 보면 실제 목적에 비해 개발 비용이 과도해지는 경우가 많습니다.

아래 예제는 프론트엔드 없이 YAML만으로 UI·동작을 정의하고,
백엔드 서버는 해당 YAML을 기반으로 LLM에게 스캐폴딩하도록 하는 방식을 보여줍니다.


구성 개요

  • YAML은 UI 구조 + API I/O를 동시에 정의합니다.
  • 서버는 YAML을 기준으로:
    • POST: /local/qr/save-json
    • GET: /local/qr/list
    • GET: /local/qr/read?id=...
    • GET: /local/qr/download?id=...
      등의 엔드포인트를 제공하면 됩니다.
  • UI는 셀렉트 어드민이 자동으로 렌더링합니다.
  • 서버 구현은 초기에는 LLM에게 생성시키고, 이후 원하는 형태로 확장하면 됩니다.

전체 YAML 스펙 (UI + API 요청 스펙)

blocks:
  # 1) QR 생성 폼
  - type: http
    name: QR 생성
    method: POST
    display: form
    formOptions: { firstLabelWidth: 200px, width: 800px }
    fetchFn: |
      try {
        const base = API || 'http://localhost:9500'
        const payload = {
          text: String(text || '').trim(),
          size: Number(size || 256),
          margin: Number(margin || 2),
          ecLevel: String(ecLevel || 'M'),
          fmt: String(fmt || 'png'),
        }
        if (!payload.text) return { error: '텍스트를 입력하세요.' }
        const r = await fetch(`${base}/local/qr/save-json`, {
          method: 'POST', headers: {'Content-Type':'application/json'},
          body: JSON.stringify({ payload })
        })
        const data = await r.json().catch(() => ({}))
        return { result: data }
      } catch (err) { return { error: String(err) } }
    params:
      - { key: text, label: 텍스트/URL, placeholder: QR에 넣을 내용 }
      - { key: size, label: 사이즈(px), defaultValue: 256 }
      - { key: margin, label: 여백(px), defaultValue: 2 }
      - key: ecLevel
        label: 오류 보정
        dropdown: [L, M, Q, H]
        defaultValue: M
      - key: fmt
        label: 포맷
        dropdown: [png, svg]
        defaultValue: png

  # 2) QR 생성 내역 리스트
  - type: http
    name: 생성 내역
    method: GET
    fetchFn: |
      try {
        const r = await fetch((API || 'http://localhost:9500') + '/local/qr/list')
        const data = await r.json().catch(() => ({}))
        return (data.rows || []).map(row => ({
          ...row, label: row.text?.slice(0,60) || ''
        }))
      } catch (err) { return [] }
    searchOptions: { enabled: true }
    columns:
      label: { width: 380px, openModal: preview-:id }
      imageUrl:
        width: 180px
        template: |
          <img src="{{imageUrl}}" style="width:120px;height:120px;object-fit:contain;border:1px solid #eee;border-radius:8px;padding:4px;" />
      ecLevel: { width: 80px }
      fmt: { width: 70px }
      createdAt: { width: 180px }
      ' ':
        append: true
        template: |
          <a href="http://localhost:9500/local/qr/download?id={{id}}" class="bg-slate-100 p-2">다운로드</a>

    # 3) 미리보기 모달
    modals:
      - path: preview-:id
        width: 720px
        title: QR 미리보기
        blocks:
          - type: http
            method: GET
            fetchFn: |
              try {
                const base = API || 'http://localhost:9500'
                const url = new URL(`${base}/local/qr/read`)
                url.searchParams.set('id', id)
                const r = await fetch(url)
                const data = await r.json().catch(()=>({}))
                return [data.row || {}]
              } catch(e){ return [] }
            params:
              - { key: id, valueFromRow: id }
            columns:
              text: { width: 520px }
              imageUrl:
                template: |
                  <div style="display:flex;gap:20px;align-items:center;">
                    <img src="{{imageUrl}}" style="width:260px;height:260px;object-fit:contain;border:1px solid #eee;border-radius:12px;padding:8px;" />
                    <a href="{{imageUrl}}" target="_blank" class="no-underline bg-slate-500/5 rounded-lg p-2 flex items-center">
                      <span class="mdi mdi-download mr-1"></span>
                      <span>원본 다운로드</span>
                    </a>
                  </div>

이 YAML이 곧:

  • UI 구성
  • API 요청 방식
  • 데이터 구조
  • 모달/리스트/다운로드 흐름

을 정의합니다.


서버는 어떻게 구성하나?

서버는 별도의 UI 작업 없이 다음 역할만 충족하면 됩니다.

  • save-json → QR 생성 요청을 받아 JSON(또는 DB)에 저장
  • list → 생성된 QR 목록 반환
  • read → 단건 조회
  • download → QR 이미지 생성 및 응답

초기 개발 속도를 높이고 싶다면,
서버 초안은 LLM에게 YAML을 그대로 넘겨 생성할 수도 있습니다.

예시 요청

아래 YAML 스펙에 맞는 Node.js + Express 서버 코드를 작성해주세요.

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

<여기에 위 YAML 전체 붙여넣기>

이 방식은 “코드를 대신 작성해준다”기보다
“스캐폴딩을 자동화하고, 필요한 부분만 개발자가 덧붙인다”에 가깝습니다.

이후 원하는 스토리지 형태(JSON/SQLite/Postgres)로 쉽게 전환할 수 있습니다.


마무리

위 QR 생성기 예제는 작은 구성입니다.
이외에 여러 내부용 도구에도 동일하게 적용할 수 있습니다.

  • URL 단축 툴
  • 내부용 토큰 뷰어
  • 간단한 쿠폰/코드 발급기
  • 파일 처리 도구
  • 운영/테스트용 작은 CRUD 도구

등은 모두 이 패턴으로 빠르게 구축할 수 있습니다. 서버는 어떤 언어로 구현해도 상관 없습니다.

프론트 개발 및 관리, 배포, 운영에 고민이 있으시다면
셀렉트 어드민을 통해 지금 필요한 도구를 만들어보세요.

개발자 문서 보기:

Introduction - Select Admin Docs
Build admin, dashboard, and tools directly from your backend.

Read more

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

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

By LEE JINHYUK

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

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

By LEE JINHYUK