Install
$ agentstack add skill-jonayeon-claude-code-skills-work-coding-web-scaffold ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
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.
Write a review
Versions
- v0.1.0 Imported from the upstream source.