first
This commit is contained in:
46
README.md
Normal file
46
README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Key Generator
|
||||
|
||||
JWT Secret, API Key, Operation Key 등 다양한 보안 키를 생성하는 Python 데스크톱 애플리케이션.
|
||||
|
||||
## 실행
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 지원 키 타입
|
||||
|
||||
| 타입 | 비트 | 포맷 |
|
||||
|------|------|------|
|
||||
| JWT Secret (HS256) | 256-bit | Hex |
|
||||
| JWT Secret (HS384) | 384-bit | Hex |
|
||||
| JWT Secret (HS512) | 512-bit | Hex |
|
||||
| JWT Secret (Base64URL) | 256-bit | Base64URL |
|
||||
| API Key `sk-...` | 256-bit | Base64URL |
|
||||
| Operation Key `ops-...` | 192-bit | Base64URL |
|
||||
| Random Hex 256-bit | 256-bit | Hex |
|
||||
| Random Hex 512-bit | 512-bit | Hex |
|
||||
| Alphanumeric | 256-bit | A-Za-z0-9 |
|
||||
| UUID v4 | 128-bit | UUID |
|
||||
| Custom | 자유 | 직접 선택 |
|
||||
|
||||
---
|
||||
|
||||
## 기능
|
||||
|
||||
- **Generate** 버튼 또는 `Ctrl+Enter`로 즉시 생성
|
||||
- **Copy** 버튼으로 클립보드 복사
|
||||
- **대량 생성** 체크박스 활성화 시 최대 20개 한번에 생성, **Copy All**로 전체 복사
|
||||
- **Custom** 타입 선택 시 바이트 수(8~512)와 출력 포맷 직접 지정
|
||||
- 모든 키는 Python `secrets` 모듈(암호학적 난수) 사용
|
||||
|
||||
---
|
||||
|
||||
## 의존성
|
||||
|
||||
- Python 3.8+
|
||||
- customtkinter 5.2+
|
||||
- pyperclip 1.9+
|
||||
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-312.pyc
Normal file
BIN
__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
161
app.py
Normal file
161
app.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import secrets
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
import uuid
|
||||
import os
|
||||
import string
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
KEY_CONFIGS = {
|
||||
"jwt_hs256": {
|
||||
"label": "JWT Secret (HS256)",
|
||||
"description": "HMAC-SHA256용 JWT 시크릿 키",
|
||||
"bytes": 32,
|
||||
"format": "hex",
|
||||
},
|
||||
"jwt_hs384": {
|
||||
"label": "JWT Secret (HS384)",
|
||||
"description": "HMAC-SHA384용 JWT 시크릿 키",
|
||||
"bytes": 48,
|
||||
"format": "hex",
|
||||
},
|
||||
"jwt_hs512": {
|
||||
"label": "JWT Secret (HS512)",
|
||||
"description": "HMAC-SHA512용 JWT 시크릿 키",
|
||||
"bytes": 64,
|
||||
"format": "hex",
|
||||
},
|
||||
"jwt_base64": {
|
||||
"label": "JWT Secret (Base64URL)",
|
||||
"description": "Base64URL 인코딩 JWT 시크릿 (256-bit)",
|
||||
"bytes": 32,
|
||||
"format": "base64url",
|
||||
},
|
||||
"api_key": {
|
||||
"label": "API Key",
|
||||
"description": "sk- 접두사 포함 API 키",
|
||||
"bytes": 32,
|
||||
"format": "api_key",
|
||||
},
|
||||
"op_key": {
|
||||
"label": "Operation Key",
|
||||
"description": "ops- 접두사 포함 운영 키",
|
||||
"bytes": 24,
|
||||
"format": "op_key",
|
||||
},
|
||||
"hex_256": {
|
||||
"label": "Random Hex (256-bit)",
|
||||
"description": "순수 랜덤 Hex 문자열",
|
||||
"bytes": 32,
|
||||
"format": "hex",
|
||||
},
|
||||
"hex_512": {
|
||||
"label": "Random Hex (512-bit)",
|
||||
"description": "순수 랜덤 Hex 문자열 (512-bit)",
|
||||
"bytes": 64,
|
||||
"format": "hex",
|
||||
},
|
||||
"alphanumeric": {
|
||||
"label": "Alphanumeric Key",
|
||||
"description": "영숫자 조합 랜덤 키",
|
||||
"bytes": 32,
|
||||
"format": "alphanumeric",
|
||||
},
|
||||
"uuid_v4": {
|
||||
"label": "UUID v4",
|
||||
"description": "표준 UUID v4",
|
||||
"bytes": 16,
|
||||
"format": "uuid",
|
||||
},
|
||||
"custom": {
|
||||
"label": "Custom Length",
|
||||
"description": "직접 바이트 수 지정",
|
||||
"bytes": None,
|
||||
"format": "hex",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def generate_key(key_type: str, custom_bytes: int = 32, custom_format: str = "hex") -> dict:
|
||||
config = KEY_CONFIGS.get(key_type)
|
||||
if not config:
|
||||
raise ValueError(f"Unknown key type: {key_type}")
|
||||
|
||||
byte_length = config["bytes"] if config["bytes"] is not None else custom_bytes
|
||||
fmt = custom_format if key_type == "custom" else config["format"]
|
||||
|
||||
raw = secrets.token_bytes(byte_length)
|
||||
|
||||
if fmt == "hex":
|
||||
key = raw.hex()
|
||||
elif fmt == "base64url":
|
||||
key = base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
elif fmt == "api_key":
|
||||
encoded = base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
key = f"sk-{encoded}"
|
||||
elif fmt == "op_key":
|
||||
encoded = base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
key = f"ops-{encoded}"
|
||||
elif fmt == "alphanumeric":
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
key = "".join(secrets.choice(alphabet) for _ in range(byte_length))
|
||||
elif fmt == "uuid":
|
||||
key = str(uuid.uuid4())
|
||||
else:
|
||||
key = raw.hex()
|
||||
|
||||
return {
|
||||
"key": key,
|
||||
"type": key_type,
|
||||
"label": config["label"],
|
||||
"bits": byte_length * 8,
|
||||
"length": len(key),
|
||||
"algorithm": fmt.upper(),
|
||||
}
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html", key_configs=KEY_CONFIGS)
|
||||
|
||||
|
||||
@app.route("/api/generate", methods=["POST"])
|
||||
def api_generate():
|
||||
data = request.get_json() or {}
|
||||
key_type = data.get("type", "jwt_hs256")
|
||||
custom_bytes = int(data.get("custom_bytes", 32))
|
||||
custom_format = data.get("custom_format", "hex")
|
||||
|
||||
if custom_bytes < 8:
|
||||
custom_bytes = 8
|
||||
elif custom_bytes > 512:
|
||||
custom_bytes = 512
|
||||
|
||||
try:
|
||||
result = generate_key(key_type, custom_bytes, custom_format)
|
||||
return jsonify({"success": True, "data": result})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/generate/bulk", methods=["POST"])
|
||||
def api_generate_bulk():
|
||||
data = request.get_json() or {}
|
||||
key_type = data.get("type", "jwt_hs256")
|
||||
count = min(int(data.get("count", 5)), 20)
|
||||
custom_bytes = int(data.get("custom_bytes", 32))
|
||||
custom_format = data.get("custom_format", "hex")
|
||||
|
||||
try:
|
||||
results = [generate_key(key_type, custom_bytes, custom_format) for _ in range(count)]
|
||||
return jsonify({"success": True, "data": results})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, host="0.0.0.0", port=5050)
|
||||
378
main.py
Normal file
378
main.py
Normal file
@@ -0,0 +1,378 @@
|
||||
import secrets
|
||||
import base64
|
||||
import uuid
|
||||
import string
|
||||
import pyperclip
|
||||
import customtkinter as ctk
|
||||
from tkinter import messagebox
|
||||
|
||||
ctk.set_appearance_mode("dark")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
KEY_TYPES = [
|
||||
("JWT Secret (HS256)", "jwt_hs256", "32 bytes · Hex", 32, "hex"),
|
||||
("JWT Secret (HS384)", "jwt_hs384", "48 bytes · Hex", 48, "hex"),
|
||||
("JWT Secret (HS512)", "jwt_hs512", "64 bytes · Hex", 64, "hex"),
|
||||
("JWT Secret (Base64)", "jwt_base64", "32 bytes · Base64URL", 32, "base64url"),
|
||||
("API Key (sk-...)", "api_key", "32 bytes · Base64URL", 32, "api_key"),
|
||||
("Operation Key (ops-)", "op_key", "24 bytes · Base64URL", 24, "op_key"),
|
||||
("Random Hex 256-bit", "hex_256", "32 bytes · Hex", 32, "hex"),
|
||||
("Random Hex 512-bit", "hex_512", "64 bytes · Hex", 64, "hex"),
|
||||
("Alphanumeric", "alphanumeric","32 chars · A-Za-z0-9", 32, "alphanumeric"),
|
||||
("UUID v4", "uuid_v4", "128-bit · Standard UUID", 16, "uuid"),
|
||||
("Custom", "custom", "직접 설정", 32, "hex"),
|
||||
]
|
||||
|
||||
FORMATS = ["hex", "base64url", "alphanumeric", "api_key (sk-)", "op_key (ops-)"]
|
||||
FORMAT_MAP = {
|
||||
"hex": "hex",
|
||||
"base64url": "base64url",
|
||||
"alphanumeric": "alphanumeric",
|
||||
"api_key (sk-)": "api_key",
|
||||
"op_key (ops-)": "op_key",
|
||||
}
|
||||
|
||||
|
||||
def generate_key(byte_length: int, fmt: str) -> str:
|
||||
raw = secrets.token_bytes(byte_length)
|
||||
if fmt == "hex":
|
||||
return raw.hex()
|
||||
elif fmt == "base64url":
|
||||
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
elif fmt == "api_key":
|
||||
return "sk-" + base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
elif fmt == "op_key":
|
||||
return "ops-" + base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
elif fmt == "alphanumeric":
|
||||
alpha = string.ascii_letters + string.digits
|
||||
return "".join(secrets.choice(alpha) for _ in range(byte_length))
|
||||
elif fmt == "uuid":
|
||||
return str(uuid.uuid4())
|
||||
return raw.hex()
|
||||
|
||||
|
||||
class App(ctk.CTk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("Key Generator")
|
||||
self.geometry("760x700")
|
||||
self.resizable(False, False)
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self):
|
||||
# ── Title ──────────────────────────────────────────────
|
||||
title = ctk.CTkLabel(
|
||||
self, text=" Key Generator",
|
||||
font=ctk.CTkFont(size=22, weight="bold"),
|
||||
anchor="w",
|
||||
)
|
||||
title.pack(padx=28, pady=(24, 2), anchor="w")
|
||||
|
||||
sub = ctk.CTkLabel(
|
||||
self,
|
||||
text="JWT Secret · API Key · Operation Key · UUID · Random Hex",
|
||||
font=ctk.CTkFont(size=12),
|
||||
text_color="#8892a4",
|
||||
anchor="w",
|
||||
)
|
||||
sub.pack(padx=30, pady=(0, 16), anchor="w")
|
||||
|
||||
# ── Key Type ───────────────────────────────────────────
|
||||
self._section("키 타입")
|
||||
|
||||
self._type_var = ctk.StringVar(value="jwt_hs256")
|
||||
type_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
type_frame.pack(padx=28, pady=(4, 0), fill="x")
|
||||
|
||||
self._type_combo = ctk.CTkComboBox(
|
||||
type_frame,
|
||||
values=[t[0] for t in KEY_TYPES],
|
||||
width=340,
|
||||
height=36,
|
||||
font=ctk.CTkFont(size=13),
|
||||
command=self._on_type_change,
|
||||
)
|
||||
self._type_combo.set(KEY_TYPES[0][0])
|
||||
self._type_combo.pack(side="left")
|
||||
|
||||
self._desc_label = ctk.CTkLabel(
|
||||
type_frame,
|
||||
text=KEY_TYPES[0][2],
|
||||
font=ctk.CTkFont(size=11),
|
||||
text_color="#8892a4",
|
||||
)
|
||||
self._desc_label.pack(side="left", padx=(14, 0))
|
||||
|
||||
# ── Custom Options ─────────────────────────────────────
|
||||
self._custom_frame = ctk.CTkFrame(self, fg_color=("#1a1d27", "#1a1d27"), corner_radius=10)
|
||||
self._custom_frame.pack(padx=28, pady=(12, 0), fill="x")
|
||||
|
||||
ctk.CTkLabel(
|
||||
self._custom_frame, text="바이트 수",
|
||||
font=ctk.CTkFont(size=11), text_color="#8892a4",
|
||||
).grid(row=0, column=0, padx=(16, 0), pady=(12, 2), sticky="w")
|
||||
ctk.CTkLabel(
|
||||
self._custom_frame, text="출력 포맷",
|
||||
font=ctk.CTkFont(size=11), text_color="#8892a4",
|
||||
).grid(row=0, column=1, padx=(20, 0), pady=(12, 2), sticky="w")
|
||||
|
||||
self._bytes_entry = ctk.CTkEntry(
|
||||
self._custom_frame, width=100, height=34,
|
||||
font=ctk.CTkFont(size=13), placeholder_text="32",
|
||||
)
|
||||
self._bytes_entry.insert(0, "32")
|
||||
self._bytes_entry.grid(row=1, column=0, padx=(16, 0), pady=(0, 14), sticky="w")
|
||||
|
||||
self._fmt_combo = ctk.CTkComboBox(
|
||||
self._custom_frame, values=FORMATS,
|
||||
width=200, height=34, font=ctk.CTkFont(size=13),
|
||||
)
|
||||
self._fmt_combo.set("hex")
|
||||
self._fmt_combo.grid(row=1, column=1, padx=(20, 0), pady=(0, 14), sticky="w")
|
||||
|
||||
self._custom_frame.pack_forget() # hidden by default
|
||||
|
||||
# ── Bulk ───────────────────────────────────────────────
|
||||
self._section("옵션")
|
||||
|
||||
bulk_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
bulk_frame.pack(padx=28, pady=(4, 0), fill="x")
|
||||
|
||||
self._bulk_var = ctk.BooleanVar(value=False)
|
||||
bulk_chk = ctk.CTkCheckBox(
|
||||
bulk_frame, text="대량 생성",
|
||||
font=ctk.CTkFont(size=13),
|
||||
variable=self._bulk_var,
|
||||
command=self._on_bulk_toggle,
|
||||
)
|
||||
bulk_chk.pack(side="left")
|
||||
|
||||
self._bulk_label = ctk.CTkLabel(
|
||||
bulk_frame, text="개수",
|
||||
font=ctk.CTkFont(size=11), text_color="#8892a4",
|
||||
)
|
||||
self._bulk_count = ctk.CTkEntry(
|
||||
bulk_frame, width=64, height=30,
|
||||
font=ctk.CTkFont(size=13), placeholder_text="5",
|
||||
)
|
||||
self._bulk_count.insert(0, "5")
|
||||
# hidden initially
|
||||
|
||||
# ── Generate Button ────────────────────────────────────
|
||||
self._gen_btn = ctk.CTkButton(
|
||||
self,
|
||||
text="Generate",
|
||||
height=44,
|
||||
font=ctk.CTkFont(size=14, weight="bold"),
|
||||
corner_radius=10,
|
||||
command=self._on_generate,
|
||||
)
|
||||
self._gen_btn.pack(padx=28, pady=(20, 0), fill="x")
|
||||
|
||||
# ── Result ─────────────────────────────────────────────
|
||||
self._section("생성된 키")
|
||||
|
||||
# Single result
|
||||
self._result_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
self._result_frame.pack(padx=28, pady=(4, 0), fill="x")
|
||||
|
||||
self._key_box = ctk.CTkTextbox(
|
||||
self._result_frame,
|
||||
height=72,
|
||||
font=ctk.CTkFont(family="Consolas", size=12),
|
||||
fg_color=("#1a1d27", "#1a1d27"),
|
||||
text_color="#a5f3fc",
|
||||
corner_radius=10,
|
||||
wrap="word",
|
||||
state="disabled",
|
||||
)
|
||||
self._key_box.pack(fill="x")
|
||||
|
||||
# Meta row
|
||||
self._meta_label = ctk.CTkLabel(
|
||||
self._result_frame, text="",
|
||||
font=ctk.CTkFont(size=11), text_color="#8892a4", anchor="w",
|
||||
)
|
||||
self._meta_label.pack(pady=(6, 0), anchor="w")
|
||||
|
||||
# Copy button
|
||||
copy_row = ctk.CTkFrame(self._result_frame, fg_color="transparent")
|
||||
copy_row.pack(fill="x", pady=(8, 0))
|
||||
|
||||
self._copy_btn = ctk.CTkButton(
|
||||
copy_row,
|
||||
text="Copy",
|
||||
width=100, height=34,
|
||||
font=ctk.CTkFont(size=12),
|
||||
fg_color=("#22263a", "#22263a"),
|
||||
hover_color=("#2e3250", "#2e3250"),
|
||||
border_width=1,
|
||||
border_color="#2e3250",
|
||||
corner_radius=8,
|
||||
command=self._copy_single,
|
||||
)
|
||||
self._copy_btn.pack(side="right")
|
||||
|
||||
# Bulk result
|
||||
self._bulk_frame_result = ctk.CTkScrollableFrame(
|
||||
self, height=200,
|
||||
fg_color=("#1a1d27", "#1a1d27"),
|
||||
corner_radius=10,
|
||||
)
|
||||
self._bulk_frame_result.pack(padx=28, pady=(4, 0), fill="x")
|
||||
self._bulk_frame_result.pack_forget()
|
||||
|
||||
self._bulk_copy_all_btn = ctk.CTkButton(
|
||||
self,
|
||||
text="Copy All",
|
||||
height=34,
|
||||
font=ctk.CTkFont(size=12),
|
||||
fg_color=("#22263a", "#22263a"),
|
||||
hover_color=("#2e3250", "#2e3250"),
|
||||
border_width=1,
|
||||
border_color="#2e3250",
|
||||
corner_radius=8,
|
||||
command=self._copy_all,
|
||||
)
|
||||
|
||||
self._bulk_keys = []
|
||||
|
||||
# Keyboard shortcut
|
||||
self.bind("<Control-Return>", lambda e: self._on_generate())
|
||||
|
||||
def _section(self, text: str):
|
||||
lbl = ctk.CTkLabel(
|
||||
self, text=text.upper(),
|
||||
font=ctk.CTkFont(size=10, weight="bold"),
|
||||
text_color="#8892a4", anchor="w",
|
||||
)
|
||||
lbl.pack(padx=30, pady=(16, 0), anchor="w")
|
||||
|
||||
def _on_type_change(self, value: str):
|
||||
entry = next((t for t in KEY_TYPES if t[0] == value), None)
|
||||
if not entry:
|
||||
return
|
||||
self._desc_label.configure(text=entry[2])
|
||||
if entry[1] == "custom":
|
||||
self._custom_frame.pack(padx=28, pady=(12, 0), fill="x")
|
||||
else:
|
||||
self._custom_frame.pack_forget()
|
||||
|
||||
def _on_bulk_toggle(self):
|
||||
if self._bulk_var.get():
|
||||
self._bulk_label.pack(side="left", padx=(16, 4))
|
||||
self._bulk_count.pack(side="left")
|
||||
else:
|
||||
self._bulk_label.pack_forget()
|
||||
self._bulk_count.pack_forget()
|
||||
|
||||
def _get_selected_type(self):
|
||||
label = self._type_combo.get()
|
||||
return next((t for t in KEY_TYPES if t[0] == label), KEY_TYPES[0])
|
||||
|
||||
def _on_generate(self):
|
||||
entry = self._get_selected_type()
|
||||
_, key_id, _, byte_len, fmt = entry
|
||||
|
||||
if key_id == "custom":
|
||||
try:
|
||||
byte_len = int(self._bytes_entry.get())
|
||||
byte_len = max(8, min(512, byte_len))
|
||||
except ValueError:
|
||||
byte_len = 32
|
||||
fmt = FORMAT_MAP.get(self._fmt_combo.get(), "hex")
|
||||
|
||||
if self._bulk_var.get():
|
||||
try:
|
||||
count = int(self._bulk_count.get())
|
||||
count = max(1, min(20, count))
|
||||
except ValueError:
|
||||
count = 5
|
||||
self._generate_bulk(byte_len, fmt, count, entry)
|
||||
else:
|
||||
self._generate_single(byte_len, fmt, entry)
|
||||
|
||||
def _generate_single(self, byte_len, fmt, entry):
|
||||
key = generate_key(byte_len, fmt)
|
||||
bits = byte_len * 8
|
||||
|
||||
self._key_box.configure(state="normal")
|
||||
self._key_box.delete("1.0", "end")
|
||||
self._key_box.insert("1.0", key)
|
||||
self._key_box.configure(state="disabled")
|
||||
|
||||
self._meta_label.configure(
|
||||
text=f"{entry[0]} · {bits}-bit · {fmt.upper()} · {len(key)} chars"
|
||||
)
|
||||
|
||||
# Reset copy button
|
||||
self._copy_btn.configure(text="Copy", fg_color=("#22263a", "#22263a"))
|
||||
|
||||
# Show single, hide bulk
|
||||
self._result_frame.pack(padx=28, pady=(4, 0), fill="x")
|
||||
self._bulk_frame_result.pack_forget()
|
||||
self._bulk_copy_all_btn.pack_forget()
|
||||
|
||||
def _generate_bulk(self, byte_len, fmt, count, entry):
|
||||
self._bulk_keys = [generate_key(byte_len, fmt) for _ in range(count)]
|
||||
|
||||
# Clear old rows
|
||||
for w in self._bulk_frame_result.winfo_children():
|
||||
w.destroy()
|
||||
|
||||
for i, key in enumerate(self._bulk_keys):
|
||||
row = ctk.CTkFrame(self._bulk_frame_result, fg_color="transparent")
|
||||
row.pack(fill="x", pady=3)
|
||||
|
||||
ctk.CTkLabel(
|
||||
row, text=f"#{i+1:02d}",
|
||||
font=ctk.CTkFont(family="Consolas", size=11),
|
||||
text_color="#4a5568", width=32, anchor="w",
|
||||
).pack(side="left", padx=(4, 0))
|
||||
|
||||
ctk.CTkLabel(
|
||||
row, text=key,
|
||||
font=ctk.CTkFont(family="Consolas", size=11),
|
||||
text_color="#a5f3fc", anchor="w",
|
||||
wraplength=500,
|
||||
).pack(side="left", padx=(6, 0), fill="x", expand=True)
|
||||
|
||||
btn = ctk.CTkButton(
|
||||
row, text="Copy", width=60, height=26,
|
||||
font=ctk.CTkFont(size=11),
|
||||
fg_color=("#22263a", "#22263a"),
|
||||
hover_color=("#2e3250", "#2e3250"),
|
||||
border_width=1, border_color="#2e3250",
|
||||
corner_radius=6,
|
||||
command=lambda k=key, b=None: self._copy_item(k),
|
||||
)
|
||||
btn.pack(side="right", padx=(0, 4))
|
||||
|
||||
# Show bulk, hide single
|
||||
self._result_frame.pack_forget()
|
||||
self._bulk_frame_result.pack(padx=28, pady=(4, 0), fill="x")
|
||||
self._bulk_copy_all_btn.pack(padx=28, pady=(8, 0), anchor="e")
|
||||
|
||||
def _copy_single(self):
|
||||
key = self._key_box.get("1.0", "end").strip()
|
||||
if not key:
|
||||
return
|
||||
pyperclip.copy(key)
|
||||
self._copy_btn.configure(text="Copied!", fg_color=("#1a3a2a", "#1a3a2a"))
|
||||
self.after(2000, lambda: self._copy_btn.configure(
|
||||
text="Copy", fg_color=("#22263a", "#22263a")
|
||||
))
|
||||
|
||||
def _copy_item(self, key: str):
|
||||
pyperclip.copy(key)
|
||||
|
||||
def _copy_all(self):
|
||||
if self._bulk_keys:
|
||||
pyperclip.copy("\n".join(self._bulk_keys))
|
||||
self._bulk_copy_all_btn.configure(text="Copied!")
|
||||
self.after(2000, lambda: self._bulk_copy_all_btn.configure(text="Copy All"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = App()
|
||||
app.mainloop()
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
customtkinter>=5.2.0
|
||||
pyperclip>=1.9.0
|
||||
631
templates/index.html
Normal file
631
templates/index.html
Normal file
@@ -0,0 +1,631 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Key Generator</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--surface2: #22263a;
|
||||
--border: #2e3250;
|
||||
--accent: #6c63ff;
|
||||
--accent-hover: #7c74ff;
|
||||
--accent-dim: rgba(108, 99, 255, 0.15);
|
||||
--success: #22c55e;
|
||||
--success-dim: rgba(34, 197, 94, 0.12);
|
||||
--text: #e2e8f0;
|
||||
--text-muted: #8892a4;
|
||||
--text-dim: #4a5568;
|
||||
--mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 16px 80px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
background: linear-gradient(135deg, #6c63ff, #a78bfa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
header p {
|
||||
margin-top: 8px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Main card */
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
/* Section label */
|
||||
.section-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Key type grid */
|
||||
.type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.15s;
|
||||
color: var(--text);
|
||||
}
|
||||
.type-btn:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
.type-btn.active {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
box-shadow: 0 0 0 1px var(--accent);
|
||||
}
|
||||
.type-btn .btn-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.type-btn .btn-desc {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 3px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Custom options */
|
||||
.custom-options {
|
||||
display: none;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.custom-options.visible {
|
||||
display: flex;
|
||||
}
|
||||
.field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
.field-group label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.field-group input,
|
||||
.field-group select {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
padding: 8px 12px;
|
||||
font-size: 0.85rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.field-group input:focus,
|
||||
.field-group select:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.field-group select option {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 8px 0 24px;
|
||||
}
|
||||
|
||||
/* Bulk toggle */
|
||||
.bulk-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.bulk-row label {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.bulk-row input[type=number] {
|
||||
width: 64px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
font-size: 0.82rem;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
}
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 38px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-switch input { display: none; }
|
||||
.toggle-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--border);
|
||||
border-radius: 999px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.toggle-switch input:checked ~ .toggle-track { background: var(--accent); }
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.toggle-switch input:checked ~ .toggle-track .toggle-thumb {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
/* Generate button */
|
||||
.generate-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.02em;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.generate-btn:hover { background: var(--accent-hover); }
|
||||
.generate-btn:active { transform: scale(0.99); }
|
||||
.generate-btn .spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
display: none;
|
||||
}
|
||||
.generate-btn.loading .spinner { display: block; }
|
||||
.generate-btn.loading .btn-text { opacity: 0.7; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Result area */
|
||||
.result-area {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.result-single {
|
||||
position: relative;
|
||||
}
|
||||
.key-display {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px 16px 14px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
color: #a5f3fc;
|
||||
min-height: 56px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.key-meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.meta-tag {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.meta-tag.highlight {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Copy button */
|
||||
.copy-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-muted);
|
||||
padding: 7px 14px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
font-weight: 500;
|
||||
}
|
||||
.copy-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
.copy-btn.copied {
|
||||
border-color: var(--success);
|
||||
color: var(--success);
|
||||
background: var(--success-dim);
|
||||
}
|
||||
|
||||
/* Bulk results */
|
||||
.bulk-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.bulk-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
.bulk-index {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--mono);
|
||||
min-width: 20px;
|
||||
}
|
||||
.bulk-key {
|
||||
flex: 1;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
color: #a5f3fc;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.bulk-copy {
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-muted);
|
||||
padding: 5px 10px;
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bulk-copy:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.bulk-copy.copied {
|
||||
border-color: var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
.bulk-copy-all {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Icon SVG */
|
||||
svg { display: inline-block; vertical-align: middle; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="url(#g)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:8px">
|
||||
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#6c63ff"/><stop offset="100%" stop-color="#a78bfa"/></linearGradient></defs>
|
||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
||||
</svg>
|
||||
Key Generator
|
||||
</h1>
|
||||
<p>JWT Secret · API Key · Operation Key · UUID · Random Hex</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
|
||||
<!-- Key Type Selection -->
|
||||
<div class="section-label">키 타입 선택</div>
|
||||
<div class="type-grid" id="typeGrid">
|
||||
{% for key, cfg in key_configs.items() %}
|
||||
<button class="type-btn {% if key == 'jwt_hs256' %}active{% endif %}"
|
||||
data-type="{{ key }}"
|
||||
onclick="selectType('{{ key }}', this)">
|
||||
<div class="btn-label">{{ cfg.label }}</div>
|
||||
<div class="btn-desc">{{ cfg.description }}</div>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Custom Options (visible when custom is selected) -->
|
||||
<div class="custom-options" id="customOptions">
|
||||
<div class="field-group">
|
||||
<label>바이트 수 (8 ~ 512)</label>
|
||||
<input type="number" id="customBytes" value="32" min="8" max="512" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label>출력 포맷</label>
|
||||
<select id="customFormat">
|
||||
<option value="hex">Hex</option>
|
||||
<option value="base64url">Base64URL</option>
|
||||
<option value="alphanumeric">Alphanumeric</option>
|
||||
<option value="api_key">API Key (sk- prefix)</option>
|
||||
<option value="op_key">Op Key (ops- prefix)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<!-- Bulk toggle -->
|
||||
<div class="bulk-row">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="bulkToggle" onchange="toggleBulk()" />
|
||||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||
</label>
|
||||
<label for="bulkToggle" style="cursor:pointer">대량 생성</label>
|
||||
<input type="number" id="bulkCount" value="5" min="1" max="20"
|
||||
style="display:none" />
|
||||
<label id="bulkCountLabel" style="display:none">개</label>
|
||||
</div>
|
||||
|
||||
<!-- Generate Button -->
|
||||
<button class="generate-btn" id="generateBtn" onclick="generate()">
|
||||
<div class="spinner"></div>
|
||||
<span class="btn-text">Generate</span>
|
||||
</button>
|
||||
|
||||
<!-- Result -->
|
||||
<div class="result-area" id="resultArea" style="display:none">
|
||||
<div class="section-label" style="margin-bottom:12px">생성된 키</div>
|
||||
|
||||
<!-- Single result -->
|
||||
<div class="result-single" id="resultSingle">
|
||||
<div class="key-display" id="keyDisplay"></div>
|
||||
<div class="key-meta" id="keyMeta"></div>
|
||||
<div class="copy-row">
|
||||
<button class="copy-btn" id="copyBtn" onclick="copyKey()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||
</svg>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk result -->
|
||||
<div id="resultBulk" style="display:none">
|
||||
<div class="bulk-list" id="bulkList"></div>
|
||||
<div class="bulk-copy-all">
|
||||
<button class="copy-btn" onclick="copyAll()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||
</svg>
|
||||
Copy All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedType = 'jwt_hs256';
|
||||
let bulkMode = false;
|
||||
let lastBulkKeys = [];
|
||||
|
||||
function selectType(type, el) {
|
||||
selectedType = type;
|
||||
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
|
||||
const customOpts = document.getElementById('customOptions');
|
||||
if (type === 'custom') {
|
||||
customOpts.classList.add('visible');
|
||||
} else {
|
||||
customOpts.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleBulk() {
|
||||
bulkMode = document.getElementById('bulkToggle').checked;
|
||||
const countInput = document.getElementById('bulkCount');
|
||||
const countLabel = document.getElementById('bulkCountLabel');
|
||||
countInput.style.display = bulkMode ? 'block' : 'none';
|
||||
countLabel.style.display = bulkMode ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
const btn = document.getElementById('generateBtn');
|
||||
btn.classList.add('loading');
|
||||
|
||||
const payload = {
|
||||
type: selectedType,
|
||||
custom_bytes: parseInt(document.getElementById('customBytes').value) || 32,
|
||||
custom_format: document.getElementById('customFormat').value,
|
||||
};
|
||||
|
||||
try {
|
||||
if (bulkMode) {
|
||||
const count = parseInt(document.getElementById('bulkCount').value) || 5;
|
||||
const res = await fetch('/api/generate/bulk', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...payload, count }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) renderBulk(json.data);
|
||||
} else {
|
||||
const res = await fetch('/api/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) renderSingle(json.data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
btn.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
function renderSingle(data) {
|
||||
document.getElementById('resultArea').style.display = 'block';
|
||||
document.getElementById('resultSingle').style.display = 'block';
|
||||
document.getElementById('resultBulk').style.display = 'none';
|
||||
|
||||
document.getElementById('keyDisplay').textContent = data.key;
|
||||
|
||||
const meta = document.getElementById('keyMeta');
|
||||
meta.innerHTML = `
|
||||
<span class="meta-tag highlight">${data.label}</span>
|
||||
<span class="meta-tag">${data.bits} bit</span>
|
||||
<span class="meta-tag">${data.algorithm}</span>
|
||||
<span class="meta-tag">${data.length} chars</span>
|
||||
`;
|
||||
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
copyBtn.classList.remove('copied');
|
||||
copyBtn.innerHTML = `
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||
</svg> Copy`;
|
||||
}
|
||||
|
||||
function renderBulk(items) {
|
||||
lastBulkKeys = items.map(i => i.key);
|
||||
document.getElementById('resultArea').style.display = 'block';
|
||||
document.getElementById('resultSingle').style.display = 'none';
|
||||
document.getElementById('resultBulk').style.display = 'block';
|
||||
|
||||
const list = document.getElementById('bulkList');
|
||||
list.innerHTML = items.map((item, idx) => `
|
||||
<div class="bulk-item">
|
||||
<span class="bulk-index">#${idx + 1}</span>
|
||||
<span class="bulk-key">${item.key}</span>
|
||||
<button class="bulk-copy" onclick="copyBulkItem(this, '${escHtml(item.key)}')">Copy</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return s.replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
function copyKey() {
|
||||
const key = document.getElementById('keyDisplay').textContent;
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
const btn = document.getElementById('copyBtn');
|
||||
btn.classList.add('copied');
|
||||
btn.innerHTML = `
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg> Copied!`;
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('copied');
|
||||
btn.innerHTML = `
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||
</svg> Copy`;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function copyBulkItem(btn, key) {
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
btn.classList.add('copied');
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('copied');
|
||||
btn.textContent = 'Copy';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function copyAll() {
|
||||
const text = lastBulkKeys.join('\n');
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('전체 키가 클립보드에 복사되었습니다!');
|
||||
});
|
||||
}
|
||||
|
||||
// Enter key shortcut
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) generate();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user