AgentStack
SKILL verified MIT Self-run

Work Coding Web Scaffold

skill-jonayeon-claude-code-skills-work-coding-web-scaffold · by JoNaYeon

Use this skill when the user wants to create a new web project from scratch with a title, role, and features. This skill generates a complete Flask + JS/HTML/CSS web application with GitHub repo, modular folder structure, AWS-ready config, and local dev support. Triggers on: 'make a website', 'create a web app', 'build a web project', 'scaffold a web app', '웹 만들어줘', '사이트 만들어줘', '웹 프로젝트 생성', '웹앱 만…

No reviews yet
0 installs
7 views
0.0% view→install

Install

$ agentstack add skill-jonayeon-claude-code-skills-work-coding-web-scaffold

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Work Coding Web Scaffold? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Web Scaffold — 웹 프로젝트 자동 생성

목적

사용자가 제목, 역할, 기능을 알려주면, Flask + JavaScript/HTML/CSS 기반의 완성된 웹 프로젝트를 자동으로 구성하고 GitHub 레포에 업로드합니다.

기술 스택 (고정)

  • Backend: Python 3.11+ / Flask
  • Frontend: Vanilla JavaScript + HTML5 + CSS3
  • Template Engine: Jinja2
  • Config: JSON 기반 (config.json)
  • Deploy Target: AWS (EC2/ECS) — 로컬에서도 즉시 실행 가능

지침

Phase 1 — 요구사항 수집

사용자에게 다음 3가지를 확인합니다:

1. 제목 (Title) > 프로젝트 이름이 무엇인가요?

2. 역할 (Role) > 이 웹의 주요 역할은 무엇인가요? > (예: 대시보드, 관리 페이지, 랜딩 페이지, CRUD 앱, API 서버 + 프론트 등)

3. 기능 목록 (Features) > 필요한 기능을 나열해 주세요. > (예: 로그인, 데이터 조회, 파일 업로드, 차트 표시, 검색 등)

추가 확인 (선택):

  • DB 사용 여부 및 종류 (SQLite / PostgreSQL / DynamoDB 등)
  • 인증 필요 여부 (로그인 / 공개)
  • AWS 서비스 연동 계획 (S3, RDS, SES 등)

Phase 2 — 프로젝트 생성

2-1. GitHub 레포 생성

프로젝트명을 kebab-case로 변환하여 레포를 생성합니다.

mkdir 
cd 
git init
2-2. 폴더 구조 생성

아래 구조를 반드시 따릅니다:

/
├── config.json              # 환경 설정 (local/dev/prod)
├── app.py                   # Flask 앱 진입점
├── requirements.txt         # Python 의존성
├── .env.example             # 환경변수 템플릿
├── .gitignore
├── README.md
├── data/                    # 데이터 파일 (CSV, JSON 등)
│   └── .gitkeep
├── logs/                    # 로그 파일
│   └── .gitkeep
├── models/                  # 데이터 모델 / ORM 정의
│   ├── __init__.py
│   └── base_model.py
├── repositories/            # 데이터 접근 계층 (Repository 패턴)
│   ├── __init__.py
│   └── base_repo.py
├── routes/                  # Flask Blueprint 라우트
│   ├── __init__.py
│   └── main_routes.py
├── scripts/                 # 유틸리티 스크립트 (DB 초기화, 마이그레이션 등)
│   └── .gitkeep
├── services/                # 비즈니스 로직 계층
│   ├── __init__.py
│   └── base_service.py
├── static/                  # 정적 파일
│   ├── css/
│   │   └── styles.css
│   ├── js/
│   │   └── scripts.js
│   └── templates/           # Jinja2 HTML 템플릿
│       ├── base.html        # 레이아웃 마스터 (모든 페이지 상속)
│       ├── components/      # 재사용 컴포넌트 (header, footer, sidebar 등)
│       │   ├── header.html
│       │   ├── footer.html
│       │   └── sidebar.html
│       ├── pages/           # 개별 페이지
│       │   └── index.html
│       └── macros/          # Jinja2 매크로 (폼, 버튼, 테이블 등 재사용 블록)
│           └── forms.html
└── tests/                   # 테스트
    └── .gitkeep

Phase 3 — 핵심 파일 작성

3-1. config.json — 환경별 설정
{
  "app": {
    "title": "",
    "role": "",
    "version": "0.1.0"
  },
  "environments": {
    "local": {
      "debug": true,
      "host": "127.0.0.1",
      "port": 5000,
      "database": {
        "type": "sqlite",
        "path": "data/local.db"
      },
      "static_url": "/static",
      "log_level": "DEBUG"
    },
    "dev": {
      "debug": true,
      "host": "0.0.0.0",
      "port": 5000,
      "database": {
        "type": "postgresql",
        "host": "${DB_HOST}",
        "port": 5432,
        "name": "${DB_NAME}",
        "user": "${DB_USER}",
        "password": "${DB_PASSWORD}"
      },
      "aws": {
        "region": "${AWS_REGION}",
        "s3_bucket": "${S3_BUCKET}"
      },
      "log_level": "INFO"
    },
    "prod": {
      "debug": false,
      "host": "0.0.0.0",
      "port": 8000,
      "database": {
        "type": "postgresql",
        "host": "${DB_HOST}",
        "port": 5432,
        "name": "${DB_NAME}",
        "user": "${DB_USER}",
        "password": "${DB_PASSWORD}"
      },
      "aws": {
        "region": "${AWS_REGION}",
        "s3_bucket": "${S3_BUCKET}"
      },
      "log_level": "WARNING"
    }
  }
}
3-2. app.py — Flask 진입점
import json
import os
import logging
from flask import Flask

def load_config(env=None):
    """config.json에서 환경별 설정 로드"""
    env = env or os.getenv("FLASK_ENV", "local")
    with open("config.json", "r", encoding="utf-8") as f:
        config = json.load(f)
    return {**config["app"], **config["environments"].get(env, {})}

def create_app(env=None):
    """Flask 앱 팩토리"""
    config = load_config(env)

    app = Flask(
        __name__,
        static_folder="static",
        template_folder="static/templates"
    )
    app.config.update(config)
    app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret-change-me")

    # 로깅 설정
    logging.basicConfig(
        level=getattr(logging, config.get("log_level", "INFO")),
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        handlers=[
            logging.StreamHandler(),
            logging.FileHandler("logs/app.log", encoding="utf-8")
        ]
    )

    # Blueprint 등록
    from routes.main_routes import main_bp
    app.register_blueprint(main_bp)

    return app

if __name__ == "__main__":
    app = create_app()
    app.run(
        host=app.config.get("host", "127.0.0.1"),
        port=app.config.get("port", 5000),
        debug=app.config.get("debug", True)
    )
3-3. routes/main_routes.py — 메인 라우트
from flask import Blueprint, render_template

main_bp = Blueprint("main", __name__)

@main_bp.route("/")
def index():
    return render_template("pages/index.html", title="Home")

@main_bp.route("/health")
def health():
    return {"status": "ok"}, 200

사용자가 요청한 기능에 따라 추가 라우트를 생성합니다. 기능이 많으면 Blueprint를 분리합니다:

  • routes/auth_routes.py — 인증 관련
  • routes/api_routes.py — API 엔드포인트
  • routes/_routes.py — 기능별 분리
3-4. repositories/base_repo.py — 데이터 접근 베이스
import json
import os
import sqlite3
from abc import ABC, abstractmethod

class BaseRepository(ABC):
    """Repository 패턴 베이스 클래스"""

    @abstractmethod
    def find_all(self):
        pass

    @abstractmethod
    def find_by_id(self, id):
        pass

    @abstractmethod
    def create(self, data):
        pass

    @abstractmethod
    def update(self, id, data):
        pass

    @abstractmethod
    def delete(self, id):
        pass

class SQLiteRepository(BaseRepository):
    """SQLite 기반 Repository (로컬 개발용)"""

    def __init__(self, db_path="data/local.db", table_name="items"):
        self.db_path = db_path
        self.table_name = table_name

    def _get_connection(self):
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        return conn

    def find_all(self):
        conn = self._get_connection()
        cursor = conn.execute(f"SELECT * FROM {self.table_name}")
        rows = [dict(row) for row in cursor.fetchall()]
        conn.close()
        return rows

    def find_by_id(self, id):
        conn = self._get_connection()
        cursor = conn.execute(
            f"SELECT * FROM {self.table_name} WHERE id = ?", (id,)
        )
        row = cursor.fetchone()
        conn.close()
        return dict(row) if row else None

    def create(self, data):
        conn = self._get_connection()
        keys = ", ".join(data.keys())
        placeholders = ", ".join(["?"] * len(data))
        conn.execute(
            f"INSERT INTO {self.table_name} ({keys}) VALUES ({placeholders})",
            list(data.values())
        )
        conn.commit()
        conn.close()

    def update(self, id, data):
        conn = self._get_connection()
        set_clause = ", ".join([f"{k} = ?" for k in data.keys()])
        conn.execute(
            f"UPDATE {self.table_name} SET {set_clause} WHERE id = ?",
            list(data.values()) + [id]
        )
        conn.commit()
        conn.close()

    def delete(self, id):
        conn = self._get_connection()
        conn.execute(
            f"DELETE FROM {self.table_name} WHERE id = ?", (id,)
        )
        conn.commit()
        conn.close()

AWS 환경에서는 이 베이스를 상속하여 DynamoDBRepository, PostgreSQLRepository 등을 구현합니다.

3-5. static/templates/base.html — 마스터 레이아웃

    
    
    {% block title %}{{ config.get('title', 'App') }}{% endblock %}

    
    

    
    {% block extra_css %}{% endblock %}

    {% include 'components/header.html' %}

    
        {% block sidebar %}{% endblock %}

        
            {% with messages = get_flashed_messages(with_categories=true) %}
                {% if messages %}
                    {% for category, message in messages %}
                        {{ message }}
                    {% endfor %}
                {% endif %}
            {% endwith %}

            {% block content %}{% endblock %}
        
    

    {% include 'components/footer.html' %}

    
    

    
    {% block extra_js %}{% endblock %}
3-6. static/templates/pages/index.html
{% extends 'base.html' %}

{% block title %}Home — {{ config.get('title', 'App') }}{% endblock %}

{% block content %}

    {{ config.get('title', 'Welcome') }}
    {{ config.get('role', '') }}

    

{% endblock %}
3-7. static/templates/components/ — 재사용 컴포넌트

header.html


    
        {{ config.get('title', 'App') }}
    
    
        {% block nav_items %}
        Home
        {% endblock %}
    

footer.html


    © {{ now.year if now else '2026' }} {{ config.get('title', 'App') }}

sidebar.html


    {% block sidebar_content %}
    
        Home
    
    {% endblock %}
3-8. static/templates/macros/forms.html — 재사용 매크로
{# 텍스트 입력 필드 #}
{% macro input(name, label, type="text", value="", required=false, placeholder="") %}

    {{ label }}
    

{% endmacro %}

{# 셀렉트 드롭다운 #}
{% macro select(name, label, options, selected="", required=false) %}

    {{ label }}
    
        선택하세요
        {% for opt in options %}
        
            {{ opt.label }}
        
        {% endfor %}
    

{% endmacro %}

{# 제출 버튼 #}
{% macro submit_button(text="저장", variant="primary") %}
{{ text }}
{% endmacro %}

{# 데이터 테이블 #}
{% macro data_table(headers, rows, id="data-table") %}

    
        
            
                {% for h in headers %}
                {{ h }}
                {% endfor %}
            
        
        
            {% for row in rows %}
            
                {% for cell in row %}
                {{ cell }}
                {% endfor %}
            
            {% endfor %}
        
    

{% endmacro %}
3-9. static/css/styles.css — 모듈화된 CSS
/* ============================================
   CSS Custom Properties (Design Tokens)
   모든 색상, 크기, 간격을 변수로 관리하여
   테마 변경 및 확장이 용이하도록 구성
   ============================================ */
:root {
    /* Colors */
    --color-primary: #2563eb;
    --color-primary-hover: #1d4ed8;
    --color-secondary: #64748b;
    --color-success: #16a34a;
    --color-warning: #d97706;
    --color-danger: #dc2626;
    --color-bg: #ffffff;
    --color-bg-secondary: #f8fafc;
    --color-text: #1e293b;
    --color-text-secondary: #64748b;
    --color-border: #e2e8f0;

    /* Typography */
    --font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
    --font-size-xs: 0.75rem;
    --font-size-sm: 0.875rem;
    --font-size-base: 1rem;
    --font-size-lg: 1.125rem;
    --font-size-xl: 1.25rem;
    --font-size-2xl: 1.5rem;
    --font-size-3xl: 1.875rem;

    /* Spacing */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-6: 1.5rem;
    --space-8: 2rem;
    --space-12: 3rem;

    /* Layout */
    --sidebar-width: 240px;
    --header-height: 60px;
    --content-max-width: 1200px;
    --border-radius: 6px;

    /* Shadows */
    --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
    --shadow-md: 0 4px 6px rgba(0,0,0,0.07);
}

/* ============================================
   Reset & Base
   ============================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; }
body {
    font-family: var(--font-family);
    color: var(--color-text);
    background: var(--color-bg);
    line-height: 1.6;
    min-height: 100vh;
}
a { color: var(--color-primary); text-decoration: none; }
a:hover { color: var(--color-primary-hover); }

/* ============================================
   Layout
   ============================================ */
.layout {
    display: flex;
    min-height: calc(100vh - var(--header-height));
}
.content {
    flex: 1;
    max-width: var(--content-max-width);
    margin: 0 auto;
    padding: var(--space-8);
}

/* ============================================
   Header
   ============================================ */
.header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: var(--header-height);
    padding: 0 var(--space-8);
    background: var(--color-bg);
    border-bottom: 1px solid var(--color-border);
    position: sticky;
    top: 0;
    z-index: 100;
}
.header__logo a { font-size: var(--font-size-xl); font-weight: 700; color: var(--color-text); }
.header__nav { display: flex; gap: var(--space-6); }
.header__link { color: var(--color-text-secondary); font-size: var(--font-size-sm); }
.header__link:hover { color: var(--color-primary); }

/* ============================================
   Sidebar
   ============================================ */
.sidebar {
    width: var(--sidebar-width);
    background: var(--color-bg-secondary);
    border-right: 1px solid var(--color-border);
    padding: var(--space-6);
    flex-shrink: 0;
}
.sidebar__nav { display: flex; flex-direction: column; gap: var(--space-2); }
.sidebar__link {
    display: block;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--border-radius);
    color: var(--color-text-secondary);
    font-size: var(--font-size-sm);
}
.sidebar__link:hover { background: var(--color-border); color: var(--color-text); }

/* ============================================
   Footer
   ============================================ */
.footer {
    padding: var(--space-6) var(--space-8);
    text-align: center;
    color: var(--color-text-secondary);
    font-size: var(--font-size-sm);
    border-top: 1px solid var(--color-border);
}

/* ============================================
   Components: Buttons
   ============================================ */
.btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border: none;
    border-radius: var(--border-radius);
    font-size: var(--font-size-sm);
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s;
}
.btn--primary { background: var(--color-primary); color: #fff; }
.btn--primary:hover { background: var(--color-primary-hover); }
.btn--secondary { background: var(--color-bg-secondary); color: var(--color-text); border: 1px solid var(--color-border); }
.btn--danger { background: var(--color-danger); color: #fff; }

/* ============================================
   Components: Forms
   ============================================ */
.form-group { margin-bottom: var(--space-4); }
.form-label { display: block; margin-bottom: var(--space-1); font-size: var(--font-size-sm); font-weight: 500; }
.form-input, .form-select {
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius);
    font-size: var(--font-size-base);
}
.form-input:focus, .form-select:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}

/* ============================================
   Components: Table
   ============================================ */
.table-wrapper { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td {
    padding: var(--space-3) var(--space-4);
    text-align: left;
    border-bottom: 1px solid var(--color-border);
}
.table th { font-size: var(--font-size-sm); font-weight: 600; background: var(--color-bg-secondary); }
.table tr:hover td { background: var(--color-bg-secondary); }

/* ============================================
   Components: Alert
   ============================================ */
.alert {
    padding: var(--space-3) var(--space-4);
    border-radius: var(--border-radius);
    margin-bottom: var(--space-4);
    font-size: v

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [JoNaYeon](https://github.com/JoNaYeon)
- **Source:** [JoNaYeon/claude-code-skills](https://github.com/JoNaYeon/claude-code-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.