chore: commit remaining workspace updates
Build & Deploy to K8s / build-and-deploy (push) Failing after 8m6s
Build & Deploy to K8s / build-and-deploy (push) Failing after 8m6s
Include the outstanding numbering-rule admin page changes, TabBar interaction updates, V5 layout theme accent styling, and auto-generation option compatibility fix. Add the local web-prototype skill assets, numbering-rule design variants, control IDE refactor note, and the table canonical cleanup plan/prompts used across phases B through F. This commit captures the remaining workspace files after the canonical table cleanup commit so the branch can be pushed without leaving local dirty work behind.
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
name: web-prototype
|
||||||
|
description: |
|
||||||
|
General-purpose desktop web prototype. Single self-contained HTML file built
|
||||||
|
by copying the seed `assets/template.html` and pasting section layouts from
|
||||||
|
`references/layouts.md`. Default for any landing / marketing / docs / SaaS
|
||||||
|
page when no more specific skill matches.
|
||||||
|
triggers:
|
||||||
|
- "prototype"
|
||||||
|
- "mockup"
|
||||||
|
- "landing"
|
||||||
|
- "single page"
|
||||||
|
- "marketing page"
|
||||||
|
- "homepage"
|
||||||
|
od:
|
||||||
|
mode: prototype
|
||||||
|
platform: desktop
|
||||||
|
scenario: design
|
||||||
|
preview:
|
||||||
|
type: html
|
||||||
|
entry: index.html
|
||||||
|
design_system:
|
||||||
|
requires: true
|
||||||
|
sections: [color, typography, layout, components]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Web Prototype Skill
|
||||||
|
|
||||||
|
Produce a single, self-contained HTML prototype using the bundled seed and layout library — **not** by writing CSS from scratch. The seed already encodes good defaults (typography, spacing, accent budget). Your job is to compose it.
|
||||||
|
|
||||||
|
## Resource map
|
||||||
|
|
||||||
|
```
|
||||||
|
web-prototype/
|
||||||
|
├── SKILL.md ← you're reading this
|
||||||
|
├── assets/
|
||||||
|
│ └── template.html ← seed: tokens + class system + chrome (READ FIRST)
|
||||||
|
└── references/
|
||||||
|
├── layouts.md ← 8 paste-ready section skeletons
|
||||||
|
└── checklist.md ← P0/P1/P2 self-review
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 0 — Pre-flight (do this once before writing anything)
|
||||||
|
|
||||||
|
1. **Read `assets/template.html` end-to-end** — at minimum through the `<style>` block. The class inventory at the top of `references/layouts.md` lists every class that must be defined there; if one is missing, add it to `<style>` rather than re-defining it inline on every section.
|
||||||
|
2. **Read `references/layouts.md`** so you know which section skeletons exist. Don't write a section type that isn't covered — pick the closest layout and adapt.
|
||||||
|
3. **Read the active DESIGN.md** (already injected into your system prompt). Map its colors to the six `:root` variables in the seed; don't introduce new tokens.
|
||||||
|
|
||||||
|
### Step 1 — Copy the seed
|
||||||
|
|
||||||
|
Copy `assets/template.html` to the project root as `index.html`. Replace the six `:root` variables with the active design system's tokens. Replace the page `<title>` and the topnav brand.
|
||||||
|
|
||||||
|
### Step 2 — Plan the section list
|
||||||
|
|
||||||
|
**Pick layouts before writing copy.** Default rhythms (from `layouts.md`):
|
||||||
|
|
||||||
|
| Page kind | Default rhythm |
|
||||||
|
|---|---|
|
||||||
|
| Landing | 1 hero → 3 features → 4 stats *or* 5 quote → custom split → 6 cta |
|
||||||
|
| Marketing / editorial | 1 hero-center → 7 log list → 6 cta |
|
||||||
|
| Pricing | 1 hero-center → 8 comparison table → 6 cta |
|
||||||
|
| Docs index | 1 hero-center → 7 log list (sections of docs) → 6 cta |
|
||||||
|
|
||||||
|
State the chosen list in one sentence to the user *before* writing — they can redirect cheaply now and not after 200 lines of HTML.
|
||||||
|
|
||||||
|
### Step 3 — Paste and fill
|
||||||
|
|
||||||
|
For each chosen layout, copy the `<section>` block from `layouts.md` into `<main id="content">` of your `index.html`. Replace bracketed `[REPLACE]` strings with real, specific copy from the user's brief. **No filler** — if a slot is empty, the section is the wrong choice; pick a different layout.
|
||||||
|
|
||||||
|
### Step 4 — Self-check
|
||||||
|
|
||||||
|
Run through `references/checklist.md` top to bottom. Every P0 item must pass before you move on. P1 items should pass; P2 are bonus.
|
||||||
|
|
||||||
|
### Step 5 — Emit the artifact
|
||||||
|
|
||||||
|
Wrap `index.html` in `<artifact>` tags. One sentence before describing what's there. Stop after `</artifact>`.
|
||||||
|
|
||||||
|
## Hard rules (the seed protects most of these — don't fight it)
|
||||||
|
|
||||||
|
- **Single accent, used at most twice per screen.** Eyebrow + primary CTA is the default budget.
|
||||||
|
- **Display font is serif** (Iowan Old Style / Charter / Georgia in the seed). Sans for body. Mono for numerics, captions, eyebrows.
|
||||||
|
- **Image placeholders, not external URLs.** Use the `.ph-img` class — never link to a stock photo CDN.
|
||||||
|
- **Mobile reflow already works** via the seed's media query at 920px. Don't break it by adding fixed widths.
|
||||||
|
- **`data-od-id` on every `<section>`** so comment mode can target it.
|
||||||
|
|
||||||
|
## Output contract
|
||||||
|
|
||||||
|
```
|
||||||
|
<artifact identifier="kebab-case-slug" type="text/html" title="Human Title">
|
||||||
|
<!doctype html>
|
||||||
|
<html>...</html>
|
||||||
|
</artifact>
|
||||||
|
```
|
||||||
|
|
||||||
|
One sentence before the artifact. Nothing after.
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<!--
|
||||||
|
OD web-prototype seed.
|
||||||
|
|
||||||
|
Copy this file to <project>/index.html, then fill `<main id="content">` by
|
||||||
|
pasting blocks from `references/layouts.md`. Every class referenced in
|
||||||
|
layouts.md is defined here — DO NOT remove unused ones unless you also
|
||||||
|
remove their callers, and DO NOT invent new global classes (use inline
|
||||||
|
style="…" for one-off tweaks).
|
||||||
|
|
||||||
|
Theme tokens at the top of `<style>` are the *only* place to set palette
|
||||||
|
and type. They map cleanly onto a DESIGN.md — when an active design system
|
||||||
|
is injected, swap these six variables and everything reflows.
|
||||||
|
-->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>[REPLACE] Page title · brand</title>
|
||||||
|
<style>
|
||||||
|
/* ─── tokens ─────────────────────────────────────────────────────────
|
||||||
|
Six variables. Bind them to the active DESIGN.md and stop.
|
||||||
|
Do not introduce raw hex anywhere else in this file. */
|
||||||
|
:root {
|
||||||
|
--bg: #fafaf7; /* page background — never #fff */
|
||||||
|
--surface: #ffffff; /* cards, modals, raised areas */
|
||||||
|
--fg: #1a1916; /* primary text — never #000 */
|
||||||
|
--muted: #6b6964; /* secondary text, captions */
|
||||||
|
--border: #e8e5df; /* hairlines, dividers */
|
||||||
|
--accent: #c96442; /* one accent — used at most 2× per screen */
|
||||||
|
|
||||||
|
/* derived — do not change */
|
||||||
|
--accent-soft: color-mix(in oklch, var(--accent) 14%, transparent);
|
||||||
|
--fg-soft: color-mix(in oklch, var(--fg) 6%, transparent);
|
||||||
|
|
||||||
|
/* type — display = serif (default), body = sans, mono for numerics */
|
||||||
|
--font-display: 'Iowan Old Style', 'Charter', Georgia, 'Times New Roman', serif;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
--font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, monospace;
|
||||||
|
|
||||||
|
/* scale — clamp() so it works at 1280, 1440, 1920 without media queries */
|
||||||
|
--fs-h1: clamp(44px, 6vw, 76px);
|
||||||
|
--fs-h2: clamp(32px, 4vw, 48px);
|
||||||
|
--fs-h3: 22px;
|
||||||
|
--fs-lead: 19px;
|
||||||
|
--fs-body: 16px;
|
||||||
|
--fs-meta: 13px;
|
||||||
|
|
||||||
|
/* spacing — 8-point grid */
|
||||||
|
--gap-xs: 8px;
|
||||||
|
--gap-sm: 12px;
|
||||||
|
--gap-md: 20px;
|
||||||
|
--gap-lg: 32px;
|
||||||
|
--gap-xl: 56px;
|
||||||
|
--gap-2xl: 96px;
|
||||||
|
--container: 1120px;
|
||||||
|
--gutter: 32px;
|
||||||
|
|
||||||
|
--radius: 10px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── reset & base ──────────────────────────────────────────────── */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
html { -webkit-text-size-adjust: 100%; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--fs-body);
|
||||||
|
line-height: 1.55;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
img, svg { display: block; max-width: 100%; }
|
||||||
|
a { color: inherit; text-decoration: none; }
|
||||||
|
button { font: inherit; cursor: pointer; }
|
||||||
|
p { text-wrap: pretty; }
|
||||||
|
h1, h2, h3, h4 { text-wrap: balance; }
|
||||||
|
|
||||||
|
/* ─── layout primitives ─────────────────────────────────────────── */
|
||||||
|
.container {
|
||||||
|
max-width: var(--container);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: var(--gutter);
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
padding-block: clamp(48px, 8vw, var(--gap-2xl));
|
||||||
|
}
|
||||||
|
.section + .section { border-top: 1px solid var(--border); }
|
||||||
|
.stack { display: flex; flex-direction: column; }
|
||||||
|
.stack > * + * { margin-top: var(--gap-md); }
|
||||||
|
.row { display: flex; align-items: center; gap: var(--gap-md); }
|
||||||
|
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--gap-md); }
|
||||||
|
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap-lg); }
|
||||||
|
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gap-lg); }
|
||||||
|
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap-md); }
|
||||||
|
.grid-2-1 { display: grid; grid-template-columns: 2fr 1fr; gap: var(--gap-xl); align-items: start; }
|
||||||
|
.grid-1-2 { display: grid; grid-template-columns: 1fr 2fr; gap: var(--gap-xl); align-items: start; }
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
.grid-2, .grid-3, .grid-4, .grid-2-1, .grid-1-2 { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── type ──────────────────────────────────────────────────────── */
|
||||||
|
.h1, h1 { font-family: var(--font-display); font-size: var(--fs-h1); line-height: 1.04; letter-spacing: -0.02em; margin: 0; }
|
||||||
|
.h2, h2 { font-family: var(--font-display); font-size: var(--fs-h2); line-height: 1.1; letter-spacing: -0.015em; margin: 0; }
|
||||||
|
.h3, h3 { font-size: var(--fs-h3); font-weight: 600; line-height: 1.3; letter-spacing: -0.005em; margin: 0; }
|
||||||
|
.lead { font-size: var(--fs-lead); line-height: 1.55; color: var(--muted); max-width: 60ch; margin: 0; }
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
margin: 0 0 var(--gap-md);
|
||||||
|
}
|
||||||
|
.meta { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--muted); }
|
||||||
|
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
||||||
|
|
||||||
|
/* ─── chrome: nav + footer ──────────────────────────────────────── */
|
||||||
|
.topnav {
|
||||||
|
position: sticky; top: 0; z-index: 10;
|
||||||
|
background: color-mix(in oklch, var(--bg) 92%, transparent);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.topnav-inner { display: flex; align-items: center; justify-content: space-between; padding-block: 14px; }
|
||||||
|
.topnav .logo { font-family: var(--font-display); font-size: 19px; font-weight: 600; letter-spacing: -0.01em; }
|
||||||
|
.topnav nav { display: flex; gap: var(--gap-lg); }
|
||||||
|
.topnav nav a { font-size: 14px; color: var(--muted); }
|
||||||
|
.topnav nav a:hover { color: var(--fg); }
|
||||||
|
.pagefoot { padding-block: var(--gap-xl); color: var(--muted); font-size: 13px; border-top: 1px solid var(--border); }
|
||||||
|
.pagefoot .row-between { flex-wrap: wrap; gap: var(--gap-md); }
|
||||||
|
|
||||||
|
/* ─── buttons ───────────────────────────────────────────────────── */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 8px;
|
||||||
|
padding: 11px 20px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
transition: transform 0.05s ease, background 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
.btn:active { transform: translateY(1px); }
|
||||||
|
.btn-primary { background: var(--accent); color: var(--surface); border-color: var(--accent); }
|
||||||
|
.btn-primary:hover { background: color-mix(in oklch, var(--accent) 88%, black); }
|
||||||
|
.btn-secondary { background: transparent; color: var(--fg); border-color: var(--border); }
|
||||||
|
.btn-secondary:hover { border-color: var(--fg); }
|
||||||
|
.btn-ghost { background: transparent; color: var(--fg); border-color: transparent; padding-inline: 8px; }
|
||||||
|
.btn-ghost:hover { color: var(--accent); }
|
||||||
|
.btn-arrow::after { content: '→'; transition: transform 0.15s ease; }
|
||||||
|
.btn-arrow:hover::after { transform: translateX(2px); }
|
||||||
|
|
||||||
|
/* ─── card / surface ────────────────────────────────────────────── */
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
.card-flat { background: transparent; border: 0; padding: 0; }
|
||||||
|
.card-rule { background: transparent; border: 0; border-top: 1px solid var(--fg); padding: 24px 0 0; border-radius: 0; }
|
||||||
|
|
||||||
|
/* ─── feature cell (icon + h3 + p) ──────────────────────────────── */
|
||||||
|
.feature .feature-mark {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: var(--gap-md);
|
||||||
|
}
|
||||||
|
.feature .feature-mark svg { width: 18px; height: 18px; }
|
||||||
|
.feature h3 { margin-bottom: 6px; }
|
||||||
|
.feature p { margin: 0; color: var(--muted); font-size: 15px; }
|
||||||
|
|
||||||
|
/* ─── stat (big number + label) ─────────────────────────────────── */
|
||||||
|
.stat .stat-num {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(56px, 8vw, 96px);
|
||||||
|
line-height: 0.95;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.stat .stat-label { color: var(--muted); font-size: 14px; margin-top: 8px; max-width: 24ch; }
|
||||||
|
.stat .stat-unit { font-size: 0.5em; opacity: 0.7; margin-left: 2px; }
|
||||||
|
|
||||||
|
/* ─── quote / testimonial ───────────────────────────────────────── */
|
||||||
|
.quote {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(24px, 2.6vw, 32px);
|
||||||
|
line-height: 1.32;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
max-width: 28ch;
|
||||||
|
}
|
||||||
|
.quote-author { color: var(--muted); font-size: 14px; margin-top: var(--gap-md); }
|
||||||
|
.quote-mark {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 140px; line-height: 0.7;
|
||||||
|
color: var(--accent); opacity: 0.18;
|
||||||
|
margin-bottom: -28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── pill / badge / tag ────────────────────────────────────────── */
|
||||||
|
.pill {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
display: inline-flex; align-items: center;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── form field ────────────────────────────────────────────────── */
|
||||||
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.field label { font-size: 13px; color: var(--muted); }
|
||||||
|
.input, .textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--fg);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.input:focus, .textarea:focus {
|
||||||
|
outline: 2px solid var(--accent-soft);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.textarea { min-height: 96px; resize: vertical; line-height: 1.55; }
|
||||||
|
|
||||||
|
/* ─── table (data-style, no chrome) ─────────────────────────────── */
|
||||||
|
.ds-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||||
|
.ds-table th, .ds-table td { padding: 12px 14px; text-align: left; border-bottom: 1px solid var(--border); }
|
||||||
|
.ds-table th { color: var(--muted); font-weight: 500; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
|
||||||
|
.ds-table tbody tr:hover { background: var(--fg-soft); }
|
||||||
|
.ds-table .num-col { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; }
|
||||||
|
|
||||||
|
/* ─── image placeholder (DS-tinted block, never broken <img>) ──── */
|
||||||
|
.ph-img {
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, var(--accent-soft), var(--fg-soft)),
|
||||||
|
var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.ph-img.square { aspect-ratio: 1 / 1; }
|
||||||
|
.ph-img.portrait { aspect-ratio: 3 / 4; }
|
||||||
|
.ph-img.wide { aspect-ratio: 16 / 9; }
|
||||||
|
|
||||||
|
/* ─── divider ───────────────────────────────────────────────────── */
|
||||||
|
.rule { border: 0; border-top: 1px solid var(--border); margin: 0; }
|
||||||
|
.rule-strong { border: 0; border-top: 1px solid var(--fg); margin: 0; }
|
||||||
|
|
||||||
|
/* ─── hero variants used by layouts.md ──────────────────────────── */
|
||||||
|
.hero { padding-block: clamp(80px, 12vw, 160px); }
|
||||||
|
.hero-center { text-align: center; max-width: 32ch; margin-inline: auto; }
|
||||||
|
.hero h1 { margin-bottom: var(--gap-md); }
|
||||||
|
.hero .lead { margin-bottom: var(--gap-lg); }
|
||||||
|
.hero-cta { display: inline-flex; gap: var(--gap-sm); flex-wrap: wrap; }
|
||||||
|
.hero-center .hero-cta { justify-content: center; }
|
||||||
|
.hero-split { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-2xl); align-items: center; }
|
||||||
|
@media (max-width: 920px) { .hero-split { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
/* ─── log row (newsletter, blog list, changelog) ────────────────── */
|
||||||
|
.log-row { display: grid; grid-template-columns: 120px 1fr 100px; gap: var(--gap-lg); padding: 22px 0; border-top: 1px solid var(--border); align-items: baseline; }
|
||||||
|
.log-row .meta { color: var(--muted); }
|
||||||
|
.log-row h3 { font-size: 19px; }
|
||||||
|
.log-row .pull { text-align: right; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topnav" data-od-id="topnav">
|
||||||
|
<div class="container topnav-inner">
|
||||||
|
<span class="logo">[REPLACE] Brand</span>
|
||||||
|
<nav>
|
||||||
|
<a href="#">[REPLACE] Link 1</a>
|
||||||
|
<a href="#">[REPLACE] Link 2</a>
|
||||||
|
<a href="#">[REPLACE] Link 3</a>
|
||||||
|
</nav>
|
||||||
|
<button class="btn btn-primary">[REPLACE] CTA</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="content">
|
||||||
|
<!--
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ PASTE LAYOUTS FROM references/layouts.md HERE. │
|
||||||
|
│ ► Each layout block is a self-contained `<section>` — │
|
||||||
|
│ drop in 3–6 of them per page. │
|
||||||
|
│ ► Always lead with one hero (Layout 1 or 2). │
|
||||||
|
│ ► End with a CTA strip + footer (Layout 6 / footer below). │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
-->
|
||||||
|
<section class="section hero" data-od-id="hero">
|
||||||
|
<div class="container hero-center">
|
||||||
|
<p class="eyebrow">[REPLACE] Eyebrow</p>
|
||||||
|
<h1>[REPLACE] One sharp sentence about what this is.</h1>
|
||||||
|
<p class="lead">[REPLACE] One subhead sentence — concrete value, not a tagline.</p>
|
||||||
|
<div class="hero-cta">
|
||||||
|
<button class="btn btn-primary">[REPLACE] Primary CTA</button>
|
||||||
|
<button class="btn btn-secondary">[REPLACE] Secondary</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="pagefoot" data-od-id="footer">
|
||||||
|
<div class="container row-between">
|
||||||
|
<span>© [REPLACE] Brand · [REPLACE] Year</span>
|
||||||
|
<span class="meta">[REPLACE] tagline · contact@example.com</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Tomato — focused work timer</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #fafaf9; --fg: #1c1b1a; --muted: #6b6964; --border: #e6e4e0;
|
||||||
|
--accent: #c96442; --surface: #ffffff;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.55 -apple-system, system-ui, sans-serif; }
|
||||||
|
header, main, footer { max-width: 1080px; margin: 0 auto; padding: 0 32px; }
|
||||||
|
header { display: flex; justify-content: space-between; align-items: center; padding-top: 20px; }
|
||||||
|
.logo { font-weight: 600; font-size: 17px; letter-spacing: -0.01em; }
|
||||||
|
nav a { color: var(--fg); text-decoration: none; margin-left: 24px; font-size: 14px; }
|
||||||
|
nav a:hover { color: var(--accent); }
|
||||||
|
.hero { padding: 96px 0 80px; text-align: center; }
|
||||||
|
.hero h1 { font-size: clamp(44px, 6vw, 76px); line-height: 1.05; letter-spacing: -0.02em; margin: 0 0 20px; max-width: 18ch; margin-inline: auto; }
|
||||||
|
.hero p { color: var(--muted); font-size: 19px; max-width: 52ch; margin: 0 auto 32px; }
|
||||||
|
.cta { display: inline-flex; gap: 12px; }
|
||||||
|
button { font: inherit; cursor: pointer; padding: 12px 22px; border-radius: 8px; }
|
||||||
|
.btn-primary { background: var(--accent); color: white; border: 1px solid var(--accent); font-weight: 500; }
|
||||||
|
.btn-secondary { background: transparent; color: var(--fg); border: 1px solid var(--border); }
|
||||||
|
.features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; padding: 56px 0 96px; }
|
||||||
|
@media (max-width: 720px) { .features { grid-template-columns: 1fr; } }
|
||||||
|
.feature { padding: 28px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; }
|
||||||
|
.feature .icon { width: 36px; height: 36px; border-radius: 8px; background: var(--accent); margin-bottom: 16px; opacity: 0.12; }
|
||||||
|
.feature h3 { margin: 0 0 8px; font-size: 17px; letter-spacing: -0.01em; }
|
||||||
|
.feature p { margin: 0; color: var(--muted); font-size: 14px; }
|
||||||
|
.closing { padding: 64px 0 96px; text-align: center; border-top: 1px solid var(--border); }
|
||||||
|
.closing h2 { font-size: 32px; margin: 0 0 12px; letter-spacing: -0.01em; }
|
||||||
|
.closing p { color: var(--muted); margin: 0 0 28px; }
|
||||||
|
footer { padding: 32px; color: var(--muted); font-size: 13px; text-align: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header data-od-id="topnav">
|
||||||
|
<span class="logo">🍅 Tomato</span>
|
||||||
|
<nav>
|
||||||
|
<a href="#features">Features</a>
|
||||||
|
<a href="#pricing">Pricing</a>
|
||||||
|
<a href="#login">Sign in</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section class="hero" data-od-id="hero">
|
||||||
|
<h1>Twenty-five minutes at a time.</h1>
|
||||||
|
<p>The pomodoro timer that actually keeps your hands off Slack. Block notifications, log every cycle, ship more before lunch.</p>
|
||||||
|
<div class="cta">
|
||||||
|
<button class="btn-primary">Start a session</button>
|
||||||
|
<button class="btn-secondary">See how it works</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="features" id="features">
|
||||||
|
<div class="feature" data-od-id="feature-block">
|
||||||
|
<div class="icon"></div>
|
||||||
|
<h3>Block on, not off</h3>
|
||||||
|
<p>Slack and email go quiet for 25 minutes. They come back loud at the break, with a digest.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature" data-od-id="feature-stats">
|
||||||
|
<div class="icon"></div>
|
||||||
|
<h3>Stats that don't lie</h3>
|
||||||
|
<p>Weekly review tells you which days you actually shipped versus which you only seemed busy.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature" data-od-id="feature-team">
|
||||||
|
<div class="icon"></div>
|
||||||
|
<h3>Team-friendly silences</h3>
|
||||||
|
<p>Your status auto-updates so teammates know when to ask, when to wait, and when you're done.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="closing" data-od-id="closing">
|
||||||
|
<h2>Stop measuring meetings. Start measuring focus.</h2>
|
||||||
|
<p>Free for solo. $4/mo per teammate after that.</p>
|
||||||
|
<button class="btn-primary">Try Tomato free</button>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer>© Tomato Labs · Made for people who'd rather be making.</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
|
||||||
|
"specVersion": "1.0.0",
|
||||||
|
"name": "example-web-prototype",
|
||||||
|
"title": "Web Prototype",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"description": "General-purpose desktop web prototype. Single self-contained HTML file built\nby copying the seed `assets/template.html` and pasting section layouts from\n`references/layouts.md`. Default for any landing / marketing / docs / SaaS\npage when no more specific skill matches.",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "Open Design",
|
||||||
|
"url": "https://github.com/nexu-io"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/web-prototype",
|
||||||
|
"tags": [
|
||||||
|
"example",
|
||||||
|
"first-party",
|
||||||
|
"prototype",
|
||||||
|
"design",
|
||||||
|
"web",
|
||||||
|
"desktop",
|
||||||
|
"mockup",
|
||||||
|
"landing",
|
||||||
|
"single-page",
|
||||||
|
"marketing-page",
|
||||||
|
"homepage"
|
||||||
|
],
|
||||||
|
"compat": {
|
||||||
|
"agentSkills": [
|
||||||
|
{
|
||||||
|
"path": "./SKILL.md"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"od": {
|
||||||
|
"kind": "scenario",
|
||||||
|
"taskKind": "new-generation",
|
||||||
|
"mode": "prototype",
|
||||||
|
"platform": "desktop",
|
||||||
|
"scenario": "design",
|
||||||
|
"surface": "web",
|
||||||
|
"preview": {
|
||||||
|
"type": "html",
|
||||||
|
"entry": "./example.html"
|
||||||
|
},
|
||||||
|
"useCase": {
|
||||||
|
"query": {
|
||||||
|
"en": "Build a {{fidelity}} {{artifactKind}} for {{audience}}. Use {{designSystem}} as the design-system direction and start from {{template}}. Single self-contained HTML file built by copying the seed `assets/template.html` and pasting section layouts from `references/layouts.md`.",
|
||||||
|
"zh-CN": "使用这个插件完成以下任务:为 {{audience}} 构建一个 {{fidelity}} 的 {{artifactKind}}。设计系统方向使用 {{designSystem}},从 {{template}} 开始。使用 `assets/template.html` 种子并从 `references/layouts.md` 粘贴版面,输出单文件 HTML。"
|
||||||
|
},
|
||||||
|
"exampleOutputs": [
|
||||||
|
{
|
||||||
|
"path": "./example.html",
|
||||||
|
"title": "Web Prototype"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "artifactKind",
|
||||||
|
"label": "Artifact kind",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"placeholder": "SaaS landing page",
|
||||||
|
"default": "web prototype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fidelity",
|
||||||
|
"label": "Fidelity",
|
||||||
|
"type": "select",
|
||||||
|
"required": true,
|
||||||
|
"options": [
|
||||||
|
"wireframe",
|
||||||
|
"high-fidelity"
|
||||||
|
],
|
||||||
|
"default": "high-fidelity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "audience",
|
||||||
|
"label": "Audience",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"placeholder": "startup founders evaluating an AI CRM",
|
||||||
|
"default": "product evaluators"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "designSystem",
|
||||||
|
"label": "Design system",
|
||||||
|
"type": "string",
|
||||||
|
"placeholder": "OpenAI, Linear, shadcn, or custom brand notes",
|
||||||
|
"default": "the active project design system"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "template",
|
||||||
|
"label": "Template",
|
||||||
|
"type": "string",
|
||||||
|
"placeholder": "marketing homepage, dashboard, docs page",
|
||||||
|
"default": "the bundled web prototype seed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"context": {
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"path": "./SKILL.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"designSystem": {
|
||||||
|
"primary": true
|
||||||
|
},
|
||||||
|
"assets": [
|
||||||
|
"./example.html",
|
||||||
|
"./assets/template.html",
|
||||||
|
"./references/checklist.md",
|
||||||
|
"./references/layouts.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pipeline": {
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"id": "generate",
|
||||||
|
"atoms": [
|
||||||
|
"file-write",
|
||||||
|
"live-artifact"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"capabilities": [
|
||||||
|
"prompt:inject",
|
||||||
|
"fs:write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Web prototype checklist
|
||||||
|
|
||||||
|
Run this before emitting `<artifact>`. P0 = must pass; P1 = should pass; P2 = nice to have.
|
||||||
|
|
||||||
|
## P0 — must pass
|
||||||
|
|
||||||
|
- [ ] **No raw hex outside `:root` token block.** Every color is `var(--bg)` / `var(--fg)` / `var(--muted)` / `var(--border)` / `var(--accent)` / `var(--surface)` (or a `color-mix()` of those). Grep `#[0-9a-fA-F]{3,8}` outside `:root{}` should return nothing.
|
||||||
|
- [ ] **All headings use `var(--font-display)`.** No sans-serif `<h1>` / `<h2>`. Inter / Roboto / system-sans never serve as a display face.
|
||||||
|
- [ ] **Accent appears at most twice per screen.** Count: eyebrow color, primary CTA fill, anything else? If three or more, demote one to `var(--fg)` or `var(--muted)`.
|
||||||
|
- [ ] **No purple/violet gradient backgrounds.** No `linear-gradient(... #a855f7 / #8b5cf6 / purple ...)`. The seed template has no gradients on backgrounds — keep it that way.
|
||||||
|
- [ ] **No emoji used as feature icons.** Use the inline SVG monoline marks shipped in Layout 3, or a tasteful single-character glyph in `--font-mono`. ✨ 🚀 🎯 are out.
|
||||||
|
- [ ] **No invented metrics.** Every number on the page came from the user, the brief, or is clearly labelled as a placeholder (e.g. `[REPLACE] · 38×`). "10× faster", "99.9% uptime" without source = remove.
|
||||||
|
- [ ] **No filler copy.** Zero "Feature One / Feature Two", lorem ipsum, "Lorem ipsum dolor". If a section feels empty, delete it; do not pad.
|
||||||
|
- [ ] **`data-od-id` on every top-level `<section>`.** Used by comment mode to target sections.
|
||||||
|
- [ ] **Mobile reflow works.** All `grid-2`, `grid-3`, `grid-4`, `grid-2-1`, `grid-1-2` collapse to one column at ≤920px (the default media query in `template.html` does this). Verify by mentally narrowing — no horizontal scroll.
|
||||||
|
- [ ] **No `scrollIntoView()` calls.** Breaks the OD preview iframe. Use `scrollTo({...})` if you need scroll behaviour.
|
||||||
|
|
||||||
|
## P1 — should pass
|
||||||
|
|
||||||
|
- [ ] **One decisive flourish.** A pull quote, a striking stat, a real-feeling photograph, one micro-animation on the hero. *One.* Not three.
|
||||||
|
- [ ] **Section rhythm alternates.** No two stat rows in a row. No two feature triplets in a row. No two quote blocks in a row.
|
||||||
|
- [ ] **Headlines under 14 words.** If longer, the writing is doing the design's job.
|
||||||
|
- [ ] **Lead text under 56 ch / two sentences.** `max-width: 60ch` on `.lead` enforces this; don't override.
|
||||||
|
- [ ] **CTA buttons say what happens.** "Start free" beats "Get Started". "Read the story" beats "Learn More".
|
||||||
|
- [ ] **Hover states present** for all `<a>` and `.btn`. Seed template covers this.
|
||||||
|
- [ ] **Numerics use `.num` (mono, tabular).** Prices, stats, version numbers, dates.
|
||||||
|
- [ ] **One image style per page.** Don't mix square portrait headshots with widescreen product hero with vertical phone mock — pick a lane.
|
||||||
|
|
||||||
|
## P2 — nice to have
|
||||||
|
|
||||||
|
- [ ] **`text-wrap: pretty` / `balance`** on long paragraphs / headings (already on `<p>` and `h*` in seed).
|
||||||
|
- [ ] **`color-mix()` for derived tones.** No additional `--accent-50` / `--accent-300` Bootstrap-style tokens — derive on the spot.
|
||||||
|
- [ ] **Sticky topnav has frosted glass** (already in seed via `backdrop-filter: blur()`).
|
||||||
|
- [ ] **Loaded fonts are system-first.** Iowan Old Style / Charter for serif, system stack for sans. Only pull a Google Font if DESIGN.md specifies one.
|
||||||
|
|
||||||
|
## Anti-slop spot-check
|
||||||
|
|
||||||
|
Look at the page for two seconds. If your gut says any of:
|
||||||
|
|
||||||
|
- "looks like every Cursor / Linear / Vercel ripoff I've seen this month"
|
||||||
|
- "this could be any AI startup's homepage"
|
||||||
|
- "the feature row has an icon, a heading, and three lines of vague benefit copy"
|
||||||
|
|
||||||
|
…go back, replace one feature cell with something more specific to *this* product (a screenshot, a concrete example, a sample of the actual output), and remove one accent.
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# Web prototype layouts
|
||||||
|
|
||||||
|
**8 paste-ready section skeletons.** Drop into `<main id="content">` of `assets/template.html`. Don't write sections from scratch — pick the closest layout, paste, swap copy.
|
||||||
|
|
||||||
|
## Pre-flight (do this once before pasting anything)
|
||||||
|
|
||||||
|
1. **Read `assets/template.html`** through the end of the `<style>` block. Every class used below must exist there. If one is missing, add it to `<style>` rather than inlining it on each section.
|
||||||
|
2. **Pick a section list before writing copy.** Default rhythms:
|
||||||
|
- **Landing**: 1 hero → 2 features → 3 stat-row OR quote → 4 split → 6 cta-strip → footer
|
||||||
|
- **Marketing / editorial**: 1 hero-center → 7 log-list → 4 split → 6 cta-strip
|
||||||
|
- **Pricing / docs**: 1 hero-center → table-driven → 6 cta-strip
|
||||||
|
3. **One accent per screen, used at most twice.** The hero eyebrow and the primary button already use it; budget any third usage carefully.
|
||||||
|
|
||||||
|
## Class inventory (must exist in `template.html`)
|
||||||
|
|
||||||
|
> `section` `container` `hero` `hero-center` `hero-split` `hero-cta` `eyebrow` `lead` `h1` `h2` `h3` `meta` `num` `btn` `btn-primary` `btn-secondary` `btn-ghost` `btn-arrow` `card` `card-flat` `card-rule` `feature` `feature-mark` `stat` `stat-num` `stat-label` `stat-unit` `quote` `quote-mark` `quote-author` `pill` `tag` `field` `input` `textarea` `ds-table` `num-col` `ph-img` `square` `portrait` `wide` `rule` `rule-strong` `grid-2` `grid-3` `grid-4` `grid-2-1` `grid-1-2` `row` `row-between` `stack` `log-row` `pull` `topnav` `pagefoot`
|
||||||
|
|
||||||
|
If you reach for a class not on this list, define it in `<style>` first or use `style="…"` inline. Never invent a global class on a `<section>` that isn't backed by CSS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layout 1 — Hero, centered
|
||||||
|
|
||||||
|
Use when the page leads with a thesis sentence (most landings, most marketing pages). One eyebrow, one h1 (≤14 words), one lead sentence, two CTAs.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="section hero" data-od-id="hero">
|
||||||
|
<div class="container hero-center">
|
||||||
|
<p class="eyebrow">EYEBROW · CONTEXT</p>
|
||||||
|
<h1>One sharp sentence about what this is.</h1>
|
||||||
|
<p class="lead">One concrete-value subhead — what changes for the reader.</p>
|
||||||
|
<div class="hero-cta">
|
||||||
|
<button class="btn btn-primary">Primary action</button>
|
||||||
|
<button class="btn btn-secondary">Secondary</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout 2 — Hero, split (text + visual)
|
||||||
|
|
||||||
|
Use when there is a real product visual (product UI, screenshot, photograph). Left half copy, right half a `ph-img` placeholder the user replaces.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="section" data-od-id="hero-split">
|
||||||
|
<div class="container hero-split">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">EYEBROW · ROLE</p>
|
||||||
|
<h1>Headline that names the change.</h1>
|
||||||
|
<p class="lead" style="margin-top: 20px;">A short subhead — concrete, not corporate. Two sentences max.</p>
|
||||||
|
<div class="hero-cta" style="margin-top: 28px;">
|
||||||
|
<button class="btn btn-primary">Primary action</button>
|
||||||
|
<button class="btn btn-ghost btn-arrow">Read the story</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ph-img wide" aria-label="Hero visual placeholder">[ Hero visual · 16:9 ]</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout 3 — Feature triplet
|
||||||
|
|
||||||
|
Three feature cells. Lead with a small `<h2>` framing the row. Don't put an icon on every heading — one tasteful mark per cell, monoline.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="section" data-od-id="features">
|
||||||
|
<div class="container stack" style="gap: 56px;">
|
||||||
|
<div style="max-width: 36ch;">
|
||||||
|
<p class="eyebrow">WHAT'S DIFFERENT</p>
|
||||||
|
<h2>Three things you'll notice in the first ten minutes.</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid-3">
|
||||||
|
<div class="feature card-flat">
|
||||||
|
<div class="feature-mark">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M12 3v18M3 12h18"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>Specific feature one</h3>
|
||||||
|
<p>Two-sentence description that names the user value, not the technology.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature card-flat">
|
||||||
|
<div class="feature-mark">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="12" r="8"/><path d="M12 8v4l3 2"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>Specific feature two</h3>
|
||||||
|
<p>Two-sentence description that names the user value, not the technology.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature card-flat">
|
||||||
|
<div class="feature-mark">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 7h16M4 12h10M4 17h16"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3>Specific feature three</h3>
|
||||||
|
<p>Two-sentence description that names the user value, not the technology.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout 4 — Stat row (data billboard)
|
||||||
|
|
||||||
|
Use when there are real numbers. Three stats max — four feels like a brochure. **Don't invent metrics.** If you don't have a number, use a different layout.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="section" data-od-id="stats">
|
||||||
|
<div class="container">
|
||||||
|
<p class="eyebrow" style="margin-bottom: 40px;">BY THE NUMBERS · 2026</p>
|
||||||
|
<div class="grid-3">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-num num">38<span class="stat-unit">×</span></div>
|
||||||
|
<p class="stat-label">less data moved over the wire vs. naive sync, on real customer workloads.</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-num num">3,184</div>
|
||||||
|
<p class="stat-label">paying teams, including 14 of the YC W26 batch.</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-num num">$0.04<span class="stat-unit">/GB</span></div>
|
||||||
|
<p class="stat-label">average egress saved — typical $1,800/mo bill drops to $200.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout 5 — Pull quote (testimonial)
|
||||||
|
|
||||||
|
A single decisive quote with attribution. Use sparingly — one per page, never two in a row.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="section" data-od-id="quote">
|
||||||
|
<div class="container" style="max-width: 800px;">
|
||||||
|
<div class="quote-mark">"</div>
|
||||||
|
<blockquote class="quote">Filebase pays for itself in the first month. We were going to hire a dedicated DevOps person to babysit our sync — instead we just switched.</blockquote>
|
||||||
|
<p class="quote-author">— Mira Hassan, CTO at Northwind Studios</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout 6 — CTA strip (closing)
|
||||||
|
|
||||||
|
End the page on one decisive ask. Centered, generous whitespace, one primary button. No secondary unless the page has zero other buttons.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="section" data-od-id="cta-strip" style="text-align: center;">
|
||||||
|
<div class="container" style="max-width: 600px;">
|
||||||
|
<h2>Stop measuring meetings. Start measuring focus.</h2>
|
||||||
|
<p class="lead" style="margin: 16px auto 32px;">Free for solo. $4/mo per teammate after that.</p>
|
||||||
|
<button class="btn btn-primary">Start free</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout 7 — Log list (changelog / blog index / posts)
|
||||||
|
|
||||||
|
Editorial layout for a list of dated entries. Date in mono on the left, title + dek in the middle, optional pull stat on the right. Borders on top, never around — boxes feel like a brochure.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="section" data-od-id="log">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row-between" style="margin-bottom: 32px;">
|
||||||
|
<h2>Recent changes</h2>
|
||||||
|
<a class="btn btn-ghost btn-arrow" href="#">View all</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<article class="log-row">
|
||||||
|
<span class="meta">Apr 27, 2026</span>
|
||||||
|
<div>
|
||||||
|
<h3>Sync engine v3 — half the wire bytes</h3>
|
||||||
|
<p style="margin: 4px 0 0; color: var(--muted); font-size: 14px;">A new content-defined chunker that produces 38× fewer post-edit changes on Final Cut projects.</p>
|
||||||
|
</div>
|
||||||
|
<span class="pull meta">Engineering</span>
|
||||||
|
</article>
|
||||||
|
<article class="log-row">
|
||||||
|
<span class="meta">Apr 19, 2026</span>
|
||||||
|
<div>
|
||||||
|
<h3>Per-folder bandwidth budgets</h3>
|
||||||
|
<p style="margin: 4px 0 0; color: var(--muted); font-size: 14px;">Cap how much a single project can pull each month — useful for archive folders.</p>
|
||||||
|
</div>
|
||||||
|
<span class="pull meta">Product</span>
|
||||||
|
</article>
|
||||||
|
<article class="log-row">
|
||||||
|
<span class="meta">Apr 04, 2026</span>
|
||||||
|
<div>
|
||||||
|
<h3>S3 + R2 dual-region replication</h3>
|
||||||
|
<p style="margin: 4px 0 0; color: var(--muted); font-size: 14px;">Two providers, automatic failover. Enterprise tier only for now.</p>
|
||||||
|
</div>
|
||||||
|
<span class="pull meta">Infra</span>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout 8 — Comparison table (pricing, plan matrix, before/after)
|
||||||
|
|
||||||
|
Hairline borders, mono numerics, one column highlighted via an accent border. Don't put the whole row in surface-color — that screams "table".
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section class="section" data-od-id="pricing">
|
||||||
|
<div class="container">
|
||||||
|
<div style="text-align: center; max-width: 36ch; margin: 0 auto 56px;">
|
||||||
|
<p class="eyebrow">PRICING</p>
|
||||||
|
<h2>One row of features. Three lines of pricing.</h2>
|
||||||
|
</div>
|
||||||
|
<table class="ds-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Feature</th>
|
||||||
|
<th class="num-col">Solo</th>
|
||||||
|
<th class="num-col">Team</th>
|
||||||
|
<th class="num-col">Enterprise</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Sync engine v3</td><td class="num-col">✓</td><td class="num-col">✓</td><td class="num-col">✓</td></tr>
|
||||||
|
<tr><td>Per-folder budgets</td><td class="num-col">—</td><td class="num-col">✓</td><td class="num-col">✓</td></tr>
|
||||||
|
<tr><td>SAML / SCIM</td><td class="num-col">—</td><td class="num-col">—</td><td class="num-col">✓</td></tr>
|
||||||
|
<tr><td>Dedicated infra</td><td class="num-col">—</td><td class="num-col">—</td><td class="num-col">✓</td></tr>
|
||||||
|
<tr style="border-top: 1px solid var(--fg);">
|
||||||
|
<td><strong>Monthly</strong></td>
|
||||||
|
<td class="num-col"><strong>$0</strong></td>
|
||||||
|
<td class="num-col"><strong>$4 / seat</strong></td>
|
||||||
|
<td class="num-col"><strong>Talk to us</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section rhythm — when in doubt
|
||||||
|
|
||||||
|
For a 5-section landing:
|
||||||
|
1. Hero (Layout 1 or 2)
|
||||||
|
2. Features (Layout 3)
|
||||||
|
3. Stats *or* quote (Layout 4 or 5)
|
||||||
|
4. Split detail (custom, using `grid-2-1` / `grid-1-2`)
|
||||||
|
5. CTA + footer (Layout 6)
|
||||||
|
|
||||||
|
For a 4-section docs/marketing index:
|
||||||
|
1. Hero center (Layout 1)
|
||||||
|
2. Log list (Layout 7)
|
||||||
|
3. CTA + footer (Layout 6)
|
||||||
|
|
||||||
|
Two stat rows in a row, two quote blocks in a row, two feature triplets in a row — all visual fatigue. Alternate.
|
||||||
@@ -4,14 +4,13 @@ import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
Search, Hash, Plus, RefreshCw, Trash2, Save, RotateCcw, Loader2,
|
Search, Hash, Plus, RefreshCw, Trash2, Save, RotateCcw, Loader2,
|
||||||
Table2, Copy, Download, Share2, AlertTriangle, ArrowRight, Edit3,
|
Table2, AlertTriangle, Edit3,
|
||||||
Calendar, Type, Hash as HashIcon, Link2, Code, Minus, Layers,
|
Calendar, Type, Hash as HashIcon, Link2, Layers,
|
||||||
CheckCircle, Eye, Upload, LayoutGrid, X, Sparkles,
|
CheckCircle, X, Sparkles,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||||
import {
|
import {
|
||||||
getNumberingRulesFromTest,
|
getNumberingRulesFromTest,
|
||||||
getNumberingRuleById,
|
|
||||||
deleteNumberingRuleFromTest,
|
deleteNumberingRuleFromTest,
|
||||||
resetSequence,
|
resetSequence,
|
||||||
updateRuleSequence,
|
updateRuleSequence,
|
||||||
@@ -34,23 +33,25 @@ import {
|
|||||||
type ResetPeriod = "none" | "daily" | "monthly" | "yearly";
|
type ResetPeriod = "none" | "daily" | "monthly" | "yearly";
|
||||||
type SideFilter = "all" | "active" | "warn" | "unused";
|
type SideFilter = "all" | "active" | "warn" | "unused";
|
||||||
|
|
||||||
const PART_TONE_BY_TYPE: Record<CodePartType, string> = {
|
const PART_KO_LABEL: Record<CodePartType, string> = {
|
||||||
text: "text", // prefix
|
text: "고정",
|
||||||
date: "date",
|
date: "날짜",
|
||||||
sequence: "sequence",
|
sequence: "순번",
|
||||||
number: "number",
|
number: "숫자",
|
||||||
category: "category",
|
category: "카테고리",
|
||||||
reference: "reference",
|
reference: "참조",
|
||||||
};
|
};
|
||||||
|
|
||||||
const PART_LABEL_BY_TYPE: Record<CodePartType, string> = {
|
function buildPartEnTag(p: NumberingRulePart): string {
|
||||||
text: "TEXT",
|
switch (p.part_type) {
|
||||||
date: "DATE",
|
case "text": return "text";
|
||||||
sequence: "SEQ",
|
case "date": return p.auto_config?.date_format ?? "date";
|
||||||
number: "NUM",
|
case "sequence": return `${p.auto_config?.sequence_length ?? 4}자리`;
|
||||||
category: "CAT",
|
case "number": return `${p.auto_config?.number_length ?? 3}자리`;
|
||||||
reference: "REF",
|
case "category": return "cat";
|
||||||
};
|
case "reference": return "ref";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const RESET_LABEL: Record<ResetPeriod, string> = {
|
const RESET_LABEL: Record<ResetPeriod, string> = {
|
||||||
none: "없음",
|
none: "없음",
|
||||||
@@ -74,6 +75,17 @@ export default function NumberingRuleManagementPage() {
|
|||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [draftSequence, setDraftSequence] = useState<string>("");
|
const [draftSequence, setDraftSequence] = useState<string>("");
|
||||||
const [mutating, setMutating] = useState(false);
|
const [mutating, setMutating] = useState(false);
|
||||||
|
const draftRuleIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
const dirtyRef = useRef(false);
|
||||||
|
const editingRuleIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dirtyRef.current = dirty;
|
||||||
|
}, [dirty]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editingRuleIdRef.current = editingRule?.rule_id ? String(editingRule.rule_id) : null;
|
||||||
|
}, [editingRule?.rule_id]);
|
||||||
|
|
||||||
// 회사별 채번 목록 로드
|
// 회사별 채번 목록 로드
|
||||||
const loadRules = useCallback(async () => {
|
const loadRules = useCallback(async () => {
|
||||||
@@ -108,6 +120,13 @@ export default function NumberingRuleManagementPage() {
|
|||||||
}
|
}
|
||||||
const fromList = rules.find((r) => String(r.rule_id) === selectedRuleId);
|
const fromList = rules.find((r) => String(r.rule_id) === selectedRuleId);
|
||||||
if (fromList) {
|
if (fromList) {
|
||||||
|
if (
|
||||||
|
draftRuleIdsRef.current.has(selectedRuleId) &&
|
||||||
|
dirtyRef.current &&
|
||||||
|
editingRuleIdRef.current === selectedRuleId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const cloned = structuredClone(fromList);
|
const cloned = structuredClone(fromList);
|
||||||
setEditingRule(cloned);
|
setEditingRule(cloned);
|
||||||
setDraftSequence(String(cloned.current_sequence ?? 0));
|
setDraftSequence(String(cloned.current_sequence ?? 0));
|
||||||
@@ -145,7 +164,6 @@ export default function NumberingRuleManagementPage() {
|
|||||||
|
|
||||||
// 사이드바 섹션 분류
|
// 사이드바 섹션 분류
|
||||||
const groupedRules = useMemo(() => {
|
const groupedRules = useMemo(() => {
|
||||||
const live: NumberingRuleConfig[] = [];
|
|
||||||
const warn: NumberingRuleConfig[] = [];
|
const warn: NumberingRuleConfig[] = [];
|
||||||
const others: NumberingRuleConfig[] = [];
|
const others: NumberingRuleConfig[] = [];
|
||||||
const unused: NumberingRuleConfig[] = [];
|
const unused: NumberingRuleConfig[] = [];
|
||||||
@@ -191,6 +209,12 @@ export default function NumberingRuleManagementPage() {
|
|||||||
const resp = await saveNumberingRuleToTest(editingRule);
|
const resp = await saveNumberingRuleToTest(editingRule);
|
||||||
if (resp.success) {
|
if (resp.success) {
|
||||||
toast.success("채번 규칙이 저장되었습니다");
|
toast.success("채번 규칙이 저장되었습니다");
|
||||||
|
if (resp.data) {
|
||||||
|
draftRuleIdsRef.current.delete(String(editingRule.rule_id));
|
||||||
|
setRules((prev) => prev.map((r) => String(r.rule_id) === String(editingRule.rule_id) ? resp.data! : r));
|
||||||
|
setEditingRule(structuredClone(resp.data));
|
||||||
|
setSelectedRuleId(String(resp.data.rule_id));
|
||||||
|
}
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
setRefreshKey((k) => k + 1);
|
setRefreshKey((k) => k + 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -216,8 +240,18 @@ export default function NumberingRuleManagementPage() {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!editingRule) return;
|
if (!editingRule) return;
|
||||||
if (!confirm(`"${editingRule.rule_name}" 규칙을 삭제하시겠습니까?`)) return;
|
if (!confirm(`"${editingRule.rule_name}" 규칙을 삭제하시겠습니까?`)) return;
|
||||||
|
const ruleId = String(editingRule.rule_id);
|
||||||
|
if (draftRuleIdsRef.current.has(ruleId)) {
|
||||||
|
draftRuleIdsRef.current.delete(ruleId);
|
||||||
|
setRules((prev) => prev.filter((r) => String(r.rule_id) !== ruleId));
|
||||||
|
setSelectedRuleId(null);
|
||||||
|
setEditingRule(null);
|
||||||
|
setDirty(false);
|
||||||
|
toast.success("작성 중인 규칙을 삭제했습니다");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await deleteNumberingRuleFromTest(String(editingRule.rule_id));
|
const resp = await deleteNumberingRuleFromTest(ruleId);
|
||||||
if (resp.success) {
|
if (resp.success) {
|
||||||
toast.success("규칙이 삭제되었습니다");
|
toast.success("규칙이 삭제되었습니다");
|
||||||
setSelectedRuleId(null);
|
setSelectedRuleId(null);
|
||||||
@@ -233,9 +267,19 @@ export default function NumberingRuleManagementPage() {
|
|||||||
const handleResetSequence = async () => {
|
const handleResetSequence = async () => {
|
||||||
if (!editingRule?.rule_id) return;
|
if (!editingRule?.rule_id) return;
|
||||||
if (!confirm("시퀀스를 0 으로 초기화 하시겠습니까?")) return;
|
if (!confirm("시퀀스를 0 으로 초기화 하시겠습니까?")) return;
|
||||||
|
const ruleId = String(editingRule.rule_id);
|
||||||
|
if (draftRuleIdsRef.current.has(ruleId)) {
|
||||||
|
const nextRule = { ...editingRule, current_sequence: 0 };
|
||||||
|
setEditingRule(nextRule);
|
||||||
|
setRules((prev) => prev.map((r) => (String(r.rule_id) === ruleId ? nextRule : r)));
|
||||||
|
setDraftSequence("0");
|
||||||
|
setDirty(true);
|
||||||
|
toast.success("작성 중인 규칙의 시퀀스를 0으로 맞췄습니다");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setMutating(true);
|
setMutating(true);
|
||||||
try {
|
try {
|
||||||
const resp = await resetSequence(String(editingRule.rule_id));
|
const resp = await resetSequence(ruleId);
|
||||||
if (resp.success) {
|
if (resp.success) {
|
||||||
toast.success("시퀀스가 초기화되었습니다");
|
toast.success("시퀀스가 초기화되었습니다");
|
||||||
setDraftSequence("0");
|
setDraftSequence("0");
|
||||||
@@ -261,9 +305,18 @@ export default function NumberingRuleManagementPage() {
|
|||||||
toast.info("현재 시퀀스와 동일합니다");
|
toast.info("현재 시퀀스와 동일합니다");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const ruleId = String(editingRule.rule_id);
|
||||||
|
if (draftRuleIdsRef.current.has(ruleId)) {
|
||||||
|
const nextRule = { ...editingRule, current_sequence: newSeq };
|
||||||
|
setEditingRule(nextRule);
|
||||||
|
setRules((prev) => prev.map((r) => (String(r.rule_id) === ruleId ? nextRule : r)));
|
||||||
|
setDirty(true);
|
||||||
|
toast.success("작성 중인 규칙의 시퀀스를 반영했습니다");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setMutating(true);
|
setMutating(true);
|
||||||
try {
|
try {
|
||||||
const resp = await updateRuleSequence(String(editingRule.rule_id), newSeq);
|
const resp = await updateRuleSequence(ruleId, newSeq);
|
||||||
if (resp.success) {
|
if (resp.success) {
|
||||||
toast.success(`시퀀스 ${newSeq} 적용됨`);
|
toast.success(`시퀀스 ${newSeq} 적용됨`);
|
||||||
setRefreshKey((k) => k + 1);
|
setRefreshKey((k) => k + 1);
|
||||||
@@ -277,15 +330,6 @@ export default function NumberingRuleManagementPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePart = (order: number, patch: Partial<NumberingRulePart>) => {
|
|
||||||
if (!editingRule) return;
|
|
||||||
setEditingRule({
|
|
||||||
...editingRule,
|
|
||||||
parts: editingRule.parts.map((p) => (p.order === order ? { ...p, ...patch } : p)),
|
|
||||||
});
|
|
||||||
setDirty(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePartAutoConfig = (order: number, patch: Partial<NonNullable<NumberingRulePart["auto_config"]>>) => {
|
const updatePartAutoConfig = (order: number, patch: Partial<NonNullable<NumberingRulePart["auto_config"]>>) => {
|
||||||
if (!editingRule) return;
|
if (!editingRule) return;
|
||||||
setEditingRule({
|
setEditingRule({
|
||||||
@@ -297,12 +341,16 @@ export default function NumberingRuleManagementPage() {
|
|||||||
setDirty(true);
|
setDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addPart = (type: CodePartType) => {
|
const addPart = (type: CodePartType, insertAfterOrder?: number) => {
|
||||||
if (!editingRule) return;
|
if (!editingRule) return;
|
||||||
const order = (editingRule.parts?.length ?? 0) + 1;
|
const sortedParts = [...(editingRule.parts ?? [])].sort((a, b) => a.order - b.order);
|
||||||
|
const insertIndex =
|
||||||
|
insertAfterOrder == null
|
||||||
|
? sortedParts.length
|
||||||
|
: Math.max(0, sortedParts.findIndex((p) => p.order === insertAfterOrder) + 1);
|
||||||
const newPart: NumberingRulePart = {
|
const newPart: NumberingRulePart = {
|
||||||
id: `part-${Date.now()}`,
|
id: `part-${Date.now()}-${type}`,
|
||||||
order,
|
order: insertIndex + 1,
|
||||||
part_type: type,
|
part_type: type,
|
||||||
generation_method: "auto",
|
generation_method: "auto",
|
||||||
auto_config:
|
auto_config:
|
||||||
@@ -316,8 +364,13 @@ export default function NumberingRuleManagementPage() {
|
|||||||
? { number_length: 3, number_value: 1 }
|
? { number_length: 3, number_value: 1 }
|
||||||
: {},
|
: {},
|
||||||
};
|
};
|
||||||
setEditingRule({ ...editingRule, parts: [...editingRule.parts, newPart] });
|
const nextParts = [
|
||||||
setSelectedPartOrder(order);
|
...sortedParts.slice(0, insertIndex),
|
||||||
|
newPart,
|
||||||
|
...sortedParts.slice(insertIndex),
|
||||||
|
].map((p, i) => ({ ...p, order: i + 1 }));
|
||||||
|
setEditingRule({ ...editingRule, parts: nextParts });
|
||||||
|
setSelectedPartOrder(insertIndex + 1);
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -342,6 +395,7 @@ export default function NumberingRuleManagementPage() {
|
|||||||
const seq = (editingRule.current_sequence ?? 0) + 1;
|
const seq = (editingRule.current_sequence ?? 0) + 1;
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
return editingRule.parts
|
return editingRule.parts
|
||||||
|
.slice()
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
.map((p) => renderPartValue(p, today, seq))
|
.map((p) => renderPartValue(p, today, seq))
|
||||||
.join(sep);
|
.join(sep);
|
||||||
@@ -353,55 +407,82 @@ export default function NumberingRuleManagementPage() {
|
|||||||
const seq = editingRule.current_sequence ?? 0;
|
const seq = editingRule.current_sequence ?? 0;
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
return editingRule.parts
|
return editingRule.parts
|
||||||
|
.slice()
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
.map((p) => renderPartValue(p, today, seq))
|
.map((p) => renderPartValue(p, today, seq))
|
||||||
.join(sep);
|
.join(sep);
|
||||||
}, [editingRule]);
|
}, [editingRule]);
|
||||||
|
|
||||||
|
const handleCreateDraftRule = (name?: string, preset: "blank" | "simple" | "monthly" | "daily" = "blank") => {
|
||||||
|
const id = `rule-${Date.now()}`;
|
||||||
|
const baseName = name?.trim() || (
|
||||||
|
preset === "monthly" ? "월별 리셋 채번" :
|
||||||
|
preset === "daily" ? "일별 리셋 채번" :
|
||||||
|
preset === "simple" ? "간단 채번" :
|
||||||
|
"새 채번 규칙"
|
||||||
|
);
|
||||||
|
const makePart = (order: number, part_type: CodePartType, auto_config: NumberingRulePart["auto_config"]): NumberingRulePart => ({
|
||||||
|
id: `part-${Date.now()}-${order}-${part_type}`,
|
||||||
|
order,
|
||||||
|
part_type,
|
||||||
|
generation_method: "auto",
|
||||||
|
auto_config,
|
||||||
|
});
|
||||||
|
const presetParts =
|
||||||
|
preset === "blank"
|
||||||
|
? []
|
||||||
|
: preset === "daily"
|
||||||
|
? [
|
||||||
|
makePart(1, "text", { text_value: "PFX" }),
|
||||||
|
makePart(2, "date", { date_format: "YYYYMMDD" }),
|
||||||
|
makePart(3, "sequence", { sequence_length: 3, start_from: 1 }),
|
||||||
|
]
|
||||||
|
: preset === "monthly"
|
||||||
|
? [
|
||||||
|
makePart(1, "text", { text_value: "PFX" }),
|
||||||
|
makePart(2, "date", { date_format: "YYYYMM" }),
|
||||||
|
makePart(3, "sequence", { sequence_length: 4, start_from: 1 }),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
makePart(1, "text", { text_value: "PFX" }),
|
||||||
|
makePart(2, "sequence", { sequence_length: 4, start_from: 1 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const draft: NumberingRuleConfig = {
|
||||||
|
rule_id: id,
|
||||||
|
rule_name: baseName,
|
||||||
|
parts: presetParts,
|
||||||
|
separator: "-",
|
||||||
|
reset_period: preset === "daily" ? "daily" : preset === "monthly" ? "monthly" : "none",
|
||||||
|
current_sequence: 0,
|
||||||
|
};
|
||||||
|
draftRuleIdsRef.current.add(id);
|
||||||
|
setRules((prev) => [draft, ...prev]);
|
||||||
|
setSelectedRuleId(id);
|
||||||
|
setEditingRule(structuredClone(draft));
|
||||||
|
setSelectedPartOrder(draft.parts[0]?.order ?? null);
|
||||||
|
setDraftSequence("0");
|
||||||
|
setDirty(true);
|
||||||
|
setCmdkOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="v5-nrm">
|
<div className="v5-nrm">
|
||||||
<div
|
<div className="v5-nrm-topbar">
|
||||||
style={{
|
<div className="v5-nrm-topcopy">
|
||||||
padding: "1.1rem 1.4rem .9rem",
|
<h1>
|
||||||
borderBottom: "1px solid var(--v5-border)",
|
<span className="v5-nrm-titlemark">#</span>
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-end",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: "1rem",
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h1
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontSize: "1.1rem",
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: "-.02em",
|
|
||||||
color: "var(--v5-text)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
채번 관리
|
채번 관리
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<p>규칙을 설계하고 컬럼에 연결해 실제 발번 흐름을 관리합니다.</p>
|
||||||
style={{
|
|
||||||
margin: ".3rem 0 0",
|
|
||||||
fontSize: ".72rem",
|
|
||||||
color: "var(--v5-text-muted)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
채번 규칙을 만들어두고 컬럼에 자유롭게 연결합니다 ·{" "}
|
|
||||||
<b style={{ color: "var(--v5-text)" }}>{stats.total}</b> 규칙 ·{" "}
|
|
||||||
<b style={{ color: "rgb(var(--v5-green-rgb))" }}>{stats.linked}</b> 연결
|
|
||||||
{stats.unused > 0 && (
|
|
||||||
<> · <b style={{ color: "var(--v5-text)" }}>{stats.unused}</b> 미사용</>
|
|
||||||
)}
|
|
||||||
{stats.warn > 0 && (
|
|
||||||
<> · <b style={{ color: "rgb(var(--v5-amber-rgb))" }}>{stats.warn}</b> 충돌</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: ".35rem", alignItems: "center" }}>
|
<div className="v5-nrm-topstats" aria-label="채번 관리 요약">
|
||||||
|
<span className="v5-nrm-stat"><b>{stats.total}</b> 규칙</span>
|
||||||
|
<span className="v5-nrm-stat green"><b>{stats.linked}</b> 연결</span>
|
||||||
|
<span className="v5-nrm-stat"><b>{stats.unused}</b> 미사용</span>
|
||||||
|
{stats.warn > 0 && <span className="v5-nrm-stat amber"><b>{stats.warn}</b> 충돌</span>}
|
||||||
|
</div>
|
||||||
|
<div className="v5-nrm-topactions">
|
||||||
<span className="v5-nrm-kbd">⌘ K</span>
|
<span className="v5-nrm-kbd">⌘ K</span>
|
||||||
<button
|
<button
|
||||||
className="v5-nrm-btn icon-only ghost"
|
className="v5-nrm-btn icon-only ghost"
|
||||||
@@ -410,9 +491,6 @@ export default function NumberingRuleManagementPage() {
|
|||||||
>
|
>
|
||||||
{loading ? <Loader2 size={13} className="animate-spin" /> : <RefreshCw size={13} />}
|
{loading ? <Loader2 size={13} className="animate-spin" /> : <RefreshCw size={13} />}
|
||||||
</button>
|
</button>
|
||||||
<button className="v5-nrm-btn ghost">
|
|
||||||
<Download size={13} /> 내보내기
|
|
||||||
</button>
|
|
||||||
<button className="v5-nrm-btn primary" onClick={() => setCmdkOpen(true)}>
|
<button className="v5-nrm-btn primary" onClick={() => setCmdkOpen(true)}>
|
||||||
<Plus size={13} /> 새 채번
|
<Plus size={13} /> 새 채번
|
||||||
</button>
|
</button>
|
||||||
@@ -462,7 +540,7 @@ export default function NumberingRuleManagementPage() {
|
|||||||
<div className="v5-nrm-empty">
|
<div className="v5-nrm-empty">
|
||||||
<Hash size={28} />
|
<Hash size={28} />
|
||||||
<div>채번 규칙이 없습니다</div>
|
<div>채번 규칙이 없습니다</div>
|
||||||
<div className="hint">상단 "새 채번" 또는 ⌘K 로 시작</div>
|
<div className="hint">상단 "새 채번" 또는 ⌘K 로 시작</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -523,17 +601,10 @@ export default function NumberingRuleManagementPage() {
|
|||||||
<div className="v5-nrm-side-foot">
|
<div className="v5-nrm-side-foot">
|
||||||
<button
|
<button
|
||||||
className="v5-nrm-btn sm"
|
className="v5-nrm-btn sm"
|
||||||
style={{ flex: 1 }}
|
|
||||||
onClick={() => setCmdkOpen(true)}
|
onClick={() => setCmdkOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus size={11} /> 새 채번
|
<Plus size={11} /> 새 채번
|
||||||
</button>
|
</button>
|
||||||
<button className="v5-nrm-btn sm icon-only" title="가져오기">
|
|
||||||
<Upload size={11} />
|
|
||||||
</button>
|
|
||||||
<button className="v5-nrm-btn sm icon-only" title="템플릿">
|
|
||||||
<LayoutGrid size={11} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -549,85 +620,114 @@ export default function NumberingRuleManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* HERO */}
|
{/* DETAIL HEAD */}
|
||||||
<div className="v5-nrm-hero">
|
<div className="v5-nrm-detail-head">
|
||||||
<div className="v5-nrm-hero-top">
|
<div className="l">
|
||||||
<div className="v5-nrm-tone cyan" style={{ width: 40, height: 40, borderRadius: 10 }}>
|
<h2>
|
||||||
<Hash size={18} />
|
{editingRule.rule_name || "(이름 없음)"}
|
||||||
|
{editingRule.parts?.length > 0 && (
|
||||||
|
<span className="v5-nrm-chip active">사용 중</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className="v5-nrm-chip"
|
||||||
|
style={{ fontFamily: "var(--v5-font-mono)", fontSize: ".56rem" }}
|
||||||
|
>
|
||||||
|
{editingRule.rule_id}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div className="meta">
|
||||||
|
{editingRule.created_at && (
|
||||||
|
<span><b>생성</b> {editingRule.created_at.slice(0, 10)}</span>
|
||||||
|
)}
|
||||||
|
{editingRule.updated_at && (
|
||||||
|
<span><b>마지막 수정</b> {editingRule.updated_at.slice(0, 16).replace("T", " ")}</span>
|
||||||
|
)}
|
||||||
|
{editingRule.created_by && (
|
||||||
|
<span><b>by</b> {editingRule.created_by}</span>
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
<b>지금까지</b> {(editingRule.current_sequence ?? 0).toLocaleString()}건 발번
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-hero-info">
|
</div>
|
||||||
<div className="v5-nrm-hero-row1">
|
<div className="r">
|
||||||
<h2>{editingRule.rule_name || "(이름 없음)"}</h2>
|
<button
|
||||||
{editingRule.parts?.length > 0 && (
|
className="v5-nrm-btn sm ghost danger"
|
||||||
<span className="v5-nrm-chip active live">LIVE</span>
|
title="삭제"
|
||||||
)}
|
onClick={handleDelete}
|
||||||
<span className="v5-nrm-chip">{editingRule.rule_id}</span>
|
>
|
||||||
|
<Trash2 size={11} /> 삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HERO PREVIEW (이 채번이 만드는 코드) */}
|
||||||
|
<div className="v5-nrm-hero-wrap">
|
||||||
|
{editingRule.parts.length === 0 ? (
|
||||||
|
<div className="v5-nrm-hero-empty">
|
||||||
|
<Hash size={24} />
|
||||||
|
<b>아직 코드 구조가 없습니다</b>
|
||||||
|
<span>아래 팔레트에서 첫 조각을 추가하면 다음 발번 preview가 만들어집니다.</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="v5-nrm-hero-card">
|
||||||
|
<span className="v5-nrm-hero-tab">
|
||||||
|
<HashIcon size={11} /> NEXT CODE
|
||||||
|
</span>
|
||||||
|
<span className="v5-nrm-hero-corner">
|
||||||
|
{editingRule.table_name && editingRule.column_name
|
||||||
|
? `${editingRule.table_name}.${editingRule.column_name}`
|
||||||
|
: "미연결 규칙"}
|
||||||
|
</span>
|
||||||
|
<div className="v5-nrm-hero-code" aria-label={`다음 발번 ${previewCode}`}>
|
||||||
|
{editingRule.parts
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
.map((p, idx, arr) => {
|
||||||
|
const today = new Date();
|
||||||
|
const seq = (editingRule.current_sequence ?? 0) + 1;
|
||||||
|
const val = renderPartValue(p, today, seq) || "—";
|
||||||
|
return (
|
||||||
|
<span key={`hero-${p.order}-${p.id}`} className="v5-nrm-hero-piece-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`v5-nrm-hero-part ${p.part_type}${selectedPartOrder === p.order ? " sel" : ""}`}
|
||||||
|
onClick={() => setSelectedPartOrder(p.order)}
|
||||||
|
>
|
||||||
|
<span className="v">{val}</span>
|
||||||
|
<span className="lbl">{PART_KO_LABEL[p.part_type]}</span>
|
||||||
|
</button>
|
||||||
|
{idx < arr.length - 1 && (
|
||||||
|
<span className="v5-nrm-hero-sep">{editingRule.separator || "-"}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-hero-meta">
|
<div className="v5-nrm-hero-intent">
|
||||||
{editingRule.created_at && (
|
<b>{previewCode || "—"}</b> 로 다음 값이 생성됩니다.
|
||||||
<span><b>생성</b> {editingRule.created_at.slice(0, 10)}</span>
|
</div>
|
||||||
)}
|
<div className="v5-nrm-hero-foot">
|
||||||
{editingRule.updated_at && (
|
<span className="it"><span className="k">현재</span><span className="v">{currentCode || "—"}</span></span>
|
||||||
<span><b>수정</b> {editingRule.updated_at.slice(0, 16).replace("T", " ")}</span>
|
<span className="it"><span className="k">다음</span><span className="v up">{previewCode || "—"}</span></span>
|
||||||
)}
|
<span className="it"><span className="k">순번</span><span className="v">{(editingRule.current_sequence ?? 0).toLocaleString()} → {((editingRule.current_sequence ?? 0) + 1).toLocaleString()}</span></span>
|
||||||
{editingRule.created_by && (
|
<span className="it">
|
||||||
<span><b>by</b> {editingRule.created_by}</span>
|
<span className="k">리셋</span>
|
||||||
)}
|
<span className="v">
|
||||||
<span>
|
{RESET_PERIOD_OPTIONS.find((o) => o.value === editingRule.reset_period)?.label ?? "초기화 안함"}
|
||||||
<b>연결 컬럼</b> {editingRule.table_name && editingRule.column_name ? 1 : 0}
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-hero-actions">
|
)}
|
||||||
<button className="v5-nrm-btn sm icon-only ghost" title="복제">
|
|
||||||
<Copy size={11} />
|
|
||||||
</button>
|
|
||||||
<button className="v5-nrm-btn sm icon-only ghost" title="내보내기">
|
|
||||||
<Download size={11} />
|
|
||||||
</button>
|
|
||||||
<button className="v5-nrm-btn sm icon-only ghost" title="공유">
|
|
||||||
<Share2 size={11} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="v5-nrm-btn sm icon-only ghost danger"
|
|
||||||
title="삭제"
|
|
||||||
onClick={handleDelete}
|
|
||||||
>
|
|
||||||
<Trash2 size={11} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="v5-nrm-hero-codes">
|
|
||||||
<div className="v5-nrm-hero-code-block">
|
|
||||||
<div className="lbl">현재 발번 시퀀스</div>
|
|
||||||
<div className="code" style={{ fontSize: "1.3rem" }}>
|
|
||||||
{currentCode || <span style={{ color: "var(--v5-text-muted)" }}>(파트 없음)</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="v5-nrm-hero-arrow">
|
|
||||||
<ArrowRight size={18} />
|
|
||||||
</span>
|
|
||||||
<div className="v5-nrm-hero-code-block right">
|
|
||||||
<div className="lbl">다음 발번</div>
|
|
||||||
<div className="code">
|
|
||||||
{previewCode || "—"}
|
|
||||||
</div>
|
|
||||||
<div className="sub">
|
|
||||||
시퀀스 {editingRule.current_sequence ?? 0} → {(editingRule.current_sequence ?? 0) + 1}
|
|
||||||
{editingRule.reset_period && editingRule.reset_period !== "none" && (
|
|
||||||
<> · {RESET_PERIOD_OPTIONS.find((o) => o.value === editingRule.reset_period)?.label}</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PIPELINE EDITOR */}
|
{/* PIPELINE EDITOR */}
|
||||||
<div className="v5-nrm-row">
|
<div className="v5-nrm-row">
|
||||||
<div className="v5-nrm-row-hd">
|
<div className="v5-nrm-row-hd">
|
||||||
<h3>코드 파이프라인</h3>
|
<h3>코드를 이루는 조각</h3>
|
||||||
<span className="num">{editingRule.parts.length} / 8</span>
|
<span className="num">{editingRule.parts.length}개</span>
|
||||||
<span className="desc">part 클릭 = 인스펙터</span>
|
<span className="desc">조각 클릭 = 인스펙터 · 사이 hover = 추가</span>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<button className="v5-nrm-btn sm ghost" onClick={handleRevert}>
|
<button className="v5-nrm-btn sm ghost" onClick={handleRevert}>
|
||||||
<RotateCcw size={11} /> 초기화
|
<RotateCcw size={11} /> 초기화
|
||||||
@@ -637,31 +737,54 @@ export default function NumberingRuleManagementPage() {
|
|||||||
|
|
||||||
<div className="v5-nrm-pipe-canvas">
|
<div className="v5-nrm-pipe-canvas">
|
||||||
{editingRule.parts.length === 0 ? (
|
{editingRule.parts.length === 0 ? (
|
||||||
<div style={{ flex: 1, textAlign: "center", color: "var(--v5-text-muted)", padding: ".75rem", fontSize: ".72rem" }}>
|
<div
|
||||||
아래 팔레트에서 파트를 추가하세요
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
textAlign: "center",
|
||||||
|
color: "var(--v5-text-muted)",
|
||||||
|
padding: ".75rem",
|
||||||
|
fontSize: ".72rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
아래 팔레트에서 조각을 추가하세요
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
editingRule.parts.sort((a, b) => a.order - b.order).map((p, idx, arr) => (
|
editingRule.parts
|
||||||
<PipelineBlock
|
.slice()
|
||||||
key={`${p.order}-${p.id}`}
|
.sort((a, b) => a.order - b.order)
|
||||||
part={p}
|
.map((p, idx, arr) => (
|
||||||
selected={selectedPartOrder === p.order}
|
<span key={`pipe-${p.order}-${p.id}`} style={{ display: "contents" }}>
|
||||||
onClick={() => setSelectedPartOrder(p.order)}
|
<PipelineBlock
|
||||||
onRemove={() => removePart(p.order)}
|
part={p}
|
||||||
isLast={idx === arr.length - 1}
|
selected={selectedPartOrder === p.order}
|
||||||
separator={editingRule.separator ?? "-"}
|
onClick={() => setSelectedPartOrder(p.order)}
|
||||||
/>
|
onRemove={() => removePart(p.order)}
|
||||||
))
|
/>
|
||||||
|
{idx < arr.length - 1 && (
|
||||||
|
<span className="v5-nrm-slot">
|
||||||
|
<span className="sep">{editingRule.separator || "-"}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="add-here"
|
||||||
|
title="여기에 조각 추가"
|
||||||
|
onClick={() => addPart("sequence", p.order)}
|
||||||
|
>
|
||||||
|
<Plus size={9} />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
<button className="v5-nrm-drop" onClick={() => addPart("sequence")}>
|
<button className="v5-nrm-drop" onClick={() => addPart("sequence")}>
|
||||||
<Plus size={12} /> 추가
|
<Plus size={11} /> 끝에 추가
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedPart && (
|
{selectedPart && (
|
||||||
<PartInspector
|
<PartInspector
|
||||||
|
key={selectedPart.id}
|
||||||
part={selectedPart}
|
part={selectedPart}
|
||||||
onUpdate={(patch) => updatePart(selectedPart.order, patch)}
|
|
||||||
onUpdateConfig={(patch) => updatePartAutoConfig(selectedPart.order, patch)}
|
onUpdateConfig={(patch) => updatePartAutoConfig(selectedPart.order, patch)}
|
||||||
onRemove={() => removePart(selectedPart.order)}
|
onRemove={() => removePart(selectedPart.order)}
|
||||||
rule={editingRule}
|
rule={editingRule}
|
||||||
@@ -673,24 +796,24 @@ export default function NumberingRuleManagementPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="v5-nrm-palette">
|
<div className="v5-nrm-palette">
|
||||||
<span className="lbl">+ add part</span>
|
<span className="lbl">조각 종류:</span>
|
||||||
<button className="v5-nrm-palette-item" onClick={() => addPart("text")}>
|
<button className="v5-nrm-palette-item" onClick={() => addPart("text")}>
|
||||||
<Type size={11} /> text
|
<Type size={11} /> 고정 글자 <span className="en">text</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="v5-nrm-palette-item" onClick={() => addPart("date")}>
|
<button className="v5-nrm-palette-item" onClick={() => addPart("date")}>
|
||||||
<Calendar size={11} /> date
|
<Calendar size={11} /> 날짜 <span className="en">date</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="v5-nrm-palette-item" onClick={() => addPart("sequence")}>
|
<button className="v5-nrm-palette-item" onClick={() => addPart("sequence")}>
|
||||||
<HashIcon size={11} /> sequence
|
<HashIcon size={11} /> 순번 <span className="en">seq</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="v5-nrm-palette-item" onClick={() => addPart("number")}>
|
<button className="v5-nrm-palette-item" onClick={() => addPart("number")}>
|
||||||
<Edit3 size={11} /> number
|
<Edit3 size={11} /> 고정 숫자 <span className="en">num</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="v5-nrm-palette-item" onClick={() => addPart("category")}>
|
<button className="v5-nrm-palette-item" onClick={() => addPart("category")}>
|
||||||
<Layers size={11} /> category
|
<Layers size={11} /> 카테고리 <span className="en">cat</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="v5-nrm-palette-item" onClick={() => addPart("reference")}>
|
<button className="v5-nrm-palette-item" onClick={() => addPart("reference")}>
|
||||||
<Link2 size={11} /> reference
|
<Link2 size={11} /> 참조 <span className="en">ref</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -811,6 +934,7 @@ export default function NumberingRuleManagementPage() {
|
|||||||
open={cmdkOpen}
|
open={cmdkOpen}
|
||||||
onClose={() => setCmdkOpen(false)}
|
onClose={() => setCmdkOpen(false)}
|
||||||
rules={rules}
|
rules={rules}
|
||||||
|
onCreateRule={handleCreateDraftRule}
|
||||||
onSelectRule={(id) => {
|
onSelectRule={(id) => {
|
||||||
setSelectedRuleId(id);
|
setSelectedRuleId(id);
|
||||||
setCmdkOpen(false);
|
setCmdkOpen(false);
|
||||||
@@ -870,57 +994,49 @@ function PipelineBlock({
|
|||||||
selected,
|
selected,
|
||||||
onClick,
|
onClick,
|
||||||
onRemove,
|
onRemove,
|
||||||
isLast,
|
|
||||||
separator,
|
|
||||||
}: {
|
}: {
|
||||||
part: NumberingRulePart;
|
part: NumberingRulePart;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
isLast: boolean;
|
|
||||||
separator: string;
|
|
||||||
}) {
|
}) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const previewSeq = 1;
|
const previewSeq = 1;
|
||||||
const val = renderPartValue(part, today, previewSeq);
|
const val = renderPartValue(part, today, previewSeq);
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
className={`v5-nrm-block ${part.part_type}${selected ? " sel" : ""}`}
|
||||||
className={`v5-nrm-block ${part.part_type}${selected ? " sel" : ""}`}
|
onClick={onClick}
|
||||||
onClick={onClick}
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="x"
|
||||||
|
title="이 조각 삭제"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="pin">{part.order}</span>
|
×
|
||||||
<span
|
</button>
|
||||||
className="x"
|
<div className="top-row">
|
||||||
onClick={(e) => {
|
<span className="ord">{part.order}번</span>
|
||||||
e.stopPropagation();
|
<span className="lbl">{PART_KO_LABEL[part.part_type]}</span>
|
||||||
onRemove();
|
<span className="typ-en">{buildPartEnTag(part)}</span>
|
||||||
}}
|
|
||||||
>×</span>
|
|
||||||
<span className="typ">
|
|
||||||
{PART_LABEL_BY_TYPE[part.part_type]}
|
|
||||||
{part.part_type === "date" && part.auto_config?.date_format ? ` · ${part.auto_config.date_format}` : ""}
|
|
||||||
{part.part_type === "sequence" && part.auto_config?.sequence_length
|
|
||||||
? ` · ${part.auto_config.sequence_length}d`
|
|
||||||
: ""}
|
|
||||||
</span>
|
|
||||||
<span className="val">{val || "—"}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{!isLast && <span className="v5-nrm-jn">{separator}</span>}
|
<span className="val">{val || "—"}</span>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PartInspector({
|
function PartInspector({
|
||||||
part,
|
part,
|
||||||
onUpdate,
|
|
||||||
onUpdateConfig,
|
onUpdateConfig,
|
||||||
onRemove,
|
onRemove,
|
||||||
rule,
|
rule,
|
||||||
onUpdateRule,
|
onUpdateRule,
|
||||||
}: {
|
}: {
|
||||||
part: NumberingRulePart;
|
part: NumberingRulePart;
|
||||||
onUpdate: (patch: Partial<NumberingRulePart>) => void;
|
|
||||||
onUpdateConfig: (patch: Partial<NonNullable<NumberingRulePart["auto_config"]>>) => void;
|
onUpdateConfig: (patch: Partial<NonNullable<NumberingRulePart["auto_config"]>>) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
rule: NumberingRuleConfig;
|
rule: NumberingRuleConfig;
|
||||||
@@ -930,13 +1046,13 @@ function PartInspector({
|
|||||||
<div className="v5-nrm-insp">
|
<div className="v5-nrm-insp">
|
||||||
<div className="v5-nrm-insp-hd">
|
<div className="v5-nrm-insp-hd">
|
||||||
<div className="l">
|
<div className="l">
|
||||||
<span className="pin">#{part.order}</span>
|
<span className="pin">{part.order}번 조각</span>
|
||||||
<span>
|
<span>
|
||||||
<b>{PART_LABEL_BY_TYPE[part.part_type]}</b> 설정
|
<b>{PART_KO_LABEL[part.part_type]}</b> 설정
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="v5-nrm-btn sm ghost danger" onClick={onRemove}>
|
<button className="v5-nrm-btn sm ghost danger" onClick={onRemove}>
|
||||||
<Trash2 size={11} /> 삭제
|
<Trash2 size={11} /> 이 조각 삭제
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-insp-grid">
|
<div className="v5-nrm-insp-grid">
|
||||||
@@ -975,7 +1091,7 @@ function PartInspector({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-insp-field grow">
|
<div className="v5-nrm-insp-field grow">
|
||||||
<label>리셋 주기 (규칙 전체)</label>
|
<label>이 채번의 초기화 주기</label>
|
||||||
<div className="v5-nrm-seg">
|
<div className="v5-nrm-seg">
|
||||||
{(["none", "daily", "monthly", "yearly"] as ResetPeriod[]).map((p) => (
|
{(["none", "daily", "monthly", "yearly"] as ResetPeriod[]).map((p) => (
|
||||||
<button
|
<button
|
||||||
@@ -987,6 +1103,13 @@ function PartInspector({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{rule.reset_period && rule.reset_period !== "none" && (
|
||||||
|
<span className="hint">
|
||||||
|
{rule.reset_period === "daily" && "매일 00:00 에 순번이 1 부터 다시 시작"}
|
||||||
|
{rule.reset_period === "monthly" && "매월 1일 00:00 에 순번이 1 부터 다시 시작"}
|
||||||
|
{rule.reset_period === "yearly" && "매년 1월 1일 00:00 에 순번이 1 부터 다시 시작"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1025,6 +1148,7 @@ function PartInspector({
|
|||||||
onClick={() => onUpdateConfig({ date_format: f.value })}
|
onClick={() => onUpdateConfig({ date_format: f.value })}
|
||||||
>
|
>
|
||||||
{f.value}
|
{f.value}
|
||||||
|
<span className="sub">{f.example}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1072,9 +1196,9 @@ function UsageList({ rule }: { rule: NumberingRuleConfig }) {
|
|||||||
아직 어떤 컬럼에도 연결되지 않음
|
아직 어떤 컬럼에도 연결되지 않음
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button className="v5-nrm-usage-add">
|
<div className="v5-nrm-usage-note">
|
||||||
<Plus size={11} /> 컬럼에 이 채번 연결 — 테이블 타입 관리로 이동
|
<Table2 size={11} /> 테이블 타입 관리에서 채번 컬럼에 이 규칙을 연결하세요.
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1083,11 +1207,13 @@ function CommandPalette({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
rules,
|
rules,
|
||||||
|
onCreateRule,
|
||||||
onSelectRule,
|
onSelectRule,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
rules: NumberingRuleConfig[];
|
rules: NumberingRuleConfig[];
|
||||||
|
onCreateRule: (name?: string, preset?: "blank" | "simple" | "monthly" | "daily") => void;
|
||||||
onSelectRule: (id: string) => void;
|
onSelectRule: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@@ -1131,13 +1257,16 @@ function CommandPalette({
|
|||||||
<div className="v5-nrm-cmdk-group">
|
<div className="v5-nrm-cmdk-group">
|
||||||
<span>빠른 액션</span>
|
<span>빠른 액션</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-cmdk-item action focus">
|
<div
|
||||||
|
className="v5-nrm-cmdk-item action focus"
|
||||||
|
onClick={() => onCreateRule(query || undefined, "blank")}
|
||||||
|
>
|
||||||
<span className="ic">
|
<span className="ic">
|
||||||
<Plus size={12} />
|
<Plus size={12} />
|
||||||
</span>
|
</span>
|
||||||
<div className="v5-nrm-cmdk-row">
|
<div className="v5-nrm-cmdk-row">
|
||||||
<div className="v5-nrm-cmdk-title">
|
<div className="v5-nrm-cmdk-title">
|
||||||
새 채번{query && <> — <b>"{query}"</b> 이름으로</>} · 빈 캔버스
|
새 채번{query && <> — <b>"{query}"</b> 이름으로</>} · 빈 캔버스
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-cmdk-meta">파이프라인 디자이너 열기</div>
|
<div className="v5-nrm-cmdk-meta">파이프라인 디자이너 열기</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1151,7 +1280,7 @@ function CommandPalette({
|
|||||||
<span>프리셋</span>
|
<span>프리셋</span>
|
||||||
<span className="num">3</span>
|
<span className="num">3</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-cmdk-item preset">
|
<div className="v5-nrm-cmdk-item preset" onClick={() => onCreateRule(query || undefined, "simple")}>
|
||||||
<span className="ic"><HashIcon size={12} /></span>
|
<span className="ic"><HashIcon size={12} /></span>
|
||||||
<div className="v5-nrm-cmdk-row">
|
<div className="v5-nrm-cmdk-row">
|
||||||
<div className="v5-nrm-cmdk-title">간단형</div>
|
<div className="v5-nrm-cmdk-title">간단형</div>
|
||||||
@@ -1160,7 +1289,7 @@ function CommandPalette({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-cmdk-item preset">
|
<div className="v5-nrm-cmdk-item preset" onClick={() => onCreateRule(query || undefined, "monthly")}>
|
||||||
<span className="ic"><Calendar size={12} /></span>
|
<span className="ic"><Calendar size={12} /></span>
|
||||||
<div className="v5-nrm-cmdk-row">
|
<div className="v5-nrm-cmdk-row">
|
||||||
<div className="v5-nrm-cmdk-title">월별 리셋 (권장)</div>
|
<div className="v5-nrm-cmdk-title">월별 리셋 (권장)</div>
|
||||||
@@ -1169,7 +1298,7 @@ function CommandPalette({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="v5-nrm-cmdk-item preset">
|
<div className="v5-nrm-cmdk-item preset" onClick={() => onCreateRule(query || undefined, "daily")}>
|
||||||
<span className="ic"><Sparkles size={12} /></span>
|
<span className="ic"><Sparkles size={12} /></span>
|
||||||
<div className="v5-nrm-cmdk-row">
|
<div className="v5-nrm-cmdk-row">
|
||||||
<div className="v5-nrm-cmdk-title">일별 리셋</div>
|
<div className="v5-nrm-cmdk-title">일별 리셋</div>
|
||||||
@@ -1304,9 +1433,9 @@ function buildPatternPreview(rule: NumberingRuleConfig): string {
|
|||||||
return `<span>${"0".repeat(len)}</span>`;
|
return `<span>${"0".repeat(len)}</span>`;
|
||||||
}
|
}
|
||||||
case "category":
|
case "category":
|
||||||
return `<span>CAT</span>`;
|
return "<span>CAT</span>";
|
||||||
case "reference":
|
case "reference":
|
||||||
return `<span>REF</span>`;
|
return "<span>REF</span>";
|
||||||
default:
|
default:
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
const dragLeaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const dragLeaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const dropGhostRef = useRef<HTMLDivElement>(null);
|
const dropGhostRef = useRef<HTMLDivElement>(null);
|
||||||
const prevTabCountRef = useRef(tabs.length);
|
const prevTabCountRef = useRef(tabs.length);
|
||||||
|
const activeTabElRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const [visibleCount, setVisibleCount] = useState(tabs.length);
|
const [visibleCount, setVisibleCount] = useState(tabs.length);
|
||||||
@@ -80,6 +81,12 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
const [dragState, setDragState] = useState<DragState | null>(null);
|
const [dragState, setDragState] = useState<DragState | null>(null);
|
||||||
const [externalDragIdx, setExternalDragIdx] = useState<number | null>(null);
|
const [externalDragIdx, setExternalDragIdx] = useState<number | null>(null);
|
||||||
const [dropGhost, setDropGhost] = useState<DropGhost | null>(null);
|
const [dropGhost, setDropGhost] = useState<DropGhost | null>(null);
|
||||||
|
const [closingIds, setClosingIds] = useState<Set<string>>(() => new Set());
|
||||||
|
const [indicatorStyle, setIndicatorStyle] = useState<{ left: number; width: number; opacity: number }>({
|
||||||
|
left: 0,
|
||||||
|
width: 0,
|
||||||
|
opacity: 0,
|
||||||
|
});
|
||||||
|
|
||||||
dragActiveRef.current = !!dragState;
|
dragActiveRef.current = !!dragState;
|
||||||
|
|
||||||
@@ -91,6 +98,53 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// --- 탭 leave 애니메이션: closing 표시 → 180ms 후 store splice ---
|
||||||
|
const CLOSE_ANIM_MS = 180;
|
||||||
|
const markClosing = useCallback((ids: string[]) => {
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
setClosingIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
ids.forEach((id) => next.add(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const handleCloseTab = useCallback(
|
||||||
|
(tabId: string) => {
|
||||||
|
markClosing([tabId]);
|
||||||
|
setTimeout(() => closeTab(tabId), CLOSE_ANIM_MS);
|
||||||
|
},
|
||||||
|
[closeTab, markClosing],
|
||||||
|
);
|
||||||
|
const handleCloseOtherTabs = useCallback(
|
||||||
|
(tabId: string) => {
|
||||||
|
markClosing(tabs.filter((t) => t.id !== tabId).map((t) => t.id));
|
||||||
|
setTimeout(() => closeOtherTabs(tabId), CLOSE_ANIM_MS);
|
||||||
|
},
|
||||||
|
[tabs, closeOtherTabs, markClosing],
|
||||||
|
);
|
||||||
|
const handleCloseTabsToLeft = useCallback(
|
||||||
|
(tabId: string) => {
|
||||||
|
const idx = tabs.findIndex((t) => t.id === tabId);
|
||||||
|
if (idx <= 0) return;
|
||||||
|
markClosing(tabs.slice(0, idx).map((t) => t.id));
|
||||||
|
setTimeout(() => closeTabsToLeft(tabId), CLOSE_ANIM_MS);
|
||||||
|
},
|
||||||
|
[tabs, closeTabsToLeft, markClosing],
|
||||||
|
);
|
||||||
|
const handleCloseTabsToRight = useCallback(
|
||||||
|
(tabId: string) => {
|
||||||
|
const idx = tabs.findIndex((t) => t.id === tabId);
|
||||||
|
if (idx === -1 || idx >= tabs.length - 1) return;
|
||||||
|
markClosing(tabs.slice(idx + 1).map((t) => t.id));
|
||||||
|
setTimeout(() => closeTabsToRight(tabId), CLOSE_ANIM_MS);
|
||||||
|
},
|
||||||
|
[tabs, closeTabsToRight, markClosing],
|
||||||
|
);
|
||||||
|
const handleCloseAllTabs = useCallback(() => {
|
||||||
|
markClosing(tabs.map((t) => t.id));
|
||||||
|
setTimeout(() => closeAllTabs(), CLOSE_ANIM_MS);
|
||||||
|
}, [tabs, closeAllTabs, markClosing]);
|
||||||
|
|
||||||
// --- 드롭 고스트: Web Animations API로 드롭 위치 → 목표 슬롯 이동 ---
|
// --- 드롭 고스트: Web Animations API로 드롭 위치 → 목표 슬롯 이동 ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dropGhost) return;
|
if (!dropGhost) return;
|
||||||
@@ -178,6 +232,20 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
prevTabCountRef.current = tabs.length;
|
prevTabCountRef.current = tabs.length;
|
||||||
}, [tabs.length, externalDragIdx]);
|
}, [tabs.length, externalDragIdx]);
|
||||||
|
|
||||||
|
// --- Active 탭 underline indicator 위치 측정 (Chrome devtools 식 슬라이드) ---
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (collapsed || !activeTabElRef.current || !containerRef.current) {
|
||||||
|
setIndicatorStyle((s) => (s.opacity === 0 ? s : { ...s, opacity: 0 }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tabEl = activeTabElRef.current;
|
||||||
|
const left = tabEl.offsetLeft;
|
||||||
|
const width = tabEl.offsetWidth;
|
||||||
|
setIndicatorStyle((s) =>
|
||||||
|
s.left === left && s.width === width && s.opacity === 1 ? s : { left, width, opacity: 1 },
|
||||||
|
);
|
||||||
|
}, [activeTabId, displayVisible, collapsed, tabs.length]);
|
||||||
|
|
||||||
const resolveMenuAndOpenTab = async (
|
const resolveMenuAndOpenTab = async (
|
||||||
menuName: string,
|
menuName: string,
|
||||||
menuObjid: string | number,
|
menuObjid: string | number,
|
||||||
@@ -484,6 +552,7 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
|
|
||||||
const renderTab = (tab: Tab, displayIndex: number) => {
|
const renderTab = (tab: Tab, displayIndex: number) => {
|
||||||
const isActive = tab.id === activeTabId;
|
const isActive = tab.id === activeTabId;
|
||||||
|
const isClosing = closingIds.has(tab.id);
|
||||||
const animStyle = getTabAnimStyle(tab.id, displayIndex);
|
const animStyle = getTabAnimStyle(tab.id, displayIndex);
|
||||||
const hiddenByGhost =
|
const hiddenByGhost =
|
||||||
!!dropGhost && displayIndex === dropGhost.targetIdx && tabs.length > dropGhost.tabCountAtCreation;
|
!!dropGhost && displayIndex === dropGhost.targetIdx && tabs.length > dropGhost.tabCountAtCreation;
|
||||||
@@ -491,6 +560,7 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
|
ref={isActive ? (el) => { activeTabElRef.current = el; } : undefined}
|
||||||
onPointerDown={(e) => handlePointerDown(e, tab.id, displayIndex)}
|
onPointerDown={(e) => handlePointerDown(e, tab.id, displayIndex)}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
@@ -499,6 +569,7 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
className={cn(
|
className={cn(
|
||||||
"v5-tab group relative flex shrink-0 cursor-pointer items-center gap-1 px-3 select-none",
|
"v5-tab group relative flex shrink-0 cursor-pointer items-center gap-1 px-3 select-none",
|
||||||
isActive && "on",
|
isActive && "on",
|
||||||
|
isClosing && "closing",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: TAB_WIDTH,
|
width: TAB_WIDTH,
|
||||||
@@ -526,7 +597,7 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeTab(tab.id);
|
handleCloseTab(tab.id);
|
||||||
}}
|
}}
|
||||||
className="v5-tab-x"
|
className="v5-tab-x"
|
||||||
>
|
>
|
||||||
@@ -555,6 +626,15 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
)}
|
)}
|
||||||
{displayVisible.map((tab, i) => renderTab(tab, i))}
|
{displayVisible.map((tab, i) => renderTab(tab, i))}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="v5-tab-indicator"
|
||||||
|
style={{
|
||||||
|
transform: `translateX(${indicatorStyle.left}px)`,
|
||||||
|
width: indicatorStyle.width,
|
||||||
|
opacity: indicatorStyle.opacity,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{hasOverflow && (
|
{hasOverflow && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -574,7 +654,7 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeTab(tab.id);
|
handleCloseTab(tab.id);
|
||||||
}}
|
}}
|
||||||
className="hover:bg-destructive/10 hover:text-destructive flex h-4 w-4 shrink-0 items-center justify-center rounded-sm"
|
className="hover:bg-destructive/10 hover:text-destructive flex h-4 w-4 shrink-0 items-center justify-center rounded-sm"
|
||||||
>
|
>
|
||||||
@@ -646,21 +726,21 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
label="왼쪽 탭 닫기"
|
label="왼쪽 탭 닫기"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeTabsToLeft(contextMenu.tabId);
|
handleCloseTabsToLeft(contextMenu.tabId);
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
label="오른쪽 탭 닫기"
|
label="오른쪽 탭 닫기"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeTabsToRight(contextMenu.tabId);
|
handleCloseTabsToRight(contextMenu.tabId);
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
label="다른 탭 모두 닫기"
|
label="다른 탭 모두 닫기"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeOtherTabs(contextMenu.tabId);
|
handleCloseOtherTabs(contextMenu.tabId);
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -668,7 +748,7 @@ export function TabBar({ collapsed = false, onToggleCollapse, modeTransition = "
|
|||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
label="모든 탭 닫기"
|
label="모든 탭 닫기"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeAllTabs();
|
handleCloseAllTabs();
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
destructive
|
destructive
|
||||||
|
|||||||
@@ -208,12 +208,19 @@ export class AutoGenerationUtils {
|
|||||||
return timeValue;
|
return timeValue;
|
||||||
|
|
||||||
case "sequence":
|
case "sequence":
|
||||||
return this.generateSequence(columnName || "default", options.startValue || 1, options.prefix, options.suffix);
|
return this.generateSequence(
|
||||||
|
columnName || "default",
|
||||||
|
(options as any).startValue ?? options.start_value ?? 1,
|
||||||
|
options.prefix,
|
||||||
|
options.suffix,
|
||||||
|
);
|
||||||
|
|
||||||
case "numbering_rule":
|
case "numbering_rule":
|
||||||
// 채번 규칙 ID가 있으면 API 호출
|
// 채번 규칙 ID가 있으면 API 호출
|
||||||
if (options.numberingRuleId) {
|
const numberingRuleId =
|
||||||
return await this.generateNumberingRuleCode(options.numberingRuleId, formData);
|
(options as any).numberingRuleId ?? options.numbering_rule_id;
|
||||||
|
if (numberingRuleId) {
|
||||||
|
return await this.generateNumberingRuleCode(numberingRuleId, formData);
|
||||||
}
|
}
|
||||||
console.warn("numbering_rule 타입인데 numberingRuleId가 없습니다");
|
console.warn("numbering_rule 타입인데 numberingRuleId가 없습니다");
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+212
-78
@@ -14,6 +14,8 @@
|
|||||||
--v5-red-rgb:255,71,87;
|
--v5-red-rgb:255,71,87;
|
||||||
--v5-green-rgb:0,184,148;
|
--v5-green-rgb:0,184,148;
|
||||||
--v5-amber-rgb:253,203,110;
|
--v5-amber-rgb:253,203,110;
|
||||||
|
/* Gradient accent — far end of primary→accent header/logo gradient. Picked per-theme to stay visually distinct from primary even when cyan is too close (e.g. blue/cyan themes). */
|
||||||
|
--v5-accent-rgb:0,206,201;
|
||||||
|
|
||||||
--v5-bg:#fafaff; --v5-bg-subtle:#f3f2fa;
|
--v5-bg:#fafaff; --v5-bg-subtle:#f3f2fa;
|
||||||
--v5-surface:rgba(255,255,255,0.55); --v5-surface-solid:#ffffff;
|
--v5-surface:rgba(255,255,255,0.55); --v5-surface-solid:#ffffff;
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
--v5-cyan:rgb(var(--v5-cyan-rgb)); --v5-cyan-glow:rgba(var(--v5-cyan-rgb),0.2);
|
--v5-cyan:rgb(var(--v5-cyan-rgb)); --v5-cyan-glow:rgba(var(--v5-cyan-rgb),0.2);
|
||||||
--v5-pink:rgb(var(--v5-pink-rgb)); --v5-pink-glow:rgba(var(--v5-pink-rgb),0.15);
|
--v5-pink:rgb(var(--v5-pink-rgb)); --v5-pink-glow:rgba(var(--v5-pink-rgb),0.15);
|
||||||
--v5-red:rgb(var(--v5-red-rgb)); --v5-green:rgb(var(--v5-green-rgb)); --v5-amber:rgb(var(--v5-amber-rgb));
|
--v5-red:rgb(var(--v5-red-rgb)); --v5-green:rgb(var(--v5-green-rgb)); --v5-amber:rgb(var(--v5-amber-rgb));
|
||||||
|
--v5-accent:rgb(var(--v5-accent-rgb)); --v5-accent-glow:rgba(var(--v5-accent-rgb),0.2);
|
||||||
--v5-border:rgba(var(--v5-primary-rgb),0.12); --v5-border-subtle:rgba(0,0,0,0.05);
|
--v5-border:rgba(var(--v5-primary-rgb),0.12); --v5-border-subtle:rgba(0,0,0,0.05);
|
||||||
--v5-glass:rgba(255,255,255,0.45); --v5-glass-strong:rgba(255,255,255,0.65);
|
--v5-glass:rgba(255,255,255,0.45); --v5-glass-strong:rgba(255,255,255,0.65);
|
||||||
--v5-glass-border:rgba(var(--v5-primary-rgb),0.12);
|
--v5-glass-border:rgba(var(--v5-primary-rgb),0.12);
|
||||||
@@ -109,6 +112,7 @@
|
|||||||
--v5-red-rgb:255,107,107;
|
--v5-red-rgb:255,107,107;
|
||||||
--v5-green-rgb:85,239,196;
|
--v5-green-rgb:85,239,196;
|
||||||
--v5-amber-rgb:255,234,167;
|
--v5-amber-rgb:255,234,167;
|
||||||
|
--v5-accent-rgb:85,239,196;
|
||||||
|
|
||||||
--v5-bg:#0a0b0d; --v5-bg-subtle:#111215;
|
--v5-bg:#0a0b0d; --v5-bg-subtle:#111215;
|
||||||
--v5-surface:rgba(23,24,27,0.5); --v5-surface-solid:#17181b;
|
--v5-surface:rgba(23,24,27,0.5); --v5-surface-solid:#17181b;
|
||||||
@@ -117,6 +121,7 @@
|
|||||||
--v5-primary:rgb(var(--v5-primary-rgb)); --v5-primary-light:#c8c4ff; --v5-primary-glow:rgba(var(--v5-primary-rgb),0.25);
|
--v5-primary:rgb(var(--v5-primary-rgb)); --v5-primary-light:#c8c4ff; --v5-primary-glow:rgba(var(--v5-primary-rgb),0.25);
|
||||||
--v5-cyan:rgb(var(--v5-cyan-rgb)); --v5-cyan-glow:rgba(var(--v5-cyan-rgb),0.15);
|
--v5-cyan:rgb(var(--v5-cyan-rgb)); --v5-cyan-glow:rgba(var(--v5-cyan-rgb),0.15);
|
||||||
--v5-pink:rgb(var(--v5-pink-rgb)); --v5-red:rgb(var(--v5-red-rgb)); --v5-green:rgb(var(--v5-green-rgb)); --v5-amber:rgb(var(--v5-amber-rgb));
|
--v5-pink:rgb(var(--v5-pink-rgb)); --v5-red:rgb(var(--v5-red-rgb)); --v5-green:rgb(var(--v5-green-rgb)); --v5-amber:rgb(var(--v5-amber-rgb));
|
||||||
|
--v5-accent:rgb(var(--v5-accent-rgb)); --v5-accent-glow:rgba(var(--v5-accent-rgb),0.15);
|
||||||
--v5-border:rgba(255,255,255,0.08); --v5-border-subtle:rgba(255,255,255,0.04);
|
--v5-border:rgba(255,255,255,0.08); --v5-border-subtle:rgba(255,255,255,0.04);
|
||||||
--v5-glass:rgba(23,24,27,0.45); --v5-glass-strong:rgba(23,24,27,0.65);
|
--v5-glass:rgba(23,24,27,0.45); --v5-glass-strong:rgba(23,24,27,0.65);
|
||||||
--v5-glass-border:rgba(var(--v5-primary-rgb),0.12);
|
--v5-glass-border:rgba(var(--v5-primary-rgb),0.12);
|
||||||
@@ -139,41 +144,51 @@
|
|||||||
/* --- BLUE --- */
|
/* --- BLUE --- */
|
||||||
html[data-color="blue"]{
|
html[data-color="blue"]{
|
||||||
--v5-primary-rgb:59,130,246; --v5-cyan-rgb:14,165,233; --v5-pink-rgb:99,102,241;
|
--v5-primary-rgb:59,130,246; --v5-cyan-rgb:14,165,233; --v5-pink-rgb:99,102,241;
|
||||||
|
--v5-accent-rgb:251,146,60;
|
||||||
--primary:217 91% 60%; --ring:217 91% 60%; --sidebar-primary:217 91% 60%; --sidebar-ring:217 91% 60%;}
|
--primary:217 91% 60%; --ring:217 91% 60%; --sidebar-primary:217 91% 60%; --sidebar-ring:217 91% 60%;}
|
||||||
html.dark[data-color="blue"]{
|
html.dark[data-color="blue"]{
|
||||||
--v5-primary-rgb:147,197,253; --v5-cyan-rgb:125,211,252; --v5-pink-rgb:129,140,248;
|
--v5-primary-rgb:147,197,253; --v5-cyan-rgb:125,211,252; --v5-pink-rgb:129,140,248;
|
||||||
|
--v5-accent-rgb:253,186,116;
|
||||||
--primary:213 93% 78%; --ring:213 93% 78%; --sidebar-primary:213 93% 78%; --sidebar-ring:213 93% 78%;}
|
--primary:213 93% 78%; --ring:213 93% 78%; --sidebar-primary:213 93% 78%; --sidebar-ring:213 93% 78%;}
|
||||||
|
|
||||||
/* --- GREEN --- */
|
/* --- GREEN --- */
|
||||||
html[data-color="green"]{
|
html[data-color="green"]{
|
||||||
--v5-primary-rgb:16,185,129; --v5-cyan-rgb:20,184,166; --v5-pink-rgb:132,204,22;
|
--v5-primary-rgb:16,185,129; --v5-cyan-rgb:20,184,166; --v5-pink-rgb:132,204,22;
|
||||||
|
--v5-accent-rgb:236,72,153;
|
||||||
--primary:160 84% 39%; --ring:160 84% 39%; --sidebar-primary:160 84% 39%; --sidebar-ring:160 84% 39%;}
|
--primary:160 84% 39%; --ring:160 84% 39%; --sidebar-primary:160 84% 39%; --sidebar-ring:160 84% 39%;}
|
||||||
html.dark[data-color="green"]{
|
html.dark[data-color="green"]{
|
||||||
--v5-primary-rgb:110,231,183; --v5-cyan-rgb:94,234,212; --v5-pink-rgb:190,242,100;
|
--v5-primary-rgb:110,231,183; --v5-cyan-rgb:94,234,212; --v5-pink-rgb:190,242,100;
|
||||||
|
--v5-accent-rgb:244,114,182;
|
||||||
--primary:156 73% 67%; --ring:156 73% 67%; --sidebar-primary:156 73% 67%; --sidebar-ring:156 73% 67%;}
|
--primary:156 73% 67%; --ring:156 73% 67%; --sidebar-primary:156 73% 67%; --sidebar-ring:156 73% 67%;}
|
||||||
|
|
||||||
/* --- ORANGE --- */
|
/* --- ORANGE --- */
|
||||||
html[data-color="orange"]{
|
html[data-color="orange"]{
|
||||||
--v5-primary-rgb:249,115,22; --v5-cyan-rgb:6,182,212; --v5-pink-rgb:251,146,60;
|
--v5-primary-rgb:249,115,22; --v5-cyan-rgb:6,182,212; --v5-pink-rgb:251,146,60;
|
||||||
|
--v5-accent-rgb:6,182,212;
|
||||||
--primary:25 95% 53%; --ring:25 95% 53%; --sidebar-primary:25 95% 53%; --sidebar-ring:25 95% 53%;}
|
--primary:25 95% 53%; --ring:25 95% 53%; --sidebar-primary:25 95% 53%; --sidebar-ring:25 95% 53%;}
|
||||||
html.dark[data-color="orange"]{
|
html.dark[data-color="orange"]{
|
||||||
--v5-primary-rgb:253,186,116; --v5-cyan-rgb:103,232,249; --v5-pink-rgb:252,165,165;
|
--v5-primary-rgb:253,186,116; --v5-cyan-rgb:103,232,249; --v5-pink-rgb:252,165,165;
|
||||||
|
--v5-accent-rgb:103,232,249;
|
||||||
--primary:32 97% 72%; --ring:32 97% 72%; --sidebar-primary:32 97% 72%; --sidebar-ring:32 97% 72%;}
|
--primary:32 97% 72%; --ring:32 97% 72%; --sidebar-primary:32 97% 72%; --sidebar-ring:32 97% 72%;}
|
||||||
|
|
||||||
/* --- PINK --- */
|
/* --- PINK --- */
|
||||||
html[data-color="pink"]{
|
html[data-color="pink"]{
|
||||||
--v5-primary-rgb:236,72,153; --v5-cyan-rgb:168,85,247; --v5-pink-rgb:244,114,182;
|
--v5-primary-rgb:236,72,153; --v5-cyan-rgb:168,85,247; --v5-pink-rgb:244,114,182;
|
||||||
|
--v5-accent-rgb:0,206,201;
|
||||||
--primary:330 81% 60%; --ring:330 81% 60%; --sidebar-primary:330 81% 60%; --sidebar-ring:330 81% 60%;}
|
--primary:330 81% 60%; --ring:330 81% 60%; --sidebar-primary:330 81% 60%; --sidebar-ring:330 81% 60%;}
|
||||||
html.dark[data-color="pink"]{
|
html.dark[data-color="pink"]{
|
||||||
--v5-primary-rgb:244,114,182; --v5-cyan-rgb:192,132,252; --v5-pink-rgb:249,168,212;
|
--v5-primary-rgb:244,114,182; --v5-cyan-rgb:192,132,252; --v5-pink-rgb:249,168,212;
|
||||||
|
--v5-accent-rgb:94,234,212;
|
||||||
--primary:330 86% 70%; --ring:330 86% 70%; --sidebar-primary:330 86% 70%; --sidebar-ring:330 86% 70%;}
|
--primary:330 86% 70%; --ring:330 86% 70%; --sidebar-primary:330 86% 70%; --sidebar-ring:330 86% 70%;}
|
||||||
|
|
||||||
/* --- CYAN --- */
|
/* --- CYAN --- */
|
||||||
html[data-color="cyan"]{
|
html[data-color="cyan"]{
|
||||||
--v5-primary-rgb:8,145,178; --v5-cyan-rgb:14,165,233; --v5-pink-rgb:6,182,212;
|
--v5-primary-rgb:8,145,178; --v5-cyan-rgb:14,165,233; --v5-pink-rgb:6,182,212;
|
||||||
|
--v5-accent-rgb:249,115,22;
|
||||||
--primary:191 91% 36%; --ring:191 91% 36%; --sidebar-primary:191 91% 36%; --sidebar-ring:191 91% 36%;}
|
--primary:191 91% 36%; --ring:191 91% 36%; --sidebar-primary:191 91% 36%; --sidebar-ring:191 91% 36%;}
|
||||||
html.dark[data-color="cyan"]{
|
html.dark[data-color="cyan"]{
|
||||||
--v5-primary-rgb:125,211,252; --v5-cyan-rgb:103,232,249; --v5-pink-rgb:165,243,252;
|
--v5-primary-rgb:125,211,252; --v5-cyan-rgb:103,232,249; --v5-pink-rgb:165,243,252;
|
||||||
|
--v5-accent-rgb:253,186,116;
|
||||||
--primary:200 94% 74%; --ring:200 94% 74%; --sidebar-primary:200 94% 74%; --sidebar-ring:200 94% 74%;}
|
--primary:200 94% 74%; --ring:200 94% 74%; --sidebar-primary:200 94% 74%; --sidebar-ring:200 94% 74%;}
|
||||||
|
|
||||||
/* --- PURPLE (기본) — 사이트 :root 토큰을 그대로 쓰지만 명시적으로 매핑해서
|
/* --- PURPLE (기본) — 사이트 :root 토큰을 그대로 쓰지만 명시적으로 매핑해서
|
||||||
@@ -235,7 +250,7 @@ html:not(.dark) .v5-hdr{
|
|||||||
box-shadow:0 4px 20px rgba(var(--v5-primary-rgb),0.06);}
|
box-shadow:0 4px 20px rgba(var(--v5-primary-rgb),0.06);}
|
||||||
.v5-hdr-l{display:flex;align-items:center;gap:1rem;}
|
.v5-hdr-l{display:flex;align-items:center;gap:1rem;}
|
||||||
.v5-hdr-logo{font-size:1.05rem;font-weight:900;letter-spacing:-.03em;
|
.v5-hdr-logo{font-size:1.05rem;font-weight:900;letter-spacing:-.03em;
|
||||||
background:linear-gradient(135deg,var(--v5-primary),var(--v5-cyan));-webkit-background-clip:text;
|
background:linear-gradient(135deg,var(--v5-primary),var(--v5-accent));-webkit-background-clip:text;
|
||||||
-webkit-text-fill-color:transparent;background-clip:text;cursor:default;}
|
-webkit-text-fill-color:transparent;background-clip:text;cursor:default;}
|
||||||
.v5-hdr-bc{font-size:.8125rem;color:var(--v5-text-muted);}
|
.v5-hdr-bc{font-size:.8125rem;color:var(--v5-text-muted);}
|
||||||
.v5-hdr-bc b{color:var(--v5-text);font-weight:600;}
|
.v5-hdr-bc b{color:var(--v5-text);font-weight:600;}
|
||||||
@@ -387,7 +402,7 @@ html:not(.dark) .v5-hdr{
|
|||||||
/* Admin mode header accent */
|
/* Admin mode header accent */
|
||||||
.v5-admin-mode .v5-hdr{border-bottom-color:var(--v5-primary);}
|
.v5-admin-mode .v5-hdr{border-bottom-color:var(--v5-primary);}
|
||||||
.v5-admin-mode .v5-hdr::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:2px;
|
.v5-admin-mode .v5-hdr::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:2px;
|
||||||
background:linear-gradient(90deg,var(--v5-primary),var(--v5-cyan));animation:v5-glowLine .6s ease-out both;}
|
background:linear-gradient(90deg,var(--v5-primary),var(--v5-accent));animation:v5-glowLine .6s ease-out both;}
|
||||||
@keyframes v5-glowLine{from{transform:scaleX(0);opacity:0}to{transform:scaleX(1);opacity:1}}
|
@keyframes v5-glowLine{from{transform:scaleX(0);opacity:0}to{transform:scaleX(1);opacity:1}}
|
||||||
|
|
||||||
/* Admin badge — display:none 대신 opacity/transform 으로 hidden 해서 zoom-in/out 애니메이션 가능 */
|
/* Admin badge — display:none 대신 opacity/transform 으로 hidden 해서 zoom-in/out 애니메이션 가능 */
|
||||||
@@ -413,19 +428,25 @@ html:not(.dark) .v5-hdr{
|
|||||||
@keyframes v5-bdPulse{0%,100%{box-shadow:0 0 4px var(--v5-primary-glow)}50%{box-shadow:0 0 12px var(--v5-primary-glow)}}
|
@keyframes v5-bdPulse{0%,100%{box-shadow:0 0 4px var(--v5-primary-glow)}50%{box-shadow:0 0 12px var(--v5-primary-glow)}}
|
||||||
|
|
||||||
/* ===== SOLID TABS ===== */
|
/* ===== SOLID TABS ===== */
|
||||||
.v5-tabs{height:36px;display:flex;align-items:stretch;padding:4px .5rem 0;gap:2px;overflow-x:auto;
|
.v5-tabs{height:36px;display:flex;align-items:stretch;padding:0 .5rem;gap:1px;overflow-x:auto;
|
||||||
background:var(--v5-surface-solid);
|
background:var(--v5-surface-solid);
|
||||||
border-bottom:1px solid var(--v5-border);position:relative;z-index:15;flex-shrink:0;
|
border-bottom:1px solid var(--v5-border);position:relative;z-index:15;flex-shrink:0;
|
||||||
scrollbar-width:none;-ms-overflow-style:none;}
|
scrollbar-width:none;-ms-overflow-style:none;}
|
||||||
.v5-tabs::-webkit-scrollbar{display:none;}
|
.v5-tabs::-webkit-scrollbar{display:none;}
|
||||||
/* Chrome 식 outline 탭: 비활성도 카드처럼 각각 outline. 활성 탭은 본문과 seamless + primary 강조선 */
|
|
||||||
.v5-tab{display:flex;align-items:center;gap:.4rem;padding:0 .85rem;font-size:.7rem;font-weight:500;
|
.v5-tab{display:flex;align-items:center;gap:.4rem;padding:0 .85rem;font-size:.7rem;font-weight:500;
|
||||||
color:var(--v5-text-muted);cursor:pointer;white-space:nowrap;transition:color .15s,border-color .15s,background .15s;
|
color:var(--v5-text-muted);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all .25s;
|
||||||
border:1px solid var(--v5-border);border-radius:8px 8px 0 0;margin-bottom:-1px;}
|
transform-origin:bottom center;animation:v5-tabIn .24s cubic-bezier(.16,1,.3,1) both;}
|
||||||
.v5-tab:hover{color:var(--v5-text-sec);background:var(--v5-surface-hover);}
|
.v5-tab:hover{color:var(--v5-text-sec);background:var(--v5-surface-hover);}
|
||||||
.v5-tab.on{color:var(--v5-primary);font-weight:600;
|
.v5-tab.on{color:var(--v5-primary);font-weight:600;border-bottom-color:transparent;background:var(--v5-surface);}
|
||||||
border-color:var(--v5-border);border-bottom-color:var(--v5-surface-hover);
|
.v5-tab.closing{animation:v5-tabOut .18s cubic-bezier(.4,0,1,1) both;pointer-events:none;}
|
||||||
background:var(--v5-surface-hover);box-shadow:0 -1px 0 var(--v5-primary) inset;}
|
@keyframes v5-tabIn{from{opacity:0;transform:translateY(-3px) scale(.92);}to{opacity:1;transform:none;}}
|
||||||
|
@keyframes v5-tabOut{to{opacity:0;transform:translateY(-2px) scale(.85);}}
|
||||||
|
/* Active tab underline indicator — slides between tabs (Chrome devtools style) */
|
||||||
|
.v5-tab-indicator{position:absolute;left:0;bottom:-1px;height:2px;
|
||||||
|
background:var(--v5-primary);box-shadow:0 0 8px rgba(var(--v5-primary-rgb),.6);
|
||||||
|
border-radius:1px;pointer-events:none;z-index:2;
|
||||||
|
transition:transform .38s cubic-bezier(.83,0,.17,1),width .38s cubic-bezier(.83,0,.17,1),opacity .2s ease-out;
|
||||||
|
will-change:transform,width;}
|
||||||
.v5-tab-x{width:14px;height:14px;border-radius:3px;border:none;background:transparent;color:var(--v5-text-muted);
|
.v5-tab-x{width:14px;height:14px;border-radius:3px;border:none;background:transparent;color:var(--v5-text-muted);
|
||||||
font-size:.6rem;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:all .15s;}
|
font-size:.6rem;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:all .15s;}
|
||||||
.v5-tab:hover .v5-tab-x{opacity:1;}
|
.v5-tab:hover .v5-tab-x{opacity:1;}
|
||||||
@@ -1524,9 +1545,22 @@ html.vt-color-changing .v5-admin-btn{
|
|||||||
=================================================================== */
|
=================================================================== */
|
||||||
/* .v5-nrm 자체에는 background 안 깔아서 body 의 .dark radial-gradient (globals.css) 가 비치게 둠.
|
/* .v5-nrm 자체에는 background 안 깔아서 body 의 .dark radial-gradient (globals.css) 가 비치게 둠.
|
||||||
sidebar 와 main 만 surface-solid 로 깔고, 헤더 영역은 투명 → 테마 컬러가 자연스럽게 헤더에 비침. */
|
sidebar 와 main 만 surface-solid 로 깔고, 헤더 영역은 투명 → 테마 컬러가 자연스럽게 헤더에 비침. */
|
||||||
.v5-nrm{display:flex;flex-direction:column;height:100%;overflow:hidden;}
|
.v5-nrm{display:flex;flex-direction:column;height:100%;overflow:hidden;background:var(--v5-surface-solid);}
|
||||||
.v5-nrm-body{flex:1;min-height:0;display:grid;grid-template-columns:320px 1fr;overflow:hidden;}
|
.v5-nrm-body{flex:1;min-height:0;display:grid;grid-template-columns:320px 1fr;overflow:hidden;}
|
||||||
|
|
||||||
|
/* top workbench header */
|
||||||
|
.v5-nrm-topbar{padding:.85rem 1.25rem;border-bottom:1px solid var(--v5-border);display:grid;grid-template-columns:minmax(260px,1fr) auto auto;align-items:center;gap:1rem;flex-shrink:0;background:var(--v5-surface-solid);}
|
||||||
|
.v5-nrm-topcopy{min-width:0;}
|
||||||
|
.v5-nrm-topcopy h1{margin:0;display:flex;align-items:center;gap:.5rem;font-size:.98rem;font-weight:800;letter-spacing:-.01em;color:var(--v5-text);}
|
||||||
|
.v5-nrm-titlemark{width:20px;height:20px;border-radius:5px;display:inline-flex;align-items:center;justify-content:center;background:var(--v5-primary);color:#fff;font-family:var(--v5-font-mono);font-size:.82rem;font-weight:900;box-shadow:0 8px 20px -12px rgba(var(--v5-primary-rgb),.8);}
|
||||||
|
.v5-nrm-topcopy p{margin:.18rem 0 0;color:var(--v5-text-muted);font-size:.66rem;}
|
||||||
|
.v5-nrm-topstats{display:flex;align-items:center;gap:.45rem;padding-left:1rem;border-left:1px solid var(--v5-border);font-family:var(--v5-font-mono);font-size:.58rem;color:var(--v5-text-muted);white-space:nowrap;}
|
||||||
|
.v5-nrm-stat{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 .45rem;border:1px solid var(--v5-border);border-radius:5px;background:var(--v5-bg-subtle);}
|
||||||
|
.v5-nrm-stat b{color:var(--v5-text);font-weight:800;}
|
||||||
|
.v5-nrm-stat.green b{color:rgb(var(--v5-green-rgb));}
|
||||||
|
.v5-nrm-stat.amber b{color:rgb(var(--v5-amber-rgb));}
|
||||||
|
.v5-nrm-topactions{display:flex;align-items:center;justify-content:flex-end;gap:.35rem;}
|
||||||
|
|
||||||
/* ── 좌측 sidebar ── */
|
/* ── 좌측 sidebar ── */
|
||||||
.v5-nrm-side{border-right:1px solid var(--v5-border);background:var(--v5-surface-solid);display:flex;flex-direction:column;overflow:hidden;}
|
.v5-nrm-side{border-right:1px solid var(--v5-border);background:var(--v5-surface-solid);display:flex;flex-direction:column;overflow:hidden;}
|
||||||
.v5-nrm-side-srch{position:relative;padding:.55rem .65rem;border-bottom:1px solid var(--v5-border);}
|
.v5-nrm-side-srch{position:relative;padding:.55rem .65rem;border-bottom:1px solid var(--v5-border);}
|
||||||
@@ -1562,6 +1596,7 @@ html.vt-color-changing .v5-admin-btn{
|
|||||||
.v5-nrm-dot.off{background:var(--v5-text-muted);opacity:.4;}
|
.v5-nrm-dot.off{background:var(--v5-text-muted);opacity:.4;}
|
||||||
@keyframes v5-nrm-pulse{0%,100%{opacity:1;}50%{opacity:.35;transform:scale(.88);}}
|
@keyframes v5-nrm-pulse{0%,100%{opacity:1;}50%{opacity:.35;transform:scale(.88);}}
|
||||||
.v5-nrm-side-foot{padding:.5rem .65rem;border-top:1px solid var(--v5-border);display:flex;gap:.3rem;}
|
.v5-nrm-side-foot{padding:.5rem .65rem;border-top:1px solid var(--v5-border);display:flex;gap:.3rem;}
|
||||||
|
.v5-nrm-side-foot .v5-nrm-btn{flex:1;justify-content:center;}
|
||||||
|
|
||||||
/* tone colors */
|
/* tone colors */
|
||||||
.v5-nrm-tone{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
.v5-nrm-tone{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
||||||
@@ -1575,94 +1610,171 @@ html.vt-color-changing .v5-admin-btn{
|
|||||||
.v5-nrm-tone.muted{background:var(--v5-bg-subtle);color:var(--v5-text-muted);}
|
.v5-nrm-tone.muted{background:var(--v5-bg-subtle);color:var(--v5-text-muted);}
|
||||||
|
|
||||||
/* ── 우측 main (통짜) ── */
|
/* ── 우측 main (통짜) ── */
|
||||||
.v5-nrm-main{overflow-y:auto;background:var(--v5-surface-solid);position:relative;}
|
.v5-nrm-main{overflow-y:auto;background:var(--v5-surface-solid);background-image:radial-gradient(circle,rgba(var(--v5-primary-rgb),.08) 1px,transparent 1px);background-size:14px 14px;background-position:0 0;position:relative;}
|
||||||
|
|
||||||
/* HERO (통짜, 카드 X) */
|
/* keyframes — v4 EDIT 모드 마이크로 애니메이션 */
|
||||||
.v5-nrm-hero{padding:1rem 1.5rem 1.1rem;border-bottom:1px solid var(--v5-border);position:relative;}
|
@keyframes v5-nrm-pop{0%{opacity:0;transform:scale(.6);}60%{opacity:1;transform:scale(1.06);}100%{transform:scale(1);}}
|
||||||
.v5-nrm-hero::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,rgb(var(--v5-cyan-rgb)),var(--v5-primary),rgb(var(--v5-pink-rgb)));}
|
@keyframes v5-nrm-slide-up{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
|
||||||
.v5-nrm-hero-top{display:flex;align-items:center;gap:.9rem;margin-bottom:.85rem;}
|
@keyframes v5-nrm-slide-down{from{opacity:0;transform:translateY(-6px);}to{opacity:1;transform:translateY(0);}}
|
||||||
.v5-nrm-hero-top .v5-nrm-tone{width:40px;height:40px;border-radius:10px;}
|
@keyframes v5-nrm-pulse-cur{0%,100%{box-shadow:0 0 0 0 rgba(var(--v5-amber-rgb),.5);}50%{box-shadow:0 0 0 5px rgba(var(--v5-amber-rgb),0);}}
|
||||||
.v5-nrm-hero-top .v5-nrm-tone svg{width:18px;height:18px;}
|
|
||||||
.v5-nrm-hero-info{flex:1;min-width:0;}
|
/* DETAIL HEAD (제목 + chip + meta + 우측 액션) */
|
||||||
.v5-nrm-hero-row1{display:flex;align-items:center;gap:6px;margin-bottom:3px;flex-wrap:wrap;min-width:0;}
|
.v5-nrm-detail-head{padding:.95rem 1.4rem;border-bottom:1px solid var(--v5-border);display:flex;align-items:center;justify-content:space-between;gap:1rem;background:var(--v5-surface-solid);animation:v5-nrm-slide-down .45s cubic-bezier(.16,1,.3,1);}
|
||||||
.v5-nrm-hero-info{min-width:0;}
|
.v5-nrm-detail-head .l{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1;}
|
||||||
.v5-nrm-hero-info h2{margin:0;font-size:1.2rem;font-weight:800;letter-spacing:-.02em;color:var(--v5-text);}
|
.v5-nrm-detail-head h2{margin:0;font-size:.98rem;font-weight:700;letter-spacing:-.01em;display:flex;align-items:center;gap:6px;flex-wrap:wrap;color:var(--v5-text);}
|
||||||
.v5-nrm-hero-meta{display:flex;gap:1rem;font-family:'JetBrains Mono',monospace;font-size:.62rem;color:var(--v5-text-muted);}
|
.v5-nrm-detail-head .meta{display:flex;gap:.85rem;font-size:.6rem;color:var(--v5-text-muted);flex-wrap:wrap;font-family:'JetBrains Mono',monospace;}
|
||||||
.v5-nrm-hero-meta b{color:var(--v5-text-sec);font-weight:600;}
|
.v5-nrm-detail-head .meta b{color:var(--v5-text-sec);font-weight:600;}
|
||||||
.v5-nrm-hero-actions{display:flex;gap:.3rem;align-items:center;}
|
.v5-nrm-detail-head .r{display:flex;gap:.25rem;}
|
||||||
.v5-nrm-hero-codes{display:grid;grid-template-columns:1fr auto 1fr;align-items:end;gap:1.5rem;}
|
|
||||||
.v5-nrm-hero-code-block .lbl{font-family:'JetBrains Mono',monospace;font-size:.52rem;color:var(--v5-text-muted);font-weight:800;letter-spacing:.14em;text-transform:uppercase;margin-bottom:5px;}
|
/* PATTERN-VIS (현재/다음 발번 시각화) */
|
||||||
.v5-nrm-hero-code-block .code{font-family:'JetBrains Mono',monospace;font-size:2rem;font-weight:800;letter-spacing:.04em;line-height:1;color:var(--v5-text);}
|
.v5-nrm-pattern-vis{padding:1rem 1.4rem 1.1rem;border-bottom:1px solid var(--v5-border);background:var(--v5-bg-subtle);animation:v5-nrm-slide-up .5s cubic-bezier(.16,1,.3,1) .08s both;}
|
||||||
.v5-nrm-hero-code-block.right{text-align:right;}
|
.v5-nrm-pv-bar{display:flex;align-items:center;gap:8px;margin-bottom:.55rem;font-family:'JetBrains Mono',monospace;font-size:.58rem;color:var(--v5-text-muted);font-weight:800;letter-spacing:.12em;text-transform:uppercase;}
|
||||||
.v5-nrm-hero-code-block .code .pfx{color:rgb(var(--v5-cyan-rgb));}
|
.v5-nrm-pv-bar .e{font-family:'JetBrains Mono',monospace;font-size:.62rem;color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.08);padding:1px 7px;border-radius:4px;text-transform:none;letter-spacing:0;}
|
||||||
.v5-nrm-hero-code-block .code .seq{color:rgb(var(--v5-pink-rgb));}
|
.v5-nrm-pattern-line{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
|
||||||
.v5-nrm-hero-code-block .code .sep{color:var(--v5-text-muted);opacity:.55;margin:0 .08em;font-weight:400;}
|
.v5-nrm-pattern-piece{display:flex;flex-direction:column;align-items:center;gap:3px;animation:v5-nrm-pop .5s cubic-bezier(.34,1.56,.64,1) both;}
|
||||||
.v5-nrm-hero-code-block .sub{margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:.58rem;color:var(--v5-text-muted);}
|
.v5-nrm-pattern-piece:nth-child(2){animation-delay:.15s;}
|
||||||
.v5-nrm-hero-arrow{color:var(--v5-primary);align-self:center;margin-bottom:4px;}
|
.v5-nrm-pattern-piece:nth-child(4){animation-delay:.25s;}
|
||||||
.v5-nrm-hero-arrow svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;}
|
.v5-nrm-pattern-piece:nth-child(6){animation-delay:.35s;}
|
||||||
|
.v5-nrm-pattern-piece:nth-child(8){animation-delay:.45s;}
|
||||||
|
.v5-nrm-pattern-piece:nth-child(10){animation-delay:.55s;}
|
||||||
|
.v5-nrm-pattern-piece:nth-child(12){animation-delay:.65s;}
|
||||||
|
.v5-nrm-pattern-piece .top-lbl{font-size:.55rem;color:var(--v5-text-muted);font-weight:700;letter-spacing:.04em;}
|
||||||
|
.v5-nrm-pattern-piece .v{font-family:'JetBrains Mono',monospace;font-size:1.1rem;font-weight:700;color:var(--v5-text);padding:2px 8px;background:var(--v5-surface-solid);border:1px solid var(--v5-border);border-radius:5px;transition:transform .3s cubic-bezier(.34,1.56,.64,1),border-color .3s,color .3s,box-shadow .3s;cursor:pointer;}
|
||||||
|
.v5-nrm-pattern-piece .v:hover{transform:translateY(-2px) scale(1.05);border-color:var(--v5-primary);box-shadow:0 0 0 3px rgba(var(--v5-primary-rgb),.10);}
|
||||||
|
.v5-nrm-pattern-piece .v.cur{color:var(--v5-primary);border-color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.08);animation:v5-nrm-pulse-cur 2.2s ease-in-out infinite;}
|
||||||
|
.v5-nrm-pattern-sep{font-family:'JetBrains Mono',monospace;font-size:1rem;color:var(--v5-text-muted);font-weight:600;align-self:flex-end;padding-bottom:3px;}
|
||||||
|
.v5-nrm-pattern-next{margin-top:.7rem;font-size:.65rem;color:var(--v5-text-muted);display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
|
||||||
|
.v5-nrm-pattern-next b{font-family:'JetBrains Mono',monospace;color:var(--v5-primary);font-weight:700;padding:1px 5px;background:rgba(var(--v5-primary-rgb),.08);border-radius:3px;transition:transform .25s cubic-bezier(.34,1.56,.64,1);}
|
||||||
|
.v5-nrm-pattern-next b:hover{transform:scale(1.05);}
|
||||||
|
|
||||||
|
/* hero preview — receipt/ticket metaphor for generated identity */
|
||||||
|
.v5-nrm-hero-wrap{padding:1.35rem 1.4rem 1.1rem;border-bottom:1px solid var(--v5-border);display:flex;justify-content:center;background:transparent;animation:v5-nrm-slide-up .5s cubic-bezier(.16,1,.3,1) .08s both;}
|
||||||
|
.v5-nrm-hero-card{position:relative;width:100%;max-width:720px;padding:2rem 1.45rem 1.05rem;background:var(--v5-bg-paper,var(--v5-bg-subtle));border:1.5px dashed var(--v5-border);border-radius:10px;box-shadow:0 14px 40px -30px rgba(var(--v5-primary-rgb),.8);}
|
||||||
|
.v5-nrm-hero-card::before,.v5-nrm-hero-card::after{content:'';position:absolute;top:50%;width:16px;height:16px;background:var(--v5-surface-solid);border:1.5px dashed var(--v5-border);border-radius:50%;transform:translateY(-50%);}
|
||||||
|
.v5-nrm-hero-card::before{left:-9px;}
|
||||||
|
.v5-nrm-hero-card::after{right:-9px;}
|
||||||
|
.v5-nrm-hero-tab{position:absolute;top:-11px;left:1.35rem;padding:2px 10px;background:var(--v5-primary);color:#fff;border-radius:4px;font-family:var(--v5-font-mono);font-size:.58rem;font-weight:800;letter-spacing:.06em;display:inline-flex;align-items:center;gap:5px;}
|
||||||
|
.v5-nrm-hero-tab svg{width:11px;height:11px;stroke:currentColor;fill:none;stroke-width:2.2;}
|
||||||
|
.v5-nrm-hero-corner{position:absolute;top:.85rem;right:1rem;max-width:46%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--v5-font-mono);font-size:.54rem;color:var(--v5-text-muted);letter-spacing:.04em;text-transform:uppercase;font-weight:700;}
|
||||||
|
.v5-nrm-hero-code{display:flex;align-items:flex-start;justify-content:center;gap:0;flex-wrap:wrap;font-family:var(--v5-font-mono);font-weight:800;letter-spacing:.015em;margin-bottom:.55rem;min-height:54px;}
|
||||||
|
.v5-nrm-hero-piece-wrap{display:inline-flex;align-items:flex-start;}
|
||||||
|
.v5-nrm-hero-part{appearance:none;border:0;background:transparent;padding:0 .35rem;display:inline-flex;flex-direction:column;align-items:center;gap:3px;color:var(--v5-text);cursor:pointer;border-radius:7px;transition:background .15s,transform .2s cubic-bezier(.34,1.56,.64,1),color .15s;}
|
||||||
|
.v5-nrm-hero-part:hover{background:rgba(var(--v5-primary-rgb),.06);transform:translateY(-2px);}
|
||||||
|
.v5-nrm-hero-part .v{font-family:var(--v5-font-mono);font-size:clamp(1.3rem,2.6vw,1.95rem);line-height:1.08;font-weight:900;color:currentColor;}
|
||||||
|
.v5-nrm-hero-part .lbl{font-size:.54rem;color:var(--v5-text-muted);font-weight:800;letter-spacing:.04em;font-family:var(--v5-font-sans);}
|
||||||
|
.v5-nrm-hero-part.sequence,.v5-nrm-hero-part.sel{color:var(--v5-primary);}
|
||||||
|
.v5-nrm-hero-part.sequence .v{background:rgba(var(--v5-primary-rgb),.08);border:1.5px solid var(--v5-primary);border-radius:6px;padding:1px .5rem 2px;box-shadow:0 0 0 4px rgba(var(--v5-primary-rgb),.08);}
|
||||||
|
.v5-nrm-hero-part.text{color:rgb(var(--v5-cyan-rgb));}
|
||||||
|
.v5-nrm-hero-part.date{color:var(--v5-text);}
|
||||||
|
.v5-nrm-hero-part.number{color:rgb(var(--v5-amber-rgb));}
|
||||||
|
.v5-nrm-hero-part.category{color:var(--v5-primary);}
|
||||||
|
.v5-nrm-hero-part.reference{color:rgb(var(--v5-green-rgb));}
|
||||||
|
.v5-nrm-hero-sep{font-family:var(--v5-font-mono);font-size:clamp(1.3rem,2.6vw,1.95rem);line-height:1.1;color:var(--v5-text-muted);font-weight:900;padding:0 2px;align-self:flex-start;margin-top:-3px;}
|
||||||
|
.v5-nrm-hero-intent{text-align:center;font-size:.68rem;color:var(--v5-text-sec);margin:.25rem 0 .9rem;}
|
||||||
|
.v5-nrm-hero-intent b{font-family:var(--v5-font-mono);color:var(--v5-text);font-weight:900;}
|
||||||
|
.v5-nrm-hero-foot{border-top:1px dashed var(--v5-border);padding-top:.7rem;display:flex;justify-content:space-between;flex-wrap:wrap;gap:.55rem;font-family:var(--v5-font-mono);font-size:.58rem;color:var(--v5-text-muted);letter-spacing:.03em;}
|
||||||
|
.v5-nrm-hero-foot .it{display:inline-flex;align-items:center;gap:5px;}
|
||||||
|
.v5-nrm-hero-foot .k{color:var(--v5-text-muted);font-weight:800;text-transform:uppercase;letter-spacing:.08em;font-size:.52rem;}
|
||||||
|
.v5-nrm-hero-foot .v{color:var(--v5-text);font-weight:800;font-size:.66rem;}
|
||||||
|
.v5-nrm-hero-foot .v.up{color:rgb(var(--v5-green-rgb));}
|
||||||
|
.v5-nrm-hero-empty{width:100%;max-width:720px;min-height:132px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.35rem;background:var(--v5-bg-subtle);border:1.5px dashed var(--v5-border);border-radius:10px;color:var(--v5-text-muted);font-size:.7rem;}
|
||||||
|
.v5-nrm-hero-empty b{font-size:.82rem;color:var(--v5-text);}
|
||||||
|
.v5-nrm-hero-empty svg{opacity:.55;stroke:currentColor;fill:none;stroke-width:1.7;}
|
||||||
|
|
||||||
/* row + 2-col split */
|
/* row + 2-col split */
|
||||||
.v5-nrm-row{padding:1rem 1.5rem 1.1rem;border-bottom:1px solid var(--v5-border);}
|
.v5-nrm-row{padding:1rem 1.5rem 1.1rem;border-bottom:1px solid var(--v5-border);background:var(--v5-surface-solid);}
|
||||||
.v5-nrm-row-hd{display:flex;align-items:center;gap:8px;margin-bottom:.75rem;}
|
.v5-nrm-row-hd{display:flex;align-items:center;gap:8px;margin-bottom:.75rem;}
|
||||||
.v5-nrm-row-hd h3{margin:0;font-size:.82rem;font-weight:800;color:var(--v5-text);letter-spacing:-.005em;}
|
.v5-nrm-row-hd h3{margin:0;font-size:.82rem;font-weight:800;color:var(--v5-text);letter-spacing:-.005em;}
|
||||||
.v5-nrm-row-hd .num{font-family:'JetBrains Mono',monospace;font-size:.58rem;color:rgb(var(--v5-cyan-rgb));font-weight:800;padding:1px 6px;background:rgba(var(--v5-cyan-rgb),.1);border-radius:4px;}
|
.v5-nrm-row-hd .num{font-family:'JetBrains Mono',monospace;font-size:.58rem;color:rgb(var(--v5-cyan-rgb));font-weight:800;padding:1px 6px;background:rgba(var(--v5-cyan-rgb),.1);border-radius:4px;}
|
||||||
.v5-nrm-row-hd .desc{font-family:'JetBrains Mono',monospace;font-size:.58rem;color:var(--v5-text-muted);margin-left:6px;}
|
.v5-nrm-row-hd .desc{font-family:'JetBrains Mono',monospace;font-size:.58rem;color:var(--v5-text-muted);margin-left:6px;}
|
||||||
.v5-nrm-row-hd .actions{margin-left:auto;display:flex;gap:.25rem;}
|
.v5-nrm-row-hd .actions{margin-left:auto;display:flex;gap:.25rem;}
|
||||||
.v5-nrm-row-split{display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--v5-border);}
|
.v5-nrm-row-split{display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--v5-border);}
|
||||||
.v5-nrm-row-split > div{padding:1rem 1.5rem 1.1rem;}
|
.v5-nrm-row-split > div{padding:1rem 1.5rem 1.1rem;background:var(--v5-surface-solid);}
|
||||||
.v5-nrm-row-split > div + div{border-left:1px solid var(--v5-border);}
|
.v5-nrm-row-split > div + div{border-left:1px solid var(--v5-border);}
|
||||||
|
|
||||||
/* pipeline editor */
|
/* pipeline editor — v4 시안: pipe-slot + pipe-block + add-here + drop(end) */
|
||||||
.v5-nrm-pipe-canvas{padding:.9rem 1rem;background:var(--v5-bg-subtle);border:1px dashed var(--v5-border);border-radius:8px;display:flex;align-items:stretch;gap:4px;flex-wrap:wrap;margin-bottom:.55rem;}
|
.v5-nrm-pipe-canvas{padding:.65rem .75rem .5rem;background:var(--v5-bg-subtle);border:1px solid var(--v5-border);border-radius:8px;display:flex;align-items:stretch;flex-wrap:wrap;gap:0;margin-bottom:.55rem;}
|
||||||
.v5-nrm-block{position:relative;display:flex;flex-direction:column;align-items:center;gap:3px;padding:.5rem .75rem;background:var(--v5-surface-solid);border:2px solid var(--v5-border);border-radius:8px;cursor:pointer;transition:all .15s;min-width:78px;}
|
|
||||||
.v5-nrm-block:hover{transform:translateY(-1px);box-shadow:0 4px 12px -4px rgba(var(--v5-primary-rgb),.25);}
|
/* pipe-slot — 블록 사이 separator + hover 시 + 등장 */
|
||||||
.v5-nrm-block.sel{border-color:var(--v5-primary);box-shadow:0 0 0 3px rgba(var(--v5-primary-rgb),.14);}
|
.v5-nrm-slot{display:inline-flex;align-items:center;width:18px;position:relative;align-self:stretch;}
|
||||||
.v5-nrm-block .pin{position:absolute;top:-6px;left:-6px;width:16px;height:16px;background:var(--v5-primary);color:#fff;border-radius:50%;font-family:'JetBrains Mono',monospace;font-size:.55rem;font-weight:800;display:flex;align-items:center;justify-content:center;border:2px solid var(--v5-surface-solid);}
|
.v5-nrm-slot .sep{font-family:'JetBrains Mono',monospace;font-size:.7rem;color:var(--v5-text-muted);font-weight:600;user-select:none;width:100%;text-align:center;transition:color .2s,transform .25s;align-self:center;}
|
||||||
.v5-nrm-block .x{position:absolute;top:-6px;right:-6px;width:15px;height:15px;background:var(--v5-surface-solid);border:1px solid var(--v5-border);color:var(--v5-text-muted);border-radius:50%;font-size:.68rem;line-height:1;display:none;align-items:center;justify-content:center;cursor:pointer;}
|
.v5-nrm-slot:hover .sep{color:var(--v5-primary);transform:scale(.9);}
|
||||||
.v5-nrm-block:hover .x{display:flex;}
|
.v5-nrm-slot .add-here{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) scale(.5);width:18px;height:18px;border-radius:50%;background:var(--v5-primary);color:#fff;border:2px solid var(--v5-bg-subtle);display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0;transition:opacity .2s cubic-bezier(.16,1,.3,1),transform .3s cubic-bezier(.34,1.56,.64,1),box-shadow .25s;z-index:2;padding:0;}
|
||||||
.v5-nrm-block .x:hover{color:rgb(var(--v5-red-rgb));border-color:rgb(var(--v5-red-rgb));}
|
.v5-nrm-slot .add-here svg{width:9px;height:9px;stroke:currentColor;fill:none;stroke-width:2.5;}
|
||||||
.v5-nrm-block .typ{font-family:'JetBrains Mono',monospace;font-size:.52rem;color:var(--v5-text-muted);font-weight:800;letter-spacing:.08em;text-transform:uppercase;}
|
.v5-nrm-slot:hover .add-here{opacity:1;transform:translate(-50%,-50%) scale(1);}
|
||||||
.v5-nrm-block .val{font-family:'JetBrains Mono',monospace;font-size:.9rem;font-weight:800;color:var(--v5-text);}
|
.v5-nrm-slot .add-here:hover{transform:translate(-50%,-50%) scale(1.2) rotate(90deg);box-shadow:0 0 0 4px rgba(var(--v5-primary-rgb),.25);}
|
||||||
.v5-nrm-block.text{border-color:rgba(var(--v5-cyan-rgb),.4);}
|
|
||||||
|
/* pipe-block — v4 디자인 (top-row 에 ord+lbl+typ-en, 아래 val) */
|
||||||
|
.v5-nrm-block{display:inline-flex;flex-direction:column;gap:3px;padding:8px 11px;background:var(--v5-surface-solid);border:1px solid var(--v5-border);border-radius:6px;cursor:pointer;position:relative;transition:border-color .25s cubic-bezier(.16,1,.3,1),background .25s,transform .25s cubic-bezier(.34,1.56,.64,1),box-shadow .3s;min-width:90px;animation:v5-nrm-slide-up .4s cubic-bezier(.16,1,.3,1) both;}
|
||||||
|
.v5-nrm-block:hover{border-color:rgba(var(--v5-primary-rgb),.22);transform:translateY(-3px);box-shadow:0 6px 22px -8px rgba(var(--v5-primary-rgb),.35);}
|
||||||
|
.v5-nrm-block.sel{border-color:var(--v5-primary);box-shadow:0 0 0 4px rgba(var(--v5-primary-rgb),.16);transform:translateY(-2px);}
|
||||||
|
.v5-nrm-block .top-row{display:flex;align-items:center;gap:5px;}
|
||||||
|
.v5-nrm-block .ord{font-family:'JetBrains Mono',monospace;font-size:.54rem;font-weight:700;color:var(--v5-text-muted);}
|
||||||
|
.v5-nrm-block .lbl{font-size:.64rem;font-weight:600;color:var(--v5-text-sec);transition:color .25s;}
|
||||||
|
.v5-nrm-block:hover .lbl{color:var(--v5-text);}
|
||||||
|
.v5-nrm-block .typ-en{font-family:'JetBrains Mono',monospace;font-size:.52rem;color:var(--v5-text-muted);text-transform:uppercase;letter-spacing:.06em;}
|
||||||
|
.v5-nrm-block .val{font-family:'JetBrains Mono',monospace;font-size:.8rem;font-weight:700;color:var(--v5-text);margin-top:1px;transition:color .25s;}
|
||||||
|
.v5-nrm-block.sel .val{color:var(--v5-primary);}
|
||||||
|
.v5-nrm-block .x{position:absolute;top:-6px;right:-6px;width:16px;height:16px;background:var(--v5-surface-solid);border:1px solid var(--v5-border);border-radius:50%;color:var(--v5-text-muted);font-size:11px;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0;transform:scale(0);transition:opacity .2s,transform .3s cubic-bezier(.34,1.56,.64,1),color .2s,border-color .2s;padding:0;}
|
||||||
|
.v5-nrm-block:hover .x{opacity:1;transform:scale(1);}
|
||||||
|
.v5-nrm-block .x:hover{color:rgb(var(--v5-red-rgb));border-color:rgb(var(--v5-red-rgb));transform:scale(1.15) rotate(90deg);}
|
||||||
|
/* 타입별 액센트 (val 색만, border 는 hover/sel 만) */
|
||||||
.v5-nrm-block.text .val{color:rgb(var(--v5-cyan-rgb));}
|
.v5-nrm-block.text .val{color:rgb(var(--v5-cyan-rgb));}
|
||||||
.v5-nrm-block.date{border-color:rgba(var(--v5-primary-rgb),.35);}
|
|
||||||
.v5-nrm-block.date .val{color:var(--v5-primary);}
|
.v5-nrm-block.date .val{color:var(--v5-primary);}
|
||||||
.v5-nrm-block.sequence{border-color:rgba(var(--v5-pink-rgb),.4);}
|
|
||||||
.v5-nrm-block.sequence .val{color:rgb(var(--v5-pink-rgb));}
|
.v5-nrm-block.sequence .val{color:rgb(var(--v5-pink-rgb));}
|
||||||
.v5-nrm-block.number{border-color:rgba(var(--v5-amber-rgb),.4);}
|
|
||||||
.v5-nrm-block.number .val{color:rgb(var(--v5-amber-rgb));}
|
.v5-nrm-block.number .val{color:rgb(var(--v5-amber-rgb));}
|
||||||
.v5-nrm-block.category{border-color:rgba(var(--v5-primary-rgb),.35);}
|
|
||||||
.v5-nrm-block.category .val{color:var(--v5-primary);}
|
.v5-nrm-block.category .val{color:var(--v5-primary);}
|
||||||
.v5-nrm-block.reference{border-color:rgba(var(--v5-green-rgb),.4);}
|
|
||||||
.v5-nrm-block.reference .val{color:rgb(var(--v5-green-rgb));}
|
.v5-nrm-block.reference .val{color:rgb(var(--v5-green-rgb));}
|
||||||
|
|
||||||
|
/* 옛 jn 클래스는 page.tsx 가 더 이상 사용하지 않지만 안전을 위해 보존 */
|
||||||
.v5-nrm-jn{align-self:center;color:var(--v5-text-muted);font-family:'JetBrains Mono',monospace;font-weight:700;padding:0 2px;font-size:.85rem;}
|
.v5-nrm-jn{align-self:center;color:var(--v5-text-muted);font-family:'JetBrains Mono',monospace;font-weight:700;padding:0 2px;font-size:.85rem;}
|
||||||
.v5-nrm-drop{display:flex;align-items:center;justify-content:center;flex-direction:column;gap:2px;padding:.5rem .65rem;border:2px dashed rgba(var(--v5-primary-rgb),.22);border-radius:8px;color:var(--v5-text-muted);cursor:pointer;font-size:.56rem;font-weight:700;min-width:64px;background:transparent;transition:all .15s;font-family:inherit;}
|
|
||||||
.v5-nrm-drop:hover{border-color:var(--v5-primary);color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.03);}
|
|
||||||
.v5-nrm-drop svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;}
|
|
||||||
|
|
||||||
/* inspector */
|
/* end-of-pipe 추가 버튼 (v4 의 pipe-add-end) */
|
||||||
.v5-nrm-insp{background:var(--v5-bg-subtle);border:1px solid var(--v5-border);border-radius:8px;padding:.65rem .85rem;margin-bottom:.55rem;}
|
.v5-nrm-drop{display:inline-flex;align-items:center;gap:5px;padding:8px 11px;background:transparent;border:1px dashed rgba(var(--v5-primary-rgb),.22);border-radius:6px;color:var(--v5-text-muted);font-size:.68rem;cursor:pointer;margin-left:8px;align-self:stretch;transition:color .25s,border-color .25s,background .25s,transform .25s cubic-bezier(.34,1.56,.64,1);font-family:inherit;font-weight:600;animation:v5-nrm-slide-up .4s cubic-bezier(.16,1,.3,1) .55s both;}
|
||||||
.v5-nrm-insp-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:.55rem;}
|
.v5-nrm-drop:hover{color:var(--v5-primary);border-color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.06);transform:translateY(-2px) scale(1.02);}
|
||||||
.v5-nrm-insp-hd .l{display:flex;align-items:center;gap:6px;font-family:'JetBrains Mono',monospace;font-size:.62rem;color:var(--v5-text);font-weight:700;letter-spacing:.04em;text-transform:uppercase;}
|
.v5-nrm-drop svg{width:11px;height:11px;stroke:currentColor;fill:none;stroke-width:2;transition:transform .3s cubic-bezier(.34,1.56,.64,1);}
|
||||||
.v5-nrm-insp-hd .l .pin{background:var(--v5-primary);color:#fff;padding:1px 6px;border-radius:4px;font-size:.54rem;font-weight:800;}
|
.v5-nrm-drop:hover svg{transform:rotate(90deg);}
|
||||||
.v5-nrm-insp-hd .l b{color:rgb(var(--v5-pink-rgb));}
|
|
||||||
.v5-nrm-insp-grid{display:flex;flex-wrap:wrap;gap:.65rem;align-items:end;}
|
|
||||||
.v5-nrm-insp-field{display:flex;flex-direction:column;gap:3px;}
|
|
||||||
.v5-nrm-insp-field.w80{flex:0 0 80px;}
|
|
||||||
.v5-nrm-insp-field.grow{flex:1;min-width:200px;}
|
|
||||||
.v5-nrm-insp-field label{font-size:.56rem;color:var(--v5-text-sec);font-weight:600;}
|
|
||||||
.v5-nrm-insp-inp{height:26px;padding:0 .45rem;background:var(--v5-surface-solid);border:1px solid var(--v5-border);border-radius:5px;color:var(--v5-text);font-family:'JetBrains Mono',monospace;font-size:.68rem;outline:none;width:100%;}
|
|
||||||
.v5-nrm-insp-inp:focus{border-color:var(--v5-primary);box-shadow:0 0 0 2px rgba(var(--v5-primary-rgb),.14);}
|
|
||||||
.v5-nrm-seg{display:inline-flex;gap:1px;padding:2px;background:var(--v5-surface-solid);border:1px solid var(--v5-border);border-radius:5px;width:fit-content;}
|
|
||||||
.v5-nrm-seg button{padding:3px 12px;border:none;background:transparent;color:var(--v5-text-muted);font-size:.58rem;font-weight:700;border-radius:3px;cursor:pointer;min-width:36px;font-family:inherit;}
|
|
||||||
.v5-nrm-seg button.on{background:var(--v5-primary);color:#fff;min-width:44px;}
|
|
||||||
|
|
||||||
/* palette */
|
/* inspector — v4 디자인 (위쪽 화살표 + fadeSlideDown + glow ring) */
|
||||||
.v5-nrm-palette{display:flex;gap:5px;align-items:center;flex-wrap:wrap;}
|
.v5-nrm-insp{margin-top:.75rem;padding:.7rem .85rem .8rem;background:var(--v5-surface-solid);border:1px solid var(--v5-primary);border-radius:8px;box-shadow:0 0 0 4px rgba(var(--v5-primary-rgb),.16);position:relative;animation:v5-nrm-slide-down .4s cubic-bezier(.16,1,.3,1);transform-origin:top center;margin-bottom:.55rem;}
|
||||||
.v5-nrm-palette .lbl{font-family:'JetBrains Mono',monospace;font-size:.54rem;color:var(--v5-text-muted);font-weight:800;letter-spacing:.08em;margin-right:4px;text-transform:uppercase;}
|
.v5-nrm-insp::before{content:'';position:absolute;top:-7px;left:1.5rem;width:12px;height:12px;background:var(--v5-surface-solid);border-top:1px solid var(--v5-primary);border-left:1px solid var(--v5-primary);transform:rotate(45deg);}
|
||||||
.v5-nrm-palette-item{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;background:transparent;border:1px solid var(--v5-border);border-radius:5px;color:var(--v5-text);font-size:.62rem;font-weight:700;cursor:pointer;transition:all .12s;font-family:inherit;}
|
.v5-nrm-insp-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:.65rem;padding-bottom:.55rem;border-bottom:1px solid var(--v5-divider);}
|
||||||
.v5-nrm-palette-item:hover{border-color:var(--v5-primary);color:var(--v5-primary);background:var(--v5-surface-hover);}
|
.v5-nrm-insp-hd .l{display:flex;align-items:center;gap:8px;font-size:.72rem;color:var(--v5-text);}
|
||||||
.v5-nrm-palette-item svg{width:11px;height:11px;stroke:currentColor;fill:none;stroke-width:2;}
|
.v5-nrm-insp-hd .l .pin{font-family:'JetBrains Mono',monospace;font-size:.58rem;font-weight:700;color:var(--v5-primary);padding:2px 6px;background:rgba(var(--v5-primary-rgb),.08);border-radius:4px;}
|
||||||
|
.v5-nrm-insp-hd .l b{font-weight:700;color:var(--v5-text);}
|
||||||
|
.v5-nrm-insp-grid{display:flex;flex-wrap:wrap;gap:.8rem 1rem;align-items:flex-end;}
|
||||||
|
.v5-nrm-insp-field{display:flex;flex-direction:column;gap:4px;}
|
||||||
|
.v5-nrm-insp-field.w80{flex:0 0 90px;}
|
||||||
|
.v5-nrm-insp-field.grow{flex:1;min-width:180px;}
|
||||||
|
.v5-nrm-insp-field label{font-size:.62rem;color:var(--v5-text-sec);font-weight:600;}
|
||||||
|
.v5-nrm-insp-field .hint{font-size:.58rem;color:var(--v5-text-muted);}
|
||||||
|
.v5-nrm-insp-inp{height:30px;padding:0 .55rem;background:var(--v5-surface-solid);border:1px solid var(--v5-border);border-radius:5px;color:var(--v5-text);font-family:'JetBrains Mono',monospace;font-size:.72rem;outline:none;width:100%;font-weight:600;}
|
||||||
|
.v5-nrm-insp-inp:focus{border-color:var(--v5-primary);box-shadow:0 0 0 3px rgba(var(--v5-primary-rgb),.12);}
|
||||||
|
|
||||||
|
/* segment — v4 디자인 (sliding indicator + sub) */
|
||||||
|
.v5-nrm-seg{display:inline-flex;border:1px solid var(--v5-border);border-radius:5px;overflow:hidden;background:var(--v5-surface-solid);flex-wrap:wrap;position:relative;}
|
||||||
|
.v5-nrm-seg button{padding:0 .7rem;height:30px;background:transparent;border:none;border-right:1px solid var(--v5-border);color:var(--v5-text-sec);font-size:.66rem;font-family:'JetBrains Mono',monospace;cursor:pointer;transition:background .2s cubic-bezier(.16,1,.3,1),color .25s,transform .15s;position:relative;overflow:hidden;font-weight:600;}
|
||||||
|
.v5-nrm-seg button:last-child{border-right:none;}
|
||||||
|
.v5-nrm-seg button:hover{background:var(--v5-surface-hover);color:var(--v5-text);}
|
||||||
|
.v5-nrm-seg button:active{transform:scale(.94);}
|
||||||
|
.v5-nrm-seg button.on{background:var(--v5-primary);color:#fff;}
|
||||||
|
.v5-nrm-seg button .sub{font-size:.52rem;opacity:.7;display:block;margin-top:1px;font-family:inherit;}
|
||||||
|
|
||||||
|
/* palette — v4 디자인 (en 추가, hover 회전) */
|
||||||
|
.v5-nrm-palette{margin-top:.6rem;display:flex;flex-wrap:wrap;gap:4px;align-items:center;}
|
||||||
|
.v5-nrm-palette .lbl{font-size:.62rem;color:var(--v5-text-muted);font-weight:600;margin-right:4px;}
|
||||||
|
.v5-nrm-palette-item{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 9px;background:var(--v5-surface-solid);border:1px solid var(--v5-border);border-radius:5px;color:var(--v5-text-sec);font-size:.66rem;cursor:pointer;transition:border-color .2s,color .2s,background .2s,transform .2s cubic-bezier(.34,1.56,.64,1);font-family:inherit;font-weight:600;animation:v5-nrm-slide-up .35s cubic-bezier(.16,1,.3,1) both;}
|
||||||
|
.v5-nrm-palette-item:nth-child(2){animation-delay:.6s;}
|
||||||
|
.v5-nrm-palette-item:nth-child(3){animation-delay:.65s;}
|
||||||
|
.v5-nrm-palette-item:nth-child(4){animation-delay:.7s;}
|
||||||
|
.v5-nrm-palette-item:nth-child(5){animation-delay:.75s;}
|
||||||
|
.v5-nrm-palette-item:nth-child(6){animation-delay:.8s;}
|
||||||
|
.v5-nrm-palette-item:nth-child(7){animation-delay:.85s;}
|
||||||
|
.v5-nrm-palette-item:hover{border-color:var(--v5-primary);color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.06);transform:translateY(-2px);}
|
||||||
|
.v5-nrm-palette-item:active{transform:translateY(0) scale(.95);}
|
||||||
|
.v5-nrm-palette-item svg{width:11px;height:11px;stroke:currentColor;fill:none;stroke-width:1.75;transition:transform .3s cubic-bezier(.34,1.56,.64,1);}
|
||||||
|
.v5-nrm-palette-item:hover svg{transform:rotate(-8deg) scale(1.15);}
|
||||||
|
.v5-nrm-palette-item .en{font-family:'JetBrains Mono',monospace;font-size:.56rem;color:var(--v5-text-muted);}
|
||||||
|
|
||||||
/* usage list */
|
/* usage list */
|
||||||
.v5-nrm-usage-list{display:flex;flex-direction:column;}
|
.v5-nrm-usage-list{display:flex;flex-direction:column;}
|
||||||
@@ -1688,6 +1800,8 @@ html.vt-color-changing .v5-admin-btn{
|
|||||||
.v5-nrm-usage-add{margin-top:.55rem;display:flex;align-items:center;justify-content:center;gap:6px;padding:.4rem .5rem;background:transparent;border:1.5px dashed rgba(var(--v5-primary-rgb),.22);border-radius:6px;color:var(--v5-text-muted);cursor:pointer;font-size:.66rem;font-weight:600;transition:all .15s;width:100%;font-family:inherit;}
|
.v5-nrm-usage-add{margin-top:.55rem;display:flex;align-items:center;justify-content:center;gap:6px;padding:.4rem .5rem;background:transparent;border:1.5px dashed rgba(var(--v5-primary-rgb),.22);border-radius:6px;color:var(--v5-text-muted);cursor:pointer;font-size:.66rem;font-weight:600;transition:all .15s;width:100%;font-family:inherit;}
|
||||||
.v5-nrm-usage-add:hover{border-color:var(--v5-primary);color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.03);}
|
.v5-nrm-usage-add:hover{border-color:var(--v5-primary);color:var(--v5-primary);background:rgba(var(--v5-primary-rgb),.03);}
|
||||||
.v5-nrm-usage-add svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;}
|
.v5-nrm-usage-add svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;}
|
||||||
|
.v5-nrm-usage-note{margin-top:.55rem;display:flex;align-items:center;justify-content:center;gap:6px;padding:.4rem .5rem;background:var(--v5-bg-subtle);border:1px dashed var(--v5-border);border-radius:6px;color:var(--v5-text-muted);font-size:.64rem;font-weight:600;}
|
||||||
|
.v5-nrm-usage-note svg{width:11px;height:11px;stroke:currentColor;fill:none;stroke-width:2;}
|
||||||
|
|
||||||
/* sequence stats */
|
/* sequence stats */
|
||||||
.v5-nrm-seq-stats{display:grid;grid-template-columns:1fr 1fr;gap:.85rem 1.4rem;margin-bottom:.85rem;}
|
.v5-nrm-seq-stats{display:grid;grid-template-columns:1fr 1fr;gap:.85rem 1.4rem;margin-bottom:.85rem;}
|
||||||
@@ -1800,3 +1914,23 @@ html.vt-color-changing .v5-admin-btn{
|
|||||||
.v5-nrm-empty{display:flex;align-items:center;justify-content:center;flex-direction:column;gap:.5rem;padding:4rem 2rem;color:var(--v5-text-muted);font-size:.78rem;text-align:center;}
|
.v5-nrm-empty{display:flex;align-items:center;justify-content:center;flex-direction:column;gap:.5rem;padding:4rem 2rem;color:var(--v5-text-muted);font-size:.78rem;text-align:center;}
|
||||||
.v5-nrm-empty svg{width:32px;height:32px;opacity:.5;stroke:currentColor;fill:none;stroke-width:1.5;}
|
.v5-nrm-empty svg{width:32px;height:32px;opacity:.5;stroke:currentColor;fill:none;stroke-width:1.5;}
|
||||||
.v5-nrm-empty .hint{font-size:.66rem;color:var(--v5-text-muted);}
|
.v5-nrm-empty .hint{font-size:.66rem;color:var(--v5-text-muted);}
|
||||||
|
|
||||||
|
@media (max-width:1180px){
|
||||||
|
.v5-nrm-body{grid-template-columns:280px 1fr;}
|
||||||
|
.v5-nrm-topbar{grid-template-columns:1fr auto;}
|
||||||
|
.v5-nrm-topstats{display:none;}
|
||||||
|
.v5-nrm-row-split{grid-template-columns:1fr;}
|
||||||
|
.v5-nrm-row-split > div + div{border-left:0;border-top:1px solid var(--v5-border);}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:860px){
|
||||||
|
.v5-nrm-body{grid-template-columns:1fr;}
|
||||||
|
.v5-nrm-side{display:none;}
|
||||||
|
.v5-nrm-topbar{padding:.75rem .9rem;grid-template-columns:1fr;align-items:start;}
|
||||||
|
.v5-nrm-topactions{justify-content:flex-start;}
|
||||||
|
.v5-nrm-detail-head,.v5-nrm-row,.v5-nrm-row-split > div{padding-left:1rem;padding-right:1rem;}
|
||||||
|
.v5-nrm-hero-wrap{padding-left:1rem;padding-right:1rem;}
|
||||||
|
.v5-nrm-hero-card{padding:1.75rem 1rem 1rem;}
|
||||||
|
.v5-nrm-hero-corner{position:static;display:block;margin:-.55rem 0 .75rem;max-width:100%;text-align:center;}
|
||||||
|
.v5-nrm-savebar{padding:.55rem 1rem;align-items:stretch;flex-direction:column;}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
# Control IDE EditCanvas 구조 리팩토링
|
||||||
|
|
||||||
|
> 작성일: 2026-05-20
|
||||||
|
> 작업자: gbpark
|
||||||
|
> 관련 mockup: `frontend/control-mode.standalone.html` (V3 EditCanvas)
|
||||||
|
> 관련 시안 산출물: `notes/gbpark/2026-05-19-control-mockup/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 한 줄 요약
|
||||||
|
|
||||||
|
INVYONE 제어 모드(Control IDE)의 EditCanvas — 비즈니스 룰 시각 에디터를 **mockup V3 시안 톤으로 정밀하게** 정렬하면서, 사용자가 지적한 **4가지 구조 결함**을 Phase 1~3로 분해해 정정. 작업 중간 GPT(Codex) 와 2차례 의견 교환·검증을 거쳐 critical 버그까지 fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 배경
|
||||||
|
|
||||||
|
### 1.1 mockup 출발점
|
||||||
|
|
||||||
|
- `control-mode.standalone.html` 의 `v3-canvas.jsx` 가 V3 EditCanvas 의 진실의 원천.
|
||||||
|
- 노드(V3RuleNode): cat-color stripe + cat-chip header + label + summary + ports.
|
||||||
|
- 연결선: orthogonal-with-rounded-corners SVG path, **화살표 마커 없음**.
|
||||||
|
- mockup 의 RULE_NODES 는 **액션 노드만** (status-change / validation / condition / auto-insert / ...) — **테이블 카드는 mockup standalone 에 없음**.
|
||||||
|
- 사용자가 별도로 그린 user_info 컬럼 리스트 카드는 invyone 자체 확장. 이 확장의 정확한 모델이 핵심 논의 대상.
|
||||||
|
|
||||||
|
### 1.2 작업 시작 시점의 결함
|
||||||
|
|
||||||
|
- 노드 비주얼이 V3RuleNode 시안과 톤 어긋남 (`ide/V3RuleNode.tsx`, `ide/Canvas.tsx` 자체 구현). 시안 inline style 을 그대로 복붙.
|
||||||
|
- 컴포넌트 이름이 `V3RuleNode`, `V3CtxMenu`, `EditCanvas` 등 **시안 단계 명칭** 박힘 (CLAUDE.md 의 "시안 명칭 사용 금지" 위반).
|
||||||
|
- 컬럼별 port stripe(좌·우)가 컬럼마다 N개 → **50컬럼 테이블에서 시각 폭발**.
|
||||||
|
- 노드 설정창의 "필드/대상 테이블/값" 입력이 **영어 텍스트 자유 입력** → 사용자가 컬럼명을 타이핑해야 동작. 실사용 불가.
|
||||||
|
- 노드↔노드, 컬럼↔노드 연결 시 SVG path 가 **dashed + 마칭 ant 애니메이션** + 화살표 marker → mockup 의 깔끔한 solid line 과 어긋남.
|
||||||
|
- mouseup race condition 으로 **연결선 자체가 절대 안 생성됨** (cleanup 이 PortHandle.onMouseUp 보다 먼저 발생 → `dragRef.current = null` 처리).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 사용자 지적 (구조 결함 4가지)
|
||||||
|
|
||||||
|
1. **컬럼별 port stripe는 50컬럼 테이블에서 안 통함** — user_info 8개도 빡빡. port hit target N×2 = 시각 노이즈.
|
||||||
|
2. **노드 설정의 "필드" 영어 자유 입력 = 실사용 불가** — 연결된 테이블의 컬럼 한글 라벨 dropdown 이어야 함.
|
||||||
|
3. **조건분기 → 다른 테이블 값 변경 흐름이 빌더에서 명시 안 됨** — 예시: 수주.결재상태 = "결재완료" 면 발주.결재상태 = "수주결재완료" 로 자동 변경. 발주 테이블 카드는 연결만 됐을 뿐, "어느 컬럼 / 어떤 값" 설정 어디서 하는지 사용자 입장에서 불명.
|
||||||
|
4. **컬럼별 port 가 너무 많아 직각 라우팅이 카드 사이를 휘감음** — 라우팅 자체보다 port 모델이 잘못된 것.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Codex (GPT) 검증 사이클
|
||||||
|
|
||||||
|
### 3.1 사전 의견 교환 (Phase 1~3 시작 전)
|
||||||
|
|
||||||
|
| 항목 | 내가 제시한 1안 | Codex 추가 권장 |
|
||||||
|
|---|---|---|
|
||||||
|
| 테이블 port 모델 | 좌·우 단일 port | ✓ 동의 (n8n/Retool/Appsmith 컨벤션) |
|
||||||
|
| 테이블 카드 비주얼 | 그대로 + dot 변경 | **V3RuleNode 와 동일한 컴팩트 카드** (180px, 9px radius, 3px top stripe, Lucide `Database` 아이콘, mono 영문 sub, `N cols · K FK · M used` stats row) |
|
||||||
|
| Field dropdown | 한글 + 영문 sub | ✓ + multi-table namespace (`수주관리 · 결재상태` / `ORDER_MGMT.APPROVAL_STATUS` / `select · enum`) + 상단 filter tabs |
|
||||||
|
| 대상 테이블 자동 추론 | 항상 자동 | **연결된 테이블 1개일 때만 자동, 2개+ 면 required dropdown** |
|
||||||
|
|
||||||
|
Codex 가 지적한 추가 blind spot (작업 범위에 반영):
|
||||||
|
- [HIGH] Edge 타입 정의 (data context / execution flow / table mutation / lookup)
|
||||||
|
- [HIGH] Multi-table field collision → 내부 ID는 fully qualified `{table, column}` 저장, 한글 라벨은 presentation only
|
||||||
|
- [HIGH] 실행 순서·순환 검증 (Phase 5+ 로 deferral)
|
||||||
|
- [MEDIUM] Enum metadata fallback, undo/redo, copy-paste subgraph remap
|
||||||
|
|
||||||
|
### 3.2 사후 검증 (Phase 1~3 완료 후)
|
||||||
|
|
||||||
|
| Bug | Severity | 내용 | 조치 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 10 | HIGH | `usePortDrag.finishDrag` 가 `to_port` 가 `in` 인지 검증 안 함 → output → output 역방향 엣지 저장 가능 | port direction validation 추가 (action 노드 도착 시 to_port 가 `in` 이어야, action 노드 출발 시 from_port 가 `in` 이 아니어야) |
|
||||||
|
| 4 | MEDIUM | `TablePicker` 의 `queueMicrotask` 가 렌더 클로저 캡처 → Strict 모드 stale render 위험 | `useEffect` 로 이전 (committed lifecycle) |
|
||||||
|
| 1 | LOW | `TableNode` head bottom padding `.25rem` vs V3 `.5rem`, stats font `.58rem` vs V3 `.5rem` | `.35rem` + `.5rem` 으로 V3 매칭 |
|
||||||
|
| 6b | LOW | duplicate 검출이 render-captured stale `ruleConnections` 사용 | `useControlMode.getState().ruleConnections` 로 변경 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Phase 별 작업
|
||||||
|
|
||||||
|
### Phase 1 — 테이블 카드 V3 컴팩트 + 단일 port
|
||||||
|
|
||||||
|
**변경 파일**:
|
||||||
|
- `frontend/components/control/TableNode.tsx` — 완전 재작성
|
||||||
|
- `frontend/components/control/RuleBuilder.tsx` — `portPos` 의 `col:*` 분기 폐기
|
||||||
|
- `frontend/components/control/ControlMode.tsx` — `slice(0, 8)` 제거 (모든 컬럼 로드)
|
||||||
|
- `frontend/styles/control-mode.css` — `.tbl-node-compact` 클래스 신규
|
||||||
|
|
||||||
|
**핵심 변화**:
|
||||||
|
```
|
||||||
|
[기존] 220px 폭, 컬럼 N개 row, 컬럼마다 좌·우 stripe port, 카드 좌측 cyan 그라데이션 head
|
||||||
|
[변경] 180px 폭, 3px top stripe, Lucide Database 아이콘, 한글 라벨 + mono 영문 sub,
|
||||||
|
stats row `N cols · K PK · M FK`, 좌·우 edge 단일 port 1개씩
|
||||||
|
```
|
||||||
|
|
||||||
|
**RuleBuilder.portPos 단순화**:
|
||||||
|
```ts
|
||||||
|
if (node.type === 'table') {
|
||||||
|
const cardW = 180, cardH = 70;
|
||||||
|
const yMid = node.y + cardH / 2;
|
||||||
|
if (port === 'in') return { x: node.x, y: yMid };
|
||||||
|
return { x: node.x + cardW, y: yMid };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`col:*` 분기 완전 폐기. 컬럼 선택은 노드 설정창 dropdown 에서 처리.
|
||||||
|
|
||||||
|
### Phase 2 — NodeConfigPopover schema-driven dropdown
|
||||||
|
|
||||||
|
**변경 파일**:
|
||||||
|
- `frontend/components/control/NodeConfigPopover.tsx` — 완전 재작성
|
||||||
|
- `frontend/styles/control-mode.css` — `.cfg-empty`, `.cfg-static`, optgroup 색감 추가
|
||||||
|
|
||||||
|
**새 helper**:
|
||||||
|
- `flattenColumns(tables)` — 연결된 테이블들의 모든 컬럼을 flat ColumnMeta[] 로
|
||||||
|
- `serializeField` / `deserializeField` — `{table, column}` ↔ `"table|column"` 문자열 직렬화 (HTML select value 호환)
|
||||||
|
- `findColumn(cols, field)` — string(legacy) 와 object 양방향 매칭
|
||||||
|
- `displayField(field, cols)` — 한글 라벨 → 영문 컬럼명 fallback
|
||||||
|
|
||||||
|
**새 컴포넌트**:
|
||||||
|
| 컴포넌트 | 역할 | 데이터 출처 |
|
||||||
|
|---|---|---|
|
||||||
|
| `FieldPicker` | `<select>` + `<optgroup>` 으로 테이블별 컬럼 그룹화. value 는 `"table|column"` 직렬화. PK/enum 태그 부착 | connectedTables[].columns |
|
||||||
|
| `TablePicker` | 1개면 `useEffect` 로 자동 채움 + readonly 표시. 2개+ 면 `<select>` | connectedTables |
|
||||||
|
| `ValuePicker` | field type 이 `select` + `options` 있으면 enum dropdown, 아니면 typed input | findColumn(field).options |
|
||||||
|
|
||||||
|
**핵심 4종 schema-driven 화**: `condition` / `status-change` / `calculation` / `validation` 의 `field` / `table` / `value` 입력을 새 컴포넌트로 교체. summary 생성 시 `displayField` helper 로 한글 라벨 사용.
|
||||||
|
|
||||||
|
**연결된 테이블 추적**:
|
||||||
|
```ts
|
||||||
|
const connectedTables = useMemo(() => {
|
||||||
|
const tableNodeIds = new Set<string>();
|
||||||
|
ruleConnections.forEach((c) => {
|
||||||
|
if (c.from_node_id === configNodeId) tableNodeIds.add(c.to_node_id);
|
||||||
|
if (c.to_node_id === configNodeId) tableNodeIds.add(c.from_node_id);
|
||||||
|
});
|
||||||
|
return ruleNodes.filter((n) => n.type === 'table' && tableNodeIds.has(n.id));
|
||||||
|
}, [configNodeId, ruleNodes, ruleConnections]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3 — Edge 타입 4종 + fully qualified ID
|
||||||
|
|
||||||
|
**변경 파일**:
|
||||||
|
- `frontend/components/control/hooks/usePortDrag.ts` — `finishDrag` 에 `edge_type` 자동 추론 + port direction validation
|
||||||
|
- `frontend/components/control/RuleBuilder.tsx` — path className 에 `edge-${type}` + 선 중간 라벨 chip
|
||||||
|
- `frontend/styles/control-mode.css` — `.rule-conn-path.edge-*` 4종 stroke 정의
|
||||||
|
|
||||||
|
**Edge 타입 자동 추론** (from/to 노드 타입 기반):
|
||||||
|
| 패턴 | edge_type | stroke |
|
||||||
|
|---|---|---|
|
||||||
|
| table → table | `lookup` | dashed green |
|
||||||
|
| table → action | `data-context` | solid cyan |
|
||||||
|
| action → table | `table-mutation` | solid pink |
|
||||||
|
| action → action | `execution-flow` | solid purple |
|
||||||
|
|
||||||
|
YES/NO port 의 stroke specificity 가 더 높아 override 가능 (조건분기 yes 분기는 항상 초록 solid 등).
|
||||||
|
|
||||||
|
**선 중간 라벨 chip** (mockup v3 EditCanvas 스타일):
|
||||||
|
| from_port | 라벨 | 색 |
|
||||||
|
|---|---|---|
|
||||||
|
| `yes` | 예 | green |
|
||||||
|
| `no` | 아니오 | muted gray |
|
||||||
|
| `pass` | 통과 | green |
|
||||||
|
| `fail` | 실패 | red |
|
||||||
|
| `approved` | 승인 | green |
|
||||||
|
| `rejected` | 반려 | muted gray |
|
||||||
|
| `each` / `done` | 반복 / 완료 | (edge_type 색) |
|
||||||
|
|
||||||
|
라벨 폭은 글자 수 기반 동적 (`max(36, length*8+14)`). `c.label` 명시 시 자동 라벨 override 가능.
|
||||||
|
|
||||||
|
**fully qualified ID 저장**:
|
||||||
|
- node.config.field, node.config.table 등은 모두 `{table, column}` 객체 또는 `string` (legacy 호환).
|
||||||
|
- presentation 은 `displayField` 로 한글 라벨, 저장은 객체 그대로.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 작업 중 fix 한 critical 버그
|
||||||
|
|
||||||
|
### 5.1 mouseup race condition (연결선 절대 안 생성)
|
||||||
|
|
||||||
|
**증상**: 컬럼 dot 에서 끌어 노드 in port 위에서 떼도 연결선 안 그어짐.
|
||||||
|
|
||||||
|
**원인**: `usePortDrag.ts` 의 document mouseup 핸들러가 PortHandle 의 onMouseUp 보다 먼저 발생 → `cleanup()` 으로 `dragRef.current = null` → 그 직후 PortHandle 의 onMouseUp → `finishDrag` 첫 줄 `if (!d) return` 으로 즉시 탈출.
|
||||||
|
|
||||||
|
**수정**: document mouseup 에서 직접 `e.target.closest('.ctrl-io-port')` 로 port 찾고 `finishDrag` 호출. PortHandle.onMouseUp 의존 폐기. 추가로 6x6 dot hit-target 문제 해결을 위해 좌표 fallback (마우스 위치 24px 반경 내 가장 가까운 port 검색):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const onUp = (e: MouseEvent) => {
|
||||||
|
if (!dragRef.current) return;
|
||||||
|
let portEl = (e.target as HTMLElement | null)?.closest?.('.ctrl-io-port') as HTMLElement | null;
|
||||||
|
if (!portEl) {
|
||||||
|
// 좌표 fallback — 24px 반경 가장 가까운 port
|
||||||
|
let best: { el: HTMLElement; dist: number } | null = null;
|
||||||
|
document.querySelectorAll<HTMLElement>('.ctrl-io-port').forEach((el) => {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
const dx = e.clientX - (r.left + r.width / 2);
|
||||||
|
const dy = e.clientY - (r.top + r.height / 2);
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (dist < 24 && (!best || dist < best.dist)) best = { el, dist };
|
||||||
|
});
|
||||||
|
if (best) portEl = best.el;
|
||||||
|
}
|
||||||
|
if (portEl?.dataset.node && portEl.dataset.port) {
|
||||||
|
finishDrag(portEl.dataset.node, portEl.dataset.port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 NodeConfigPopover 가 안 뜸
|
||||||
|
|
||||||
|
**증상**: 노드 클릭해도 설정 팝오버 안 보임.
|
||||||
|
|
||||||
|
**원인**: 외부 클릭 닫기 핸들러가 `.ctrl-an-body` 만 무시. ControlNode 를 V3 비주얼로 갈아치우면서 클래스가 `.v3-rule-node-body` 로 바뀜 → 노드 클릭 = 토글 이지만 같은 click event 가 document handler 도착 → closest('.ctrl-an-body') = null → 즉시 setConfigNodeId(null).
|
||||||
|
|
||||||
|
**수정**: close handler 에 `.v3-rule-node`, `.tbl-node` 도 무시 추가.
|
||||||
|
|
||||||
|
### 5.3 SVG path 가 dashed + 마칭 ant
|
||||||
|
|
||||||
|
**증상**: 연결선이 점선 + 흘러가는 애니메이션. mockup 의 solid line 과 어긋남.
|
||||||
|
|
||||||
|
**원인**: `.rule-conn-path` CSS 에 `stroke-dasharray: 6 3; animation: ctrlPulse 1.5s linear infinite` 박힘. path 함수를 mockup orthogonal-with-rounded-corners 로 바꿔도 CSS 가 덮어씀.
|
||||||
|
|
||||||
|
**수정**: `.rule-conn-path` 의 dasharray + animation 제거. solid + `stroke-linecap/linejoin: round` + opacity 0.75. `.conn-no` 만 dashed 유지.
|
||||||
|
|
||||||
|
### 5.4 ControlNode summary 가 `[object Object]`
|
||||||
|
|
||||||
|
**증상**: 노드 body 에 `field: [object Object]` 표시.
|
||||||
|
|
||||||
|
**원인**: Phase 2 에서 field 가 `{table, column}` 객체 저장. ControlNode 의 summary fallback `Object.entries(node.config).slice(0, 1).map(([k, v]) => `${k}: ${v}`)[0]` 가 객체를 그대로 string concat → `[object Object]`.
|
||||||
|
|
||||||
|
**수정**: `formatVal(v)` helper — 객체면 `v.label ?? v.column`, primitive 면 `String(v)`. summary 우선순위 명확히:
|
||||||
|
1. `node.config.summary` (NodeConfigPopover 가 저장한 한글 라벨)
|
||||||
|
2. `node.summary[0]`
|
||||||
|
3. config entries fallback (formatVal 적용)
|
||||||
|
4. `'클릭하여 설정'`
|
||||||
|
|
||||||
|
### 5.5 화살표 마커 + 컬럼 stripe 큰 dot
|
||||||
|
|
||||||
|
mockup v3 EditCanvas 도 화살표 마커 없음 → `markerEnd` 제거.
|
||||||
|
|
||||||
|
컬럼 stripe 도 14x14 너무 큼 (사용자 분노) → 8x8 으로 롤백, 결국 컬럼별 stripe 모델 자체 폐기 (Phase 1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 데이터 모델 변화
|
||||||
|
|
||||||
|
### 6.1 RuleNode (table 노드)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
id: 'tbl-user_info',
|
||||||
|
type: 'table',
|
||||||
|
table_name: 'user_info',
|
||||||
|
label: '사용자정보',
|
||||||
|
columns: FieldConfig[], // 모든 컬럼 (이전 slice(0, 8) 폐기)
|
||||||
|
x, y,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 RuleNode.config (action 노드 설정값)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 이전 (legacy)
|
||||||
|
{ field: 'STATUS', table: 'user_info', value: 'completed' }
|
||||||
|
|
||||||
|
// Phase 2/3
|
||||||
|
{
|
||||||
|
field: { table: 'user_info', column: 'APPROVAL_STATUS' }, // fully qualified
|
||||||
|
table: 'user_info',
|
||||||
|
value: 'completed',
|
||||||
|
summary: '결재상태 = "결재완료"', // 한글 라벨 (NodeConfigPopover 가 자동 생성)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
호환: `findColumn`, `displayField`, `serializeField` 가 string/object 둘 다 처리.
|
||||||
|
|
||||||
|
### 6.3 Connection
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
id: 'conn-3',
|
||||||
|
from_node_id: 'ctrl-condition-1',
|
||||||
|
from_port: 'yes',
|
||||||
|
to_node_id: 'ctrl-status-change-2',
|
||||||
|
to_port: 'in',
|
||||||
|
edge_type: 'execution-flow', // 신규 (자동 추론)
|
||||||
|
label?: '예 · 부족', // 선택, 자동 라벨 override
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 변경 파일 일람
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/components/control/
|
||||||
|
TableNode.tsx ★ 완전 재작성 (180px 컴팩트, Lucide Database, stats row, 단일 port)
|
||||||
|
ControlNode.tsx ★ V3 비주얼 갈아치움 + summary 우선순위 수정 + formatVal
|
||||||
|
RuleBuilder.tsx portPos col:* 폐기, path 에 edge_type 클래스 + 선 중간 label chip
|
||||||
|
PortHandle.tsx type 분기 폐기 (mousedown + mouseup 둘 다 수신)
|
||||||
|
NodeConfigPopover.tsx ★ 완전 재작성 (FieldPicker / TablePicker / ValuePicker)
|
||||||
|
ConnectionLine.tsx bezierPath → orthogonal + rounded corners (mockup 일치)
|
||||||
|
hooks/usePortDrag.ts edge_type 자동 추론 + port direction validation + 좌표 fallback
|
||||||
|
hooks/useControlMode.ts (변경 없음 — connection 의 edge_type 은 Record<string,any> 라 자동 호환)
|
||||||
|
ControlMode.tsx slice(0, 8) 제거, primary_table 자동 등장 폐기
|
||||||
|
ide/Canvas.tsx EditCanvas → RuleBuilder 위임 (PanZoom+V3RuleNode 자체구현 폐기)
|
||||||
|
|
||||||
|
frontend/styles/control-mode.css
|
||||||
|
.tbl-node-compact ... ★ 신규 (V3 컴팩트 톤)
|
||||||
|
.cfg-empty / .cfg-static ★ 신규 (schema-driven dropdown 보조)
|
||||||
|
.rule-conn-path ★ dashed + animation 제거 → solid + linecap round
|
||||||
|
.rule-conn-path.edge-* ★ 4종 stroke 색
|
||||||
|
.ctrl-io-port.tbl-io 제거 (컬럼 port 폐기)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 남은 작업 (Phase 4+)
|
||||||
|
|
||||||
|
### [HIGH] (실사용 위험)
|
||||||
|
1. **순환 참조 검증** — DFS cycle check on connection 추가 전. 현재 self-loop 만 차단.
|
||||||
|
2. **Topological 정렬** — 실행 시 노드 순서. 현재 좌표만 저장.
|
||||||
|
3. **legacy string field × 다중 테이블 ambiguity** — 첫 번째 동명 컬럼에 매칭. `displayField` 에 "(알 수 없는 테이블)" 경고 추가.
|
||||||
|
|
||||||
|
### [MEDIUM] (UX 향상)
|
||||||
|
4. **Undo / Redo** — zustand store 에 command history 추가.
|
||||||
|
5. **Copy / Paste subgraph** — node ID remap, table binding 보존.
|
||||||
|
6. **used-field chips** — 노드/카드에 실제 사용 중인 컬럼 시각 표시.
|
||||||
|
|
||||||
|
### [LOW]
|
||||||
|
7. **컴포넌트 이름 rename** — `V3RuleNode`, `V3CtxMenu`, `EditCanvas` 등 시안 명칭 → `RuleNode`, `RuleContextMenu`, `RuleCanvas`. `ide/` 폴더 통째 rename.
|
||||||
|
8. **inline rgb → v5 토큰** — 일부 inline `rgba(108,92,231, ...)` 가 남아있음. `--v5-primary-rgb` 등으로 일괄 교체.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 검증 결과 (Codex 사후)
|
||||||
|
|
||||||
|
| 항목 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| Phase 1 단일 port + col:* 폐기 | ✓ VERIFIED |
|
||||||
|
| Phase 2 dropdown 데이터 흐름 | ✓ VERIFIED |
|
||||||
|
| Phase 3 edge_type 추론 | ✓ VERIFIED |
|
||||||
|
| useControlMode.getState() 패턴 | ✓ VERIFIED |
|
||||||
|
| legacy connection (edge_type 없음) fallback | ✓ VERIFIED |
|
||||||
|
| string/object field 양방향 호환 | ✓ VERIFIED |
|
||||||
|
| multi-table optgroup + 직렬화 충돌 없음 | ✓ VERIFIED |
|
||||||
|
| TypeScript 컴파일 (control 폴더) | ✓ 에러 0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 교훈 / 메모
|
||||||
|
|
||||||
|
### 10.1 mockup html 은 "시각 참고"지 "복붙 대상" 아님
|
||||||
|
- 처음에 `V3RuleNode.tsx`, `V3CtxMenu`, `EditCanvas` 등 시안 명칭을 코드에 박고 inline style 도 mockup 의 rgb 값 그대로 들고 옴 → CLAUDE.md 의 "시안 명칭 사용 금지" + cp 시스템 컨벤션 위반.
|
||||||
|
- 옳은 흐름: mockup → 시각·UX 이해 → invyone v5 디자인 시스템(`--v5-*` 토큰, cp 프리미티브, v5-layout.css) 위에 다시 그림.
|
||||||
|
|
||||||
|
### 10.2 컬럼 단위 port 는 직관적이지만 확장 불가
|
||||||
|
- 컬럼별 port 가 데이터 lineage 를 가장 명확히 보여줌은 사실이지만, ERP 의 50+ 컬럼 테이블에서는 시각 무너짐.
|
||||||
|
- 테이블 단위 port + 노드 설정에서 컬럼 dropdown 이 n8n / Retool / Appsmith 의 공통 패턴.
|
||||||
|
|
||||||
|
### 10.3 단일 dot 양방향화의 race condition
|
||||||
|
- HTML5 native event + React synthetic event 의 mouseup 발화 순서 차이로 race 발생. document mouseup 에서 직접 처리 + 좌표 fallback 이 가장 안정적.
|
||||||
|
|
||||||
|
### 10.4 schema-driven dropdown 의 multi-table 처리
|
||||||
|
- `<optgroup>` 으로 테이블별 group 분리 + value 직렬화 `"table|column"` 가 가장 단순한 충돌 회피 패턴.
|
||||||
|
- 라벨은 한글 우선, 영문 sub 는 mono font 로 보조. 같은 영문 컬럼명이 두 테이블에 있어도 optgroup 으로 시각 + value 구분.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 관련 문서
|
||||||
|
|
||||||
|
- mockup 시각 시안: `frontend/control-mode.standalone.html`
|
||||||
|
- 추출본 (참고): `/tmp/ctrl-standalone/` (v3-canvas.jsx, shared.jsx, rich-ui.jsx 등 — 임시)
|
||||||
|
- 이전 작업 누적 기록: `notes/gbpark/2026-05-19-control-mockup/`
|
||||||
|
- CLAUDE.md 컨벤션: 디자인 시스템 + 시안 명칭 금지 + cp 프리미티브 표준
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V2 시안 갤러리</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v2.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="gallery">
|
||||||
|
<div class="gallery-inner">
|
||||||
|
<section class="gallery-hero" data-od-id="gallery-hero">
|
||||||
|
<div>
|
||||||
|
<span class="chip primary">INVYONE NUMBERING ADMIN · V2</span>
|
||||||
|
<h1 style="margin-top:14px;">채번을 관리가 아니라 조작 가능한 시스템으로 보이게</h1>
|
||||||
|
<p>메뉴관리의 좌우 밀도, 회사 멀티테넌시의 3컬럼 캐스케이드, CP 디자인의 속성 편집 문법을 섞어서 다시 만든 HTML 시안입니다. 운영 파일은 건드리지 않았고, 아래 세 방향을 따로 비교할 수 있습니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="status-chip"><span class="dot"></span>HTML 시안 3개 · 독립 화면</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="gallery-grid" data-od-id="gallery-grid">
|
||||||
|
<article class="gallery-card">
|
||||||
|
<div>
|
||||||
|
<span class="chip green">A · CASCADE</span>
|
||||||
|
<h2 style="margin-top:12px;">3컬럼 관리형</h2>
|
||||||
|
<p>회사·조직 관리처럼 범위, 규칙 묶음, 상세를 단계적으로 고르는 구조입니다. 어드민 사용자가 빠르게 위치를 파악하기 좋습니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="preview-mini">
|
||||||
|
<div class="mini-side"></div>
|
||||||
|
<div class="mini-main">
|
||||||
|
<div class="mini-line short"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
<div class="mini-line" style="width:65%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn primary" href="./variant-a-cascade.html">A 시안 열기</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="gallery-card">
|
||||||
|
<div>
|
||||||
|
<span class="chip primary">B · CP INSPECTOR</span>
|
||||||
|
<h2 style="margin-top:12px;">조립 작업대형</h2>
|
||||||
|
<p>중앙에서 채번 포맷을 직접 조립하고, 우측 CP 패널에서 선택 파트 속성을 편집합니다. “채번 디자이너” 느낌이 가장 강합니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="preview-mini" style="grid-template-columns:.35fr 1fr .45fr;">
|
||||||
|
<div class="mini-side"></div>
|
||||||
|
<div class="mini-main"><div class="mini-line short"></div><div class="mini-line"></div></div>
|
||||||
|
<div class="mini-side"></div>
|
||||||
|
</div>
|
||||||
|
<a class="btn primary" href="./variant-b-cp-workbench.html">B 시안 열기</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="gallery-card">
|
||||||
|
<div>
|
||||||
|
<span class="chip amber">C · GUARD</span>
|
||||||
|
<h2 style="margin-top:12px;">검증 보드형</h2>
|
||||||
|
<p>중복, 리셋, 테이블 연결, 마지막 발번 이력을 전면화한 운영자 친화형입니다. 실수 방지와 승인 플로우에 가장 적합합니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="preview-mini" style="grid-template-columns:.45fr 1fr;">
|
||||||
|
<div class="mini-side"></div>
|
||||||
|
<div class="mini-main">
|
||||||
|
<div class="mini-line short" style="background:var(--amber);"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
<div class="mini-line" style="width:72%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn primary" href="./variant-c-guard.html">C 시안 열기</a>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,641 @@
|
|||||||
|
:root {
|
||||||
|
--bg: oklch(98% 0.004 255);
|
||||||
|
--bg-subtle: oklch(95% 0.008 255);
|
||||||
|
--surface: oklch(100% 0 0);
|
||||||
|
--surface-hover: oklch(96.5% 0.009 255);
|
||||||
|
--surface-strong: oklch(92% 0.012 255);
|
||||||
|
--text: oklch(18% 0.018 255);
|
||||||
|
--text-sec: oklch(44% 0.018 255);
|
||||||
|
--text-muted: oklch(62% 0.014 255);
|
||||||
|
--text-faint: oklch(78% 0.01 255);
|
||||||
|
--border: oklch(88% 0.008 255);
|
||||||
|
--border-strong: oklch(76% 0.018 255);
|
||||||
|
--primary: oklch(58% 0.17 258);
|
||||||
|
--primary-soft: color-mix(in oklch, var(--primary) 10%, transparent);
|
||||||
|
--cyan: oklch(67% 0.14 205);
|
||||||
|
--cyan-soft: color-mix(in oklch, var(--cyan) 13%, transparent);
|
||||||
|
--green: oklch(61% 0.16 155);
|
||||||
|
--green-soft: color-mix(in oklch, var(--green) 12%, transparent);
|
||||||
|
--amber: oklch(70% 0.15 78);
|
||||||
|
--amber-soft: color-mix(in oklch, var(--amber) 14%, transparent);
|
||||||
|
--red: oklch(58% 0.18 25);
|
||||||
|
--red-soft: color-mix(in oklch, var(--red) 11%, transparent);
|
||||||
|
--shadow-sm: 0 10px 26px color-mix(in oklch, var(--primary) 9%, transparent);
|
||||||
|
--shadow-md: 0 24px 72px color-mix(in oklch, var(--primary) 14%, transparent);
|
||||||
|
--font-sans: -apple-system, BlinkMacSystemFont, "Pretendard", "Apple SD Gothic Neo", "Malgun Gothic", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "D2Coding", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
--radius-sm: 7px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
--topbar-h: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg: oklch(11% 0.012 260);
|
||||||
|
--bg-subtle: oklch(15% 0.014 260);
|
||||||
|
--surface: oklch(18% 0.014 260);
|
||||||
|
--surface-hover: oklch(22% 0.015 260);
|
||||||
|
--surface-strong: oklch(25% 0.016 260);
|
||||||
|
--text: oklch(94% 0.004 260);
|
||||||
|
--text-sec: oklch(72% 0.01 260);
|
||||||
|
--text-muted: oklch(54% 0.011 260);
|
||||||
|
--text-faint: oklch(38% 0.012 260);
|
||||||
|
--border: oklch(29% 0.012 260);
|
||||||
|
--border-strong: oklch(42% 0.018 260);
|
||||||
|
--primary: oklch(73% 0.13 258);
|
||||||
|
--cyan: oklch(80% 0.13 185);
|
||||||
|
--green: oklch(76% 0.14 155);
|
||||||
|
--amber: oklch(82% 0.13 82);
|
||||||
|
--red: oklch(72% 0.16 25);
|
||||||
|
--shadow-sm: 0 0 28px color-mix(in oklch, var(--primary) 13%, transparent);
|
||||||
|
--shadow-md: 0 0 70px color-mix(in oklch, var(--primary) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; min-height: 100%; }
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
button, input, select, textarea { font: inherit; color: inherit; }
|
||||||
|
button { cursor: pointer; }
|
||||||
|
a { color: inherit; text-decoration: none; }
|
||||||
|
h1, h2, h3, h4, p { margin: 0; }
|
||||||
|
h1, h2, h3 { letter-spacing: -0.02em; text-wrap: balance; }
|
||||||
|
p { text-wrap: pretty; }
|
||||||
|
svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
|
|
||||||
|
.demo-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: var(--topbar-h) 1fr;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 82% 10%, color-mix(in oklch, var(--primary) 10%, transparent), transparent 26rem),
|
||||||
|
radial-gradient(circle at 12% 88%, color-mix(in oklch, var(--cyan) 10%, transparent), transparent 28rem),
|
||||||
|
var(--bg);
|
||||||
|
}
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 0 22px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: color-mix(in oklch, var(--surface) 94%, transparent);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.brand-mark {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 9px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--cyan));
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 900;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.brand h1 { font-size: 16px; font-weight: 800; }
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.breadcrumb b { color: var(--text-sec); }
|
||||||
|
.top-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
||||||
|
.btn {
|
||||||
|
height: 30px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0 11px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 750;
|
||||||
|
transition: background .14s, border-color .14s, color .14s, transform .05s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn:hover { border-color: var(--border-strong); background: var(--surface-hover); }
|
||||||
|
.btn:active { transform: translateY(1px); }
|
||||||
|
.btn.primary { background: var(--primary); border-color: var(--primary); color: white; }
|
||||||
|
.btn.cyan { background: var(--cyan); border-color: var(--cyan); color: oklch(15% 0.012 240); }
|
||||||
|
.btn.ghost { background: transparent; border-color: transparent; color: var(--text-sec); }
|
||||||
|
.btn.ghost:hover { background: var(--surface-hover); color: var(--text); }
|
||||||
|
.btn.icon { width: 30px; padding: 0; }
|
||||||
|
.kbd, .chip, .status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
min-height: 23px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--text-sec);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 750;
|
||||||
|
}
|
||||||
|
.chip.primary { color: var(--primary); background: var(--primary-soft); border-color: color-mix(in oklch, var(--primary) 30%, var(--border)); }
|
||||||
|
.chip.green { color: var(--green); background: var(--green-soft); border-color: color-mix(in oklch, var(--green) 28%, var(--border)); }
|
||||||
|
.chip.amber { color: var(--amber); background: var(--amber-soft); border-color: color-mix(in oklch, var(--amber) 30%, var(--border)); }
|
||||||
|
.chip.red { color: var(--red); background: var(--red-soft); border-color: color-mix(in oklch, var(--red) 30%, var(--border)); }
|
||||||
|
.dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; flex: none; }
|
||||||
|
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
||||||
|
|
||||||
|
.viewport { min-height: 0; display: grid; overflow: hidden; }
|
||||||
|
.cascade { grid-template-columns: clamp(248px, 18vw, 310px) clamp(260px, 19vw, 330px) minmax(0, 1fr); }
|
||||||
|
.workbench { grid-template-columns: clamp(260px, 19vw, 320px) minmax(0, 1fr) clamp(310px, 24vw, 380px); }
|
||||||
|
.guard { grid-template-columns: clamp(260px, 18vw, 320px) minmax(0, 1fr); }
|
||||||
|
.col {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: color-mix(in oklch, var(--surface) 92%, transparent);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.col:last-child { border-right: 0; }
|
||||||
|
.panel-head {
|
||||||
|
padding: 15px 15px 11px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.step-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: .12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.step-label b {
|
||||||
|
color: var(--primary);
|
||||||
|
background: var(--primary-soft);
|
||||||
|
border: 1px solid color-mix(in oklch, var(--primary) 30%, var(--border));
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.panel-head h2 { margin-top: 7px; font-size: 17px; font-weight: 850; }
|
||||||
|
.panel-head p { margin-top: 3px; color: var(--text-muted); font-size: 12px; }
|
||||||
|
.search {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.search input {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 34px 0 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
outline: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.search input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 14%, transparent);
|
||||||
|
}
|
||||||
|
.search .kbd { position: absolute; right: 18px; top: 15px; min-height: 22px; }
|
||||||
|
.scroll { min-height: 0; overflow: auto; padding: 10px; }
|
||||||
|
.scroll::-webkit-scrollbar, .main-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||||
|
.scroll::-webkit-scrollbar-thumb, .main-scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }
|
||||||
|
|
||||||
|
.scope-card,
|
||||||
|
.nav-card,
|
||||||
|
.rule-row {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: left;
|
||||||
|
padding: 11px;
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
transition: background .14s, border-color .14s, transform .14s;
|
||||||
|
}
|
||||||
|
.scope-card:hover, .nav-card:hover, .rule-row:hover { background: var(--surface-hover); border-color: var(--border); }
|
||||||
|
.scope-card.on, .nav-card.on, .rule-row.on {
|
||||||
|
background: var(--primary-soft);
|
||||||
|
border-color: color-mix(in oklch, var(--primary) 28%, var(--border));
|
||||||
|
}
|
||||||
|
.scope-top, .nav-top, .rule-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.scope-name, .nav-name, .rule-name { font-weight: 850; font-size: 13px; letter-spacing: -0.01em; }
|
||||||
|
.scope-meta, .nav-meta, .rule-meta { color: var(--text-muted); font-size: 11px; }
|
||||||
|
.scope-code, .rule-pattern {
|
||||||
|
color: var(--text-sec);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.mini-meter {
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.mini-meter span { display: block; height: 100%; background: linear-gradient(90deg, var(--primary), var(--cyan)); border-radius: inherit; }
|
||||||
|
.section-title {
|
||||||
|
padding: 12px 4px 6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-scroll {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.detail-head {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: color-mix(in oklch, var(--bg) 88%, transparent);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.title-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.title-line h2 { font-size: 22px; font-weight: 900; }
|
||||||
|
.detail-meta { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 7px; color: var(--text-muted); font-size: 12px; }
|
||||||
|
.detail-meta b { color: var(--text-sec); font-weight: 800; }
|
||||||
|
.content-pad { padding: 18px 20px 86px; display: grid; gap: 14px; }
|
||||||
|
.two-col { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(260px, .75fr); gap: 14px; }
|
||||||
|
.three-col { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 0 color-mix(in oklch, var(--surface) 80%, transparent);
|
||||||
|
}
|
||||||
|
.card.pad { padding: 16px; }
|
||||||
|
.card-head {
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 13px 15px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.card-head h3 { font-size: 14px; font-weight: 850; }
|
||||||
|
.card-head p { margin-top: 2px; color: var(--text-muted); font-size: 11px; }
|
||||||
|
|
||||||
|
.code-hero {
|
||||||
|
position: relative;
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 24px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, color-mix(in oklch, var(--primary) 9%, transparent), transparent 42%),
|
||||||
|
radial-gradient(circle at 85% 20%, color-mix(in oklch, var(--cyan) 12%, transparent), transparent 16rem),
|
||||||
|
var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
.code-hero::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 24px 22px;
|
||||||
|
border-bottom: 1px dashed var(--border-strong);
|
||||||
|
}
|
||||||
|
.hero-kicker {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.code-line {
|
||||||
|
margin-top: 26px;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: clamp(34px, 5vw, 62px);
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 950;
|
||||||
|
letter-spacing: -0.055em;
|
||||||
|
}
|
||||||
|
.code-token {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: color-mix(in oklch, var(--surface) 74%, var(--bg-subtle));
|
||||||
|
}
|
||||||
|
.code-token.prefix { color: var(--text); }
|
||||||
|
.code-token.date { color: var(--cyan); }
|
||||||
|
.code-token.seq { color: var(--primary); box-shadow: inset 0 -3px 0 color-mix(in oklch, var(--primary) 24%, transparent); }
|
||||||
|
.code-token.scope { color: var(--green); }
|
||||||
|
.code-dash { color: var(--text-faint); }
|
||||||
|
.hero-foot {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.hero-foot span { color: var(--text-muted); font-size: 11px; }
|
||||||
|
.hero-foot b { display: block; color: var(--text); margin-top: 2px; font-family: var(--font-mono); font-size: 12px; }
|
||||||
|
|
||||||
|
.composer { padding: 15px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||||
|
.part {
|
||||||
|
min-width: 106px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
padding: 11px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.part:hover { border-color: var(--border-strong); background: var(--surface-hover); }
|
||||||
|
.part.on { border-color: var(--primary); background: var(--primary-soft); }
|
||||||
|
.part .type { color: var(--text-muted); font-family: var(--font-mono); font-size: 9px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase; }
|
||||||
|
.part .value { display: block; margin-top: 5px; font-family: var(--font-mono); font-size: 16px; font-weight: 900; }
|
||||||
|
.joiner { color: var(--text-faint); font-family: var(--font-mono); font-weight: 900; }
|
||||||
|
.add-part {
|
||||||
|
border: 1px dashed color-mix(in oklch, var(--primary) 40%, var(--border));
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
.field-grid { padding: 15px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||||
|
.field { display: grid; gap: 6px; color: var(--text-sec); font-size: 11px; font-weight: 750; }
|
||||||
|
.input, .field input, .field select, .field textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 32px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
padding: 0 10px;
|
||||||
|
outline: 0;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.field textarea { min-height: 74px; padding: 8px 10px; resize: vertical; }
|
||||||
|
.input:focus, .field input:focus, .field select:focus, .field textarea:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 12%, transparent);
|
||||||
|
}
|
||||||
|
.metric-card { padding: 14px; border-right: 1px solid var(--border); }
|
||||||
|
.metric-card:last-child { border-right: 0; }
|
||||||
|
.metric-card span { color: var(--text-muted); font-family: var(--font-mono); font-size: 9px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase; }
|
||||||
|
.metric-card b { display: block; margin-top: 4px; font-size: 24px; font-family: var(--font-mono); }
|
||||||
|
.metric-card small { display: block; margin-top: 3px; color: var(--text-muted); font-size: 11px; }
|
||||||
|
|
||||||
|
.cp-panel { background: var(--surface); }
|
||||||
|
.cp-bar {
|
||||||
|
min-height: 54px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 104px 1fr;
|
||||||
|
}
|
||||||
|
.cp-kind, .cp-type {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 11px;
|
||||||
|
}
|
||||||
|
.cp-kind { background: var(--bg-subtle); border-right: 1px solid var(--border); color: var(--text-sec); font-weight: 850; }
|
||||||
|
.cp-type strong { display: block; font-size: 12px; }
|
||||||
|
.cp-type span { display: block; color: var(--text-muted); font-size: 10px; }
|
||||||
|
.cp-icon {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 7px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: var(--primary-soft);
|
||||||
|
color: var(--primary);
|
||||||
|
border: 1px solid color-mix(in oklch, var(--primary) 24%, var(--border));
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.cp-scroll { min-height: 0; overflow: auto; padding: 12px; display: grid; gap: 12px; }
|
||||||
|
.cp-section {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
.cp-section:first-child { border-top: 0; padding-top: 0; }
|
||||||
|
.cp-section-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cp-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 82px 1fr;
|
||||||
|
gap: 9px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.cp-row label { color: var(--text-sec); font-size: 11px; font-weight: 750; }
|
||||||
|
.cp-help { color: var(--text-muted); font-size: 10px; margin-top: 4px; }
|
||||||
|
.segment {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 2px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.segment button {
|
||||||
|
min-height: 25px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--text-sec);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.segment button.on { background: var(--surface); color: var(--primary); box-shadow: 0 1px 0 var(--border); }
|
||||||
|
.toggle {
|
||||||
|
width: 34px;
|
||||||
|
height: 19px;
|
||||||
|
padding: 2px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.toggle::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-faint);
|
||||||
|
transition: transform .18s, background .18s;
|
||||||
|
}
|
||||||
|
.toggle.on { background: var(--primary-soft); border-color: color-mix(in oklch, var(--primary) 35%, var(--border)); }
|
||||||
|
.toggle.on::after { transform: translateX(14px); background: var(--primary); }
|
||||||
|
|
||||||
|
.validation-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 14px; }
|
||||||
|
.risk-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||||
|
.risk-card {
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.risk-card strong { display: block; font-size: 18px; margin-top: 8px; font-family: var(--font-mono); }
|
||||||
|
.risk-card p { color: var(--text-muted); font-size: 11px; margin-top: 4px; }
|
||||||
|
.risk-card.good { border-color: color-mix(in oklch, var(--green) 35%, var(--border)); background: var(--green-soft); }
|
||||||
|
.risk-card.warn { border-color: color-mix(in oklch, var(--amber) 40%, var(--border)); background: var(--amber-soft); }
|
||||||
|
.risk-card.bad { border-color: color-mix(in oklch, var(--red) 35%, var(--border)); background: var(--red-soft); }
|
||||||
|
.audit-row, .test-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 130px 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.audit-row:last-child, .test-row:last-child { border-bottom: 0; }
|
||||||
|
.audit-row .time, .test-row .case { color: var(--text-muted); font-family: var(--font-mono); font-size: 10px; }
|
||||||
|
.audit-row strong, .test-row strong { display: block; }
|
||||||
|
.audit-row p, .test-row p { color: var(--text-muted); font-size: 11px; margin-top: 2px; }
|
||||||
|
.savebar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 12;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: color-mix(in oklch, var(--surface) 94%, transparent);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.savebar .note { color: var(--text-sec); font-weight: 800; }
|
||||||
|
.savebar .note span { color: var(--amber); }
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 34px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 86% 8%, color-mix(in oklch, var(--primary) 12%, transparent), transparent 28rem),
|
||||||
|
var(--bg);
|
||||||
|
}
|
||||||
|
.gallery-inner { max-width: 1180px; margin: 0 auto; display: grid; gap: 22px; }
|
||||||
|
.gallery-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: end;
|
||||||
|
padding-bottom: 22px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.gallery-hero h1 { font-size: clamp(32px, 4vw, 52px); max-width: 14ch; }
|
||||||
|
.gallery-hero p { color: var(--text-sec); max-width: 58ch; margin-top: 10px; font-size: 15px; }
|
||||||
|
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
||||||
|
.gallery-card {
|
||||||
|
min-height: 330px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 22px;
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.gallery-card h2 { font-size: 20px; }
|
||||||
|
.gallery-card p { color: var(--text-sec); margin-top: 8px; }
|
||||||
|
.preview-mini {
|
||||||
|
height: 150px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, var(--primary-soft), transparent),
|
||||||
|
var(--bg-subtle);
|
||||||
|
padding: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: .45fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.mini-side, .mini-main {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.mini-main { display: grid; align-content: center; justify-items: start; padding: 14px; gap: 8px; }
|
||||||
|
.mini-line { height: 7px; border-radius: 999px; background: var(--border); width: 80%; }
|
||||||
|
.mini-line.short { width: 48%; background: var(--primary); }
|
||||||
|
.gallery-card .btn { align-self: flex-start; }
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.cascade, .workbench, .guard { grid-template-columns: 1fr; overflow: auto; }
|
||||||
|
.col { min-height: auto; border-right: 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.col.scope, .col.nav { max-height: none; }
|
||||||
|
.two-col, .validation-grid, .gallery-grid { grid-template-columns: 1fr; }
|
||||||
|
.field-grid, .three-col, .hero-foot, .risk-grid { grid-template-columns: 1fr; }
|
||||||
|
.topbar, .detail-head, .gallery-hero { align-items: flex-start; flex-direction: column; height: auto; padding: 14px; }
|
||||||
|
.demo-shell { grid-template-rows: auto 1fr; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V2 A · 3컬럼 관리형</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v2.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="demo-shell">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<span class="brand-mark">#</span>
|
||||||
|
<div>
|
||||||
|
<h1>채번 관리</h1>
|
||||||
|
<div class="breadcrumb"><span>ADMIN</span><span>/</span><b>시스템관리</b><span>/</span><b>채번 규칙</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<span class="status-chip"><span class="dot" style="color:var(--green);"></span>14개 테이블 연결</span>
|
||||||
|
<span class="kbd">Ctrl K</span>
|
||||||
|
<button class="btn ghost" id="themeBtn">라이트</button>
|
||||||
|
<button class="btn">변경 이력</button>
|
||||||
|
<button class="btn primary">새 채번 규칙</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="viewport cascade">
|
||||||
|
<aside class="col scope">
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="step-label"><b>01</b>범위 선택</span>
|
||||||
|
<h2>업무 도메인</h2>
|
||||||
|
<p>채번을 쓰는 업무 기준으로 먼저 좁힙니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="search"><input id="domainSearch" placeholder="영업, 구매, 생산 검색" /><span class="kbd">/</span></div>
|
||||||
|
<div class="scroll" id="domainList">
|
||||||
|
<button class="scope-card on" data-domain="sales">
|
||||||
|
<div class="scope-top"><span class="scope-name">영업 관리</span><span class="chip green">정상</span></div>
|
||||||
|
<span class="scope-code">sales_order · quote · delivery</span>
|
||||||
|
<div class="mini-meter"><span style="width:82%;"></span></div>
|
||||||
|
<span class="scope-meta">규칙 6개 · 테이블 연결 5개</span>
|
||||||
|
</button>
|
||||||
|
<button class="scope-card" data-domain="purchase">
|
||||||
|
<div class="scope-top"><span class="scope-name">구매 관리</span><span class="chip primary">운영</span></div>
|
||||||
|
<span class="scope-code">purchase_order · inbound</span>
|
||||||
|
<div class="mini-meter"><span style="width:64%;"></span></div>
|
||||||
|
<span class="scope-meta">규칙 4개 · 테이블 연결 4개</span>
|
||||||
|
</button>
|
||||||
|
<button class="scope-card" data-domain="production">
|
||||||
|
<div class="scope-top"><span class="scope-name">생산 관리</span><span class="chip amber">검토</span></div>
|
||||||
|
<span class="scope-code">work_order · lot · routing</span>
|
||||||
|
<div class="mini-meter"><span style="width:48%;"></span></div>
|
||||||
|
<span class="scope-meta">규칙 5개 · 미연결 2개</span>
|
||||||
|
</button>
|
||||||
|
<button class="scope-card" data-domain="master">
|
||||||
|
<div class="scope-top"><span class="scope-name">기준 정보</span><span class="chip">초안</span></div>
|
||||||
|
<span class="scope-code">item_master · customer · bom</span>
|
||||||
|
<div class="mini-meter"><span style="width:34%;"></span></div>
|
||||||
|
<span class="scope-meta">규칙 3개 · 포맷 정리 필요</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<aside class="col nav">
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="step-label"><b>02</b>규칙 묶음</span>
|
||||||
|
<h2>영업 관리</h2>
|
||||||
|
<p>테이블과 컬럼 단위로 규칙을 훑습니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="search"><input id="ruleSearch" placeholder="수주, 견적, 출하 번호 검색" /><span class="kbd">K</span></div>
|
||||||
|
<div class="scroll" id="ruleList">
|
||||||
|
<div class="section-title"><span>LIVE RULES</span><span>6</span></div>
|
||||||
|
<button class="nav-card on" data-rule="so">
|
||||||
|
<div class="nav-top"><span class="nav-name">수주번호 자동채번</span><span class="chip green">LIVE</span></div>
|
||||||
|
<span class="rule-pattern">SO-{yyyyMM}-{0000}</span>
|
||||||
|
<span class="nav-meta">sales_order.order_no · 월별 리셋</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-card" data-rule="quote">
|
||||||
|
<div class="nav-top"><span class="nav-name">견적번호</span><span class="chip green">LIVE</span></div>
|
||||||
|
<span class="rule-pattern">QT-{yyyy}-{00000}</span>
|
||||||
|
<span class="nav-meta">quote.quote_no · 연도별 리셋</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-card" data-rule="claim">
|
||||||
|
<div class="nav-top"><span class="nav-name">클레임 접수번호</span><span class="chip amber">확인</span></div>
|
||||||
|
<span class="rule-pattern">CLM-{company}-{000}</span>
|
||||||
|
<span class="nav-meta">claim.claim_no · 회사별 범위</span>
|
||||||
|
</button>
|
||||||
|
<div class="section-title"><span>NEEDS LINK</span><span>2</span></div>
|
||||||
|
<button class="nav-card" data-rule="delivery">
|
||||||
|
<div class="nav-top"><span class="nav-name">출하 배차번호</span><span class="chip">미연결</span></div>
|
||||||
|
<span class="rule-pattern">DLV-{date}-{seq}</span>
|
||||||
|
<span class="nav-meta">배송 화면에서 사용 예정</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main-scroll">
|
||||||
|
<section class="detail-head" data-od-id="detail-head">
|
||||||
|
<div>
|
||||||
|
<div class="title-line">
|
||||||
|
<h2 id="detailTitle">수주번호 자동채번</h2>
|
||||||
|
<span class="chip green">사용 중</span>
|
||||||
|
<span class="chip primary">sales_order.order_no</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-meta">
|
||||||
|
<span>생성 위치 <b>영업관리 / 수주등록</b></span>
|
||||||
|
<span>리셋 기준 <b>매월 1일</b></span>
|
||||||
|
<span>마지막 수정 <b>오늘 09:42</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<button class="btn">되돌리기</button>
|
||||||
|
<button class="btn primary">저장</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="content-pad" data-od-id="cascade-content">
|
||||||
|
<div class="code-hero">
|
||||||
|
<div class="hero-kicker"><span>NEXT NUMBER PREVIEW</span><button class="btn" id="copyCode">복사</button></div>
|
||||||
|
<div class="code-line" id="codeLine">
|
||||||
|
<span class="code-token prefix">SO</span><span class="code-dash">-</span><span class="code-token date">202605</span><span class="code-dash">-</span><span class="code-token seq">0048</span>
|
||||||
|
</div>
|
||||||
|
<div class="hero-foot">
|
||||||
|
<span>현재 순번<b>47</b></span>
|
||||||
|
<span>다음 순번<b>48</b></span>
|
||||||
|
<span>리셋 주기<b>월별</b></span>
|
||||||
|
<span>충돌 검사<b>통과</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="three-col card">
|
||||||
|
<div class="metric-card"><span>ACTIVE RULES</span><b>18</b><small>전체 채번 규칙</small></div>
|
||||||
|
<div class="metric-card"><span>TABLE LINKS</span><b>14</b><small>실제 컬럼 연결</small></div>
|
||||||
|
<div class="metric-card"><span>NEEDS REVIEW</span><b>3</b><small>미연결 또는 경고</small></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="two-col">
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>포맷 구성</h3><p>읽는 즉시 어떤 코드가 만들어지는지 보이게 합니다.</p></div>
|
||||||
|
<button class="btn">파트 추가</button>
|
||||||
|
</div>
|
||||||
|
<div class="composer">
|
||||||
|
<button class="part on"><span class="type">prefix</span><span class="value">SO</span></button><span class="joiner">-</span>
|
||||||
|
<button class="part"><span class="type">date</span><span class="value">yyyyMM</span></button><span class="joiner">-</span>
|
||||||
|
<button class="part"><span class="type">sequence</span><span class="value">0000</span></button>
|
||||||
|
<button class="add-part">+ 검증 파트</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>운영 상태</h3><p>발번 사고를 막기 위한 최소 정보입니다.</p></div>
|
||||||
|
<span class="chip green">정상</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-grid" style="grid-template-columns:1fr 1fr;">
|
||||||
|
<label class="field">현재 순번<input value="47" /></label>
|
||||||
|
<label class="field">다음 순번<input value="48" /></label>
|
||||||
|
<label class="field">동시성 정책<select><option>DB 잠금 후 증가</option><option>트랜잭션 단위 예약</option></select></label>
|
||||||
|
<label class="field">중복 허용<select><option>허용 안 함</option><option>관리자 승인</option></select></label>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="savebar">
|
||||||
|
<div class="note"><span>저장 전</span> 미리보기와 테이블 연결을 다시 확인하세요.</div>
|
||||||
|
<div class="top-actions"><button class="btn">변경 취소</button><button class="btn primary">규칙 저장</button></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const html = document.documentElement;
|
||||||
|
document.getElementById("themeBtn").addEventListener("click", (event) => {
|
||||||
|
html.classList.toggle("dark");
|
||||||
|
event.currentTarget.textContent = html.classList.contains("dark") ? "라이트" : "다크";
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".scope-card, .nav-card").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
btn.parentElement.querySelectorAll(".on").forEach((item) => item.classList.remove("on"));
|
||||||
|
btn.classList.add("on");
|
||||||
|
if (btn.dataset.rule === "quote") {
|
||||||
|
document.getElementById("detailTitle").textContent = "견적번호";
|
||||||
|
document.getElementById("codeLine").innerHTML = '<span class="code-token prefix">QT</span><span class="code-dash">-</span><span class="code-token date">2026</span><span class="code-dash">-</span><span class="code-token seq">00129</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.getElementById("ruleSearch").addEventListener("input", (event) => {
|
||||||
|
const term = event.target.value.trim().toLowerCase();
|
||||||
|
document.querySelectorAll("#ruleList .nav-card").forEach((row) => {
|
||||||
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "grid" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.getElementById("copyCode").addEventListener("click", (event) => {
|
||||||
|
event.currentTarget.textContent = "복사됨";
|
||||||
|
setTimeout(() => event.currentTarget.textContent = "복사", 900);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V2 B · CP 조립 작업대형</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v2.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="demo-shell">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<span class="brand-mark">#</span>
|
||||||
|
<div>
|
||||||
|
<h1>채번 디자이너</h1>
|
||||||
|
<div class="breadcrumb"><span>BUILDER</span><span>/</span><b>포맷 조립</b><span>/</span><b>수주번호</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<span class="status-chip"><span class="dot" style="color:var(--amber);"></span>초안 변경 2건</span>
|
||||||
|
<button class="btn ghost" id="themeBtn">라이트</button>
|
||||||
|
<button class="btn">테스트 발번</button>
|
||||||
|
<button class="btn primary">변경사항 저장</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="viewport workbench">
|
||||||
|
<aside class="col">
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="step-label"><b>01</b>규칙 목록</span>
|
||||||
|
<h2>채번 규칙</h2>
|
||||||
|
<p>실제 연결된 컬럼과 미연결 초안을 함께 봅니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="search"><input id="ruleSearch" placeholder="규칙명, 테이블, 컬럼 검색" /><span class="kbd">K</span></div>
|
||||||
|
<div class="scroll" id="rules">
|
||||||
|
<div class="section-title"><span>RECENT</span><span>오늘 수정</span></div>
|
||||||
|
<button class="rule-row on" data-name="수주번호 자동채번">
|
||||||
|
<div class="rule-top"><span class="rule-name">수주번호 자동채번</span><span class="chip green">LIVE</span></div>
|
||||||
|
<span class="rule-pattern">SO-{yyyyMM}-{0000}</span>
|
||||||
|
<span class="rule-meta">sales_order.order_no · 다음 0048</span>
|
||||||
|
</button>
|
||||||
|
<button class="rule-row" data-name="입고검사 LOT">
|
||||||
|
<div class="rule-top"><span class="rule-name">입고검사 LOT</span><span class="chip primary">LIVE</span></div>
|
||||||
|
<span class="rule-pattern">IQC-{yyyyMMdd}-{000}</span>
|
||||||
|
<span class="rule-meta">inspection.lot_no · 다음 019</span>
|
||||||
|
</button>
|
||||||
|
<button class="rule-row" data-name="작업지시번호">
|
||||||
|
<div class="rule-top"><span class="rule-name">작업지시번호</span><span class="chip amber">검토</span></div>
|
||||||
|
<span class="rule-pattern">WO-{plant}-{date}-{seq}</span>
|
||||||
|
<span class="rule-meta">work_order.work_no · 범위 확인</span>
|
||||||
|
</button>
|
||||||
|
<div class="section-title"><span>DRAFT</span><span>미연결</span></div>
|
||||||
|
<button class="rule-row" data-name="품목마스터 코드">
|
||||||
|
<div class="rule-top"><span class="rule-name">품목마스터 코드</span><span class="chip">DRAFT</span></div>
|
||||||
|
<span class="rule-pattern">ITEM-{category}-{0000}</span>
|
||||||
|
<span class="rule-meta">컬럼 연결 전 · 초안 저장됨</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main-scroll">
|
||||||
|
<section class="detail-head" data-od-id="detail-head">
|
||||||
|
<div>
|
||||||
|
<div class="title-line"><h2>수주번호 자동채번</h2><span class="chip green">활성</span><span class="chip primary">월별 리셋</span></div>
|
||||||
|
<div class="detail-meta">
|
||||||
|
<span>포맷 <b>SO-{yyyyMM}-{0000}</b></span>
|
||||||
|
<span>충돌 정책 <b>DB 잠금</b></span>
|
||||||
|
<span>테이블 <b>sales_order.order_no</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions"><button class="btn">미리보기 새로고침</button><button class="btn primary">저장</button></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="content-pad" data-od-id="workbench-content">
|
||||||
|
<div class="code-hero">
|
||||||
|
<div class="hero-kicker"><span>LIVE PREVIEW · 선택 파트: sequence</span><span class="chip amber">저장 전</span></div>
|
||||||
|
<div class="code-line">
|
||||||
|
<span class="code-token prefix">SO</span><span class="code-dash">-</span><span class="code-token date">202605</span><span class="code-dash">-</span><span class="code-token seq" id="seqToken">0048</span>
|
||||||
|
</div>
|
||||||
|
<div class="hero-foot">
|
||||||
|
<span>Prefix<b>SO</b></span>
|
||||||
|
<span>날짜 포맷<b>yyyyMM</b></span>
|
||||||
|
<span>자리수<b id="digitLabel">4자리</b></span>
|
||||||
|
<span>다음 발번<b id="nextLabel">0048</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>포맷 조립 캔버스</h3><p>파트를 선택하면 우측 CP 패널이 해당 속성으로 전환됩니다.</p></div>
|
||||||
|
<div class="top-actions"><button class="btn">정렬</button><button class="btn">검증</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="composer">
|
||||||
|
<button class="part" data-part="prefix"><span class="type">TEXT</span><span class="value">SO</span></button>
|
||||||
|
<span class="joiner">-</span>
|
||||||
|
<button class="part" data-part="date"><span class="type">DATE</span><span class="value">yyyyMM</span></button>
|
||||||
|
<span class="joiner">-</span>
|
||||||
|
<button class="part on" data-part="sequence"><span class="type">SEQUENCE</span><span class="value">0000</span></button>
|
||||||
|
<button class="add-part">+ 파트 추가</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="two-col">
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>테이블 연결</h3><p>어느 화면/컬럼에서 이 채번을 호출하는지 명확히 보여줍니다.</p></div>
|
||||||
|
<span class="chip green">1개 연결</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-grid">
|
||||||
|
<label class="field">업무 화면<input value="수주등록" /></label>
|
||||||
|
<label class="field">테이블<input value="sales_order" /></label>
|
||||||
|
<label class="field">컬럼<input value="order_no" /></label>
|
||||||
|
<label class="field">생성 시점<select><option>신규 저장 직전</option><option>행 추가 즉시</option></select></label>
|
||||||
|
<label class="field">수동 입력<select><option>관리자만 허용</option><option>허용 안 함</option></select></label>
|
||||||
|
<label class="field">중복 검사<select><option>저장 전 필수</option><option>백그라운드 경고</option></select></label>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>빠른 테스트</h3><p>저장 전 샘플을 바로 만들어 봅니다.</p></div>
|
||||||
|
<button class="btn cyan" id="generateBtn">3건 생성</button>
|
||||||
|
</div>
|
||||||
|
<div id="sampleList">
|
||||||
|
<div class="test-row"><span class="case">SAMPLE 01</span><div><strong>SO-202605-0048</strong><p>현재 설정 기준 첫 번째 발번</p></div><span class="chip green">OK</span></div>
|
||||||
|
<div class="test-row"><span class="case">SAMPLE 02</span><div><strong>SO-202605-0049</strong><p>연속 증가 확인</p></div><span class="chip green">OK</span></div>
|
||||||
|
<div class="test-row"><span class="case">SAMPLE 03</span><div><strong>SO-202605-0050</strong><p>자리수 유지 확인</p></div><span class="chip green">OK</span></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="savebar">
|
||||||
|
<div class="note"><span>sequence 파트</span> 자리수를 바꾸면 기존 발번과 길이가 달라집니다.</div>
|
||||||
|
<div class="top-actions"><button class="btn">취소</button><button class="btn primary">저장 후 적용</button></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<aside class="col cp-panel">
|
||||||
|
<div class="cp-bar">
|
||||||
|
<div class="cp-kind">규칙</div>
|
||||||
|
<div class="cp-type">
|
||||||
|
<span class="cp-icon" id="cpIcon">SEQ</span>
|
||||||
|
<div><strong id="cpTitle">순번 파트</strong><span id="cpDesc">자리수와 리셋 정책을 조정</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cp-scroll">
|
||||||
|
<section class="cp-section" data-od-id="cp-data-scope">
|
||||||
|
<div class="cp-section-title">데이터 소속</div>
|
||||||
|
<div class="cp-row"><label>테이블</label><input class="input" value="sales_order" /></div>
|
||||||
|
<div class="cp-row"><label>컬럼</label><input class="input" value="order_no" /></div>
|
||||||
|
<div class="cp-row"><label>범위</label><select class="input"><option>회사 + 월별</option><option>전체 + 월별</option><option>회사 + 일별</option></select></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cp-section" data-od-id="cp-part-settings">
|
||||||
|
<div class="cp-section-title">선택 파트 설정</div>
|
||||||
|
<div class="cp-row"><label>표시명</label><input class="input" id="partName" value="순번" /></div>
|
||||||
|
<div class="cp-row">
|
||||||
|
<label>자리수</label>
|
||||||
|
<div class="segment" id="digitSeg">
|
||||||
|
<button>3</button><button class="on">4</button><button>5</button><button>6</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cp-row"><label>초기값</label><input class="input" value="1" /></div>
|
||||||
|
<div class="cp-row"><label>패딩</label><input class="input" value="0" /></div>
|
||||||
|
<div class="cp-help">자리수는 표시 포맷만 바꾸고, 현재 순번 값은 유지됩니다.</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cp-section" data-od-id="cp-operation-options">
|
||||||
|
<div class="cp-section-title">운영 옵션</div>
|
||||||
|
<div class="cp-row"><label>자동 발번</label><button class="toggle on" aria-label="자동 발번"></button></div>
|
||||||
|
<div class="cp-row"><label>중복 차단</label><button class="toggle on" aria-label="중복 차단"></button></div>
|
||||||
|
<div class="cp-row"><label>수동 수정</label><button class="toggle" aria-label="수동 수정"></button></div>
|
||||||
|
<div class="cp-row"><label>리셋 주기</label><select class="input"><option>월별</option><option>일별</option><option>연도별</option><option>없음</option></select></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cp-section" data-od-id="cp-advanced">
|
||||||
|
<div class="cp-section-title">고급</div>
|
||||||
|
<div class="cp-row"><label>동시성</label><select class="input"><option>DB row lock</option><option>optimistic retry</option></select></div>
|
||||||
|
<div class="cp-row"><label>감사 로그</label><button class="toggle on" aria-label="감사 로그"></button></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById("themeBtn").addEventListener("click", (event) => {
|
||||||
|
document.documentElement.classList.toggle("dark");
|
||||||
|
event.currentTarget.textContent = document.documentElement.classList.contains("dark") ? "라이트" : "다크";
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".part").forEach((part) => {
|
||||||
|
part.addEventListener("click", () => {
|
||||||
|
document.querySelectorAll(".part").forEach((item) => item.classList.remove("on"));
|
||||||
|
part.classList.add("on");
|
||||||
|
const map = {
|
||||||
|
prefix: ["TXT", "고정 문자", "SO 같은 업무 접두어"],
|
||||||
|
date: ["DAY", "날짜 파트", "연월일 포맷과 기준일"],
|
||||||
|
sequence: ["SEQ", "순번 파트", "자리수와 리셋 정책을 조정"]
|
||||||
|
};
|
||||||
|
const data = map[part.dataset.part];
|
||||||
|
document.getElementById("cpIcon").textContent = data[0];
|
||||||
|
document.getElementById("cpTitle").textContent = data[1];
|
||||||
|
document.getElementById("cpDesc").textContent = data[2];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelectorAll("#digitSeg button").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
document.querySelectorAll("#digitSeg button").forEach((item) => item.classList.remove("on"));
|
||||||
|
btn.classList.add("on");
|
||||||
|
const digits = Number(btn.textContent);
|
||||||
|
const value = String(48).padStart(digits, "0");
|
||||||
|
document.getElementById("seqToken").textContent = value;
|
||||||
|
document.getElementById("digitLabel").textContent = `${digits}자리`;
|
||||||
|
document.getElementById("nextLabel").textContent = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.getElementById("ruleSearch").addEventListener("input", (event) => {
|
||||||
|
const term = event.target.value.trim().toLowerCase();
|
||||||
|
document.querySelectorAll("#rules .rule-row").forEach((row) => {
|
||||||
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "grid" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.getElementById("generateBtn").addEventListener("click", (event) => {
|
||||||
|
event.currentTarget.textContent = "생성 완료";
|
||||||
|
setTimeout(() => event.currentTarget.textContent = "3건 생성", 900);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V2 C · 검증 보드형</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v2.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="demo-shell">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<span class="brand-mark">#</span>
|
||||||
|
<div>
|
||||||
|
<h1>채번 운영 검증</h1>
|
||||||
|
<div class="breadcrumb"><span>OPS</span><span>/</span><b>번호 충돌 방지</b><span>/</span><b>승인 대기</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<span class="status-chip"><span class="dot" style="color:var(--amber);"></span>검증 필요 2건</span>
|
||||||
|
<button class="btn ghost" id="themeBtn">라이트</button>
|
||||||
|
<button class="btn">전체 테스트</button>
|
||||||
|
<button class="btn primary">승인 후 저장</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="viewport guard">
|
||||||
|
<aside class="col">
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="step-label"><b>01</b>검증 큐</span>
|
||||||
|
<h2>저장 전 점검</h2>
|
||||||
|
<p>위험도가 있는 규칙부터 먼저 봅니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="search"><input id="queueSearch" placeholder="규칙명 또는 원인 검색" /><span class="kbd">F</span></div>
|
||||||
|
<div class="scroll" id="queue">
|
||||||
|
<div class="section-title"><span>NEEDS APPROVAL</span><span>2</span></div>
|
||||||
|
<button class="rule-row on" data-kind="warn">
|
||||||
|
<div class="rule-top"><span class="rule-name">작업지시번호</span><span class="chip amber">범위 변경</span></div>
|
||||||
|
<span class="rule-pattern">WO-{plant}-{yyyyMMdd}-{000}</span>
|
||||||
|
<span class="rule-meta">plant 파트 추가로 기존 번호와 길이 변경</span>
|
||||||
|
</button>
|
||||||
|
<button class="rule-row" data-kind="bad">
|
||||||
|
<div class="rule-top"><span class="rule-name">품목마스터 코드</span><span class="chip red">중복 위험</span></div>
|
||||||
|
<span class="rule-pattern">ITEM-{category}-{000}</span>
|
||||||
|
<span class="rule-meta">category 코드가 빈 값일 때 기존 ITEM-000과 충돌</span>
|
||||||
|
</button>
|
||||||
|
<div class="section-title"><span>SAFE TO SAVE</span><span>4</span></div>
|
||||||
|
<button class="rule-row" data-kind="good">
|
||||||
|
<div class="rule-top"><span class="rule-name">수주번호 자동채번</span><span class="chip green">통과</span></div>
|
||||||
|
<span class="rule-pattern">SO-{yyyyMM}-{0000}</span>
|
||||||
|
<span class="rule-meta">샘플 20건 중복 없음</span>
|
||||||
|
</button>
|
||||||
|
<button class="rule-row" data-kind="good">
|
||||||
|
<div class="rule-top"><span class="rule-name">견적번호</span><span class="chip green">통과</span></div>
|
||||||
|
<span class="rule-pattern">QT-{yyyy}-{00000}</span>
|
||||||
|
<span class="rule-meta">연도별 리셋 정상</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main-scroll">
|
||||||
|
<section class="detail-head" data-od-id="detail-head">
|
||||||
|
<div>
|
||||||
|
<div class="title-line"><h2 id="guardTitle">작업지시번호</h2><span class="chip amber" id="guardStatus">승인 필요</span><span class="chip primary">work_order.work_no</span></div>
|
||||||
|
<div class="detail-meta">
|
||||||
|
<span>변경 유형 <b>범위 파트 추가</b></span>
|
||||||
|
<span>영향 화면 <b>생산계획 / 작업지시</b></span>
|
||||||
|
<span>요청자 <b>admin01</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions"><button class="btn">반려</button><button class="btn primary">승인</button></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="content-pad" data-od-id="guard-content">
|
||||||
|
<div class="validation-grid">
|
||||||
|
<div class="code-hero">
|
||||||
|
<div class="hero-kicker"><span>PROPOSED NUMBER</span><span class="chip amber">포맷 변경</span></div>
|
||||||
|
<div class="code-line" id="guardCode">
|
||||||
|
<span class="code-token prefix">WO</span><span class="code-dash">-</span><span class="code-token scope">A01</span><span class="code-dash">-</span><span class="code-token date">20260520</span><span class="code-dash">-</span><span class="code-token seq">019</span>
|
||||||
|
</div>
|
||||||
|
<div class="hero-foot">
|
||||||
|
<span>기존 예시<b>WO-20260520-019</b></span>
|
||||||
|
<span>변경 예시<b>WO-A01-20260520-019</b></span>
|
||||||
|
<span>영향 레코드<b>신규 발번부터</b></span>
|
||||||
|
<span>승인 상태<b>대기</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>검증 요약</h3><p>저장 전에 반드시 확인할 위험 신호입니다.</p></div>
|
||||||
|
<span class="chip amber">2개 확인</span>
|
||||||
|
</div>
|
||||||
|
<div class="risk-grid" style="grid-template-columns:1fr;">
|
||||||
|
<div class="risk-card warn"><span class="chip amber">WARNING</span><strong>길이 변경</strong><p>작업지시번호가 3자 더 길어져 외부 양식 컬럼 폭 확인이 필요합니다.</p></div>
|
||||||
|
<div class="risk-card good"><span class="chip green">PASS</span><strong>중복 없음</strong><p>최근 90일 발번 이력 기준 충돌 후보가 없습니다.</p></div>
|
||||||
|
<div class="risk-card good"><span class="chip green">PASS</span><strong>리셋 정상</strong><p>공장별 범위와 일별 리셋이 같은 트랜잭션 안에서 계산됩니다.</p></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="risk-grid">
|
||||||
|
<div class="risk-card warn"><span class="chip amber">FORMAT</span><strong>+A01</strong><p>공장 코드가 새로 들어갑니다.</p></div>
|
||||||
|
<div class="risk-card good"><span class="chip green">HISTORY</span><strong>0</strong><p>샘플 충돌 후보 없음</p></div>
|
||||||
|
<div class="risk-card good"><span class="chip green">LOCK</span><strong>ON</strong><p>동시 저장 보호 활성</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="two-col">
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>테스트 케이스</h3><p>업무 조건별로 실제 출력이 어떻게 달라지는지 보여줍니다.</p></div>
|
||||||
|
<button class="btn">케이스 추가</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="test-row"><span class="case">CASE 01</span><div><strong>공장 A01 / 오늘</strong><p>WO-A01-20260520-019</p></div><span class="chip green">PASS</span></div>
|
||||||
|
<div class="test-row"><span class="case">CASE 02</span><div><strong>공장 B03 / 오늘</strong><p>WO-B03-20260520-001</p></div><span class="chip green">PASS</span></div>
|
||||||
|
<div class="test-row"><span class="case">CASE 03</span><div><strong>공장 코드 없음</strong><p>필수값 누락으로 발번 중단</p></div><span class="chip amber">확인</span></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>승인 체크리스트</h3><p>관리자가 실수 없이 결정하게 만드는 항목입니다.</p></div>
|
||||||
|
<span class="chip primary">3단계</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-grid" style="grid-template-columns:1fr;">
|
||||||
|
<label class="field">1. 기존 문서 양식 폭 확인<select><option>확인 완료</option><option>확인 필요</option></select></label>
|
||||||
|
<label class="field">2. 누락 공장 코드 처리<select><option>발번 중단 + 메시지 표시</option><option>기본값 A01 사용</option></select></label>
|
||||||
|
<label class="field">3. 적용 시점<select><option>승인 이후 신규 발번부터</option><option>내일 00시부터</option></select></label>
|
||||||
|
<label class="field">승인 메모<textarea>생산계획 화면의 작업지시번호 폭 확인 후 신규 발번부터 적용.</textarea></label>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><h3>감사 이력</h3><p>누가 어떤 이유로 채번 규칙을 바꿨는지 남깁니다.</p></div>
|
||||||
|
<button class="btn">전체 이력 보기</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="audit-row"><span class="time">오늘 10:18</span><div><strong>plant 파트 추가 요청</strong><p>생산관리에서 공장별 작업지시 구분 필요</p></div><span class="chip amber">대기</span></div>
|
||||||
|
<div class="audit-row"><span class="time">5월 18일</span><div><strong>일별 리셋으로 변경</strong><p>월별 순번이 너무 커져 작업 현장 식별성이 낮아짐</p></div><span class="chip green">승인</span></div>
|
||||||
|
<div class="audit-row"><span class="time">5월 12일</span><div><strong>초기 규칙 생성</strong><p>WO-{yyyyMMdd}-{000} 형식 등록</p></div><span class="chip green">완료</span></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="savebar">
|
||||||
|
<div class="note"><span>승인 필요</span> 케이스 1개가 남아 있습니다. 공장 코드 누락 처리만 확정하면 저장 가능합니다.</div>
|
||||||
|
<div class="top-actions"><button class="btn">임시 저장</button><button class="btn primary">체크 완료 후 승인</button></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById("themeBtn").addEventListener("click", (event) => {
|
||||||
|
document.documentElement.classList.toggle("dark");
|
||||||
|
event.currentTarget.textContent = document.documentElement.classList.contains("dark") ? "라이트" : "다크";
|
||||||
|
});
|
||||||
|
const queueRows = document.querySelectorAll("#queue .rule-row");
|
||||||
|
queueRows.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
queueRows.forEach((item) => item.classList.remove("on"));
|
||||||
|
row.classList.add("on");
|
||||||
|
if (row.dataset.kind === "bad") {
|
||||||
|
document.getElementById("guardTitle").textContent = "품목마스터 코드";
|
||||||
|
document.getElementById("guardStatus").textContent = "중복 위험";
|
||||||
|
document.getElementById("guardStatus").className = "chip red";
|
||||||
|
document.getElementById("guardCode").innerHTML = '<span class="code-token prefix">ITEM</span><span class="code-dash">-</span><span class="code-token scope">MTR</span><span class="code-dash">-</span><span class="code-token seq">000</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.getElementById("queueSearch").addEventListener("input", (event) => {
|
||||||
|
const term = event.target.value.trim().toLowerCase();
|
||||||
|
queueRows.forEach((row) => {
|
||||||
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "grid" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V3 시안 갤러리</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v3.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="gallery">
|
||||||
|
<div class="gallery-inner">
|
||||||
|
<section class="gallery-hero" data-od-id="gallery-hero">
|
||||||
|
<div>
|
||||||
|
<span class="chip primary">INVYONE NUMBERING ADMIN · V3</span>
|
||||||
|
<h1>과한 데모를 빼고, 실제 관리자 화면처럼 다시 잡은 채번 시안</h1>
|
||||||
|
<p>메뉴관리의 낮은 헤더와 촘촘한 리스트, 회사 멀티테넌시의 accordion 운영 감각, CP 디자인의 우측 속성 패널을 참고했습니다. 이번 세트는 장식보다 반복 작업, 선택 상태, 저장 전 검증을 중심으로 봐주세요.</p>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<a class="btn" href="../2026-05-20-numbering-admin-variants-v2/index.html">이전 V2 보기</a>
|
||||||
|
<span class="chip green"><span class="dot"></span>HTML 4개</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="gallery-grid" data-od-id="gallery-grid">
|
||||||
|
<article class="gallery-card">
|
||||||
|
<div>
|
||||||
|
<span class="chip green">A · QUIET ADMIN</span>
|
||||||
|
<h2>조용한 관리자형</h2>
|
||||||
|
<p>가장 현실적인 형태입니다. 좌측 규칙 리스트, 우측 상세, 다음 발번 미리보기만 명확하게 둔 버전입니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mini">
|
||||||
|
<div class="side">
|
||||||
|
<div class="mini-line primary"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="mini-line" style="width:48%;"></div>
|
||||||
|
<div class="mini-line primary" style="width:72%;height:18px;"></div>
|
||||||
|
<div class="mini-line" style="width:88%;"></div>
|
||||||
|
<div class="mini-line" style="width:63%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn primary" href="./variant-a-quiet-admin.html">A 시안 열기</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="gallery-card">
|
||||||
|
<div>
|
||||||
|
<span class="chip primary">B · CP EDITOR</span>
|
||||||
|
<h2>CP 편집기형</h2>
|
||||||
|
<p>중앙에서 포맷을 조립하고, 우측 CP 패널에서 선택 파트만 편집합니다. 이전보다 톤을 낮춘 채번 디자이너입니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mini three">
|
||||||
|
<div class="side">
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
<div class="mini-line primary"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="mini-line primary" style="width:76%;height:18px;"></div>
|
||||||
|
<div class="mini-line" style="width:84%;"></div>
|
||||||
|
<div class="mini-line" style="width:56%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="side">
|
||||||
|
<div class="mini-line" style="width:64%;"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn primary" href="./variant-b-cp-editor.html">B 시안 열기</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="gallery-card">
|
||||||
|
<div>
|
||||||
|
<span class="chip amber">C · OPERATIONS</span>
|
||||||
|
<h2>운영 검토형</h2>
|
||||||
|
<p>운영자가 충돌, 미연결, 리셋 예정 항목을 먼저 훑고 펼쳐서 조치합니다. 회사관리 accordion 감각에 가깝습니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mini">
|
||||||
|
<div class="side">
|
||||||
|
<div class="mini-line primary"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="mini-line" style="width:42%;"></div>
|
||||||
|
<div class="mini-line primary" style="width:94%;"></div>
|
||||||
|
<div class="mini-line" style="width:94%;"></div>
|
||||||
|
<div class="mini-line" style="width:80%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn primary" href="./variant-c-operations-review.html">C 시안 열기</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="gallery-card">
|
||||||
|
<div>
|
||||||
|
<span class="chip primary">D · TABLE FIRST</span>
|
||||||
|
<h2>표 중심형</h2>
|
||||||
|
<p>가장 실무적인 초밀도 버전입니다. 전체 규칙을 표로 훑고, 선택 행만 오른쪽에서 조치합니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mini">
|
||||||
|
<div class="side">
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
<div class="mini-line"></div>
|
||||||
|
<div class="mini-line primary"></div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="mini-line primary" style="width:92%;"></div>
|
||||||
|
<div class="mini-line" style="width:92%;"></div>
|
||||||
|
<div class="mini-line" style="width:92%;"></div>
|
||||||
|
<div class="mini-line" style="width:70%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="btn primary" href="./variant-d-table-first.html">D 시안 열기</a>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,636 @@
|
|||||||
|
:root {
|
||||||
|
--bg: oklch(99% 0.002 255);
|
||||||
|
--panel: oklch(100% 0 0);
|
||||||
|
--panel-2: oklch(97.4% 0.004 255);
|
||||||
|
--panel-3: oklch(95.4% 0.006 255);
|
||||||
|
--text: oklch(19% 0.015 255);
|
||||||
|
--text-2: oklch(43% 0.016 255);
|
||||||
|
--text-3: oklch(61% 0.012 255);
|
||||||
|
--text-4: oklch(76% 0.01 255);
|
||||||
|
--line: oklch(90% 0.007 255);
|
||||||
|
--line-2: oklch(84% 0.012 255);
|
||||||
|
--primary: oklch(55% 0.17 265);
|
||||||
|
--primary-soft: color-mix(in oklch, var(--primary) 9%, transparent);
|
||||||
|
--primary-line: color-mix(in oklch, var(--primary) 24%, var(--line));
|
||||||
|
--green: oklch(56% 0.14 155);
|
||||||
|
--green-soft: color-mix(in oklch, var(--green) 10%, transparent);
|
||||||
|
--amber: oklch(66% 0.13 78);
|
||||||
|
--amber-soft: color-mix(in oklch, var(--amber) 12%, transparent);
|
||||||
|
--red: oklch(57% 0.16 24);
|
||||||
|
--red-soft: color-mix(in oklch, var(--red) 10%, transparent);
|
||||||
|
--cyan: oklch(58% 0.11 210);
|
||||||
|
--cyan-soft: color-mix(in oklch, var(--cyan) 10%, transparent);
|
||||||
|
--shadow: 0 10px 28px color-mix(in oklch, var(--text) 7%, transparent);
|
||||||
|
--font: -apple-system, BlinkMacSystemFont, "Pretendard", "Apple SD Gothic Neo", "Malgun Gothic", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--mono: "JetBrains Mono", "D2Coding", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
--r-sm: 6px;
|
||||||
|
--r-md: 8px;
|
||||||
|
--r-lg: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg: oklch(12% 0.009 260);
|
||||||
|
--panel: oklch(16% 0.01 260);
|
||||||
|
--panel-2: oklch(19% 0.012 260);
|
||||||
|
--panel-3: oklch(23% 0.014 260);
|
||||||
|
--text: oklch(94% 0.004 260);
|
||||||
|
--text-2: oklch(73% 0.009 260);
|
||||||
|
--text-3: oklch(55% 0.011 260);
|
||||||
|
--text-4: oklch(38% 0.012 260);
|
||||||
|
--line: oklch(27% 0.011 260);
|
||||||
|
--line-2: oklch(38% 0.016 260);
|
||||||
|
--primary: oklch(73% 0.12 265);
|
||||||
|
--green: oklch(75% 0.13 155);
|
||||||
|
--amber: oklch(80% 0.12 78);
|
||||||
|
--red: oklch(71% 0.15 24);
|
||||||
|
--cyan: oklch(76% 0.11 205);
|
||||||
|
--shadow: 0 0 0 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; min-height: 100%; }
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
button, input, select, textarea { font: inherit; color: inherit; }
|
||||||
|
button { cursor: pointer; }
|
||||||
|
a { color: inherit; text-decoration: none; }
|
||||||
|
h1, h2, h3, h4, p { margin: 0; }
|
||||||
|
svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
|
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||||
|
.muted { color: var(--text-3); }
|
||||||
|
.strong { color: var(--text); font-weight: 750; }
|
||||||
|
|
||||||
|
.app {
|
||||||
|
height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 48px minmax(0, 1fr);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.top-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
|
.mark {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 7px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--panel);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.top h1 { font-size: 14px; font-weight: 780; letter-spacing: -0.01em; }
|
||||||
|
.path { display: flex; align-items: center; gap: 6px; color: var(--text-3); font-size: 11px; white-space: nowrap; }
|
||||||
|
.path b { color: var(--text-2); font-weight: 650; }
|
||||||
|
.top-actions { display: flex; align-items: center; justify-content: flex-end; gap: 7px; flex-wrap: wrap; }
|
||||||
|
.btn {
|
||||||
|
height: 29px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text-2);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 680;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background .14s, border-color .14s, color .14s, transform .05s;
|
||||||
|
}
|
||||||
|
.btn:hover { border-color: var(--line-2); background: var(--panel-2); color: var(--text); }
|
||||||
|
.btn:active { transform: translateY(1px); }
|
||||||
|
.btn.primary { background: var(--primary); border-color: var(--primary); color: var(--panel); }
|
||||||
|
.btn.flat { background: transparent; border-color: transparent; }
|
||||||
|
.btn.icon { width: 29px; padding: 0; }
|
||||||
|
.btn.warn { color: var(--amber); border-color: color-mix(in oklch, var(--amber) 28%, var(--line)); background: var(--amber-soft); }
|
||||||
|
.btn.danger:hover { color: var(--red); border-color: color-mix(in oklch, var(--red) 28%, var(--line)); background: var(--red-soft); }
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.chip.primary { color: var(--primary); background: var(--primary-soft); border-color: var(--primary-line); }
|
||||||
|
.chip.green { color: var(--green); background: var(--green-soft); border-color: color-mix(in oklch, var(--green) 24%, var(--line)); }
|
||||||
|
.chip.amber { color: var(--amber); background: var(--amber-soft); border-color: color-mix(in oklch, var(--amber) 26%, var(--line)); }
|
||||||
|
.chip.red { color: var(--red); background: var(--red-soft); border-color: color-mix(in oklch, var(--red) 24%, var(--line)); }
|
||||||
|
.dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; flex: none; }
|
||||||
|
.kbd {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
color: var(--text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout { min-height: 0; display: grid; overflow: hidden; }
|
||||||
|
.layout.two { grid-template-columns: 308px minmax(0, 1fr); }
|
||||||
|
.layout.three { grid-template-columns: 288px minmax(0, 1fr) 344px; }
|
||||||
|
.layout.review { grid-template-columns: 360px minmax(0, 1fr); }
|
||||||
|
.layout.single { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
.pane {
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--panel);
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.pane:last-child { border-right: 0; }
|
||||||
|
.pane.subtle { background: var(--panel-2); }
|
||||||
|
.pane-head {
|
||||||
|
padding: 14px 14px 10px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: var(--panel);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--mono);
|
||||||
|
color: var(--text-3);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.pane-head h2 { margin-top: 6px; font-size: 16px; font-weight: 780; letter-spacing: -0.02em; }
|
||||||
|
.pane-head p { margin-top: 3px; color: var(--text-3); font-size: 12px; }
|
||||||
|
.search {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: var(--panel);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.search svg { position: absolute; left: 20px; top: 19px; color: var(--text-3); width: 13px; height: 13px; }
|
||||||
|
.search input {
|
||||||
|
width: 100%;
|
||||||
|
height: 31px;
|
||||||
|
padding: 0 34px 0 30px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
background: var(--panel-2);
|
||||||
|
outline: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.search input:focus { border-color: var(--primary); background: var(--panel); box-shadow: 0 0 0 3px var(--primary-soft); }
|
||||||
|
.search .kbd { position: absolute; right: 18px; top: 14px; }
|
||||||
|
.scroll { min-height: 0; overflow: auto; }
|
||||||
|
.list { padding: 8px; display: grid; gap: 3px; }
|
||||||
|
.section-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 9px 8px 4px;
|
||||||
|
color: var(--text-3);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.rule-row,
|
||||||
|
.scope-row,
|
||||||
|
.queue-row {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.rule-row:hover,
|
||||||
|
.scope-row:hover,
|
||||||
|
.queue-row:hover { background: var(--panel-2); border-color: var(--line); }
|
||||||
|
.rule-row.on,
|
||||||
|
.scope-row.on,
|
||||||
|
.queue-row.on { background: var(--primary-soft); border-color: var(--primary-line); }
|
||||||
|
.row-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||||
|
.row-title { font-size: 13px; font-weight: 760; letter-spacing: -0.01em; }
|
||||||
|
.rule-row.on .row-title,
|
||||||
|
.scope-row.on .row-title,
|
||||||
|
.queue-row.on .row-title { color: var(--primary); }
|
||||||
|
.pattern {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-2);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.pattern .seq { color: var(--primary); font-weight: 800; }
|
||||||
|
.pattern .date { color: var(--cyan); font-weight: 760; }
|
||||||
|
.row-meta { color: var(--text-3); font-size: 11px; display: flex; gap: 7px; flex-wrap: wrap; }
|
||||||
|
.row-meta b { color: var(--text-2); font-weight: 680; }
|
||||||
|
|
||||||
|
.content {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.content-head {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: color-mix(in oklch, var(--panel) 96%, transparent);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.title-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.title-line h2 { font-size: 17px; font-weight: 800; letter-spacing: -0.02em; }
|
||||||
|
.meta-line { margin-top: 4px; color: var(--text-3); font-size: 11px; display: flex; flex-wrap: wrap; gap: 11px; }
|
||||||
|
.meta-line b { color: var(--text-2); }
|
||||||
|
.content-pad { padding: 16px 18px 72px; display: grid; gap: 14px; }
|
||||||
|
.grid-2 { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(300px, .8fr); gap: 14px; align-items: start; }
|
||||||
|
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
|
||||||
|
.card {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
background: var(--panel);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card.pad { padding: 14px; }
|
||||||
|
.card-head {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.card-head h3 { font-size: 13px; font-weight: 780; letter-spacing: -0.01em; }
|
||||||
|
.card-head p { margin-top: 2px; color: var(--text-3); font-size: 11px; }
|
||||||
|
.card-body { padding: 14px; }
|
||||||
|
.metric {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
padding: 11px;
|
||||||
|
}
|
||||||
|
.metric span { display: block; color: var(--text-3); font-family: var(--mono); font-size: 10px; font-weight: 850; letter-spacing: .08em; text-transform: uppercase; }
|
||||||
|
.metric b { display: block; margin-top: 5px; font-family: var(--mono); font-size: 20px; letter-spacing: -0.03em; }
|
||||||
|
.metric small { display: block; margin-top: 3px; color: var(--text-3); font-size: 11px; }
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.preview-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 11px 13px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.preview-code {
|
||||||
|
padding: 20px 18px 16px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: clamp(28px, 4.4vw, 54px);
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.preview-code .dim { color: var(--text-4); }
|
||||||
|
.preview-code .pfx { color: var(--text); }
|
||||||
|
.preview-code .date { color: var(--cyan); }
|
||||||
|
.preview-code .seq { color: var(--primary); }
|
||||||
|
.preview-foot {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.preview-foot div { padding: 10px 12px; border-right: 1px solid var(--line); }
|
||||||
|
.preview-foot div:last-child { border-right: 0; }
|
||||||
|
.preview-foot span { display: block; color: var(--text-3); font-size: 10px; font-weight: 750; }
|
||||||
|
.preview-foot b { display: block; margin-top: 2px; font-family: var(--mono); font-size: 12px; }
|
||||||
|
|
||||||
|
.form-grid { display: grid; grid-template-columns: 128px minmax(0, 1fr); gap: 9px 12px; align-items: center; }
|
||||||
|
.field-label { color: var(--text-2); font-size: 12px; font-weight: 650; }
|
||||||
|
.input, .select, .textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 31px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
background: var(--panel-2);
|
||||||
|
padding: 0 9px;
|
||||||
|
outline: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.textarea { min-height: 62px; padding: 8px 9px; resize: vertical; }
|
||||||
|
.input:focus, .select:focus, .textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); background: var(--panel); }
|
||||||
|
.seg {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.seg button {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-3);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 9px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
.seg button.on { background: var(--panel); color: var(--text); box-shadow: 0 1px 2px color-mix(in oklch, var(--text) 8%, transparent); }
|
||||||
|
.switch {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-2);
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.switch i {
|
||||||
|
width: 32px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--line-2);
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.switch i::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 1px 2px color-mix(in oklch, var(--text) 20%, transparent);
|
||||||
|
}
|
||||||
|
.switch.on i { background: var(--primary); }
|
||||||
|
.switch.on i::after { left: 16px; }
|
||||||
|
|
||||||
|
.parts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||||
|
.part {
|
||||||
|
min-width: 86px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 9px 10px;
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.part:hover { border-color: var(--line-2); background: var(--panel-2); }
|
||||||
|
.part.on { border-color: var(--primary-line); background: var(--primary-soft); }
|
||||||
|
.part span { color: var(--text-3); font-family: var(--mono); font-size: 10px; font-weight: 800; text-transform: uppercase; }
|
||||||
|
.part b { font-family: var(--mono); font-size: 14px; }
|
||||||
|
.joiner { color: var(--text-4); font-family: var(--mono); font-weight: 800; }
|
||||||
|
.add-part {
|
||||||
|
border: 1px dashed var(--line-2);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-2);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
padding: 9px 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.add-part:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
|
||||||
|
|
||||||
|
.inspector {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
background: var(--panel);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.inspector-head {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.cp-bar {
|
||||||
|
min-height: 42px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 108px 1fr;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.cp-slot {
|
||||||
|
padding: 9px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.cp-slot + .cp-slot { border-left: 1px solid var(--line); }
|
||||||
|
.cp-icon {
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: var(--panel-2);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 850;
|
||||||
|
color: var(--text-2);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.cp-slot strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||||
|
.group { border-bottom: 1px solid var(--line); }
|
||||||
|
.group-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.group-title b { font-size: 12px; }
|
||||||
|
.group-body { padding: 12px; display: grid; gap: 10px; }
|
||||||
|
.small-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 12px; color: var(--text-2); }
|
||||||
|
|
||||||
|
.table { width: 100%; border-collapse: collapse; }
|
||||||
|
.table th, .table td {
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
padding: 9px 10px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.table th {
|
||||||
|
color: var(--text-3);
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: var(--panel-2);
|
||||||
|
}
|
||||||
|
.table td { font-size: 12px; }
|
||||||
|
.table tr.on td { background: var(--primary-soft); }
|
||||||
|
.table tr:hover td { background: var(--panel-2); }
|
||||||
|
.table-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; border-bottom: 1px solid var(--line); }
|
||||||
|
.filter-strip { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
|
||||||
|
.split-detail { display: grid; grid-template-columns: minmax(0, 1fr) 330px; gap: 14px; align-items: start; }
|
||||||
|
.timeline { display: grid; gap: 8px; }
|
||||||
|
.event {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 72px minmax(0,1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 9px 0;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.event:last-child { border-bottom: 0; }
|
||||||
|
.event time { color: var(--text-3); font-family: var(--mono); font-size: 11px; }
|
||||||
|
.event b { font-size: 12px; }
|
||||||
|
.event p { color: var(--text-3); font-size: 11px; margin-top: 2px; }
|
||||||
|
|
||||||
|
.savebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
min-height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
background: color-mix(in oklch, var(--panel) 97%, transparent);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.savebar .note { color: var(--text-3); font-size: 12px; }
|
||||||
|
.savebar .note b { color: var(--text); }
|
||||||
|
|
||||||
|
.accordion { display: grid; gap: 0; border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden; background: var(--panel); }
|
||||||
|
.acc-row { border-bottom: 1px solid var(--line); background: var(--panel); }
|
||||||
|
.acc-row:last-child { border-bottom: 0; }
|
||||||
|
.acc-head {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 18px minmax(220px, 1fr) 120px 120px 120px 94px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 14px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.acc-row.open { background: var(--panel-2); }
|
||||||
|
.acc-row.open .chev { transform: rotate(90deg); color: var(--primary); }
|
||||||
|
.chev { transition: transform .16s, color .16s; color: var(--text-3); }
|
||||||
|
.acc-title { font-weight: 780; letter-spacing: -0.01em; }
|
||||||
|
.acc-sub { margin-top: 2px; color: var(--text-3); font-family: var(--mono); font-size: 11px; }
|
||||||
|
.acc-cell span { display: block; color: var(--text-3); font-size: 10px; font-weight: 750; }
|
||||||
|
.acc-cell b { display: block; margin-top: 2px; font-family: var(--mono); font-size: 12px; }
|
||||||
|
.acc-body { display: none; padding: 0 14px 14px 44px; }
|
||||||
|
.acc-row.open .acc-body { display: block; }
|
||||||
|
.tabbar { display: flex; align-items: center; gap: 2px; padding: 2px; border: 1px solid var(--line); background: var(--panel); border-radius: var(--r-sm); width: fit-content; }
|
||||||
|
.tabbar button { border: 0; background: transparent; border-radius: 4px; color: var(--text-3); padding: 5px 9px; font-size: 11px; font-weight: 720; }
|
||||||
|
.tabbar button.on { background: var(--primary-soft); color: var(--primary); }
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 32px;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.gallery-inner { max-width: 1180px; margin: 0 auto; display: grid; gap: 18px; }
|
||||||
|
.gallery-hero {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 24px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
.gallery-hero h1 { margin-top: 10px; font-size: clamp(28px, 4vw, 46px); line-height: 1.06; letter-spacing: -0.04em; max-width: 720px; }
|
||||||
|
.gallery-hero p { margin-top: 12px; color: var(--text-2); max-width: 760px; font-size: 15px; }
|
||||||
|
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
||||||
|
.gallery-card {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 16px;
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.gallery-card h2 { margin-top: 10px; font-size: 18px; }
|
||||||
|
.gallery-card p { margin-top: 6px; color: var(--text-3); font-size: 12px; }
|
||||||
|
.mini {
|
||||||
|
height: 130px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
padding: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: .38fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.mini.three { grid-template-columns: .32fr 1fr .42fr; }
|
||||||
|
.mini .side,
|
||||||
|
.mini .main {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.mini-line { height: 8px; border-radius: 999px; background: var(--line-2); margin-bottom: 7px; }
|
||||||
|
.mini-line.primary { background: var(--primary); }
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
body { overflow: auto; }
|
||||||
|
.app { height: auto; min-height: 100vh; }
|
||||||
|
.layout.two, .layout.three, .layout.review, .layout.single { grid-template-columns: 1fr; }
|
||||||
|
.pane, .inspector { border-right: 0; border-bottom: 1px solid var(--line); max-height: none; }
|
||||||
|
.content { min-height: 720px; }
|
||||||
|
.grid-2, .grid-3, .gallery-grid { grid-template-columns: 1fr; }
|
||||||
|
.split-detail { grid-template-columns: 1fr; }
|
||||||
|
.savebar { position: sticky; }
|
||||||
|
.preview-foot { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.acc-head { grid-template-columns: 18px minmax(0, 1fr) 92px; }
|
||||||
|
.acc-head .hide-sm { display: none; }
|
||||||
|
.gallery { padding: 18px; }
|
||||||
|
.gallery-hero { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V3 A · 조용한 관리자형</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v3.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="top" data-od-id="topbar">
|
||||||
|
<div class="top-left">
|
||||||
|
<div class="mark">#</div>
|
||||||
|
<div>
|
||||||
|
<h1>채번 규칙 관리</h1>
|
||||||
|
<div class="path"><span>Admin</span><span>/</span><b>시스템 관리</b><span>/</span><b>채번 규칙</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<span class="chip green"><span class="dot"></span>운영 16</span>
|
||||||
|
<span class="chip amber"><span class="dot"></span>검토 3</span>
|
||||||
|
<button class="btn">변경 이력</button>
|
||||||
|
<button class="btn primary">새 규칙</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="layout two">
|
||||||
|
<aside class="pane" data-od-id="rule-list">
|
||||||
|
<div class="pane-head">
|
||||||
|
<div class="eyebrow">Rules</div>
|
||||||
|
<h2>규칙 목록</h2>
|
||||||
|
<p>테이블 연결 상태와 마지막 발번 기준으로 정렬됩니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="search">
|
||||||
|
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
||||||
|
<input id="ruleSearch" placeholder="수주번호, sales_order, SO 검색" />
|
||||||
|
<span class="kbd">/</span>
|
||||||
|
</div>
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="list" id="ruleList">
|
||||||
|
<div class="section-label"><span>영업 관리</span><span>6</span></div>
|
||||||
|
<button class="rule-row on" data-title="수주번호 자동채번" data-code="SO-202605-0048" data-table="sales_order.order_no" data-cycle="월별" data-seq="47">
|
||||||
|
<div class="row-top">
|
||||||
|
<span class="row-title">수주번호 자동채번</span>
|
||||||
|
<span class="chip green">운영</span>
|
||||||
|
</div>
|
||||||
|
<div class="pattern"><span>SO</span><span>-</span><span class="date">yyyyMM</span><span>-</span><span class="seq">0000</span></div>
|
||||||
|
<div class="row-meta"><span>sales_order.order_no</span><span>현재 <b>47</b></span></div>
|
||||||
|
</button>
|
||||||
|
<button class="rule-row" data-title="견적번호" data-code="QT-2026-00129" data-table="quote.quote_no" data-cycle="연도별" data-seq="128">
|
||||||
|
<div class="row-top">
|
||||||
|
<span class="row-title">견적번호</span>
|
||||||
|
<span class="chip green">운영</span>
|
||||||
|
</div>
|
||||||
|
<div class="pattern"><span>QT</span><span>-</span><span class="date">yyyy</span><span>-</span><span class="seq">00000</span></div>
|
||||||
|
<div class="row-meta"><span>quote.quote_no</span><span>현재 <b>128</b></span></div>
|
||||||
|
</button>
|
||||||
|
<button class="rule-row" data-title="클레임 접수번호" data-code="CLM-C07-018" data-table="claim.claim_no" data-cycle="회사별" data-seq="17">
|
||||||
|
<div class="row-top">
|
||||||
|
<span class="row-title">클레임 접수번호</span>
|
||||||
|
<span class="chip amber">검토</span>
|
||||||
|
</div>
|
||||||
|
<div class="pattern"><span>CLM</span><span>-</span><span class="date">company</span><span>-</span><span class="seq">000</span></div>
|
||||||
|
<div class="row-meta"><span>claim.claim_no</span><span>현재 <b>17</b></span></div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="section-label"><span>생산 관리</span><span>4</span></div>
|
||||||
|
<button class="rule-row" data-title="작업지시번호" data-code="WO-20260520-0094" data-table="work_order.work_no" data-cycle="일별" data-seq="93">
|
||||||
|
<div class="row-top">
|
||||||
|
<span class="row-title">작업지시번호</span>
|
||||||
|
<span class="chip green">운영</span>
|
||||||
|
</div>
|
||||||
|
<div class="pattern"><span>WO</span><span>-</span><span class="date">yyyyMMdd</span><span>-</span><span class="seq">0000</span></div>
|
||||||
|
<div class="row-meta"><span>work_order.work_no</span><span>현재 <b>93</b></span></div>
|
||||||
|
</button>
|
||||||
|
<button class="rule-row" data-title="LOT 번호" data-code="LOT-ITEM-2026-3412" data-table="lot_master.lot_no" data-cycle="품목별" data-seq="3411">
|
||||||
|
<div class="row-top">
|
||||||
|
<span class="row-title">LOT 번호</span>
|
||||||
|
<span class="chip red">충돌</span>
|
||||||
|
</div>
|
||||||
|
<div class="pattern"><span>LOT</span><span>-</span><span class="date">item</span><span>-</span><span>yyyy</span><span>-</span><span class="seq">0000</span></div>
|
||||||
|
<div class="row-meta"><span>lot_master.lot_no</span><span>중복 후보 <b>2</b></span></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="content" data-od-id="rule-detail">
|
||||||
|
<div class="content-head">
|
||||||
|
<div>
|
||||||
|
<div class="title-line">
|
||||||
|
<h2 id="detailTitle">수주번호 자동채번</h2>
|
||||||
|
<span class="chip green">운영 중</span>
|
||||||
|
<span class="chip primary mono" id="detailTable">sales_order.order_no</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-line">
|
||||||
|
<span>리셋 기준 <b id="detailCycle">월별</b></span>
|
||||||
|
<span>현재 순번 <b id="detailSeq">47</b></span>
|
||||||
|
<span>수정자 <b>admin01</b></span>
|
||||||
|
<span>마지막 저장 <b>오늘 09:42</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<button class="btn">미리 발번</button>
|
||||||
|
<button class="btn primary">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-pad">
|
||||||
|
<section class="preview" data-od-id="next-preview">
|
||||||
|
<div class="preview-top">
|
||||||
|
<div>
|
||||||
|
<div class="eyebrow">Next number</div>
|
||||||
|
<div class="muted">저장 후 다음 요청에서 발급될 번호입니다.</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="copyBtn">복사</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-code" id="previewCode">
|
||||||
|
<span class="pfx">SO</span><span class="dim">-</span><span class="date">202605</span><span class="dim">-</span><span class="seq">0048</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-foot">
|
||||||
|
<div><span>현재 순번</span><b id="footSeq">47</b></div>
|
||||||
|
<div><span>다음 순번</span><b id="footNext">48</b></div>
|
||||||
|
<div><span>리셋 정책</span><b id="footCycle">월별</b></div>
|
||||||
|
<div><span>검증</span><b class="strong">통과</b></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid-2" data-od-id="rule-settings">
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3>기본 설정</h3>
|
||||||
|
<p>관리자가 가장 자주 확인하는 값만 위로 올렸습니다.</p>
|
||||||
|
</div>
|
||||||
|
<span class="chip primary">필수</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<label class="field-label">규칙명</label>
|
||||||
|
<input class="input" value="수주번호 자동채번" />
|
||||||
|
<label class="field-label">사용 테이블</label>
|
||||||
|
<input class="input mono" value="sales_order" />
|
||||||
|
<label class="field-label">대상 컬럼</label>
|
||||||
|
<input class="input mono" value="order_no" />
|
||||||
|
<label class="field-label">발번 시점</label>
|
||||||
|
<select class="select"><option>저장 직전 자동 발번</option><option>임시 저장 시 예약</option></select>
|
||||||
|
<label class="field-label">중복 정책</label>
|
||||||
|
<select class="select"><option>DB 잠금 후 증가</option><option>중복 발견 시 재시도</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3>운영 상태</h3>
|
||||||
|
<p>실수 가능성이 있는 항목만 별도 표시합니다.</p>
|
||||||
|
</div>
|
||||||
|
<span class="chip green">정상</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid-3" style="grid-template-columns:1fr 1fr;">
|
||||||
|
<div class="metric"><span>Issued today</span><b>31</b><small>오늘 발급 수</small></div>
|
||||||
|
<div class="metric"><span>Retries</span><b>0</b><small>재시도 없음</small></div>
|
||||||
|
</div>
|
||||||
|
<div style="height:12px"></div>
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="event">
|
||||||
|
<time>09:42</time>
|
||||||
|
<div><b>패턴 저장</b><p>SO-yyyyMM-0000 유지, 리셋 정책만 확인</p></div>
|
||||||
|
<span class="chip green">완료</span>
|
||||||
|
</div>
|
||||||
|
<div class="event">
|
||||||
|
<time>08:17</time>
|
||||||
|
<div><b>자동 발번</b><p>sales_order.order_no 31건 발급</p></div>
|
||||||
|
<span class="chip">기록</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card" data-od-id="parts">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3>포맷 구성</h3>
|
||||||
|
<p>파트를 선택하면 아래 설정이 바로 바뀌는 단순한 구조입니다.</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn">파트 추가</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="parts">
|
||||||
|
<button class="part on"><span>Prefix</span><b>SO</b></button>
|
||||||
|
<span class="joiner">-</span>
|
||||||
|
<button class="part"><span>Date</span><b>yyyyMM</b></button>
|
||||||
|
<span class="joiner">-</span>
|
||||||
|
<button class="part"><span>Sequence</span><b>0000</b></button>
|
||||||
|
<button class="add-part">검증 파트 추가</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="savebar">
|
||||||
|
<div class="note"><b>저장 전 확인</b> 포맷, 대상 컬럼, 현재 순번이 다음 발번에 바로 반영됩니다.</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<button class="btn">취소</button>
|
||||||
|
<button class="btn primary">규칙 저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const rows = document.querySelectorAll(".rule-row");
|
||||||
|
const title = document.getElementById("detailTitle");
|
||||||
|
const table = document.getElementById("detailTable");
|
||||||
|
const cycle = document.getElementById("detailCycle");
|
||||||
|
const seq = document.getElementById("detailSeq");
|
||||||
|
const footSeq = document.getElementById("footSeq");
|
||||||
|
const footNext = document.getElementById("footNext");
|
||||||
|
const footCycle = document.getElementById("footCycle");
|
||||||
|
const preview = document.getElementById("previewCode");
|
||||||
|
|
||||||
|
function renderCode(code) {
|
||||||
|
const parts = code.split("-");
|
||||||
|
preview.innerHTML = parts.map((part, index) => {
|
||||||
|
const cls = index === 0 ? "pfx" : index === parts.length - 1 ? "seq" : "date";
|
||||||
|
return `<span class="${cls}">${part}</span>${index < parts.length - 1 ? '<span class="dim">-</span>' : ""}`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
rows.forEach((item) => item.classList.remove("on"));
|
||||||
|
row.classList.add("on");
|
||||||
|
title.textContent = row.dataset.title;
|
||||||
|
table.textContent = row.dataset.table;
|
||||||
|
cycle.textContent = row.dataset.cycle;
|
||||||
|
seq.textContent = row.dataset.seq;
|
||||||
|
footSeq.textContent = row.dataset.seq;
|
||||||
|
footNext.textContent = String(Number(row.dataset.seq) + 1);
|
||||||
|
footCycle.textContent = row.dataset.cycle;
|
||||||
|
renderCode(row.dataset.code);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("ruleSearch").addEventListener("input", (event) => {
|
||||||
|
const term = event.target.value.trim().toLowerCase();
|
||||||
|
rows.forEach((row) => {
|
||||||
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "grid" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("copyBtn").addEventListener("click", (event) => {
|
||||||
|
event.currentTarget.textContent = "복사됨";
|
||||||
|
setTimeout(() => event.currentTarget.textContent = "복사", 900);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V3 B · CP 편집기형</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v3.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="top" data-od-id="topbar">
|
||||||
|
<div class="top-left">
|
||||||
|
<div class="mark">#</div>
|
||||||
|
<div>
|
||||||
|
<h1>채번 디자이너</h1>
|
||||||
|
<div class="path"><span>Admin</span><span>/</span><b>채번 규칙</b><span>/</span><b>수주번호</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<button class="btn">미리 발번</button>
|
||||||
|
<button class="btn">검증 실행</button>
|
||||||
|
<button class="btn primary">저장</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="layout three">
|
||||||
|
<aside class="pane" data-od-id="left-rules">
|
||||||
|
<div class="pane-head">
|
||||||
|
<div class="eyebrow">Navigation</div>
|
||||||
|
<h2>도메인과 규칙</h2>
|
||||||
|
<p>업무 화면에서 쓰이는 채번만 묶어서 봅니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="search">
|
||||||
|
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
||||||
|
<input id="filterInput" placeholder="테이블, 컬럼, 접두어 검색" />
|
||||||
|
<span class="kbd">K</span>
|
||||||
|
</div>
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="list" id="filterList">
|
||||||
|
<div class="section-label"><span>자주 편집</span><span>4</span></div>
|
||||||
|
<button class="scope-row on">
|
||||||
|
<div class="row-top"><span class="row-title">수주번호 자동채번</span><span class="chip green">운영</span></div>
|
||||||
|
<div class="pattern"><span>SO</span><span>-</span><span class="date">yyyyMM</span><span>-</span><span class="seq">0000</span></div>
|
||||||
|
<div class="row-meta"><span>sales_order.order_no</span></div>
|
||||||
|
</button>
|
||||||
|
<button class="scope-row">
|
||||||
|
<div class="row-top"><span class="row-title">출하 배차번호</span><span class="chip">초안</span></div>
|
||||||
|
<div class="pattern"><span>DLV</span><span>-</span><span class="date">yyyyMMdd</span><span>-</span><span class="seq">000</span></div>
|
||||||
|
<div class="row-meta"><span>delivery.trip_no</span></div>
|
||||||
|
</button>
|
||||||
|
<button class="scope-row">
|
||||||
|
<div class="row-top"><span class="row-title">작업지시번호</span><span class="chip green">운영</span></div>
|
||||||
|
<div class="pattern"><span>WO</span><span>-</span><span class="date">yyyyMMdd</span><span>-</span><span class="seq">0000</span></div>
|
||||||
|
<div class="row-meta"><span>work_order.work_no</span></div>
|
||||||
|
</button>
|
||||||
|
<div class="section-label"><span>검토 필요</span><span>3</span></div>
|
||||||
|
<button class="scope-row">
|
||||||
|
<div class="row-top"><span class="row-title">LOT 번호</span><span class="chip red">충돌</span></div>
|
||||||
|
<div class="pattern"><span>LOT</span><span>-</span><span class="date">item</span><span>-</span><span class="seq">0000</span></div>
|
||||||
|
<div class="row-meta"><span>lot_master.lot_no</span></div>
|
||||||
|
</button>
|
||||||
|
<button class="scope-row">
|
||||||
|
<div class="row-top"><span class="row-title">클레임 접수번호</span><span class="chip amber">확인</span></div>
|
||||||
|
<div class="pattern"><span>CLM</span><span>-</span><span class="date">company</span><span>-</span><span class="seq">000</span></div>
|
||||||
|
<div class="row-meta"><span>claim.claim_no</span></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="content" data-od-id="editor">
|
||||||
|
<div class="content-head">
|
||||||
|
<div>
|
||||||
|
<div class="title-line">
|
||||||
|
<h2>수주번호 자동채번</h2>
|
||||||
|
<span class="chip primary">편집 중</span>
|
||||||
|
<span class="chip green">검증 통과</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-line">
|
||||||
|
<span>패턴 <b class="mono">SO-yyyyMM-0000</b></span>
|
||||||
|
<span>대상 <b class="mono">sales_order.order_no</b></span>
|
||||||
|
<span>리셋 <b>월별</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<span class="kbd">Ctrl S</span>
|
||||||
|
<button class="btn primary">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-pad">
|
||||||
|
<section class="preview" data-od-id="preview">
|
||||||
|
<div class="preview-top">
|
||||||
|
<div>
|
||||||
|
<div class="eyebrow">Live composition</div>
|
||||||
|
<div class="muted">선택한 파트의 속성은 오른쪽 CP 패널에서 바로 조정합니다.</div>
|
||||||
|
</div>
|
||||||
|
<span class="chip green"><span class="dot"></span>중복 없음</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-code">
|
||||||
|
<span class="pfx">SO</span><span class="dim">-</span><span class="date">202605</span><span class="dim">-</span><span class="seq">0048</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card" data-od-id="composer">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3>포맷 파트</h3>
|
||||||
|
<p>왼쪽에서 오른쪽으로 발번 문자열이 생성됩니다. 과한 시각화 대신 실제 조립 단위를 명확히 둡니다.</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn">파트 추가</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="parts" id="parts">
|
||||||
|
<button class="part" data-kind="prefix" data-name="고정 접두어" data-value="SO"><span>Prefix</span><b>SO</b></button>
|
||||||
|
<span class="joiner">-</span>
|
||||||
|
<button class="part on" data-kind="date" data-name="발번 일자" data-value="yyyyMM"><span>Date</span><b>yyyyMM</b></button>
|
||||||
|
<span class="joiner">-</span>
|
||||||
|
<button class="part" data-kind="sequence" data-name="순번" data-value="0000"><span>Sequence</span><b>0000</b></button>
|
||||||
|
<button class="add-part">조건 파트 추가</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid-2" data-od-id="editor-body">
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3>연결 대상</h3>
|
||||||
|
<p>실제 저장될 테이블과 컬럼을 분리해서 보여줍니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<thead><tr><th>항목</th><th>값</th><th>상태</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>테이블</td><td class="mono">sales_order</td><td><span class="chip green">확인</span></td></tr>
|
||||||
|
<tr><td>컬럼</td><td class="mono">order_no</td><td><span class="chip green">유니크</span></td></tr>
|
||||||
|
<tr><td>발번 함수</td><td class="mono">beforeSave</td><td><span class="chip">기본</span></td></tr>
|
||||||
|
<tr><td>잠금 정책</td><td class="mono">transaction_lock</td><td><span class="chip green">권장</span></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3>발번 테스트</h3>
|
||||||
|
<p>관리자가 저장 전 확인할 수 있는 최소 샘플입니다.</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn">다시 실행</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="event">
|
||||||
|
<time>현재</time>
|
||||||
|
<div><b class="mono">SO-202605-0047</b><p>마지막 발급 번호</p></div>
|
||||||
|
<span class="chip">저장됨</span>
|
||||||
|
</div>
|
||||||
|
<div class="event">
|
||||||
|
<time>다음</time>
|
||||||
|
<div><b class="mono">SO-202605-0048</b><p>미리보기 결과</p></div>
|
||||||
|
<span class="chip green">통과</span>
|
||||||
|
</div>
|
||||||
|
<div class="event">
|
||||||
|
<time>리셋</time>
|
||||||
|
<div><b>2026년 6월 1일</b><p>월별 기준으로 0001부터 시작</p></div>
|
||||||
|
<span class="chip">예정</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<aside class="inspector" data-od-id="cp-inspector">
|
||||||
|
<div class="inspector-head">
|
||||||
|
<div class="eyebrow">CP Inspector</div>
|
||||||
|
<h2 id="cpTitle" style="font-size:16px;margin-top:6px;">발번 일자</h2>
|
||||||
|
<p class="muted" id="cpDesc" style="margin-top:3px;">선택한 파트만 편집합니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="cp-bar">
|
||||||
|
<div class="cp-slot">
|
||||||
|
<span class="cp-icon">N</span>
|
||||||
|
<strong>채번</strong>
|
||||||
|
</div>
|
||||||
|
<div class="cp-slot">
|
||||||
|
<span class="cp-icon" id="cpKind">DT</span>
|
||||||
|
<strong id="cpValue">yyyyMM</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="group">
|
||||||
|
<div class="group-title"><b>파트 속성</b><span class="chip primary">선택됨</span></div>
|
||||||
|
<div class="group-body">
|
||||||
|
<div class="form-grid" style="grid-template-columns:92px 1fr;">
|
||||||
|
<label class="field-label">표시 이름</label>
|
||||||
|
<input class="input" id="partLabel" value="발번 일자" />
|
||||||
|
<label class="field-label">값</label>
|
||||||
|
<input class="input mono" id="partValue" value="yyyyMM" />
|
||||||
|
<label class="field-label">구분자</label>
|
||||||
|
<select class="select"><option>앞뒤 하이픈</option><option>뒤에만 하이픈</option><option>없음</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
|
<div class="group-title"><b>날짜 정책</b><span class="muted">월별 리셋</span></div>
|
||||||
|
<div class="group-body">
|
||||||
|
<div class="small-row"><span>서버 시간 기준</span><span class="switch on"><i></i></span></div>
|
||||||
|
<div class="small-row"><span>월 변경 시 자동 초기화</span><span class="switch on"><i></i></span></div>
|
||||||
|
<div class="small-row"><span>수동 리셋 허용</span><span class="switch"><i></i></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group">
|
||||||
|
<div class="group-title"><b>검증</b><span class="chip green">통과</span></div>
|
||||||
|
<div class="group-body">
|
||||||
|
<div class="small-row"><span>패턴 길이</span><b class="mono">13</b></div>
|
||||||
|
<div class="small-row"><span>빈 값 가능성</span><b>없음</b></div>
|
||||||
|
<div class="small-row"><span>중복 가능성</span><b>낮음</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const partButtons = document.querySelectorAll(".part");
|
||||||
|
const cpTitle = document.getElementById("cpTitle");
|
||||||
|
const cpDesc = document.getElementById("cpDesc");
|
||||||
|
const cpKind = document.getElementById("cpKind");
|
||||||
|
const cpValue = document.getElementById("cpValue");
|
||||||
|
const partLabel = document.getElementById("partLabel");
|
||||||
|
const partValue = document.getElementById("partValue");
|
||||||
|
const labels = { prefix: "PX", date: "DT", sequence: "SQ" };
|
||||||
|
|
||||||
|
partButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
partButtons.forEach((item) => item.classList.remove("on"));
|
||||||
|
button.classList.add("on");
|
||||||
|
cpTitle.textContent = button.dataset.name;
|
||||||
|
cpDesc.textContent = `${button.dataset.kind} 파트 속성만 편집합니다.`;
|
||||||
|
cpKind.textContent = labels[button.dataset.kind] || "PT";
|
||||||
|
cpValue.textContent = button.dataset.value;
|
||||||
|
partLabel.value = button.dataset.name;
|
||||||
|
partValue.value = button.dataset.value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("filterInput").addEventListener("input", (event) => {
|
||||||
|
const term = event.target.value.trim().toLowerCase();
|
||||||
|
document.querySelectorAll("#filterList .scope-row").forEach((row) => {
|
||||||
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "grid" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V3 C · 운영 검토형</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v3.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="top" data-od-id="topbar">
|
||||||
|
<div class="top-left">
|
||||||
|
<div class="mark">#</div>
|
||||||
|
<div>
|
||||||
|
<h1>채번 운영 검토</h1>
|
||||||
|
<div class="path"><span>Admin</span><span>/</span><b>운영 상태</b><span>/</span><b>채번 점검</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<span class="chip red"><span class="dot"></span>충돌 1</span>
|
||||||
|
<span class="chip amber"><span class="dot"></span>검토 3</span>
|
||||||
|
<button class="btn">전체 점검</button>
|
||||||
|
<button class="btn primary">승인 반영</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="layout review">
|
||||||
|
<aside class="pane subtle" data-od-id="review-queue">
|
||||||
|
<div class="pane-head">
|
||||||
|
<div class="eyebrow">Review queue</div>
|
||||||
|
<h2>검토 대상</h2>
|
||||||
|
<p>회사관리 accordion처럼 운영자가 먼저 위험도를 훑고 펼쳐봅니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="search">
|
||||||
|
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
||||||
|
<input id="queueSearch" placeholder="충돌, 미연결, 수동 리셋 검색" />
|
||||||
|
<span class="kbd">/</span>
|
||||||
|
</div>
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="list" id="queueList">
|
||||||
|
<button class="queue-row on" data-focus="collision">
|
||||||
|
<div class="row-top"><span class="row-title">LOT 번호 중복 후보</span><span class="chip red">P0</span></div>
|
||||||
|
<div class="row-meta"><span>lot_master.lot_no</span><span>후보 <b>2건</b></span></div>
|
||||||
|
</button>
|
||||||
|
<button class="queue-row" data-focus="draft">
|
||||||
|
<div class="row-top"><span class="row-title">출하 배차번호 미연결</span><span class="chip amber">P1</span></div>
|
||||||
|
<div class="row-meta"><span>delivery.trip_no</span><span>저장 전</span></div>
|
||||||
|
</button>
|
||||||
|
<button class="queue-row" data-focus="reset">
|
||||||
|
<div class="row-top"><span class="row-title">월별 리셋 전 점검</span><span class="chip">예정</span></div>
|
||||||
|
<div class="row-meta"><span>6월 1일 00:00</span><span>대상 <b>7개</b></span></div>
|
||||||
|
</button>
|
||||||
|
<button class="queue-row" data-focus="scope">
|
||||||
|
<div class="row-top"><span class="row-title">회사별 범위 누락</span><span class="chip amber">P1</span></div>
|
||||||
|
<div class="row-meta"><span>claim.claim_no</span><span>회사 2곳</span></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="content" data-od-id="operations">
|
||||||
|
<div class="content-head">
|
||||||
|
<div>
|
||||||
|
<div class="title-line">
|
||||||
|
<h2 id="focusTitle">LOT 번호 중복 후보</h2>
|
||||||
|
<span class="chip red" id="focusStatus">조치 필요</span>
|
||||||
|
<span class="chip primary mono">lot_master.lot_no</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-line">
|
||||||
|
<span>영향 화면 <b>생산 / LOT 등록</b></span>
|
||||||
|
<span>검출 시간 <b>오늘 10:18</b></span>
|
||||||
|
<span>권장 조치 <b id="focusAction">순번 시작값 조정</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<button class="btn">담당자 지정</button>
|
||||||
|
<button class="btn primary">조치 승인</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-pad">
|
||||||
|
<section class="grid-3" data-od-id="risk-summary">
|
||||||
|
<div class="metric"><span>Open issues</span><b>4</b><small>현재 검토 대상</small></div>
|
||||||
|
<div class="metric"><span>Collision</span><b style="color:var(--red)">1</b><small>중복 후보</small></div>
|
||||||
|
<div class="metric"><span>Unlinked</span><b style="color:var(--amber)">2</b><small>화면 연결 전</small></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="accordion" data-od-id="accordion-review">
|
||||||
|
<article class="acc-row open">
|
||||||
|
<button class="acc-head">
|
||||||
|
<svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
|
||||||
|
<div>
|
||||||
|
<div class="acc-title">LOT 번호 중복 후보</div>
|
||||||
|
<div class="acc-sub">LOT-item-yyyy-0000 · lot_master.lot_no</div>
|
||||||
|
</div>
|
||||||
|
<div class="acc-cell hide-sm"><span>위험도</span><b style="color:var(--red)">P0</b></div>
|
||||||
|
<div class="acc-cell hide-sm"><span>후보</span><b>2건</b></div>
|
||||||
|
<div class="acc-cell hide-sm"><span>현재 순번</span><b>3411</b></div>
|
||||||
|
<span class="chip red">확인</span>
|
||||||
|
</button>
|
||||||
|
<div class="acc-body">
|
||||||
|
<div class="grid-2">
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3>발견된 후보</h3>
|
||||||
|
<p>같은 품목 범위에서 이미 존재하는 번호와 충돌합니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<thead><tr><th>번호</th><th>품목</th><th>위치</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="mono">LOT-ITM-2026-3412</td><td>AX-240</td><td>기존 LOT</td></tr>
|
||||||
|
<tr><td class="mono">LOT-ITM-2026-3413</td><td>AX-240</td><td>예약 번호</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3>권장 조치</h3>
|
||||||
|
<p>현재 발번 규칙은 유지하고 시작값만 올리는 쪽이 안전합니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<label class="field-label">현재 시작값</label>
|
||||||
|
<input class="input mono" value="3412" />
|
||||||
|
<label class="field-label">권장 시작값</label>
|
||||||
|
<input class="input mono" value="3414" />
|
||||||
|
<label class="field-label">적용 범위</label>
|
||||||
|
<select class="select"><option>품목 AX-240 범위만</option><option>전체 LOT 규칙</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="acc-row">
|
||||||
|
<button class="acc-head">
|
||||||
|
<svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
|
||||||
|
<div>
|
||||||
|
<div class="acc-title">출하 배차번호 미연결</div>
|
||||||
|
<div class="acc-sub">DLV-yyyyMMdd-000 · delivery.trip_no</div>
|
||||||
|
</div>
|
||||||
|
<div class="acc-cell hide-sm"><span>위험도</span><b style="color:var(--amber)">P1</b></div>
|
||||||
|
<div class="acc-cell hide-sm"><span>후보</span><b>1건</b></div>
|
||||||
|
<div class="acc-cell hide-sm"><span>현재 순번</span><b>0</b></div>
|
||||||
|
<span class="chip amber">미연결</span>
|
||||||
|
</button>
|
||||||
|
<div class="acc-body">
|
||||||
|
<div class="card pad">
|
||||||
|
<div class="form-grid">
|
||||||
|
<label class="field-label">연결 화면</label>
|
||||||
|
<input class="input" value="영업관리 / 출하 배차 등록" />
|
||||||
|
<label class="field-label">발번 시점</label>
|
||||||
|
<select class="select"><option>배차 저장 시 자동 발번</option></select>
|
||||||
|
<label class="field-label">검토 메모</label>
|
||||||
|
<textarea class="textarea">출하 배차 화면 저장 API와 연결한 뒤 운영 전환합니다.</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="acc-row">
|
||||||
|
<button class="acc-head">
|
||||||
|
<svg class="chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>
|
||||||
|
<div>
|
||||||
|
<div class="acc-title">월별 리셋 전 점검</div>
|
||||||
|
<div class="acc-sub">SO, PO, WO 등 월별 규칙 7개</div>
|
||||||
|
</div>
|
||||||
|
<div class="acc-cell hide-sm"><span>위험도</span><b>예정</b></div>
|
||||||
|
<div class="acc-cell hide-sm"><span>대상</span><b>7개</b></div>
|
||||||
|
<div class="acc-cell hide-sm"><span>예정일</span><b>06.01</b></div>
|
||||||
|
<span class="chip green">정상</span>
|
||||||
|
</button>
|
||||||
|
<div class="acc-body">
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="event"><time>05.31</time><div><b>사전 알림</b><p>월별 리셋 규칙 담당자에게 알림 전송</p></div><span class="chip">예정</span></div>
|
||||||
|
<div class="event"><time>06.01</time><div><b>자동 초기화</b><p>리셋 기준이 월별인 규칙만 0001부터 시작</p></div><span class="chip green">정상</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const accRows = document.querySelectorAll(".acc-row");
|
||||||
|
accRows.forEach((row) => {
|
||||||
|
row.querySelector(".acc-head").addEventListener("click", () => {
|
||||||
|
accRows.forEach((item) => item.classList.remove("open"));
|
||||||
|
row.classList.add("open");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const queueRows = document.querySelectorAll(".queue-row");
|
||||||
|
const title = document.getElementById("focusTitle");
|
||||||
|
const status = document.getElementById("focusStatus");
|
||||||
|
const action = document.getElementById("focusAction");
|
||||||
|
const map = {
|
||||||
|
collision: ["LOT 번호 중복 후보", "조치 필요", "순번 시작값 조정", "red"],
|
||||||
|
draft: ["출하 배차번호 미연결", "연결 필요", "화면 저장 API 연결", "amber"],
|
||||||
|
reset: ["월별 리셋 전 점검", "정상", "담당자 사전 알림", "green"],
|
||||||
|
scope: ["회사별 범위 누락", "검토 필요", "회사 범위 조건 추가", "amber"]
|
||||||
|
};
|
||||||
|
queueRows.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
queueRows.forEach((item) => item.classList.remove("on"));
|
||||||
|
row.classList.add("on");
|
||||||
|
const next = map[row.dataset.focus];
|
||||||
|
title.textContent = next[0];
|
||||||
|
status.textContent = next[1];
|
||||||
|
status.className = `chip ${next[3]}`;
|
||||||
|
action.textContent = next[2];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("queueSearch").addEventListener("input", (event) => {
|
||||||
|
const term = event.target.value.trim().toLowerCase();
|
||||||
|
queueRows.forEach((row) => {
|
||||||
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "grid" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin V3 D · 표 중심형</title>
|
||||||
|
<link rel="stylesheet" href="./numbering-v3.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="top" data-od-id="topbar">
|
||||||
|
<div class="top-left">
|
||||||
|
<div class="mark">#</div>
|
||||||
|
<div>
|
||||||
|
<h1>채번 규칙 테이블</h1>
|
||||||
|
<div class="path"><span>Admin</span><span>/</span><b>시스템 관리</b><span>/</span><b>채번 전체</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<button class="btn">엑셀 내보내기</button>
|
||||||
|
<button class="btn">선택 검증</button>
|
||||||
|
<button class="btn primary">새 규칙</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="layout single">
|
||||||
|
<main class="content" data-od-id="table-first">
|
||||||
|
<div class="content-head">
|
||||||
|
<div>
|
||||||
|
<div class="title-line">
|
||||||
|
<h2>채번 규칙 전체 현황</h2>
|
||||||
|
<span class="chip green">운영 16</span>
|
||||||
|
<span class="chip amber">검토 3</span>
|
||||||
|
<span class="chip red">충돌 1</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-line">
|
||||||
|
<span>관리 기준 <b>테이블 / 컬럼 / 패턴</b></span>
|
||||||
|
<span>마지막 전체 검증 <b>오늘 10:18</b></span>
|
||||||
|
<span>정렬 <b>위험도 우선</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<span class="kbd">Shift F</span>
|
||||||
|
<button class="btn primary">저장</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-pad">
|
||||||
|
<section class="grid-3" data-od-id="table-metrics">
|
||||||
|
<div class="metric"><span>Total rules</span><b>20</b><small>등록된 규칙</small></div>
|
||||||
|
<div class="metric"><span>Linked columns</span><b>17</b><small>실제 컬럼 연결</small></div>
|
||||||
|
<div class="metric"><span>Today issued</span><b>142</b><small>오늘 발급 수</small></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="split-detail" data-od-id="table-and-detail">
|
||||||
|
<article class="card">
|
||||||
|
<div class="table-actions">
|
||||||
|
<div class="filter-strip">
|
||||||
|
<button class="btn primary">전체</button>
|
||||||
|
<button class="btn">운영</button>
|
||||||
|
<button class="btn warn">검토</button>
|
||||||
|
<button class="btn danger">충돌</button>
|
||||||
|
</div>
|
||||||
|
<div class="search" style="padding:0;border-bottom:0;width:min(360px,42vw);background:transparent;">
|
||||||
|
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
||||||
|
<input id="tableSearch" placeholder="규칙명, 테이블, 패턴 검색" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="scroll" style="max-height:560px;">
|
||||||
|
<table class="table" id="rulesTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>상태</th>
|
||||||
|
<th>규칙명</th>
|
||||||
|
<th>패턴</th>
|
||||||
|
<th>대상 컬럼</th>
|
||||||
|
<th>현재</th>
|
||||||
|
<th>리셋</th>
|
||||||
|
<th>검증</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="on" data-title="LOT 번호" data-code="LOT-ITM-2026-3414" data-status="충돌 조치 필요">
|
||||||
|
<td><span class="chip red">충돌</span></td>
|
||||||
|
<td><b>LOT 번호</b></td>
|
||||||
|
<td class="mono">LOT-item-yyyy-0000</td>
|
||||||
|
<td class="mono">lot_master.lot_no</td>
|
||||||
|
<td class="mono">3411</td>
|
||||||
|
<td>품목별</td>
|
||||||
|
<td><span class="chip red">P0</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-title="수주번호 자동채번" data-code="SO-202605-0048" data-status="정상 운영">
|
||||||
|
<td><span class="chip green">운영</span></td>
|
||||||
|
<td><b>수주번호 자동채번</b></td>
|
||||||
|
<td class="mono">SO-yyyyMM-0000</td>
|
||||||
|
<td class="mono">sales_order.order_no</td>
|
||||||
|
<td class="mono">47</td>
|
||||||
|
<td>월별</td>
|
||||||
|
<td><span class="chip green">통과</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-title="견적번호" data-code="QT-2026-00129" data-status="정상 운영">
|
||||||
|
<td><span class="chip green">운영</span></td>
|
||||||
|
<td><b>견적번호</b></td>
|
||||||
|
<td class="mono">QT-yyyy-00000</td>
|
||||||
|
<td class="mono">quote.quote_no</td>
|
||||||
|
<td class="mono">128</td>
|
||||||
|
<td>연도별</td>
|
||||||
|
<td><span class="chip green">통과</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-title="출하 배차번호" data-code="DLV-20260520-001" data-status="연결 필요">
|
||||||
|
<td><span class="chip amber">검토</span></td>
|
||||||
|
<td><b>출하 배차번호</b></td>
|
||||||
|
<td class="mono">DLV-yyyyMMdd-000</td>
|
||||||
|
<td class="mono">delivery.trip_no</td>
|
||||||
|
<td class="mono">0</td>
|
||||||
|
<td>일별</td>
|
||||||
|
<td><span class="chip amber">미연결</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-title="클레임 접수번호" data-code="CLM-C07-018" data-status="회사 범위 검토">
|
||||||
|
<td><span class="chip amber">검토</span></td>
|
||||||
|
<td><b>클레임 접수번호</b></td>
|
||||||
|
<td class="mono">CLM-company-000</td>
|
||||||
|
<td class="mono">claim.claim_no</td>
|
||||||
|
<td class="mono">17</td>
|
||||||
|
<td>회사별</td>
|
||||||
|
<td><span class="chip amber">확인</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-title="작업지시번호" data-code="WO-20260520-0094" data-status="정상 운영">
|
||||||
|
<td><span class="chip green">운영</span></td>
|
||||||
|
<td><b>작업지시번호</b></td>
|
||||||
|
<td class="mono">WO-yyyyMMdd-0000</td>
|
||||||
|
<td class="mono">work_order.work_no</td>
|
||||||
|
<td class="mono">93</td>
|
||||||
|
<td>일별</td>
|
||||||
|
<td><span class="chip green">통과</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<aside class="card" data-od-id="row-detail">
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<h3 id="rowTitle">LOT 번호</h3>
|
||||||
|
<p id="rowStatus">충돌 조치 필요</p>
|
||||||
|
</div>
|
||||||
|
<span class="chip red">P0</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="preview" style="box-shadow:none;">
|
||||||
|
<div class="preview-top">
|
||||||
|
<div class="eyebrow">Next after fix</div>
|
||||||
|
<button class="btn">복사</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-code" id="rowCode" style="font-size:32px;">
|
||||||
|
<span class="pfx">LOT</span><span class="dim">-</span><span class="date">ITM</span><span class="dim">-</span><span class="date">2026</span><span class="dim">-</span><span class="seq">3414</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="height:12px"></div>
|
||||||
|
<div class="form-grid" style="grid-template-columns:96px 1fr;">
|
||||||
|
<label class="field-label">조치 방식</label>
|
||||||
|
<select class="select"><option>시작값 조정</option><option>패턴 변경</option></select>
|
||||||
|
<label class="field-label">승인 메모</label>
|
||||||
|
<textarea class="textarea">기존 예약 번호 이후부터 발급되도록 시작값을 조정합니다.</textarea>
|
||||||
|
<label class="field-label">적용 범위</label>
|
||||||
|
<select class="select"><option>선택 규칙만</option><option>같은 도메인 전체</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const tableRows = document.querySelectorAll("#rulesTable tbody tr");
|
||||||
|
const rowTitle = document.getElementById("rowTitle");
|
||||||
|
const rowStatus = document.getElementById("rowStatus");
|
||||||
|
const rowCode = document.getElementById("rowCode");
|
||||||
|
|
||||||
|
function renderCode(code) {
|
||||||
|
const parts = code.split("-");
|
||||||
|
rowCode.innerHTML = parts.map((part, index) => {
|
||||||
|
const cls = index === 0 ? "pfx" : index === parts.length - 1 ? "seq" : "date";
|
||||||
|
return `<span class="${cls}">${part}</span>${index < parts.length - 1 ? '<span class="dim">-</span>' : ""}`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
tableRows.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
tableRows.forEach((item) => item.classList.remove("on"));
|
||||||
|
row.classList.add("on");
|
||||||
|
rowTitle.textContent = row.dataset.title;
|
||||||
|
rowStatus.textContent = row.dataset.status;
|
||||||
|
renderCode(row.dataset.code);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("tableSearch").addEventListener("input", (event) => {
|
||||||
|
const term = event.target.value.trim().toLowerCase();
|
||||||
|
tableRows.forEach((row) => {
|
||||||
|
row.style.display = row.textContent.toLowerCase().includes(term) ? "table-row" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin 시안 갤러리</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: oklch(98% 0.004 250);
|
||||||
|
--surface: oklch(100% 0 0);
|
||||||
|
--surface-2: oklch(96% 0.006 250);
|
||||||
|
--fg: oklch(20% 0.018 250);
|
||||||
|
--muted: oklch(52% 0.014 250);
|
||||||
|
--border: oklch(90% 0.008 250);
|
||||||
|
--accent: oklch(56% 0.14 255);
|
||||||
|
--accent-2: oklch(58% 0.15 160);
|
||||||
|
--warn: oklch(68% 0.14 70);
|
||||||
|
--on-accent: oklch(100% 0 0);
|
||||||
|
--font-display: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "D2Coding", ui-monospace, Menlo, monospace;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
a { color: inherit; text-decoration: none; }
|
||||||
|
.shell { max-width: 1160px; margin: 0 auto; padding: 56px 28px; }
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(34px, 5vw, 58px);
|
||||||
|
line-height: 1.05;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
max-width: 820px;
|
||||||
|
}
|
||||||
|
.lead { margin: 18px 0 0; color: var(--muted); font-size: 18px; max-width: 720px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; margin-top: 44px; }
|
||||||
|
.card {
|
||||||
|
min-height: 320px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 22px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: transform .16s ease, border-color .16s ease;
|
||||||
|
}
|
||||||
|
.card:hover { transform: translateY(-2px); border-color: color-mix(in oklch, var(--accent) 44%, var(--border)); }
|
||||||
|
.mark {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: color-mix(in oklch, var(--accent) 12%, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.card:nth-child(2) .mark { background: color-mix(in oklch, var(--accent-2) 12%, transparent); color: var(--accent-2); }
|
||||||
|
.card:nth-child(3) .mark { background: color-mix(in oklch, var(--warn) 16%, transparent); color: var(--warn); }
|
||||||
|
h2 { margin: 24px 0 8px; font-size: 23px; letter-spacing: -0.01em; }
|
||||||
|
p { margin: 0; color: var(--muted); }
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.meta span { border: 1px solid var(--border); border-radius: 999px; padding: 4px 8px; background: var(--surface-2); }
|
||||||
|
.open {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--fg);
|
||||||
|
color: var(--surface);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
margin-top: 28px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: 18px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
@media (max-width: 880px) {
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
.shell { padding: 36px 18px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="shell">
|
||||||
|
<div class="eyebrow">Numbering Rule Admin - HTML drafts</div>
|
||||||
|
<h1>실제 운영 파일에 넣기 전, 채번 admin을 세 방향으로 비교합니다.</h1>
|
||||||
|
<p class="lead">각 시안은 독립 HTML입니다. A는 조립감, B는 관리 밀도, C는 충돌 검증을 우선순위로 둔 방향입니다.</p>
|
||||||
|
|
||||||
|
<section class="grid" aria-label="채번 admin 시안 목록">
|
||||||
|
<a class="card" href="variant-a-workbench.html">
|
||||||
|
<div>
|
||||||
|
<div class="mark">A</div>
|
||||||
|
<h2>조립 작업대형</h2>
|
||||||
|
<p>다음 발번 프리뷰와 파트 파이프라인을 화면의 중심에 둔 방향. 규칙을 직접 만드는 느낌이 가장 강합니다.</p>
|
||||||
|
<div class="meta"><span>preview first</span><span>pipeline</span><span>inspector</span></div>
|
||||||
|
</div>
|
||||||
|
<span class="open">시안 A 열기</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="variant-b-console.html">
|
||||||
|
<div>
|
||||||
|
<div class="mark">B</div>
|
||||||
|
<h2>운영 콘솔형</h2>
|
||||||
|
<p>규칙 목록, 사용처, 현재 시퀀스, 리셋 정책을 한 화면에서 훑는 방향. 실무 admin 안정감이 큽니다.</p>
|
||||||
|
<div class="meta"><span>dense table</span><span>right inspector</span><span>governance</span></div>
|
||||||
|
</div>
|
||||||
|
<span class="open">시안 B 열기</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="variant-c-validation.html">
|
||||||
|
<div>
|
||||||
|
<div class="mark">C</div>
|
||||||
|
<h2>검증 보드형</h2>
|
||||||
|
<p>채번 변경이 운영 데이터와 충돌하지 않는지 먼저 보는 방향. 승인/검수 플로우가 필요한 팀에 맞습니다.</p>
|
||||||
|
<div class="meta"><span>validation</span><span>impact</span><span>approval</span></div>
|
||||||
|
</div>
|
||||||
|
<span class="open">시안 C 열기</span>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="note">운영 React/TSX 파일은 이 갤러리에서 건드리지 않습니다. 실제 적용은 선택한 방향을 기준으로 별도 반영하면 됩니다.</p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin 시안 A - 조립 작업대형</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: oklch(98% 0.004 250);
|
||||||
|
--surface: oklch(100% 0 0);
|
||||||
|
--surface-2: oklch(96% 0.007 250);
|
||||||
|
--surface-3: oklch(93% 0.012 250);
|
||||||
|
--fg: oklch(19% 0.018 250);
|
||||||
|
--muted: oklch(52% 0.014 250);
|
||||||
|
--border: oklch(89% 0.008 250);
|
||||||
|
--accent: oklch(58% 0.17 255);
|
||||||
|
--success: oklch(58% 0.15 155);
|
||||||
|
--warn: oklch(68% 0.14 72);
|
||||||
|
--danger: oklch(58% 0.18 25);
|
||||||
|
--cyan: oklch(66% 0.14 215);
|
||||||
|
--on-accent: oklch(100% 0 0);
|
||||||
|
--font-display: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "D2Coding", ui-monospace, Menlo, monospace;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; height: 100vh; overflow: hidden; background: var(--bg); color: var(--fg); font-family: var(--font-body); font-size: 13px; }
|
||||||
|
button, input, select { font: inherit; color: inherit; }
|
||||||
|
button { cursor: pointer; }
|
||||||
|
.app { height: 100vh; display: grid; grid-template-rows: 58px 1fr; }
|
||||||
|
.top {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 22px;
|
||||||
|
background: var(--surface);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
h1, h2, h3 { font-family: var(--font-display); margin: 0; letter-spacing: -0.01em; }
|
||||||
|
h1 { display: flex; align-items: center; gap: 10px; font-size: 18px; }
|
||||||
|
.brand-mark { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; background: var(--accent); color: var(--on-accent); font-family: var(--font-mono); font-weight: 900; }
|
||||||
|
.sub { color: var(--muted); font-size: 12px; margin-left: 8px; font-weight: 500; }
|
||||||
|
.stats { display: flex; gap: 10px; font-family: var(--font-mono); color: var(--muted); }
|
||||||
|
.stats span { padding: 5px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-2); }
|
||||||
|
.stats b { color: var(--fg); }
|
||||||
|
.actions { display: flex; gap: 8px; align-items: center; }
|
||||||
|
.kbd, .chip { font-family: var(--font-mono); font-size: 11px; border: 1px solid var(--border); border-radius: 6px; padding: 3px 7px; color: var(--muted); background: var(--surface-2); }
|
||||||
|
.btn { height: 32px; border: 1px solid var(--border); background: var(--surface); border-radius: 8px; padding: 0 12px; font-weight: 700; font-size: 12px; }
|
||||||
|
.btn:hover { border-color: color-mix(in oklch, var(--accent) 45%, var(--border)); }
|
||||||
|
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||||
|
.body { min-height: 0; display: grid; grid-template-columns: 304px 1fr; }
|
||||||
|
.side { min-height: 0; display: grid; grid-template-rows: auto auto 1fr auto; background: var(--surface); border-right: 1px solid var(--border); }
|
||||||
|
.search { padding: 12px; border-bottom: 1px solid var(--border); }
|
||||||
|
.search input { width: 100%; height: 34px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface-2); padding: 0 12px; outline: none; }
|
||||||
|
.filters { display: flex; gap: 6px; padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||||||
|
.filter { border: 0; background: transparent; border-radius: 7px; padding: 6px 9px; color: var(--muted); font-weight: 700; font-size: 12px; }
|
||||||
|
.filter.on { background: color-mix(in oklch, var(--accent) 12%, transparent); color: var(--accent); }
|
||||||
|
.list { overflow: auto; }
|
||||||
|
.section-label { padding: 13px 14px 6px; color: var(--muted); font-family: var(--font-mono); font-size: 10px; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; display: flex; justify-content: space-between; }
|
||||||
|
.rule { width: 100%; border: 0; border-bottom: 1px solid var(--border); background: transparent; text-align: left; padding: 11px 14px; display: grid; grid-template-columns: 1fr auto; gap: 10px; }
|
||||||
|
.rule:hover { background: var(--surface-2); }
|
||||||
|
.rule.on { background: color-mix(in oklch, var(--accent) 10%, transparent); box-shadow: inset 3px 0 0 var(--accent); }
|
||||||
|
.rule-name { font-weight: 800; margin-bottom: 3px; }
|
||||||
|
.pattern { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
|
||||||
|
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); margin-top: 5px; }
|
||||||
|
.dot.off { background: var(--muted); }
|
||||||
|
.side-foot { padding: 12px; border-top: 1px solid var(--border); display: grid; gap: 8px; }
|
||||||
|
.main { min-width: 0; overflow: auto; background: var(--bg); }
|
||||||
|
.detail-head { position: sticky; top: 0; z-index: 5; display: grid; grid-template-columns: 1fr auto; gap: 18px; padding: 18px 24px; background: color-mix(in oklch, var(--bg) 92%, transparent); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); }
|
||||||
|
.title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.title-row h2 { font-size: 22px; }
|
||||||
|
.pill { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--border); border-radius: 999px; padding: 4px 8px; font-family: var(--font-mono); font-size: 10px; font-weight: 800; color: var(--muted); background: var(--surface); }
|
||||||
|
.pill.good { color: var(--success); background: color-mix(in oklch, var(--success) 10%, var(--surface)); }
|
||||||
|
.meta { margin-top: 7px; color: var(--muted); display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; }
|
||||||
|
.meta b { color: var(--fg); }
|
||||||
|
.preview-wrap { padding: 26px 24px; background-image: radial-gradient(color-mix(in oklch, var(--accent) 16%, transparent) 1px, transparent 1px); background-size: 16px 16px; border-bottom: 1px solid var(--border); }
|
||||||
|
.ticket { position: relative; max-width: 1120px; min-height: 214px; margin: 0 auto; background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 30px; box-shadow: 0 24px 70px -48px var(--accent); overflow: hidden; }
|
||||||
|
.ticket::before, .ticket::after { content: ""; position: absolute; top: 86px; width: 30px; height: 30px; border-radius: 50%; background: var(--bg); border: 1px solid var(--border); }
|
||||||
|
.ticket::before { left: -15px; } .ticket::after { right: -15px; }
|
||||||
|
.ticket-top { display: flex; align-items: center; justify-content: space-between; gap: 18px; color: var(--muted); font-family: var(--font-mono); font-size: 11px; }
|
||||||
|
.code { margin: 28px 0 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: clamp(30px, 5vw, 58px); font-weight: 900; letter-spacing: -0.04em; }
|
||||||
|
.part { border: 1px solid transparent; border-radius: 10px; padding: 2px 7px; background: var(--surface-2); color: var(--fg); }
|
||||||
|
.part.date { color: var(--cyan); } .part.seq { color: var(--accent); } .part.sel { outline: 2px solid color-mix(in oklch, var(--accent) 34%, transparent); background: color-mix(in oklch, var(--accent) 9%, var(--surface)); }
|
||||||
|
.dash { color: var(--muted); }
|
||||||
|
.ticket-foot { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 22px; border-top: 1px dashed var(--border); padding-top: 16px; }
|
||||||
|
.ticket-foot span { color: var(--muted); font-size: 11px; }
|
||||||
|
.ticket-foot b { display: block; color: var(--fg); font-family: var(--font-mono); font-size: 12px; margin-top: 2px; }
|
||||||
|
.workspace { padding: 20px 24px 82px; max-width: 1180px; margin: 0 auto; display: grid; gap: 16px; }
|
||||||
|
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; }
|
||||||
|
.panel-hd { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 15px 16px; border-bottom: 1px solid var(--border); }
|
||||||
|
.panel-hd h3 { font-size: 15px; }
|
||||||
|
.panel-hd .desc { color: var(--muted); font-size: 12px; margin-left: 8px; font-weight: 500; }
|
||||||
|
.pipe { padding: 18px; display: flex; flex-wrap: wrap; gap: 9px; align-items: center; }
|
||||||
|
.block { min-width: 110px; border: 1px solid var(--border); border-radius: 12px; padding: 12px; background: var(--surface-2); }
|
||||||
|
.block.sel { border-color: var(--accent); background: color-mix(in oklch, var(--accent) 9%, var(--surface)); }
|
||||||
|
.block .t { font-family: var(--font-mono); color: var(--muted); font-size: 10px; text-transform: uppercase; font-weight: 900; }
|
||||||
|
.block .v { margin-top: 6px; font-family: var(--font-mono); font-weight: 900; font-size: 16px; }
|
||||||
|
.join { color: var(--muted); font-family: var(--font-mono); font-weight: 900; }
|
||||||
|
.drop { border: 1px dashed color-mix(in oklch, var(--accent) 45%, var(--border)); background: transparent; color: var(--accent); border-radius: 12px; padding: 12px 16px; font-weight: 800; }
|
||||||
|
.grid { display: grid; grid-template-columns: 1.25fr .75fr; gap: 16px; }
|
||||||
|
.inspector { padding: 16px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||||
|
label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; font-weight: 800; }
|
||||||
|
input, select { height: 34px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2); padding: 0 10px; outline: none; }
|
||||||
|
input:focus, select:focus { border-color: var(--accent); background: var(--surface); }
|
||||||
|
.seq { padding: 16px; display: grid; gap: 13px; }
|
||||||
|
.seq-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||||
|
.seq-card { border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); padding: 12px; }
|
||||||
|
.seq-card span { color: var(--muted); font-family: var(--font-mono); font-size: 10px; font-weight: 900; }
|
||||||
|
.seq-card b { display: block; margin-top: 6px; font-family: var(--font-mono); font-size: 24px; }
|
||||||
|
.savebar { position: sticky; bottom: 0; display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; background: var(--surface); border-top: 1px solid var(--border); box-shadow: 0 -14px 30px -28px var(--fg); }
|
||||||
|
.unsaved { color: var(--warn); font-weight: 800; }
|
||||||
|
@media (max-width: 900px) { body { overflow: auto; height: auto; } .app { height: auto; } .body { grid-template-columns: 1fr; } .side { display: none; } .grid, .ticket-foot, .inspector { grid-template-columns: 1fr; } .top { grid-template-columns: 1fr; height: auto; padding: 14px; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="top">
|
||||||
|
<h1><span class="brand-mark">#</span>채번 관리 <span class="sub">규칙을 조립하고 다음 코드를 확인</span></h1>
|
||||||
|
<div class="stats"><span><b>18</b> 규칙</span><span><b>14</b> 연결</span><span><b>4</b> 미사용</span></div>
|
||||||
|
<div class="actions"><span class="kbd">Ctrl K</span><button class="btn">새로고침</button><button class="btn primary">새 채번</button></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<aside class="side">
|
||||||
|
<div class="search"><input id="ruleSearch" placeholder="채번 이름, 코드, 컬럼명 검색" /></div>
|
||||||
|
<div class="filters"><button class="filter on">전체</button><button class="filter">연결</button><button class="filter">미사용</button></div>
|
||||||
|
<div class="list" id="ruleList">
|
||||||
|
<div class="section-label"><span>LIVE</span><span>최근 변경</span></div>
|
||||||
|
<button class="rule on" data-name="수주번호 자동채번"><span><span class="rule-name">수주번호 자동채번</span><span class="pattern">SO-{yyyyMM}-{0000}</span></span><span class="dot"></span></button>
|
||||||
|
<button class="rule" data-name="구매발주 번호"><span><span class="rule-name">구매발주 번호</span><span class="pattern">PO-{yyyy}-{00000}</span></span><span class="dot"></span></button>
|
||||||
|
<button class="rule" data-name="입고검사 LOT"><span><span class="rule-name">입고검사 LOT</span><span class="pattern">IQC-{date}-{seq}</span></span><span class="dot"></span></button>
|
||||||
|
<div class="section-label"><span>UNUSED</span><span>연결 필요</span></div>
|
||||||
|
<button class="rule" data-name="품목마스터 코드"><span><span class="rule-name">품목마스터 코드</span><span class="pattern">ITEM-{category}-{000}</span></span><span class="dot off"></span></button>
|
||||||
|
<button class="rule" data-name="작업지시 번호"><span><span class="rule-name">작업지시 번호</span><span class="pattern">WO-{yyyyMMdd}-{000}</span></span><span class="dot off"></span></button>
|
||||||
|
</div>
|
||||||
|
<div class="side-foot"><button class="btn primary">+ 새 채번 만들기</button></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<section class="detail-head">
|
||||||
|
<div>
|
||||||
|
<div class="title-row"><h2>수주번호 자동채번</h2><span class="pill good">사용 중</span><span class="pill">sales_order.order_no</span></div>
|
||||||
|
<div class="meta"><span>테이블 <b>sales_order</b></span><span>컬럼 <b>order_no</b></span><span>리셋 <b>월별</b></span><span>구분자 <b>-</b></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="actions"><button class="btn">되돌리기</button><button class="btn primary">저장</button></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="preview-wrap">
|
||||||
|
<article class="ticket">
|
||||||
|
<div class="ticket-top"><span>NEXT NUMBER PREVIEW</span><button class="btn" id="copyBtn">복사</button></div>
|
||||||
|
<div class="code" id="codePreview"><span class="part">SO</span><span class="dash">-</span><span class="part date">202605</span><span class="dash">-</span><span class="part seq sel">0048</span></div>
|
||||||
|
<div class="ticket-foot">
|
||||||
|
<span>현재 순번<b>47</b></span>
|
||||||
|
<span>다음 순번<b>48</b></span>
|
||||||
|
<span>초기화 기준<b>매월 1일</b></span>
|
||||||
|
<span>마지막 발번<b>2026-05-20 09:42</b></span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="workspace">
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-hd"><h3>파트 조립 <span class="desc">클릭하면 아래 속성이 바뀝니다</span></h3><button class="btn">순서 정리</button></div>
|
||||||
|
<div class="pipe" id="pipe">
|
||||||
|
<button class="block"><span class="t">text</span><span class="v">SO</span></button><span class="join">-</span>
|
||||||
|
<button class="block"><span class="t">date</span><span class="v">yyyyMM</span></button><span class="join">-</span>
|
||||||
|
<button class="block sel"><span class="t">sequence</span><span class="v">0000</span></button>
|
||||||
|
<button class="drop">+ 파트 추가</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-hd"><h3>선택 파트 속성</h3><span class="chip">sequence</span></div>
|
||||||
|
<div class="inspector">
|
||||||
|
<label>표시명<input value="순번" /></label>
|
||||||
|
<label>자리수<select><option>4자리</option><option>5자리</option></select></label>
|
||||||
|
<label>초기값<input value="1" /></label>
|
||||||
|
<label>리셋 주기<select><option>월별</option><option>일별</option><option>없음</option></select></label>
|
||||||
|
<label>패딩 문자<input value="0" /></label>
|
||||||
|
<label>샘플 출력<input value="0048" /></label>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="panel">
|
||||||
|
<div class="panel-hd"><h3>시퀀스 운영</h3><button class="btn">재시작</button></div>
|
||||||
|
<div class="seq">
|
||||||
|
<div class="seq-row"><div class="seq-card"><span>CURRENT</span><b>47</b></div><div class="seq-card"><span>NEXT</span><b>48</b></div></div>
|
||||||
|
<label>다음 발번 수동 조정<input value="48" /></label>
|
||||||
|
<button class="btn primary">시퀀스 적용</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="savebar"><span class="unsaved">저장하지 않은 변경 있음</span><div class="actions"><button class="btn">변경 취소</button><button class="btn primary">규칙 저장</button></div></div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const search = document.querySelector("#ruleSearch");
|
||||||
|
const rules = [...document.querySelectorAll(".rule")];
|
||||||
|
search.addEventListener("input", () => {
|
||||||
|
const term = search.value.trim().toLowerCase();
|
||||||
|
rules.forEach((rule) => {
|
||||||
|
rule.style.display = rule.dataset.name.toLowerCase().includes(term) ? "grid" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
rules.forEach((rule) => {
|
||||||
|
rule.addEventListener("click", () => {
|
||||||
|
rules.forEach((item) => item.classList.remove("on"));
|
||||||
|
rule.classList.add("on");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".block").forEach((block) => {
|
||||||
|
block.addEventListener("click", () => {
|
||||||
|
document.querySelectorAll(".block, .part").forEach((item) => item.classList.remove("sel"));
|
||||||
|
block.classList.add("sel");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelector("#copyBtn").addEventListener("click", async () => {
|
||||||
|
const code = "SO-202605-0048";
|
||||||
|
try { await navigator.clipboard.writeText(code); } catch (error) {}
|
||||||
|
document.querySelector("#copyBtn").textContent = "복사됨";
|
||||||
|
setTimeout(() => { document.querySelector("#copyBtn").textContent = "복사"; }, 1200);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin 시안 B - 운영 콘솔형</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: oklch(97% 0.004 235);
|
||||||
|
--surface: oklch(100% 0 0);
|
||||||
|
--surface-2: oklch(95% 0.006 235);
|
||||||
|
--fg: oklch(18% 0.014 235);
|
||||||
|
--muted: oklch(50% 0.012 235);
|
||||||
|
--border: oklch(88% 0.008 235);
|
||||||
|
--accent: oklch(55% 0.13 158);
|
||||||
|
--info: oklch(58% 0.14 225);
|
||||||
|
--warn: oklch(67% 0.14 76);
|
||||||
|
--danger: oklch(58% 0.18 28);
|
||||||
|
--on-accent: oklch(100% 0 0);
|
||||||
|
--font-display: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "D2Coding", ui-monospace, Menlo, monospace;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; height: 100vh; overflow: hidden; background: var(--bg); color: var(--fg); font-family: var(--font-body); font-size: 13px; }
|
||||||
|
button, input, select { font: inherit; color: inherit; }
|
||||||
|
button { cursor: pointer; }
|
||||||
|
h1, h2, h3 { margin: 0; font-family: var(--font-display); letter-spacing: -0.01em; }
|
||||||
|
.app { height: 100vh; display: grid; grid-template-rows: 64px 84px 1fr; }
|
||||||
|
.top { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 0 22px; background: var(--surface); border-bottom: 1px solid var(--border); }
|
||||||
|
.title { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.title h1 { font-size: 19px; }
|
||||||
|
.title p { margin: 2px 0 0; color: var(--muted); font-size: 12px; }
|
||||||
|
.mark { width: 32px; height: 32px; border-radius: 10px; display: grid; place-items: center; background: var(--fg); color: var(--surface); font-family: var(--font-mono); font-weight: 900; }
|
||||||
|
.toolbar { display: flex; gap: 8px; align-items: center; }
|
||||||
|
.btn { height: 32px; border: 1px solid var(--border); background: var(--surface); border-radius: 8px; padding: 0 12px; font-size: 12px; font-weight: 800; }
|
||||||
|
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||||
|
.btn:hover { border-color: color-mix(in oklch, var(--accent) 42%, var(--border)); }
|
||||||
|
.summary { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px; background: var(--border); border-bottom: 1px solid var(--border); }
|
||||||
|
.metric { background: var(--surface); padding: 14px 18px; display: grid; gap: 5px; }
|
||||||
|
.metric span { color: var(--muted); font-family: var(--font-mono); font-size: 10px; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; }
|
||||||
|
.metric b { font-family: var(--font-mono); font-size: 22px; letter-spacing: -0.03em; }
|
||||||
|
.metric.ok b { color: var(--accent); } .metric.warn b { color: var(--warn); }
|
||||||
|
.body { min-height: 0; display: grid; grid-template-columns: 1fr 380px; }
|
||||||
|
.table-pane { min-width: 0; display: grid; grid-template-rows: auto 1fr; border-right: 1px solid var(--border); background: var(--surface); }
|
||||||
|
.table-tools { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
||||||
|
.search { display: flex; gap: 8px; align-items: center; min-width: 0; flex: 1; }
|
||||||
|
.search input { width: 340px; height: 34px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2); padding: 0 11px; outline: none; }
|
||||||
|
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 9px; overflow: hidden; }
|
||||||
|
.seg button { border: 0; border-right: 1px solid var(--border); background: var(--surface); height: 32px; padding: 0 10px; color: var(--muted); font-weight: 800; font-size: 12px; }
|
||||||
|
.seg button:last-child { border-right: 0; }
|
||||||
|
.seg button.on { background: color-mix(in oklch, var(--accent) 12%, var(--surface)); color: var(--accent); }
|
||||||
|
.table-wrap { overflow: auto; }
|
||||||
|
table { width: 100%; border-collapse: collapse; min-width: 900px; }
|
||||||
|
th, td { border-bottom: 1px solid var(--border); padding: 11px 12px; text-align: left; }
|
||||||
|
th { position: sticky; top: 0; background: var(--surface); z-index: 2; color: var(--muted); font-family: var(--font-mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; }
|
||||||
|
tr { cursor: pointer; }
|
||||||
|
tbody tr:hover, tbody tr.on { background: color-mix(in oklch, var(--accent) 9%, var(--surface)); }
|
||||||
|
.name { font-weight: 850; }
|
||||||
|
.code, .mono { font-family: var(--font-mono); font-size: 12px; }
|
||||||
|
.code { font-weight: 900; }
|
||||||
|
.muted { color: var(--muted); }
|
||||||
|
.status { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 10px; font-weight: 900; border-radius: 999px; padding: 4px 8px; border: 1px solid var(--border); color: var(--muted); }
|
||||||
|
.status.on { color: var(--accent); background: color-mix(in oklch, var(--accent) 10%, var(--surface)); border-color: color-mix(in oklch, var(--accent) 30%, var(--border)); }
|
||||||
|
.status.warn { color: var(--warn); background: color-mix(in oklch, var(--warn) 12%, var(--surface)); border-color: color-mix(in oklch, var(--warn) 36%, var(--border)); }
|
||||||
|
.inspector { overflow: auto; background: var(--surface); display: grid; grid-template-rows: auto auto auto 1fr; }
|
||||||
|
.ins-head { padding: 18px; border-bottom: 1px solid var(--border); }
|
||||||
|
.ins-head h2 { font-size: 21px; }
|
||||||
|
.ins-head p { margin: 7px 0 0; color: var(--muted); }
|
||||||
|
.next-card { margin: 16px 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--fg); color: var(--surface); padding: 18px; }
|
||||||
|
.next-card span { color: color-mix(in oklch, var(--surface) 62%, transparent); font-family: var(--font-mono); font-size: 10px; font-weight: 900; letter-spacing: .08em; }
|
||||||
|
.next-card b { display: block; margin-top: 10px; font-family: var(--font-mono); font-size: 28px; letter-spacing: -0.04em; }
|
||||||
|
.tabs { display: flex; gap: 6px; padding: 0 18px 14px; border-bottom: 1px solid var(--border); }
|
||||||
|
.tabs button { border: 1px solid var(--border); border-radius: 999px; background: var(--surface); padding: 6px 10px; color: var(--muted); font-weight: 800; font-size: 12px; }
|
||||||
|
.tabs button.on { background: color-mix(in oklch, var(--accent) 12%, var(--surface)); color: var(--accent); }
|
||||||
|
.form { padding: 16px 18px; display: grid; gap: 12px; }
|
||||||
|
.field { display: grid; gap: 6px; color: var(--muted); font-size: 11px; font-weight: 850; }
|
||||||
|
.field input, .field select { height: 34px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2); padding: 0 10px; outline: none; }
|
||||||
|
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||||
|
.usage { border-top: 1px solid var(--border); padding: 16px 18px; display: grid; gap: 10px; }
|
||||||
|
.usage-row { display: grid; grid-template-columns: 1fr auto; gap: 10px; border: 1px solid var(--border); border-radius: 10px; padding: 10px; background: var(--surface-2); }
|
||||||
|
.usage-row b { font-family: var(--font-mono); }
|
||||||
|
.usage-row span { color: var(--muted); font-size: 11px; }
|
||||||
|
@media (max-width: 980px) { body { height: auto; overflow: auto; } .app { height: auto; grid-template-rows: auto; } .top, .table-tools { align-items: stretch; flex-direction: column; padding: 14px; } .summary { grid-template-columns: repeat(2, 1fr); } .body { grid-template-columns: 1fr; } .inspector { border-top: 1px solid var(--border); } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="top">
|
||||||
|
<div class="title"><div class="mark">NR</div><div><h1>채번 운영 콘솔</h1><p>규칙, 연결 컬럼, 현재 시퀀스, 리셋 정책을 한 화면에서 관리</p></div></div>
|
||||||
|
<div class="toolbar"><button class="btn">CSV 내보내기</button><button class="btn">새로고침</button><button class="btn primary">새 규칙</button></div>
|
||||||
|
</header>
|
||||||
|
<section class="summary" aria-label="채번 요약">
|
||||||
|
<div class="metric"><span>Total rules</span><b>18</b></div>
|
||||||
|
<div class="metric ok"><span>Linked</span><b>14</b></div>
|
||||||
|
<div class="metric warn"><span>Need review</span><b>2</b></div>
|
||||||
|
<div class="metric"><span>Monthly reset</span><b>9</b></div>
|
||||||
|
<div class="metric"><span>Generated today</span><b>126</b></div>
|
||||||
|
</section>
|
||||||
|
<main class="body">
|
||||||
|
<section class="table-pane">
|
||||||
|
<div class="table-tools">
|
||||||
|
<div class="search"><input id="search" placeholder="규칙명, 테이블, 컬럼, 패턴 검색" /><button class="btn">필터</button></div>
|
||||||
|
<div class="seg"><button class="on" data-filter="all">전체</button><button data-filter="linked">연결</button><button data-filter="review">검토</button><button data-filter="unused">미사용</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Rule</th><th>Pattern</th><th>Target</th><th>Status</th><th>Reset</th><th>Current</th><th>Next</th><th>Updated</th></tr></thead>
|
||||||
|
<tbody id="rows">
|
||||||
|
<tr class="on" data-filter="linked" data-title="수주번호 자동채번" data-next="SO-202605-0048"><td><div class="name">수주번호 자동채번</div><div class="muted">영업 수주 등록</div></td><td class="code">SO-{yyyyMM}-{0000}</td><td class="mono">sales_order.order_no</td><td><span class="status on">ACTIVE</span></td><td>월별</td><td class="mono">47</td><td class="mono">48</td><td class="mono">05-20</td></tr>
|
||||||
|
<tr data-filter="linked" data-title="구매발주 번호" data-next="PO-2026-00132"><td><div class="name">구매발주 번호</div><div class="muted">구매 발주서</div></td><td class="code">PO-{yyyy}-{00000}</td><td class="mono">purchase_order.po_no</td><td><span class="status on">ACTIVE</span></td><td>연도별</td><td class="mono">131</td><td class="mono">132</td><td class="mono">05-18</td></tr>
|
||||||
|
<tr data-filter="review" data-title="품목마스터 코드" data-next="ITEM-RM-008"><td><div class="name">품목마스터 코드</div><div class="muted">분류 접두어 확인 필요</div></td><td class="code">ITEM-{category}-{000}</td><td class="mono">item.item_code</td><td><span class="status warn">REVIEW</span></td><td>없음</td><td class="mono">7</td><td class="mono">8</td><td class="mono">05-16</td></tr>
|
||||||
|
<tr data-filter="unused" data-title="작업지시 번호" data-next="WO-20260520-012"><td><div class="name">작업지시 번호</div><div class="muted">아직 연결된 컬럼 없음</div></td><td class="code">WO-{yyyyMMdd}-{000}</td><td class="mono muted">-</td><td><span class="status">UNUSED</span></td><td>일별</td><td class="mono">11</td><td class="mono">12</td><td class="mono">05-12</td></tr>
|
||||||
|
<tr data-filter="linked" data-title="입고검사 LOT" data-next="IQC-20260520-0006"><td><div class="name">입고검사 LOT</div><div class="muted">품질 입고 검사</div></td><td class="code">IQC-{date}-{seq}</td><td class="mono">incoming_inspection.lot_no</td><td><span class="status on">ACTIVE</span></td><td>일별</td><td class="mono">5</td><td class="mono">6</td><td class="mono">05-20</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<aside class="inspector">
|
||||||
|
<div class="ins-head"><h2 id="insTitle">수주번호 자동채번</h2><p>선택한 채번 규칙의 운영 속성과 사용처입니다.</p></div>
|
||||||
|
<div class="next-card"><span>NEXT GENERATED CODE</span><b id="nextCode">SO-202605-0048</b></div>
|
||||||
|
<div class="tabs"><button class="on">기본</button><button>파트</button><button>이력</button></div>
|
||||||
|
<div class="form">
|
||||||
|
<label class="field">규칙명<input value="수주번호 자동채번" /></label>
|
||||||
|
<div class="split"><label class="field">구분자<input value="-" /></label><label class="field">리셋<select><option>월별</option><option>일별</option><option>없음</option></select></label></div>
|
||||||
|
<label class="field">패턴<input value="SO-{yyyyMM}-{0000}" /></label>
|
||||||
|
<div class="split"><label class="field">현재 순번<input value="47" /></label><label class="field">다음 순번<input value="48" /></label></div>
|
||||||
|
<button class="btn primary">변경 저장</button>
|
||||||
|
</div>
|
||||||
|
<div class="usage">
|
||||||
|
<div class="usage-row"><div><b>sales_order.order_no</b><br><span>수주 등록, 상세, 검색 필터</span></div><span class="mono">84/day</span></div>
|
||||||
|
<div class="usage-row"><div><b>sales_order_history.ref_no</b><br><span>변경 이력 참조번호</span></div><span class="mono">12/day</span></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const rows = [...document.querySelectorAll("#rows tr")];
|
||||||
|
const search = document.querySelector("#search");
|
||||||
|
const filterButtons = [...document.querySelectorAll(".seg button")];
|
||||||
|
let activeFilter = "all";
|
||||||
|
function applyFilter() {
|
||||||
|
const term = search.value.trim().toLowerCase();
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const matchesFilter = activeFilter === "all" || row.dataset.filter === activeFilter;
|
||||||
|
const matchesSearch = row.textContent.toLowerCase().includes(term);
|
||||||
|
row.style.display = matchesFilter && matchesSearch ? "table-row" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rows.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
rows.forEach((item) => item.classList.remove("on"));
|
||||||
|
row.classList.add("on");
|
||||||
|
document.querySelector("#insTitle").textContent = row.dataset.title;
|
||||||
|
document.querySelector("#nextCode").textContent = row.dataset.next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
filterButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
filterButtons.forEach((item) => item.classList.remove("on"));
|
||||||
|
button.classList.add("on");
|
||||||
|
activeFilter = button.dataset.filter;
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
search.addEventListener("input", applyFilter);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>채번 Admin 시안 C - 검증 보드형</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: oklch(97% 0.006 248);
|
||||||
|
--surface: oklch(100% 0 0);
|
||||||
|
--surface-2: oklch(95% 0.008 248);
|
||||||
|
--surface-3: oklch(91% 0.012 248);
|
||||||
|
--fg: oklch(17% 0.018 248);
|
||||||
|
--muted: oklch(50% 0.014 248);
|
||||||
|
--border: oklch(88% 0.01 248);
|
||||||
|
--accent: oklch(58% 0.16 226);
|
||||||
|
--success: oklch(57% 0.15 155);
|
||||||
|
--warn: oklch(68% 0.15 72);
|
||||||
|
--danger: oklch(58% 0.18 28);
|
||||||
|
--on-accent: oklch(100% 0 0);
|
||||||
|
--font-display: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "D2Coding", ui-monospace, Menlo, monospace;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--fg); font-family: var(--font-body); font-size: 13px; }
|
||||||
|
button, input { font: inherit; color: inherit; }
|
||||||
|
button { cursor: pointer; }
|
||||||
|
h1, h2, h3 { margin: 0; font-family: var(--font-display); letter-spacing: -0.01em; }
|
||||||
|
.app { min-height: 100vh; display: grid; grid-template-rows: auto 1fr; }
|
||||||
|
.top { padding: 22px 26px 18px; background: var(--surface); border-bottom: 1px solid var(--border); display: grid; gap: 18px; }
|
||||||
|
.topline { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; }
|
||||||
|
.title { display: flex; align-items: flex-start; gap: 13px; }
|
||||||
|
.mark { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--accent); color: var(--on-accent); font-family: var(--font-mono); font-weight: 900; }
|
||||||
|
h1 { font-size: 22px; }
|
||||||
|
.title p { margin: 5px 0 0; color: var(--muted); }
|
||||||
|
.actions { display: flex; gap: 8px; align-items: center; }
|
||||||
|
.btn { height: 34px; border: 1px solid var(--border); background: var(--surface); border-radius: 8px; padding: 0 12px; font-size: 12px; font-weight: 850; }
|
||||||
|
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
||||||
|
.btn.good { background: var(--success); border-color: var(--success); color: var(--on-accent); }
|
||||||
|
.btn:hover { border-color: color-mix(in oklch, var(--accent) 45%, var(--border)); }
|
||||||
|
.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||||||
|
.step { border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); padding: 12px; display: grid; gap: 5px; }
|
||||||
|
.step span { font-family: var(--font-mono); font-size: 10px; color: var(--muted); font-weight: 900; letter-spacing: .08em; text-transform: uppercase; }
|
||||||
|
.step b { font-family: var(--font-mono); font-size: 22px; }
|
||||||
|
.step.warn b { color: var(--warn); } .step.danger b { color: var(--danger); } .step.good b { color: var(--success); }
|
||||||
|
.body { min-height: 0; display: grid; grid-template-columns: 1fr 408px; gap: 16px; padding: 16px; }
|
||||||
|
.board { min-width: 0; display: grid; grid-template-rows: auto 1fr; gap: 12px; }
|
||||||
|
.board-tools { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
|
||||||
|
.search { width: 340px; height: 34px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); padding: 0 12px; outline: none; }
|
||||||
|
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 9px; overflow: hidden; background: var(--surface); }
|
||||||
|
.seg button { height: 32px; border: 0; border-right: 1px solid var(--border); background: transparent; padding: 0 11px; color: var(--muted); font-weight: 850; font-size: 12px; }
|
||||||
|
.seg button:last-child { border-right: 0; }
|
||||||
|
.seg button.on { background: color-mix(in oklch, var(--accent) 11%, var(--surface)); color: var(--accent); }
|
||||||
|
.columns { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; min-height: 0; }
|
||||||
|
.col { min-height: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; display: grid; grid-template-rows: auto 1fr; }
|
||||||
|
.col-head { padding: 13px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.col-head h2 { font-size: 15px; }
|
||||||
|
.count { font-family: var(--font-mono); font-size: 11px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 3px 7px; }
|
||||||
|
.cards { overflow: auto; padding: 10px; display: grid; gap: 10px; align-content: start; }
|
||||||
|
.card { border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2); padding: 12px; display: grid; gap: 10px; }
|
||||||
|
.card:hover, .card.on { border-color: var(--accent); background: color-mix(in oklch, var(--accent) 8%, var(--surface)); }
|
||||||
|
.card-top { display: flex; justify-content: space-between; gap: 12px; }
|
||||||
|
.card h3 { font-size: 14px; }
|
||||||
|
.tag { font-family: var(--font-mono); font-size: 10px; font-weight: 900; border-radius: 999px; padding: 3px 7px; background: var(--surface); border: 1px solid var(--border); color: var(--muted); }
|
||||||
|
.tag.danger { color: var(--danger); background: color-mix(in oklch, var(--danger) 11%, var(--surface)); }
|
||||||
|
.tag.warn { color: var(--warn); background: color-mix(in oklch, var(--warn) 13%, var(--surface)); }
|
||||||
|
.tag.good { color: var(--success); background: color-mix(in oklch, var(--success) 11%, var(--surface)); }
|
||||||
|
.pattern { font-family: var(--font-mono); font-size: 12px; font-weight: 900; padding: 8px; border-radius: 8px; background: var(--surface); border: 1px solid var(--border); }
|
||||||
|
.desc { color: var(--muted); font-size: 12px; }
|
||||||
|
.impact { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
|
.impact span { font-family: var(--font-mono); font-size: 10px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 3px 6px; background: var(--surface); }
|
||||||
|
.detail { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; min-height: 0; overflow: auto; }
|
||||||
|
.detail-head { padding: 18px; border-bottom: 1px solid var(--border); display: grid; gap: 8px; }
|
||||||
|
.detail-head h2 { font-size: 21px; }
|
||||||
|
.detail-head p { margin: 0; color: var(--muted); }
|
||||||
|
.preview { margin: 16px 18px; padding: 18px; border-radius: 14px; background: var(--fg); color: var(--surface); }
|
||||||
|
.preview span { color: color-mix(in oklch, var(--surface) 62%, transparent); font-family: var(--font-mono); font-size: 10px; font-weight: 900; letter-spacing: .08em; }
|
||||||
|
.preview b { display: block; margin-top: 10px; font-family: var(--font-mono); font-size: 26px; letter-spacing: -0.04em; }
|
||||||
|
.section { padding: 16px 18px; border-top: 1px solid var(--border); display: grid; gap: 10px; }
|
||||||
|
.section:first-of-type { border-top: 0; }
|
||||||
|
.section h3 { font-size: 14px; }
|
||||||
|
.check { display: grid; gap: 8px; }
|
||||||
|
.check-row { display: grid; grid-template-columns: 18px 1fr auto; align-items: center; gap: 8px; border: 1px solid var(--border); border-radius: 10px; padding: 10px; background: var(--surface-2); }
|
||||||
|
.check-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--success); }
|
||||||
|
.check-dot.warn { background: var(--warn); }
|
||||||
|
.check-dot.danger { background: var(--danger); }
|
||||||
|
.mono { font-family: var(--font-mono); }
|
||||||
|
.timeline { display: grid; gap: 0; }
|
||||||
|
.event { display: grid; grid-template-columns: 72px 1fr; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.event:last-child { border-bottom: 0; }
|
||||||
|
.event time { color: var(--muted); font-family: var(--font-mono); font-size: 11px; }
|
||||||
|
.event b { display: block; margin-bottom: 2px; }
|
||||||
|
.detail-actions { position: sticky; bottom: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 12px 18px; background: var(--surface); border-top: 1px solid var(--border); }
|
||||||
|
@media (max-width: 1040px) { .body { grid-template-columns: 1fr; } .columns { grid-template-columns: 1fr; } .steps { grid-template-columns: repeat(2, 1fr); } .topline, .board-tools { flex-direction: column; align-items: stretch; } .search { width: 100%; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="top">
|
||||||
|
<div class="topline">
|
||||||
|
<div class="title"><div class="mark">QA</div><div><h1>채번 변경 검증 보드</h1><p>규칙 저장 전에 중복 가능성, 연결 컬럼, 시퀀스 리셋 영향을 확인합니다.</p></div></div>
|
||||||
|
<div class="actions"><button class="btn">검증 다시 실행</button><button class="btn primary">변경 요청 생성</button></div>
|
||||||
|
</div>
|
||||||
|
<section class="steps" aria-label="검증 요약">
|
||||||
|
<div class="step danger"><span>Collision risk</span><b>2</b></div>
|
||||||
|
<div class="step warn"><span>Need mapping</span><b>3</b></div>
|
||||||
|
<div class="step"><span>Ready to save</span><b>9</b></div>
|
||||||
|
<div class="step good"><span>Approved</span><b>4</b></div>
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
<main class="body">
|
||||||
|
<section class="board">
|
||||||
|
<div class="board-tools">
|
||||||
|
<input id="search" class="search" placeholder="규칙명, 패턴, 테이블 검색" />
|
||||||
|
<div class="seg"><button class="on" data-filter="all">전체</button><button data-filter="collision">충돌</button><button data-filter="review">검토</button><button data-filter="approved">승인</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="columns" id="columns">
|
||||||
|
<div class="col">
|
||||||
|
<div class="col-head"><h2>충돌 위험</h2><span class="count">2</span></div>
|
||||||
|
<div class="cards">
|
||||||
|
<article class="card on" data-status="collision" data-title="품목마스터 코드" data-code="ITEM-RM-008"><div class="card-top"><h3>품목마스터 코드</h3><span class="tag danger">collision</span></div><div class="pattern">ITEM-{category}-{000}</div><p class="desc">기존 수동 코드와 신규 자동 순번 범위가 겹칠 수 있습니다.</p><div class="impact"><span>item.item_code</span><span>현재 7</span><span>다음 8</span></div></article>
|
||||||
|
<article class="card" data-status="collision" data-title="거래처 코드" data-code="CUST-0241"><div class="card-top"><h3>거래처 코드</h3><span class="tag danger">collision</span></div><div class="pattern">CUST-{0000}</div><p class="desc">과거 마이그레이션 데이터의 접두어가 동일합니다.</p><div class="impact"><span>customer.customer_code</span><span>legacy</span></div></article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="col-head"><h2>검토 필요</h2><span class="count">3</span></div>
|
||||||
|
<div class="cards">
|
||||||
|
<article class="card" data-status="review" data-title="작업지시 번호" data-code="WO-20260520-012"><div class="card-top"><h3>작업지시 번호</h3><span class="tag warn">mapping</span></div><div class="pattern">WO-{yyyyMMdd}-{000}</div><p class="desc">규칙은 정상이나 아직 연결된 컬럼이 없습니다.</p><div class="impact"><span>unlinked</span><span>daily reset</span></div></article>
|
||||||
|
<article class="card" data-status="review" data-title="입고검사 LOT" data-code="IQC-20260520-0006"><div class="card-top"><h3>입고검사 LOT</h3><span class="tag warn">reset</span></div><div class="pattern">IQC-{date}-{seq}</div><p class="desc">일별 리셋 변경 시 오늘 발번된 LOT와의 영향 확인이 필요합니다.</p><div class="impact"><span>incoming_inspection</span><span>5 today</span></div></article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="col-head"><h2>저장 가능</h2><span class="count">9</span></div>
|
||||||
|
<div class="cards">
|
||||||
|
<article class="card" data-status="approved" data-title="수주번호 자동채번" data-code="SO-202605-0048"><div class="card-top"><h3>수주번호 자동채번</h3><span class="tag good">ready</span></div><div class="pattern">SO-{yyyyMM}-{0000}</div><p class="desc">연결 컬럼과 다음 순번이 정상입니다.</p><div class="impact"><span>sales_order</span><span>monthly</span></div></article>
|
||||||
|
<article class="card" data-status="approved" data-title="구매발주 번호" data-code="PO-2026-00132"><div class="card-top"><h3>구매발주 번호</h3><span class="tag good">ready</span></div><div class="pattern">PO-{yyyy}-{00000}</div><p class="desc">연도별 리셋 정책과 기존 발번 이력이 일치합니다.</p><div class="impact"><span>purchase_order</span><span>yearly</span></div></article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<aside class="detail">
|
||||||
|
<div class="detail-head"><h2 id="detailTitle">품목마스터 코드</h2><p>선택된 변경의 충돌 원인과 적용 전 체크 항목입니다.</p></div>
|
||||||
|
<div class="preview"><span>NEXT CODE IF SAVED</span><b id="detailCode">ITEM-RM-008</b></div>
|
||||||
|
<section class="section">
|
||||||
|
<h3>검증 결과</h3>
|
||||||
|
<div class="check">
|
||||||
|
<div class="check-row"><span class="check-dot danger"></span><span>기존 코드 범위와 접두어가 겹침</span><span class="mono">fail</span></div>
|
||||||
|
<div class="check-row"><span class="check-dot warn"></span><span>카테고리 접두어가 수동 입력과 혼재</span><span class="mono">review</span></div>
|
||||||
|
<div class="check-row"><span class="check-dot"></span><span>시퀀스 저장소는 정상 연결</span><span class="mono">pass</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="section">
|
||||||
|
<h3>영향 범위</h3>
|
||||||
|
<div class="check">
|
||||||
|
<div class="check-row"><span class="check-dot warn"></span><span>item.item_code</span><span class="mono">primary</span></div>
|
||||||
|
<div class="check-row"><span class="check-dot"></span><span>bom_item.child_item_code</span><span class="mono">ref</span></div>
|
||||||
|
<div class="check-row"><span class="check-dot"></span><span>purchase_order_item.item_code</span><span class="mono">ref</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="section">
|
||||||
|
<h3>최근 이력</h3>
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="event"><time>09:42</time><div><b>수주번호 자동채번 발번</b><span class="muted">SO-202605-0047 생성</span></div></div>
|
||||||
|
<div class="event"><time>09:10</time><div><b>품목마스터 코드 검증</b><span class="muted">중복 가능성 2건 발견</span></div></div>
|
||||||
|
<div class="event"><time>08:58</time><div><b>구매발주 번호 승인</b><span class="muted">PO-2026-00132 대기</span></div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="detail-actions"><button class="btn">수정으로 이동</button><button class="btn good" id="approve">검증 승인</button></div>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const cards = [...document.querySelectorAll(".card")];
|
||||||
|
const filterButtons = [...document.querySelectorAll(".seg button")];
|
||||||
|
const search = document.querySelector("#search");
|
||||||
|
let activeFilter = "all";
|
||||||
|
function selectCard(card) {
|
||||||
|
cards.forEach((item) => item.classList.remove("on"));
|
||||||
|
card.classList.add("on");
|
||||||
|
document.querySelector("#detailTitle").textContent = card.dataset.title;
|
||||||
|
document.querySelector("#detailCode").textContent = card.dataset.code;
|
||||||
|
}
|
||||||
|
function applyFilter() {
|
||||||
|
const term = search.value.trim().toLowerCase();
|
||||||
|
cards.forEach((card) => {
|
||||||
|
const byFilter = activeFilter === "all" || card.dataset.status === activeFilter;
|
||||||
|
const bySearch = card.textContent.toLowerCase().includes(term);
|
||||||
|
card.style.display = byFilter && bySearch ? "grid" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cards.forEach((card) => card.addEventListener("click", () => selectCard(card)));
|
||||||
|
filterButtons.forEach((button) => button.addEventListener("click", () => {
|
||||||
|
filterButtons.forEach((item) => item.classList.remove("on"));
|
||||||
|
button.classList.add("on");
|
||||||
|
activeFilter = button.dataset.filter;
|
||||||
|
applyFilter();
|
||||||
|
}));
|
||||||
|
search.addEventListener("input", applyFilter);
|
||||||
|
document.querySelector("#approve").addEventListener("click", () => {
|
||||||
|
const active = document.querySelector(".card.on");
|
||||||
|
if (!active) return;
|
||||||
|
active.dataset.status = "approved";
|
||||||
|
active.querySelector(".tag").className = "tag good";
|
||||||
|
active.querySelector(".tag").textContent = "approved";
|
||||||
|
document.querySelector("#approve").textContent = "승인됨";
|
||||||
|
setTimeout(() => { document.querySelector("#approve").textContent = "검증 승인"; }, 1200);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,945 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>채번 관리 — v5 (차분 정돈본)</title>
|
||||||
|
<style>
|
||||||
|
:root, [data-theme="light"] {
|
||||||
|
--v5-primary-rgb: 108, 92, 231;
|
||||||
|
--v5-green-rgb: 0, 184, 148;
|
||||||
|
--v5-amber-rgb: 217, 119, 6;
|
||||||
|
--v5-red-rgb: 235, 87, 87;
|
||||||
|
|
||||||
|
--v5-bg: #ffffff;
|
||||||
|
--v5-bg-subtle: #f7f7fa;
|
||||||
|
--v5-surface-solid: #ffffff;
|
||||||
|
--v5-surface-hover: #f4f4f8;
|
||||||
|
--v5-text: #1a1a26;
|
||||||
|
--v5-text-sec: #5a5a6c;
|
||||||
|
--v5-text-muted: #9c9caa;
|
||||||
|
--v5-primary: rgb(var(--v5-primary-rgb));
|
||||||
|
--v5-primary-soft: rgba(var(--v5-primary-rgb), .07);
|
||||||
|
--v5-border: rgba(var(--v5-primary-rgb), 0.10);
|
||||||
|
--v5-border-strong: rgba(var(--v5-primary-rgb), 0.22);
|
||||||
|
--v5-divider: rgba(20, 20, 30, 0.06);
|
||||||
|
|
||||||
|
--v5-font-mono: 'JetBrains Mono', 'D2Coding', ui-monospace, SFMono-Regular, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--v5-primary-rgb: 162, 155, 254;
|
||||||
|
--v5-bg: #0c0d12;
|
||||||
|
--v5-bg-subtle: #131420;
|
||||||
|
--v5-surface-solid: #11102a;
|
||||||
|
--v5-surface-hover: #1c1d3a;
|
||||||
|
--v5-text: #ebecee;
|
||||||
|
--v5-text-sec: #a3a4b3;
|
||||||
|
--v5-text-muted: #61627a;
|
||||||
|
--v5-border: rgba(255, 255, 255, 0.08);
|
||||||
|
--v5-border-strong: rgba(255, 255, 255, 0.18);
|
||||||
|
--v5-divider: rgba(255, 255, 255, 0.05);
|
||||||
|
--v5-primary-soft: rgba(var(--v5-primary-rgb), .10);
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
html, body { height: 100%; margin: 0; }
|
||||||
|
body {
|
||||||
|
background: var(--v5-bg);
|
||||||
|
color: var(--v5-text);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Pretendard", "Apple SD Gothic Neo", system-ui, sans-serif;
|
||||||
|
font-size: .76rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
button, input, select, textarea { font-family: inherit; color: inherit; }
|
||||||
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--v5-border-strong); border-radius: 3px; }
|
||||||
|
|
||||||
|
.ico { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
|
.ico-sm { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
|
.ico-xl { width: 28px; height: 28px; stroke: currentColor; fill: none; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
|
|
||||||
|
/* ─── 공통 컨트롤 ─── */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 5px;
|
||||||
|
height: 28px; padding: 0 .7rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: .68rem; font-weight: 600;
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
color: var(--v5-text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .15s, background .15s, color .15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn:hover { border-color: var(--v5-border-strong); background: var(--v5-surface-hover); }
|
||||||
|
.btn.primary {
|
||||||
|
background: var(--v5-primary);
|
||||||
|
border-color: var(--v5-primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn.primary:hover { filter: brightness(.93); }
|
||||||
|
.btn.danger { color: var(--v5-text-sec); }
|
||||||
|
.btn.danger:hover { color: rgb(var(--v5-red-rgb)); border-color: rgba(var(--v5-red-rgb), .35); background: rgba(var(--v5-red-rgb), .04); }
|
||||||
|
.btn.ghost { border-color: transparent; background: transparent; }
|
||||||
|
.btn.ghost:hover { background: var(--v5-surface-hover); }
|
||||||
|
.btn.sm { height: 26px; padding: 0 .55rem; font-size: .64rem; gap: 4px; }
|
||||||
|
.btn.icon { padding: 0; width: 28px; justify-content: center; }
|
||||||
|
.btn.icon.sm { width: 26px; }
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--v5-bg-subtle);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
font-size: .58rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--v5-text-sec);
|
||||||
|
}
|
||||||
|
.chip.live {
|
||||||
|
background: rgba(var(--v5-green-rgb), .08);
|
||||||
|
color: rgb(var(--v5-green-rgb));
|
||||||
|
border-color: rgba(var(--v5-green-rgb), .22);
|
||||||
|
}
|
||||||
|
.chip.live::before {
|
||||||
|
content: '';
|
||||||
|
width: 5px; height: 5px;
|
||||||
|
background: rgb(var(--v5-green-rgb));
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.chip.mono {
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .56rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbd {
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .56rem;
|
||||||
|
padding: 1px 5px;
|
||||||
|
background: var(--v5-bg-subtle);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--v5-text-sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── PAGE ─── */
|
||||||
|
.page { display: flex; flex-direction: column; height: 100%; }
|
||||||
|
|
||||||
|
.pagehead {
|
||||||
|
padding: .8rem 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--v5-border);
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.pagehead .l { display: flex; align-items: baseline; gap: .65rem; }
|
||||||
|
.pagehead h1 { margin: 0; font-size: .88rem; font-weight: 700; letter-spacing: -.01em; }
|
||||||
|
.pagehead .subtitle { font-size: .66rem; color: var(--v5-text-muted); }
|
||||||
|
.pagehead .subtitle b { color: var(--v5-text-sec); font-weight: 600; }
|
||||||
|
.pagehead .r { display: flex; gap: .35rem; align-items: center; }
|
||||||
|
|
||||||
|
.body { flex: 1; min-height: 0; display: grid; grid-template-columns: 300px 1fr; overflow: hidden; }
|
||||||
|
|
||||||
|
/* ─── SIDEBAR ─── */
|
||||||
|
.side {
|
||||||
|
border-right: 1px solid var(--v5-border);
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.side-srch {
|
||||||
|
position: relative;
|
||||||
|
padding: .55rem .65rem;
|
||||||
|
border-bottom: 1px solid var(--v5-border);
|
||||||
|
}
|
||||||
|
.side-srch input {
|
||||||
|
width: 100%; height: 30px;
|
||||||
|
padding: 0 2rem 0 1.85rem;
|
||||||
|
background: var(--v5-bg-subtle);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: .72rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.side-srch input:focus { border-color: var(--v5-primary); background: var(--v5-surface-solid); }
|
||||||
|
.side-srch svg { position: absolute; left: 1.15rem; top: 50%; transform: translateY(-50%); color: var(--v5-text-muted); width: 12px; height: 12px; }
|
||||||
|
.side-srch .kbd-hint { position: absolute; right: 1rem; top: 50%; transform: translateY(-50%); font-family: var(--v5-font-mono); font-size: .55rem; color: var(--v5-text-muted); padding: 1px 5px; background: var(--v5-surface-solid); border: 1px solid var(--v5-border); border-radius: 3px; }
|
||||||
|
|
||||||
|
.side-filters { display: flex; gap: 3px; padding: .45rem .65rem; border-bottom: 1px solid var(--v5-border); }
|
||||||
|
.filt {
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--v5-text-sec);
|
||||||
|
font-size: .62rem; font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.filt:hover { background: var(--v5-surface-hover); color: var(--v5-text); }
|
||||||
|
.filt.on { background: var(--v5-primary-soft); color: var(--v5-primary); }
|
||||||
|
.filt b { font-weight: 700; }
|
||||||
|
.filt.on b { color: var(--v5-primary); }
|
||||||
|
|
||||||
|
.side-list { flex: 1; overflow-y: auto; }
|
||||||
|
.side-section {
|
||||||
|
font-size: .54rem;
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: .65rem .85rem .25rem;
|
||||||
|
display: flex; justify-content: space-between;
|
||||||
|
}
|
||||||
|
.side-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: .5rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: .55rem .85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .12s;
|
||||||
|
border-bottom: 1px solid var(--v5-divider);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.side-item:hover { background: var(--v5-surface-hover); }
|
||||||
|
.side-item.on { background: var(--v5-primary-soft); }
|
||||||
|
.side-item.on::before {
|
||||||
|
content: ''; position: absolute; left: 0; top: 0; bottom: 0;
|
||||||
|
width: 2px; background: var(--v5-primary);
|
||||||
|
}
|
||||||
|
.side-item.on .side-item-name { color: var(--v5-primary); font-weight: 700; }
|
||||||
|
.side-item-name { font-size: .76rem; font-weight: 600; margin-bottom: 1px; }
|
||||||
|
.side-item-pat { font-family: var(--v5-font-mono); font-size: .58rem; color: var(--v5-text-muted); }
|
||||||
|
.side-item-right { font-family: var(--v5-font-mono); font-size: .55rem; color: var(--v5-text-muted); display: flex; align-items: center; gap: 5px; }
|
||||||
|
.side-item-right .dot { width: 5px; height: 5px; border-radius: 50%; background: rgb(var(--v5-green-rgb)); }
|
||||||
|
.side-item.dim { opacity: .55; }
|
||||||
|
.side-item.dim .dot { background: var(--v5-text-muted); }
|
||||||
|
|
||||||
|
.side-foot { padding: .55rem .65rem; border-top: 1px solid var(--v5-border); }
|
||||||
|
|
||||||
|
/* ─── MAIN ─── */
|
||||||
|
.main {
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DETAIL HEAD */
|
||||||
|
.detail-head {
|
||||||
|
padding: .9rem 1.4rem .8rem;
|
||||||
|
border-bottom: 1px solid var(--v5-border);
|
||||||
|
display: flex; align-items: flex-start; justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.detail-head .l { display: flex; flex-direction: column; gap: 5px; min-width: 0; flex: 1; }
|
||||||
|
.detail-head h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: .98rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -.01em;
|
||||||
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.detail-head .meta {
|
||||||
|
display: flex; gap: .85rem; flex-wrap: wrap;
|
||||||
|
font-size: .62rem;
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
}
|
||||||
|
.detail-head .meta b { color: var(--v5-text-sec); font-weight: 600; }
|
||||||
|
.detail-head .r { display: flex; gap: .25rem; }
|
||||||
|
|
||||||
|
/* NEXT BAR — pattern-vis 의 압축 버전. 한 줄로 핵심 정보만 */
|
||||||
|
.next-bar {
|
||||||
|
padding: .65rem 1.4rem;
|
||||||
|
background: var(--v5-bg-subtle);
|
||||||
|
border-bottom: 1px solid var(--v5-border);
|
||||||
|
display: flex; align-items: center; gap: 1rem;
|
||||||
|
font-size: .68rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.next-bar .group { display: inline-flex; align-items: center; gap: .45rem; }
|
||||||
|
.next-bar .lbl {
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: .62rem;
|
||||||
|
}
|
||||||
|
.next-bar .code {
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--v5-text);
|
||||||
|
font-size: .82rem;
|
||||||
|
letter-spacing: .01em;
|
||||||
|
}
|
||||||
|
.next-bar .code.next {
|
||||||
|
color: var(--v5-primary);
|
||||||
|
background: var(--v5-primary-soft);
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.next-bar .arrow { color: var(--v5-text-muted); }
|
||||||
|
.next-bar .meta-inline { color: var(--v5-text-muted); font-size: .62rem; margin-left: auto; display: inline-flex; align-items: center; gap: .55rem; }
|
||||||
|
|
||||||
|
/* SECTION */
|
||||||
|
.section {
|
||||||
|
padding: 1rem 1.4rem 1.1rem;
|
||||||
|
border-bottom: 1px solid var(--v5-border);
|
||||||
|
}
|
||||||
|
.section-hd { display: flex; align-items: center; gap: 8px; margin-bottom: .65rem; }
|
||||||
|
.section-hd h3 { margin: 0; font-size: .8rem; font-weight: 700; letter-spacing: -.005em; }
|
||||||
|
.section-hd .count {
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .58rem;
|
||||||
|
color: var(--v5-text-sec);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1px 6px;
|
||||||
|
background: var(--v5-bg-subtle);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.section-hd .desc { font-size: .62rem; color: var(--v5-text-muted); }
|
||||||
|
.section-hd .actions { margin-left: auto; display: flex; gap: .25rem; }
|
||||||
|
|
||||||
|
/* PIPELINE — 컴팩트 블록 */
|
||||||
|
.pipe-wrap {
|
||||||
|
padding: .65rem .75rem .55rem;
|
||||||
|
background: var(--v5-bg-subtle);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.pipe {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.pipe-slot {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
transition: color .15s;
|
||||||
|
}
|
||||||
|
.pipe-slot .add-here {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: var(--v5-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid var(--v5-bg-subtle);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 16px; height: 16px;
|
||||||
|
left: 50%; top: 50%;
|
||||||
|
transform: translate(-50%, -50%) scale(0);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .15s, transform .15s;
|
||||||
|
}
|
||||||
|
.pipe-slot .add-here svg { width: 9px; height: 9px; stroke: currentColor; fill: none; stroke-width: 2.5; }
|
||||||
|
.pipe-slot:hover .add-here { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||||
|
|
||||||
|
/* pipe-block — 단순하게: ord+lbl 한 줄 + val 큰 글자. typ-en 제거 (인스펙터에 표시) */
|
||||||
|
.pipe-block {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 7px 11px 8px;
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: border-color .15s, background .15s;
|
||||||
|
min-width: 78px;
|
||||||
|
}
|
||||||
|
.pipe-block:hover { border-color: var(--v5-border-strong); }
|
||||||
|
.pipe-block.sel {
|
||||||
|
border-color: var(--v5-primary);
|
||||||
|
background: var(--v5-primary-soft);
|
||||||
|
}
|
||||||
|
.pipe-block .top-row {
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
font-size: .58rem;
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.pipe-block .top-row .ord { font-family: var(--v5-font-mono); }
|
||||||
|
.pipe-block .top-row .lbl { color: var(--v5-text-sec); }
|
||||||
|
.pipe-block.sel .top-row .lbl { color: var(--v5-primary); }
|
||||||
|
.pipe-block .val {
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .92rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--v5-text);
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: .01em;
|
||||||
|
}
|
||||||
|
.pipe-block.sel .val { color: var(--v5-primary); }
|
||||||
|
.pipe-block .x {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px; right: -6px;
|
||||||
|
width: 16px; height: 16px;
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
font-size: 11px; line-height: 1;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .15s, color .15s, border-color .15s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.pipe-block:hover .x { opacity: 1; }
|
||||||
|
.pipe-block .x:hover { color: rgb(var(--v5-red-rgb)); border-color: rgba(var(--v5-red-rgb), .4); }
|
||||||
|
|
||||||
|
/* end-of-pipe 추가 */
|
||||||
|
.pipe-add-end {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 11px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px dashed var(--v5-border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
font-size: .66rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
transition: color .15s, border-color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.pipe-add-end:hover {
|
||||||
|
color: var(--v5-primary);
|
||||||
|
border-color: var(--v5-primary);
|
||||||
|
background: var(--v5-primary-soft);
|
||||||
|
}
|
||||||
|
.pipe-add-end svg { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 2; }
|
||||||
|
|
||||||
|
/* INSPECTOR — 차분하게. 위쪽 화살표 X, glow ring 약화 */
|
||||||
|
.insp {
|
||||||
|
margin-top: .65rem;
|
||||||
|
padding: .7rem .85rem .8rem;
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
border: 1px solid var(--v5-border-strong);
|
||||||
|
border-left: 3px solid var(--v5-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.insp-hd {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: .55rem;
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
border-bottom: 1px solid var(--v5-divider);
|
||||||
|
}
|
||||||
|
.insp-hd .l { display: flex; align-items: center; gap: 8px; font-size: .72rem; }
|
||||||
|
.insp-hd .l .pin {
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .58rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--v5-primary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--v5-primary-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.insp-hd .l b { color: var(--v5-text); font-weight: 700; }
|
||||||
|
.insp-grid { display: flex; flex-wrap: wrap; gap: .8rem 1rem; align-items: flex-end; }
|
||||||
|
.insp-field { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.insp-field.grow { flex: 1; min-width: 180px; }
|
||||||
|
.insp-field.w90 { flex: 0 0 90px; }
|
||||||
|
.insp-field label { font-size: .62rem; color: var(--v5-text-sec); font-weight: 600; }
|
||||||
|
.insp-field .hint { font-size: .58rem; color: var(--v5-text-muted); }
|
||||||
|
.insp-inp {
|
||||||
|
height: 28px; padding: 0 .55rem;
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.insp-inp:focus { border-color: var(--v5-primary); }
|
||||||
|
|
||||||
|
/* SEG — 차분 */
|
||||||
|
.seg {
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.seg button {
|
||||||
|
padding: 0 .7rem;
|
||||||
|
height: 28px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid var(--v5-border);
|
||||||
|
color: var(--v5-text-sec);
|
||||||
|
font-size: .64rem;
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background .12s, color .12s;
|
||||||
|
display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.seg button:last-child { border-right: none; }
|
||||||
|
.seg button:hover { background: var(--v5-surface-hover); color: var(--v5-text); }
|
||||||
|
.seg button.on { background: var(--v5-primary); color: #fff; }
|
||||||
|
.seg button .sub { font-size: .5rem; opacity: .65; margin-top: 1px; font-weight: 500; }
|
||||||
|
|
||||||
|
/* PALETTE — 한글만, 차분 */
|
||||||
|
.palette {
|
||||||
|
margin-top: .7rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.palette .lbl { font-size: .62rem; color: var(--v5-text-muted); font-weight: 600; margin-right: 4px; }
|
||||||
|
.palette-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 9px;
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--v5-text-sec);
|
||||||
|
font-size: .66rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .12s, color .12s, background .12s;
|
||||||
|
}
|
||||||
|
.palette-item:hover {
|
||||||
|
border-color: var(--v5-border-strong);
|
||||||
|
color: var(--v5-text);
|
||||||
|
background: var(--v5-surface-hover);
|
||||||
|
}
|
||||||
|
.palette-item svg { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 1.75; }
|
||||||
|
|
||||||
|
/* 2-COL FOOTER (연결 컬럼 + 시퀀스) — 한 줄 요약 톤 */
|
||||||
|
.foot-split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
border-bottom: 1px solid var(--v5-border);
|
||||||
|
}
|
||||||
|
.foot-split > div { padding: .85rem 1.4rem; }
|
||||||
|
.foot-split > div + div { border-left: 1px solid var(--v5-border); }
|
||||||
|
.foot-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: .65rem;
|
||||||
|
}
|
||||||
|
.foot-row .ttl {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
font-size: .68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.foot-row .ttl .n {
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .55rem;
|
||||||
|
color: var(--v5-text-sec);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1px 5px;
|
||||||
|
background: var(--v5-bg-subtle);
|
||||||
|
border: 1px solid var(--v5-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.foot-row .act {
|
||||||
|
font-size: .62rem;
|
||||||
|
color: var(--v5-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent; border: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.foot-row .act:hover { text-decoration: underline; }
|
||||||
|
.foot-row .val {
|
||||||
|
margin-top: .35rem;
|
||||||
|
font-family: var(--v5-font-mono);
|
||||||
|
font-size: .72rem;
|
||||||
|
color: var(--v5-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.foot-row .val .arr { color: var(--v5-text-muted); margin: 0 4px; }
|
||||||
|
.foot-row .val .nx { color: var(--v5-primary); }
|
||||||
|
.foot-row .sub {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: .58rem;
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SAVE BAR */
|
||||||
|
.savebar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding: .65rem 1.4rem;
|
||||||
|
background: var(--v5-surface-solid);
|
||||||
|
border-top: 1px solid var(--v5-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: .5rem;
|
||||||
|
z-index: 5;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.savebar .l { font-size: .64rem; color: var(--v5-text-muted); display: flex; align-items: center; gap: 6px; }
|
||||||
|
.unsaved {
|
||||||
|
color: rgb(var(--v5-amber-rgb));
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-flex; align-items: center; gap: 5px;
|
||||||
|
}
|
||||||
|
.unsaved .d {
|
||||||
|
width: 5px; height: 5px;
|
||||||
|
background: rgb(var(--v5-amber-rgb));
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 공허 상태 */
|
||||||
|
.empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
gap: .5rem;
|
||||||
|
color: var(--v5-text-muted);
|
||||||
|
}
|
||||||
|
.empty svg { width: 36px; height: 36px; opacity: .4; stroke: currentColor; fill: none; stroke-width: 1.5; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></symbol>
|
||||||
|
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
||||||
|
<symbol id="i-hash" viewBox="0 0 24 24"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/></symbol>
|
||||||
|
<symbol id="i-arrow-right" viewBox="0 0 24 24"><path d="M5 12h14M13 5l7 7-7 7"/></symbol>
|
||||||
|
<symbol id="i-trash" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></symbol>
|
||||||
|
<symbol id="i-copy" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></symbol>
|
||||||
|
<symbol id="i-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></symbol>
|
||||||
|
<symbol id="i-rotate" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></symbol>
|
||||||
|
<symbol id="i-save" viewBox="0 0 24 24"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></symbol>
|
||||||
|
<symbol id="i-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol>
|
||||||
|
<symbol id="i-type" viewBox="0 0 24 24"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></symbol>
|
||||||
|
<symbol id="i-cal" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></symbol>
|
||||||
|
<symbol id="i-edit" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4z"/></symbol>
|
||||||
|
<symbol id="i-layers" viewBox="0 0 24 24"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol>
|
||||||
|
<symbol id="i-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></symbol>
|
||||||
|
<symbol id="i-share" viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></symbol>
|
||||||
|
<symbol id="i-download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<!-- PAGE HEAD -->
|
||||||
|
<div class="pagehead">
|
||||||
|
<div class="l">
|
||||||
|
<h1>채번 관리</h1>
|
||||||
|
<span class="subtitle">
|
||||||
|
자동 번호 규칙을 만들고 컬럼에 연결합니다 · <b>14</b> 규칙 · <b>9</b> 사용 중
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="r">
|
||||||
|
<span class="kbd">⌘ K</span>
|
||||||
|
<button class="btn icon ghost" title="새로고침"><svg class="ico"><use href="#i-refresh"/></svg></button>
|
||||||
|
<button class="btn primary"><svg class="ico"><use href="#i-plus"/></svg> 새 채번</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
|
||||||
|
<!-- SIDEBAR -->
|
||||||
|
<aside class="side">
|
||||||
|
<div class="side-srch">
|
||||||
|
<svg><use href="#i-search"/></svg>
|
||||||
|
<input placeholder="이름이나 컬럼으로 찾기"/>
|
||||||
|
<span class="kbd-hint">/</span>
|
||||||
|
</div>
|
||||||
|
<div class="side-filters">
|
||||||
|
<button class="filt on">전체 <b>14</b></button>
|
||||||
|
<button class="filt">사용 중 <b>9</b></button>
|
||||||
|
<button class="filt">미사용 <b>5</b></button>
|
||||||
|
</div>
|
||||||
|
<div class="side-list">
|
||||||
|
<div class="side-section"><span>사용 중</span><span>9</span></div>
|
||||||
|
<div class="side-item on">
|
||||||
|
<div>
|
||||||
|
<div class="side-item-name">수주번호</div>
|
||||||
|
<div class="side-item-pat">SO-YYYY-MM-####</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item-right"><span class="dot"></span>1곳</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item">
|
||||||
|
<div>
|
||||||
|
<div class="side-item-name">발주번호</div>
|
||||||
|
<div class="side-item-pat">PO-YYYYMMDD-###</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item-right"><span class="dot"></span>2곳</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item">
|
||||||
|
<div>
|
||||||
|
<div class="side-item-name">출고번호</div>
|
||||||
|
<div class="side-item-pat">OUT-YY-####</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item-right"><span class="dot"></span>1곳</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item">
|
||||||
|
<div>
|
||||||
|
<div class="side-item-name">제품코드</div>
|
||||||
|
<div class="side-item-pat">P-####</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item-right"><span class="dot"></span>1곳</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-section"><span>미사용</span><span>5</span></div>
|
||||||
|
<div class="side-item dim">
|
||||||
|
<div>
|
||||||
|
<div class="side-item-name">샘플 번호</div>
|
||||||
|
<div class="side-item-pat">SMP-##</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item-right"><span class="dot"></span>—</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item dim">
|
||||||
|
<div>
|
||||||
|
<div class="side-item-name">테스트 코드</div>
|
||||||
|
<div class="side-item-pat">TEST-###</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-item-right"><span class="dot"></span>—</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="side-foot">
|
||||||
|
<button class="btn sm" style="width:100%;justify-content:center;"><svg class="ico-sm"><use href="#i-plus"/></svg> 새 채번 만들기</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- MAIN -->
|
||||||
|
<main class="main">
|
||||||
|
|
||||||
|
<!-- 1. DETAIL HEAD -->
|
||||||
|
<div class="detail-head">
|
||||||
|
<div class="l">
|
||||||
|
<h2>
|
||||||
|
수주번호
|
||||||
|
<span class="chip live">사용 중</span>
|
||||||
|
<span class="chip mono">RULE-1778487089084</span>
|
||||||
|
</h2>
|
||||||
|
<div class="meta">
|
||||||
|
<span><b>생성</b> 2026-03-12 by gbpark</span>
|
||||||
|
<span><b>마지막 수정</b> 2026-05-14 16:22</span>
|
||||||
|
<span><b>지금까지</b> 142건 발번</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="r">
|
||||||
|
<button class="btn sm icon ghost" title="복제"><svg class="ico-sm"><use href="#i-copy"/></svg></button>
|
||||||
|
<button class="btn sm icon ghost" title="내보내기"><svg class="ico-sm"><use href="#i-download"/></svg></button>
|
||||||
|
<button class="btn sm icon ghost" title="공유"><svg class="ico-sm"><use href="#i-share"/></svg></button>
|
||||||
|
<button class="btn sm icon ghost danger" title="삭제"><svg class="ico-sm"><use href="#i-trash"/></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. NEXT BAR — pattern-vis 를 한 줄로 압축 -->
|
||||||
|
<div class="next-bar">
|
||||||
|
<span class="group">
|
||||||
|
<span class="lbl">현재</span>
|
||||||
|
<span class="code">SO-2026-05-0142</span>
|
||||||
|
</span>
|
||||||
|
<svg class="ico" style="color:var(--v5-text-muted)"><use href="#i-arrow-right"/></svg>
|
||||||
|
<span class="group">
|
||||||
|
<span class="lbl">다음 발번</span>
|
||||||
|
<span class="code next">SO-2026-05-0143</span>
|
||||||
|
</span>
|
||||||
|
<span class="meta-inline">
|
||||||
|
<span>매월 1일 초기화</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>시퀀스 142 → 143</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. PIPELINE -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-hd">
|
||||||
|
<h3>코드를 이루는 조각</h3>
|
||||||
|
<span class="count">4개</span>
|
||||||
|
<span class="desc">조각 클릭해서 편집 · 사이 hover 로 추가</span>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn sm ghost"><svg class="ico-sm"><use href="#i-rotate"/></svg> 초기 상태로</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pipe-wrap">
|
||||||
|
<div class="pipe">
|
||||||
|
<div class="pipe-block">
|
||||||
|
<div class="top-row"><span class="ord">1번</span><span class="lbl">고정</span></div>
|
||||||
|
<span class="val">SO</span>
|
||||||
|
<button class="x">×</button>
|
||||||
|
</div>
|
||||||
|
<span class="pipe-slot">
|
||||||
|
-<button class="add-here"><svg><use href="#i-plus"/></svg></button>
|
||||||
|
</span>
|
||||||
|
<div class="pipe-block sel">
|
||||||
|
<div class="top-row"><span class="ord">2번</span><span class="lbl">년도</span></div>
|
||||||
|
<span class="val">2026</span>
|
||||||
|
<button class="x">×</button>
|
||||||
|
</div>
|
||||||
|
<span class="pipe-slot">
|
||||||
|
-<button class="add-here"><svg><use href="#i-plus"/></svg></button>
|
||||||
|
</span>
|
||||||
|
<div class="pipe-block">
|
||||||
|
<div class="top-row"><span class="ord">3번</span><span class="lbl">월</span></div>
|
||||||
|
<span class="val">05</span>
|
||||||
|
<button class="x">×</button>
|
||||||
|
</div>
|
||||||
|
<span class="pipe-slot">
|
||||||
|
-<button class="add-here"><svg><use href="#i-plus"/></svg></button>
|
||||||
|
</span>
|
||||||
|
<div class="pipe-block">
|
||||||
|
<div class="top-row"><span class="ord">4번</span><span class="lbl">순번</span></div>
|
||||||
|
<span class="val">0143</span>
|
||||||
|
<button class="x">×</button>
|
||||||
|
</div>
|
||||||
|
<button class="pipe-add-end"><svg><use href="#i-plus"/></svg> 끝에 추가</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INSPECTOR (선택된 2번 조각) -->
|
||||||
|
<div class="insp">
|
||||||
|
<div class="insp-hd">
|
||||||
|
<div class="l">
|
||||||
|
<span class="pin">2번 조각</span>
|
||||||
|
<span><b>년도</b> 설정</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn sm ghost danger"><svg class="ico-sm"><use href="#i-trash"/></svg> 이 조각 삭제</button>
|
||||||
|
</div>
|
||||||
|
<div class="insp-grid">
|
||||||
|
<div class="insp-field grow">
|
||||||
|
<label>날짜 형식</label>
|
||||||
|
<div class="seg">
|
||||||
|
<button>YYYY<span class="sub">2026</span></button>
|
||||||
|
<button class="on">YY<span class="sub">26</span></button>
|
||||||
|
<button>YYYYMM<span class="sub">202605</span></button>
|
||||||
|
<button>YYMM<span class="sub">2605</span></button>
|
||||||
|
<button>YYYYMMDD<span class="sub">20260515</span></button>
|
||||||
|
<button>YYMMDD<span class="sub">260515</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="insp-field grow">
|
||||||
|
<label>초기화 주기 (규칙 전체)</label>
|
||||||
|
<div class="seg">
|
||||||
|
<button>안 함</button>
|
||||||
|
<button>매일</button>
|
||||||
|
<button class="on">매월</button>
|
||||||
|
<button>매년</button>
|
||||||
|
</div>
|
||||||
|
<span class="hint">매월 1일 00:00 에 순번이 1 부터 다시 시작</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PALETTE -->
|
||||||
|
<div class="palette">
|
||||||
|
<span class="lbl">조각 추가:</span>
|
||||||
|
<button class="palette-item"><svg><use href="#i-type"/></svg> 고정 글자</button>
|
||||||
|
<button class="palette-item"><svg><use href="#i-cal"/></svg> 날짜</button>
|
||||||
|
<button class="palette-item"><svg><use href="#i-hash"/></svg> 순번</button>
|
||||||
|
<button class="palette-item"><svg><use href="#i-edit"/></svg> 고정 숫자</button>
|
||||||
|
<button class="palette-item"><svg><use href="#i-layers"/></svg> 카테고리</button>
|
||||||
|
<button class="palette-item"><svg><use href="#i-link"/></svg> 참조</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. FOOTER STATS — 한 줄 요약 톤 -->
|
||||||
|
<div class="foot-split">
|
||||||
|
<div>
|
||||||
|
<div class="foot-row">
|
||||||
|
<span class="ttl">연결된 컬럼 <span class="n">1</span></span>
|
||||||
|
<button class="act">컬럼 연결 관리</button>
|
||||||
|
</div>
|
||||||
|
<div class="foot-row" style="display:block;">
|
||||||
|
<span class="val">user_info<span class="arr">·</span>order_no</span>
|
||||||
|
<div class="sub">단일 연결 · 향후 N:M 확장 예정</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="foot-row">
|
||||||
|
<span class="ttl">시퀀스 현황</span>
|
||||||
|
<button class="act">수동 수정</button>
|
||||||
|
</div>
|
||||||
|
<div class="foot-row" style="display:block;">
|
||||||
|
<span class="val">142<span class="arr">→</span><span class="nx">143</span></span>
|
||||||
|
<div class="sub">매월 1일 초기화 · 다음 리셋 2026-06-01 00:00</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5. SAVE BAR -->
|
||||||
|
<div class="savebar">
|
||||||
|
<div class="l">
|
||||||
|
<span class="unsaved"><span class="d"></span>저장하지 않은 변경 1건</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:.35rem;">
|
||||||
|
<button class="btn ghost"><svg class="ico"><use href="#i-rotate"/></svg> 되돌리기</button>
|
||||||
|
<button class="btn primary"><svg class="ico"><use href="#i-save"/></svg> 저장 <span class="kbd" style="background:rgba(255,255,255,.2);color:#fff;border-color:transparent;">⌘ S</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 사이드바 선택
|
||||||
|
document.querySelectorAll('.side-item').forEach(el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.side-item.on').forEach(e => e.classList.remove('on'));
|
||||||
|
el.classList.add('on');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// pipe-block 선택
|
||||||
|
document.querySelectorAll('.pipe-block').forEach(el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.pipe-block.sel').forEach(e => e.classList.remove('sel'));
|
||||||
|
el.classList.add('sel');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// seg 클릭
|
||||||
|
document.querySelectorAll('.seg').forEach(seg => {
|
||||||
|
seg.querySelectorAll('button').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
seg.querySelectorAll('button.on').forEach(b => b.classList.remove('on'));
|
||||||
|
btn.classList.add('on');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// filt 클릭
|
||||||
|
document.querySelectorAll('.filt').forEach(el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.filt.on').forEach(e => e.classList.remove('on'));
|
||||||
|
el.classList.add('on');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 다크 토글
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === 'j') {
|
||||||
|
e.preventDefault();
|
||||||
|
const t = document.documentElement.getAttribute('data-theme');
|
||||||
|
document.documentElement.setAttribute('data-theme', t === 'dark' ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
|||||||
|
# 2026-05-20 Claude Prompt — Table Canonical Input-Parity Cleanup
|
||||||
|
|
||||||
|
이 문서는 `table` 계열만 대상으로 하는 대형 작업용 Claude 프롬프트다.
|
||||||
|
|
||||||
|
운영 원칙:
|
||||||
|
- 큰 refactor 는 Claude 프롬프트로 진행한다.
|
||||||
|
- Codex 는 프롬프트 작성, 작은 follow-up, 검증/리뷰, 좁은 패치만 직접 처리한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 바로 실행할 명령
|
||||||
|
|
||||||
|
```text
|
||||||
|
/goal Read notes/gbpark/2026-05-20-table-canonical-input-parity-claude-prompt.md, notes/gbpark/2026-05-08-input-canonical-migration.md, notes/gbpark/2026-05-12-codex-handoff-input-canonical.md, and notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md. Then finish the table canonical cleanup to the same completion standard as canonical input: required v2-table-list/table-list behavior must be absorbed into canonical table, legacy/v2 table runtime aliases/fallback/schema/config-panel paths must be removed, FieldConfig/DataPort/sourceProvider/dataReceiver behavior must be preserved or reimplemented in canonical table, v2-input/v2-select must not be reintroduced, git diff --check must pass, backend compileJava must pass if backend is touched, and the final report must prove all remaining table-list/v2-table-list matches are either zero in code paths or explicitly non-runtime docs only.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 보조 프롬프트
|
||||||
|
|
||||||
|
```text
|
||||||
|
You are working in /Users/gbpark/invyone.
|
||||||
|
|
||||||
|
This is a large refactor. The goal is not another "preserve and classify" pass. The goal is to bring table cleanup to the same completion standard as the canonical input cleanup.
|
||||||
|
|
||||||
|
Read these files first:
|
||||||
|
- notes/gbpark/2026-05-20-table-canonical-input-parity-claude-prompt.md
|
||||||
|
- notes/gbpark/2026-05-08-input-canonical-migration.md
|
||||||
|
- notes/gbpark/2026-05-12-codex-handoff-input-canonical.md
|
||||||
|
- notes/gbpark/2026-05-19-canonical-data-view-cleanup-followup.md
|
||||||
|
|
||||||
|
Key precedent from canonical input:
|
||||||
|
- v2-input / v2-select were not kept as runtime compatibility aliases.
|
||||||
|
- v2-input / v2-select renderer folders, component bodies, config panels, alias/fallback/schema/default paths were removed.
|
||||||
|
- Required behavior was absorbed into canonical input.
|
||||||
|
- DB layout JSON migration SQL was not written.
|
||||||
|
- New-solution code paths use canonical input.
|
||||||
|
|
||||||
|
Apply that same policy to table:
|
||||||
|
- canonical id is "table".
|
||||||
|
- v2-table-list / table-list are removal targets, not long-term runtime aliases.
|
||||||
|
- Do not keep weak compatibility shims just to keep old type names alive.
|
||||||
|
- If an old behavior is needed, implement or move it into canonical table or a neutral helper with no v2-table-list/table-list component identity.
|
||||||
|
- Do not write DB layout JSON migration SQL unless the user explicitly asks later.
|
||||||
|
|
||||||
|
Non-negotiable rules:
|
||||||
|
1. Do not reintroduce v2-input, v2-select, V2InputRenderer, V2SelectRenderer, V2Input.tsx, V2Select.tsx, EntityPicker, or EntitySearchModal.
|
||||||
|
2. Do not shrink FieldConfig, DataPort, sourceProvider, dataReceiver, selectedRow/selectedRows, searchParams, or refreshTrigger contracts.
|
||||||
|
3. Do not use broad git reset/checkout/clean. Preserve unrelated user changes.
|
||||||
|
4. Do not delete domain/special components such as modal-repeater-table, simple-repeater-table, repeat-screen-modal, tax-invoice-list, universal-form-modal table sections unless equivalent canonical behavior is implemented and active references are updated.
|
||||||
|
5. Do not leave v2-table-list/table-list as a runtime alias/fallback/schema/config-panel path in final code unless you stop and report a concrete blocker. The intended final state is removal.
|
||||||
|
6. Keep the work scoped to table canonical cleanup. Do not revive the broader stats/container cleanup.
|
||||||
|
|
||||||
|
Current known table state:
|
||||||
|
- New creation path is canonical table:
|
||||||
|
frontend/lib/registry/components/table/index.ts
|
||||||
|
- table-list/ and v2-table-list/ shell folders are already deleted.
|
||||||
|
- The old runtime still survives under:
|
||||||
|
frontend/lib/registry/components/table/_shared/TableListComponent.tsx
|
||||||
|
frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx
|
||||||
|
frontend/lib/registry/components/table/_shared/V2TableListContainerWrapper.tsx
|
||||||
|
frontend/lib/registry/components/table/_shared/TableListConfigPanel.tsx
|
||||||
|
frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts
|
||||||
|
- TableComponent currently early-delegates old raw types:
|
||||||
|
rawType === "table-list" -> LegacyTableListWrapper
|
||||||
|
rawType === "v2-table-list" -> V2TableListContainerWrapper
|
||||||
|
- Aliases/schema/default/config routing still exist in:
|
||||||
|
frontend/lib/registry/DynamicComponentRenderer.tsx
|
||||||
|
frontend/lib/utils/templateMigrate.ts
|
||||||
|
frontend/lib/utils/getComponentConfigPanel.tsx
|
||||||
|
frontend/lib/schemas/componentConfig.ts
|
||||||
|
frontend/lib/utils/componentTypeUtils.ts
|
||||||
|
- InvDataConfigPanel still has a v2-table-list branch and imports V2TableListConfigPanel.
|
||||||
|
- V2List.tsx builds a component object with type: "table-list".
|
||||||
|
- repeat-container / v2-repeat-container use dataSourceType = "table-list"; if this is not a component id, rename it to a clearer non-component enum such as "legacyTableSelection" or "tableSelectionSource" and update references.
|
||||||
|
|
||||||
|
Required implementation phases:
|
||||||
|
|
||||||
|
Phase 0 - Inventory and baseline
|
||||||
|
- Run:
|
||||||
|
git status --short
|
||||||
|
rg -n "v2-table-list|table-list|V2TableList|TableListComponent|TableListConfigPanel|V2TableListContainerWrapper" frontend/lib frontend/components frontend/app frontend/types
|
||||||
|
rg -n "v2-input|v2-select|V2InputRenderer|V2SelectRenderer" frontend/lib frontend/components frontend/app frontend/types frontend/styles
|
||||||
|
- Summarize each match into: runtime path, config path, schema/default path, type/helper path, docs/comment only, domain enum.
|
||||||
|
|
||||||
|
Phase 1 - Canonical Table parity inventory
|
||||||
|
- Compare canonical TableComponent / InvTableConfigPanel against _shared/TableListComponent and _shared/V2TableListComponent.
|
||||||
|
- Build a short checklist in the final report for these old behaviors:
|
||||||
|
selection, multi-select, checkbox, sorting, pagination, column visibility/order/width/align, searchable/filterable per column, linked filter, exclude filter, entity label display, category/code select, date picker fallback, image url/fallback, inline edit, row actions, context menu, export Excel, card mode, GroupSum, ResizeObserver responsive card fallback, DataProvider/DataReceiver, FieldConfig adapter, selectedTable/tableName/dbTable compatibility.
|
||||||
|
- Implement missing behavior in canonical table, or move reusable internals to neutral files under frontend/lib/registry/components/table/ with neutral names. Do not preserve old component identity.
|
||||||
|
|
||||||
|
Phase 2 - Canonical runtime absorption
|
||||||
|
- Remove early delegation from TableComponent after parity is implemented.
|
||||||
|
- Delete or empty old runtime files only when imports are gone:
|
||||||
|
_shared/TableListComponent.tsx
|
||||||
|
_shared/V2TableListComponent.tsx
|
||||||
|
_shared/V2TableListContainerWrapper.tsx
|
||||||
|
_shared/TableListConfigPanel.tsx
|
||||||
|
- Keep neutral helpers only if they do not encode old component identity:
|
||||||
|
SingleTableWithSticky.tsx may survive if it is a neutral internal table view, but rename/comments/types must not imply table-list component ownership.
|
||||||
|
CardModeRenderer.tsx may survive as a neutral card table helper if needed.
|
||||||
|
tableListConfigTypes.ts should be renamed or absorbed if it remains a runtime type dependency.
|
||||||
|
|
||||||
|
Phase 3 - Config and Studio paths
|
||||||
|
- InvTableConfigPanel must become the only table config panel.
|
||||||
|
- Remove InvDataConfigPanel v2-table-list branch and V2TableListConfigPanel import.
|
||||||
|
- Remove V2TableListConfigPanel.tsx if no active imports remain.
|
||||||
|
- Migrate V2List.tsx away from TableListComponent/table-list object identity or delete/deprecate it if no active canonical path needs it.
|
||||||
|
- Update ComponentsPanel, V2PropertiesPanel, DetailSettingsPanel, ActionTab, DataTab, InvLegacyButtonConfigPanel, ButtonPrimaryComponent, TabsWidget, RealtimePreviewDynamic, ScreenNode, screen pages, and buttonActions so they use canonical table helpers without old component ids.
|
||||||
|
|
||||||
|
Phase 4 - Alias/schema/type cleanup
|
||||||
|
- Remove table-list/v2-table-list aliases from:
|
||||||
|
DynamicComponentRenderer.LEGACY_TO_UNIFIED
|
||||||
|
templateMigrate.LEGACY_TO_UNIFIED
|
||||||
|
getComponentConfigPanel.CONFIG_PANEL_ALIAS
|
||||||
|
componentTypeUtils.TABLE_LIKE_COMPONENT_TYPES
|
||||||
|
- Remove v2-table-list schema/default from:
|
||||||
|
frontend/lib/schemas/componentConfig.ts
|
||||||
|
- Remove old table component type examples from:
|
||||||
|
frontend/types/invyone-component.ts
|
||||||
|
frontend/types/screen-management.ts
|
||||||
|
frontend/types/v2-components.ts
|
||||||
|
frontend/types/component-events.ts
|
||||||
|
- If a value is a domain enum rather than component id, rename it away from table-list so grep is not ambiguous.
|
||||||
|
|
||||||
|
Phase 5 - Verification
|
||||||
|
- Required commands:
|
||||||
|
git diff --check
|
||||||
|
rg -n "v2-input|v2-select|V2InputRenderer|V2SelectRenderer" frontend/lib frontend/components frontend/app frontend/types frontend/styles
|
||||||
|
rg -n "EntityPicker|entity-picker|EntitySearchModal" frontend/lib/registry/components/input frontend/components/v2/config-panels/InvFieldConfigPanel.tsx
|
||||||
|
rg -n "v2-table-list|table-list|V2TableList|TableListComponent|TableListConfigPanel|V2TableListContainerWrapper" frontend/lib frontend/components frontend/app frontend/types
|
||||||
|
rg -n "components/v2-table-list|components/table-list|./v2-table-list|./table-list" frontend
|
||||||
|
- Expected:
|
||||||
|
input forbidden tokens stay 0.
|
||||||
|
table old runtime/config/schema/import tokens should be 0 in code paths.
|
||||||
|
Any remaining table-list/v2-table-list matches must be docs-only or explicitly justified non-runtime comments. Prefer removing/rewriting stale comments.
|
||||||
|
- Run targeted typecheck if practical:
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false
|
||||||
|
If global existing errors remain, report only whether modified files introduced new errors.
|
||||||
|
- Run backend compile only if backend touched:
|
||||||
|
cd backend-spring && ./gradlew compileJava
|
||||||
|
|
||||||
|
Final report requirements:
|
||||||
|
- List files changed and deleted.
|
||||||
|
- State whether final code still has any table-list/v2-table-list runtime/config/schema aliases.
|
||||||
|
- Include the rg results above.
|
||||||
|
- Include the old behavior parity checklist and mark implemented / not applicable / unresolved.
|
||||||
|
- If any blocker prevents full input-parity completion, stop with exact file/line blockers and do not pretend the cleanup is complete.
|
||||||
|
```
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
# Table Canonical Cleanup — Phase B.2 + B.3 + D.9 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md`
|
||||||
|
- §8 Phase B.2
|
||||||
|
- §8 Phase B.3
|
||||||
|
- §8 Phase D.9
|
||||||
|
- §4 dependency table
|
||||||
|
|
||||||
|
Current status:
|
||||||
|
- B.1 / B.4 complete.
|
||||||
|
- C.1 ~ C.5 complete.
|
||||||
|
- D.1 ~ D.8 complete.
|
||||||
|
- D.9 is blocked by B.2 + B.3, so this session should finish the prerequisite wiring and then complete the canonical TableComponent D.9 wiring.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- Do not edit `_shared/TableListComponent.tsx` or `_shared/V2TableListComponent.tsx`; read them only as parity references.
|
||||||
|
- Do not reintroduce `v2-input`, `v2-select`, `EntityPicker`, or `EntitySearchModal`.
|
||||||
|
- Preserve the canonical TableComponent early delegation branch for legacy table-list / v2-table-list.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
Canonical `frontend/lib/registry/components/table/TableComponent.tsx` must expose the same data-transfer and split-panel runtime contracts that legacy `_shared/TableListComponent.tsx` and `_shared/V2TableListComponent.tsx` expose:
|
||||||
|
|
||||||
|
1. ScreenContext data provider registration.
|
||||||
|
2. ScreenContext data receiver registration.
|
||||||
|
3. SplitPanelContext receiver registration when mounted inside a split panel.
|
||||||
|
4. Left-panel row selection should set selected left data for right-panel consumers.
|
||||||
|
5. Right-panel linked filters should merge split-panel linked filter values into table search where compatible.
|
||||||
|
6. Provider absence must be safe. Canonical TableComponent must still mount outside ScreenContext / SplitPanelContext.
|
||||||
|
|
||||||
|
Primary file scope:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
|
||||||
|
Allowed only if needed:
|
||||||
|
- `frontend/lib/registry/components/table/useTableData.ts`
|
||||||
|
- only to expose a minimal local-data setter needed by `DataReceivable.receiveData`.
|
||||||
|
- `frontend/types/data-transfer.ts`
|
||||||
|
- additive type-only changes only. Do not shrink existing contracts.
|
||||||
|
- `frontend/contexts/SplitPanelContext.tsx`
|
||||||
|
- only if you prove canonical cannot be supported by consuming the existing context.
|
||||||
|
|
||||||
|
Prefer not to edit:
|
||||||
|
- `frontend/lib/registry/components/split-panel-layout/SplitPanelContext.tsx`
|
||||||
|
- `frontend/lib/registry/components/v2-split-panel-layout/SplitPanelContext.tsx`
|
||||||
|
|
||||||
|
Read first:
|
||||||
|
- `frontend/types/data-transfer.ts`
|
||||||
|
- `frontend/contexts/ScreenContext.tsx`
|
||||||
|
- `frontend/contexts/SplitPanelContext.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- around `useScreenContextOptional`
|
||||||
|
- around `useSplitPanelContext`
|
||||||
|
- around `DataProvidable`
|
||||||
|
- around `DataReceivable`
|
||||||
|
- around split-panel receiver registration
|
||||||
|
- around row click / `setSelectedLeftData`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- same areas as above
|
||||||
|
- current canonical:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/useTableData.ts`
|
||||||
|
|
||||||
|
Implementation requirements:
|
||||||
|
|
||||||
|
## 1. Import / context wiring
|
||||||
|
|
||||||
|
In canonical `TableComponent.tsx`, wire:
|
||||||
|
- `useSplitPanelContext` and `SplitPanelPosition` from `@/contexts/SplitPanelContext`.
|
||||||
|
- `DataProvidable`, `DataReceivable`, `DataReceiverConfig`, `DataReceivableComponentType`, and `EntityJoinColumnMeta` from `@/types/data-transfer` as needed.
|
||||||
|
|
||||||
|
There is already `screenContext = useScreenContextOptional()` from D.2. Reuse it.
|
||||||
|
|
||||||
|
Derive the current split position the same way legacy does:
|
||||||
|
- `screenContext?.split_panel_position`
|
||||||
|
- else `splitPanelContext?.getPositionByScreenId(Number(props.screenId))`
|
||||||
|
- else `null`
|
||||||
|
|
||||||
|
Do not require `props.screenId` to exist. Guard it.
|
||||||
|
|
||||||
|
## 2. DataProvidable parity
|
||||||
|
|
||||||
|
Create a stable provider object with `useMemo<DataProvidable>`.
|
||||||
|
|
||||||
|
Contract:
|
||||||
|
- `component_id`: canonical component id (`component.id` / `_componentId`).
|
||||||
|
- `component_type`: prefer `"table"` for canonical. If you choose `"table-list"` for legacy compatibility, explain why in report.
|
||||||
|
- `table_name`: effective canonical `tableName`.
|
||||||
|
- `getSelectedData()`:
|
||||||
|
- use canonical selected row state (`selectedRows` index set) and current runtime `rows` / `tableData.data`.
|
||||||
|
- preserve the same semantics as current selection callbacks.
|
||||||
|
- `getAllData()`:
|
||||||
|
- return current runtime rows.
|
||||||
|
- `clearSelection()`:
|
||||||
|
- clear `selectedRows` and `selectedRowIdx`.
|
||||||
|
- call existing selection emission logic if needed so external selected row callbacks stay consistent.
|
||||||
|
- `getEntityJoinColumns()`:
|
||||||
|
- return snake_case `EntityJoinColumnMeta[]`.
|
||||||
|
- use `sourceColumns` metadata:
|
||||||
|
- `additionalJoinInfo` maps to `{ source_table, source_column, join_alias, reference_table }`.
|
||||||
|
- also preserve useful `entityJoinInfo` if it has enough fields.
|
||||||
|
- no crashes on missing metadata.
|
||||||
|
|
||||||
|
## 3. DataReceivable parity
|
||||||
|
|
||||||
|
Create a stable receiver object with `useMemo<DataReceivable>`.
|
||||||
|
|
||||||
|
Contract:
|
||||||
|
- `component_id`: same as provider.
|
||||||
|
- `component_type`: `"table"` as `DataReceivableComponentType`.
|
||||||
|
- `getData()` returns current runtime rows.
|
||||||
|
- `receiveData(data, config)` supports at least:
|
||||||
|
- `append`
|
||||||
|
- `replace`
|
||||||
|
- `merge`
|
||||||
|
|
||||||
|
Legacy behavior mutates local table data. Canonical `useTableData` currently exposes data but not a setter. Choose the smallest safe implementation:
|
||||||
|
- Preferred: extend `useTableData.ts` with a minimal setter API (`setLocalData` or similar) and update `UseTableDataResult`.
|
||||||
|
- Alternative: keep a local override state inside `TableComponent` only if it does not break pagination/refresh semantics.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Do not refetch just to apply received data.
|
||||||
|
- `merge` should merge by `id` when present, then by `${tableName}_id` when present, then append with a generated stable-ish fallback key.
|
||||||
|
- If config has mapping rules, apply them conservatively:
|
||||||
|
- source field -> target field.
|
||||||
|
- default value if source missing.
|
||||||
|
- required missing values should skip that field, not crash the whole receive.
|
||||||
|
- Keep mode unknown cases safe.
|
||||||
|
|
||||||
|
## 4. ScreenContext registration
|
||||||
|
|
||||||
|
Register provider and receiver when all are true:
|
||||||
|
- not design mode
|
||||||
|
- `screenContext` exists
|
||||||
|
- component id exists
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- unregister both on unmount / dependency change.
|
||||||
|
|
||||||
|
Use stable `useMemo` / `useEffect` deps. Avoid infinite re-register loops caused by non-memoized objects.
|
||||||
|
|
||||||
|
## 5. SplitPanelContext receiver registration
|
||||||
|
|
||||||
|
If `splitPanelContext`, component id, and current split position exist:
|
||||||
|
- register receiver using `splitPanelContext.registerReceiver(position, componentId, receiver)`.
|
||||||
|
- receiver should forward to canonical `DataReceivable.receiveData(...)` with `target_component_type: "table"` and the incoming mode.
|
||||||
|
- unregister on cleanup.
|
||||||
|
|
||||||
|
Do not edit legacy split-panel provider unless the existing context cannot support canonical registration. If you do edit it, make the change additive and explain why.
|
||||||
|
|
||||||
|
## 6. Left-panel selected data
|
||||||
|
|
||||||
|
When a row is selected in canonical table and current split position is `"left"`:
|
||||||
|
- if `splitPanelContext.disable_auto_data_transfer` is false, call `splitPanelContext.setSelectedLeftData(row)`.
|
||||||
|
- if selection is cleared, call `setSelectedLeftData(null)`.
|
||||||
|
|
||||||
|
Integrate this into the existing `handleRowClick`, `handleCheckboxToggle`, or central `emitSelection` path so both single and multi-selection stay coherent.
|
||||||
|
|
||||||
|
Do not break existing `onRowSelect` / `onSelectedRowsChange` callbacks.
|
||||||
|
|
||||||
|
## 7. Right-panel linked filters
|
||||||
|
|
||||||
|
When current split position is `"right"` and SplitPanelContext has linked filters:
|
||||||
|
- merge `splitPanelContext.getLinkedFilterValues()` into the canonical active search object.
|
||||||
|
- preserve D.2 search merge priority:
|
||||||
|
- props.searchParams
|
||||||
|
- TableOptions filters
|
||||||
|
- linkedFilters from ScreenContext providers
|
||||||
|
- searchApplied
|
||||||
|
- Add split-panel linked filter values at the same layer as linkedFilters, but do not let empty values override meaningful searchApplied.
|
||||||
|
- Sanitize values using the existing `_sanitizeSearchValues` path if available.
|
||||||
|
|
||||||
|
Also support `splitPanelContext.selected_left_data` fallback for configured right-panel filters if current D.2 `excludeFilter` / linked filter logic can reuse it without large changes.
|
||||||
|
|
||||||
|
## 8. Left-panel added item filtering
|
||||||
|
|
||||||
|
Legacy table filters out items whose ids are in `splitPanelContext.added_item_ids` for left panel display.
|
||||||
|
|
||||||
|
Implement only if it is straightforward in canonical without corrupting pagination/total:
|
||||||
|
- safe option: filter only `rows` render output for current page when current split position is `"left"`.
|
||||||
|
- do not mutate `tableData.data`.
|
||||||
|
- do not change server total.
|
||||||
|
|
||||||
|
If not implemented, explicitly report it as remaining parity gap.
|
||||||
|
|
||||||
|
## 9. Forbidden work in this session
|
||||||
|
|
||||||
|
- Do not implement D.10 autoGeneration.
|
||||||
|
- Do not migrate V2List / E phases.
|
||||||
|
- Do not delete `v2-table-list`, `table-list`, aliases, schemas, or registry entries.
|
||||||
|
- Do not touch config panel parity unless a type compile error forces a tiny additive fix.
|
||||||
|
- Do not add broad new abstractions.
|
||||||
|
- Do not add network dependencies.
|
||||||
|
- Do not add toast dependencies.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n "DataProvidable|DataReceivable|useSplitPanelContext|registerDataProvider|registerDataReceiver|registerReceiver|setSelectedLeftData|getLinkedFilterValues" frontend/lib/registry/components/table frontend/contexts frontend/types
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(lib/registry/components/table/(TableComponent|useTableData|types)|contexts/(ScreenContext|SplitPanelContext)|types/data-transfer)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` passes.
|
||||||
|
- Targeted tsc grep has no output.
|
||||||
|
- If full `tsc` has unrelated baseline errors, list only errors that touch the scoped files above.
|
||||||
|
|
||||||
|
Report format:
|
||||||
|
|
||||||
|
1. Files changed with line-count scale.
|
||||||
|
2. Which B.2 option was used for SplitPanel support:
|
||||||
|
- existing context consumed only
|
||||||
|
- context additive change
|
||||||
|
- other
|
||||||
|
3. DataProvidable contract summary.
|
||||||
|
4. DataReceivable contract summary.
|
||||||
|
5. SplitPanel behavior summary:
|
||||||
|
- receiver registration
|
||||||
|
- left selected data
|
||||||
|
- right linked filters
|
||||||
|
- added item filtering implemented or deferred
|
||||||
|
6. Verification results.
|
||||||
|
7. Remaining parity gaps, if any.
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# Phase C.3 Claude Prompt — Table Canonical Filter Config Parity
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase C.3
|
||||||
|
- Already completed in the current worktree: B.1, B.4, C.1, C.2, C.5
|
||||||
|
- Codex follow-up patches already applied:
|
||||||
|
- `useTableData.ts` keeps `defaultSort` initial state instead of resetting it on mount.
|
||||||
|
- `InvTableConfigPanel.tsx` keeps `striped/hoverable` aliases synchronized with `tableStyle.alternateRows/hoverEffect`.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
Complete **Phase C.3 only**: canonical table config parity for filters.
|
||||||
|
|
||||||
|
Absorb the old table filter config surface into canonical `TableConfig` and expose it in canonical `InvTableConfigPanel`, while leaving actual runtime filter execution to Phase D.2.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Primary files:
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- `frontend/lib/registry/components/table/InvTableConfigPanel.tsx`
|
||||||
|
|
||||||
|
Allowed narrow touch:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- only for `fromProps` merge and DOM prop filtering of new config fields
|
||||||
|
- no runtime filter execution in this phase
|
||||||
|
|
||||||
|
Reference-only files:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts`
|
||||||
|
- `frontend/types/screen-management.ts`
|
||||||
|
- `frontend/components/screen/config-panels/DataFilterConfigPanel.tsx`
|
||||||
|
|
||||||
|
Do not modify old runtime bodies:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListConfigPanel.tsx`
|
||||||
|
|
||||||
|
## Required Type Work
|
||||||
|
|
||||||
|
In `frontend/lib/registry/components/table/types.ts`:
|
||||||
|
|
||||||
|
1. Import `DataFilterConfig` from `@/types/screen-management` as a type-only import.
|
||||||
|
|
||||||
|
2. Add canonical filter interfaces matching the old config shape, but name them canonically to avoid collisions:
|
||||||
|
- `TableFilterConfig`
|
||||||
|
- `TableLinkedFilterConfig`
|
||||||
|
- `TableExcludeFilterConfig`
|
||||||
|
|
||||||
|
Use the old definitions in `_shared/tableListConfigTypes.ts` as the source of truth:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
FilterConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
filters: Array<{
|
||||||
|
columnName: string;
|
||||||
|
widgetType: string;
|
||||||
|
label: string;
|
||||||
|
gridColumns: number;
|
||||||
|
numberFilterMode?: "exact" | "range";
|
||||||
|
codeInfo?: string;
|
||||||
|
referenceTable?: string;
|
||||||
|
referenceColumn?: string;
|
||||||
|
displayColumn?: string;
|
||||||
|
}>;
|
||||||
|
bottomSpacing?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedFilterConfig {
|
||||||
|
sourceComponentId: string;
|
||||||
|
sourceField?: string;
|
||||||
|
targetColumn: string;
|
||||||
|
operator?: "equals" | "contains" | "in";
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExcludeFilterConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
referenceTable: string;
|
||||||
|
referenceColumn: string;
|
||||||
|
sourceColumn: string;
|
||||||
|
filterColumn?: string;
|
||||||
|
filterValueSource?: "url" | "formData" | "parentData";
|
||||||
|
filterValueField?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add these fields to `TableConfig` with clear docstrings:
|
||||||
|
- `filter?: TableFilterConfig`
|
||||||
|
- `linkedFilters?: TableLinkedFilterConfig[]`
|
||||||
|
- `excludeFilter?: TableExcludeFilterConfig`
|
||||||
|
- `dataFilter?: DataFilterConfig`
|
||||||
|
|
||||||
|
Docstrings must clearly say:
|
||||||
|
- C.3 is config parity + ConfigPanel editing.
|
||||||
|
- Runtime application is Phase D.2.
|
||||||
|
- `dataFilter` reuses the shared `DataFilterConfig` contract from `screen-management.ts`.
|
||||||
|
|
||||||
|
## Required ConfigPanel Work
|
||||||
|
|
||||||
|
In `frontend/lib/registry/components/table/InvTableConfigPanel.tsx`:
|
||||||
|
|
||||||
|
Add a new collapsed CP group for filters. Since C.5 already added groups `⑥` to `⑨`, do not churn existing numbering. Add this group after `⑨ 툴바 버튼` and before existing `동작 옵션`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<CPGroup title="⑩ 필터" defaultOpen={false}>...</CPGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the existing CP primitives and local compact row style:
|
||||||
|
- `CPGroup`, `CPRow`, `CPText`, `CPSelect`, `CPSegment`, `CPNumber`, `CPSwitch`, `Hint`
|
||||||
|
- Native `<input style={inputStyle()}>` is acceptable where `CPText` is too constrained.
|
||||||
|
- Do not import `DataFilterConfigPanel`; it is shadcn/Tailwind styled and does not match this CP panel.
|
||||||
|
|
||||||
|
### UI Sections Inside `⑩ 필터`
|
||||||
|
|
||||||
|
Keep the UI compact. Avoid nested `CPGroup` inside this `CPGroup`.
|
||||||
|
|
||||||
|
1. **검색 필터 위젯 (`filter`)**
|
||||||
|
- switch: `filter.enabled`
|
||||||
|
- number: `filter.bottomSpacing`
|
||||||
|
- list editor for `filter.filters`
|
||||||
|
- each row edits:
|
||||||
|
- `columnName` via `CPSelect` from available columns
|
||||||
|
- `widgetType` via `CPSelect` (`text`, `number`, `date`, `select`, `entity`, `code`, `checkbox`)
|
||||||
|
- `label` via `CPText` or input
|
||||||
|
- `gridColumns` via `CPNumber`
|
||||||
|
- optional `numberFilterMode` via `CPSegment` when `widgetType === "number"`
|
||||||
|
- optional `codeInfo`, `referenceTable`, `referenceColumn`, `displayColumn` as compact text inputs
|
||||||
|
- add/remove buttons can follow the existing column editor button style.
|
||||||
|
|
||||||
|
2. **연결 필터 (`linkedFilters`)**
|
||||||
|
- list editor for `linkedFilters`
|
||||||
|
- each row edits:
|
||||||
|
- `enabled`
|
||||||
|
- `sourceComponentId`
|
||||||
|
- `sourceField`
|
||||||
|
- `targetColumn` via available columns
|
||||||
|
- `operator` via `CPSegment` (`equals`, `contains`, `in`)
|
||||||
|
- add/remove controls.
|
||||||
|
|
||||||
|
3. **제외 필터 (`excludeFilter`)**
|
||||||
|
- switch: `excludeFilter.enabled`
|
||||||
|
- text/select controls:
|
||||||
|
- `referenceTable`
|
||||||
|
- `referenceColumn`
|
||||||
|
- `sourceColumn` via available columns
|
||||||
|
- `filterColumn`
|
||||||
|
- `filterValueSource` via `CPSegment` (`url`, `formData`, `parentData`)
|
||||||
|
- `filterValueField`
|
||||||
|
- Preserve optional empty strings as `undefined` where reasonable.
|
||||||
|
|
||||||
|
4. **데이터 필터 (`dataFilter`)**
|
||||||
|
- switch: `dataFilter.enabled`
|
||||||
|
- segment: `dataFilter.match_type` (`all`, `any`)
|
||||||
|
- compact list editor for `dataFilter.filters`
|
||||||
|
- each row edits:
|
||||||
|
- `column_name` via available columns
|
||||||
|
- `operator` via `CPSelect` using the `ColumnFilter.operator` values
|
||||||
|
- `value_type` via `CPSegment` (`static`, `category`, `code`, `dynamic`)
|
||||||
|
- `value` as text input. For `in` / `not_in`, comma text is acceptable and should become `string[]`; otherwise string.
|
||||||
|
- for `date_range_contains`, expose `range_config.start_column` and `range_config.end_column`.
|
||||||
|
- New filter default:
|
||||||
|
- `id: filter-${Date.now()}`
|
||||||
|
- first available column key or `""`
|
||||||
|
- `operator: "equals"`
|
||||||
|
- `value: ""`
|
||||||
|
- `value_type: "static"`
|
||||||
|
|
||||||
|
### Available Column Options
|
||||||
|
|
||||||
|
Build a small local helper or `useMemo` to normalize available columns from:
|
||||||
|
- canonical `columns` (`TableColumn[]`)
|
||||||
|
- `effectiveTableColumns` from connected DB metadata, if canonical columns are empty
|
||||||
|
|
||||||
|
Option shape:
|
||||||
|
- `value`: canonical key / DB column name
|
||||||
|
- `label`: display label / column name
|
||||||
|
|
||||||
|
Avoid unstable assumptions about backend metadata. Support common keys already used in this file:
|
||||||
|
- `key`
|
||||||
|
- `columnName`
|
||||||
|
- `column_name`
|
||||||
|
- `name`
|
||||||
|
- `label`
|
||||||
|
- `displayName`
|
||||||
|
|
||||||
|
## Optional TableComponent Narrow Work
|
||||||
|
|
||||||
|
In `frontend/lib/registry/components/table/TableComponent.tsx`, only if needed:
|
||||||
|
|
||||||
|
1. Add `fromProps` merge for:
|
||||||
|
- `filter`
|
||||||
|
- `linkedFilters`
|
||||||
|
- `excludeFilter`
|
||||||
|
- `dataFilter`
|
||||||
|
|
||||||
|
2. Add these fields to DOM prop filtering so they never leak onto DOM nodes.
|
||||||
|
|
||||||
|
3. Add comments that runtime application is Phase D.2.
|
||||||
|
|
||||||
|
Do **not** pass these fields to `useTableData` in C.3.
|
||||||
|
Do **not** implement linked filter polling, SplitPanelContext reading, excludeFilter API params, or AdvancedSearchFilters here.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify `_shared/TableListComponent.tsx` or `_shared/V2TableListComponent.tsx`.
|
||||||
|
- Do not delete old table-list/v2-table-list code.
|
||||||
|
- Do not remove early delegation in canonical `TableComponent.tsx`.
|
||||||
|
- Do not touch schema, alias routing, template migration, component registry, or input/select/entity components.
|
||||||
|
- Do not import or revive `v2-input`, `v2-select`, `EntityPicker`, `EntitySearchModal`.
|
||||||
|
- Do not do Phase D.2 runtime filtering in this phase.
|
||||||
|
- Do not do Phase C.4 actions.
|
||||||
|
- Do not change unrelated untracked notes, especially `notes/gbpark/2026-05-20-control-ide-refactor.md`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n "TableFilterConfig|TableLinkedFilterConfig|TableExcludeFilterConfig|filter\\?:|linkedFilters|excludeFilter|dataFilter" frontend/lib/registry/components/table
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "lib/registry/components/table/(InvTableConfigPanel|TableComponent|types)|types/screen-management|DataFilterConfig"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` has no output.
|
||||||
|
- Targeted `tsc` grep shows no new errors for changed table files.
|
||||||
|
- Existing unrelated errors elsewhere may still appear if running full `tsc`; report them separately.
|
||||||
|
|
||||||
|
## Report Format
|
||||||
|
|
||||||
|
After finishing, report:
|
||||||
|
- changed files and line-count summary
|
||||||
|
- exact fields added to `TableConfig`
|
||||||
|
- which filter editors were added in `InvTableConfigPanel`
|
||||||
|
- confirmation that runtime filtering was intentionally deferred to D.2
|
||||||
|
- validation command results
|
||||||
|
- any existing unrelated `tsc` errors, clearly marked unrelated
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Phase C.4 Claude Prompt — Table Canonical Action Config Parity
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase C.4
|
||||||
|
- Already completed in the current worktree: B.1, B.4, C.1, C.2, C.3, C.5
|
||||||
|
- C.3 review follow-up already applied by Codex:
|
||||||
|
- filter column options now remove empty/duplicate values
|
||||||
|
- dataFilter operator changes normalize `value` between string and string[]
|
||||||
|
- C.3 is still config-only; runtime filter application remains D.2
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
Complete **Phase C.4 only**: canonical table config parity for row actions / bulk actions.
|
||||||
|
|
||||||
|
Absorb old `ActionConfig` into canonical `TableConfig` and expose it in canonical `InvTableConfigPanel`, while leaving actual runtime action execution to Phase D.4.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Primary files:
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- `frontend/lib/registry/components/table/InvTableConfigPanel.tsx`
|
||||||
|
|
||||||
|
Allowed narrow touch:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- only for `fromProps` merge and DOM prop filtering of new action config fields
|
||||||
|
- no runtime action execution in this phase
|
||||||
|
|
||||||
|
Reference-only files:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListConfigPanel.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
|
||||||
|
Do not modify old runtime bodies:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListConfigPanel.tsx`
|
||||||
|
|
||||||
|
Also do not touch unrelated current dirty files:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
|
||||||
|
## Required Type Work
|
||||||
|
|
||||||
|
In `frontend/lib/registry/components/table/types.ts`:
|
||||||
|
|
||||||
|
1. Add canonical action interfaces matching the old config shape, but use canonical names:
|
||||||
|
- `TableActionType`
|
||||||
|
- `TableActionItemConfig`
|
||||||
|
- `TableActionConfig`
|
||||||
|
|
||||||
|
Source of truth from `_shared/tableListConfigTypes.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
ActionConfig {
|
||||||
|
showActions: boolean;
|
||||||
|
actions: Array<{
|
||||||
|
type: "view" | "edit" | "delete" | "custom";
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
|
confirmMessage?: string;
|
||||||
|
targetScreen?: string;
|
||||||
|
}>;
|
||||||
|
bulkActions: boolean;
|
||||||
|
bulkActionList: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add to `TableConfig`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
actions?: TableActionConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
Docstring must say:
|
||||||
|
- C.4 is config parity + ConfigPanel editing.
|
||||||
|
- Runtime rendering/execution is Phase D.4.
|
||||||
|
- This is table-level row/bulk actions, separate from `cardStyle.showActions` and `cardColumnMapping.actionColumns`.
|
||||||
|
|
||||||
|
## Required ConfigPanel Work
|
||||||
|
|
||||||
|
In `frontend/lib/registry/components/table/InvTableConfigPanel.tsx`:
|
||||||
|
|
||||||
|
Add a new collapsed CP group after `⑩ 필터` and before existing `동작 옵션`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<CPGroup title="⑪ 액션" defaultOpen={false}>...</CPGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use existing CP primitives and local compact helpers:
|
||||||
|
- `CPGroup`, `CPRow`, `CPText`, `CPSelect`, `CPSegment`, `CPNumber`, `CPSwitch`, `Hint`
|
||||||
|
- Native `<input style={inputStyle()}>` is acceptable where compact text fields are easier.
|
||||||
|
- Reuse C.3 helpers if appropriate (`SubSectionHeading`, `AddRowButton`, `RemoveRowButton`), but keep write scope in this file.
|
||||||
|
|
||||||
|
### UI Sections Inside `⑪ 액션`
|
||||||
|
|
||||||
|
1. **Row actions**
|
||||||
|
- switch: `actions.showActions`
|
||||||
|
- list editor for `actions.actions`
|
||||||
|
- each row edits:
|
||||||
|
- `type` via `CPSegment` or `CPSelect`: `view`, `edit`, `delete`, `custom`
|
||||||
|
- `label`
|
||||||
|
- `icon`
|
||||||
|
- `color`
|
||||||
|
- `confirmMessage`
|
||||||
|
- `targetScreen`
|
||||||
|
- add/remove controls.
|
||||||
|
- New row default:
|
||||||
|
- `type: "view"`
|
||||||
|
- `label: "보기"`
|
||||||
|
|
||||||
|
2. **Bulk actions**
|
||||||
|
- switch: `actions.bulkActions`
|
||||||
|
- compact editor for `actions.bulkActionList`
|
||||||
|
- simple comma-separated text input is acceptable, but trim empty entries and store `string[]`.
|
||||||
|
- Help text should clarify runtime execution is D.4.
|
||||||
|
|
||||||
|
### Defaults / Patch Helper
|
||||||
|
|
||||||
|
Add a local `patchActions` helper near the C.3 helpers:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const patchActions = (next: Partial<NonNullable<TableConfig["actions"]>>) =>
|
||||||
|
patch({
|
||||||
|
actions: {
|
||||||
|
showActions: false,
|
||||||
|
actions: [],
|
||||||
|
bulkActions: false,
|
||||||
|
bulkActionList: [],
|
||||||
|
...current.actions,
|
||||||
|
...next,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not overwrite existing `actions.actions` while toggling `showActions`.
|
||||||
|
Do not overwrite existing `bulkActionList` while toggling `bulkActions`.
|
||||||
|
|
||||||
|
## Optional TableComponent Narrow Work
|
||||||
|
|
||||||
|
In `frontend/lib/registry/components/table/TableComponent.tsx`, only if needed:
|
||||||
|
|
||||||
|
1. Add `fromProps` merge for:
|
||||||
|
- `actions`
|
||||||
|
|
||||||
|
2. Add `actions` to DOM prop filtering so it never leaks onto DOM nodes.
|
||||||
|
|
||||||
|
3. Add a short comment that runtime rendering/execution is Phase D.4.
|
||||||
|
|
||||||
|
Do **not** render action columns or buttons in C.4.
|
||||||
|
Do **not** wire navigation, modal opening, delete API, custom handlers, or bulk selection execution here.
|
||||||
|
|
||||||
|
## Relationship With Existing Card Actions
|
||||||
|
|
||||||
|
Current canonical types already contain:
|
||||||
|
- `cardStyle.showActions`
|
||||||
|
- `cardStyle.showViewButton`
|
||||||
|
- `cardStyle.showEditButton`
|
||||||
|
- `cardStyle.showDeleteButton`
|
||||||
|
- `cardColumnMapping.actionColumns`
|
||||||
|
|
||||||
|
Do not remove or rewrite those. They are card-mode presentation hints. The new `actions?: TableActionConfig` is table-level row/bulk action config parity with old `ActionConfig`.
|
||||||
|
|
||||||
|
If you add UI text, make this distinction clear in help text.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify `_shared/TableListComponent.tsx` or `_shared/V2TableListComponent.tsx`.
|
||||||
|
- Do not delete old table-list/v2-table-list code.
|
||||||
|
- Do not remove early delegation in canonical `TableComponent.tsx`.
|
||||||
|
- Do not touch schema, alias routing, template migration, component registry, or input/select/entity components.
|
||||||
|
- Do not import or revive `v2-input`, `v2-select`, `EntityPicker`, `EntitySearchModal`.
|
||||||
|
- Do not do Phase D.4 runtime action rendering/execution in this phase.
|
||||||
|
- Do not do Phase D.2 filtering or Phase D.1 column runtime.
|
||||||
|
- Do not change unrelated TabBar / v5 layout CSS dirty files.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n "TableActionConfig|TableActionItemConfig|TableActionType|actions\\?:|showActions|bulkActions|bulkActionList" frontend/lib/registry/components/table/{types.ts,InvTableConfigPanel.tsx,TableComponent.tsx}
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "lib/registry/components/table/(InvTableConfigPanel|TableComponent|types)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` has no output.
|
||||||
|
- Targeted `tsc` grep shows no new errors for changed table files.
|
||||||
|
- Existing unrelated errors elsewhere may still appear if running full `tsc`; report them separately.
|
||||||
|
|
||||||
|
## Report Format
|
||||||
|
|
||||||
|
After finishing, report:
|
||||||
|
- changed files and line-count summary
|
||||||
|
- exact action types/interfaces added
|
||||||
|
- `TableConfig` fields added
|
||||||
|
- which editors were added in `InvTableConfigPanel`
|
||||||
|
- confirmation that runtime action rendering/execution was intentionally deferred to D.4
|
||||||
|
- validation command results
|
||||||
|
- unrelated dirty files left untouched
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# Phase D.1 Claude Prompt — Table Canonical Column Runtime
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.1 + §9.D.1
|
||||||
|
- Already completed in the current worktree: B.1, B.4, C.1, C.2, C.3, C.4, C.5
|
||||||
|
- C.3/C.4 are config-only. Do not start D.2 filters or D.4 actions.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
Complete **Phase D.1 only**: canonical `TableComponent.tsx` runtime support for the column system.
|
||||||
|
|
||||||
|
Implement runtime support for:
|
||||||
|
- column visibility from config + TableOptions UI callback
|
||||||
|
- `hidden` behavior: design mode shows dimmed column, runtime hides it
|
||||||
|
- `fixed: "left" | "right"` sticky columns
|
||||||
|
- `fixedOrder` ordering inside fixed groups
|
||||||
|
- `horizontalScroll`
|
||||||
|
- `autoWidth`
|
||||||
|
- `stickyHeader`
|
||||||
|
|
||||||
|
Keep the canonical table lightweight. Do not replace the whole renderer unless absolutely necessary.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Primary files:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
|
||||||
|
Reference-only files:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/SingleTableWithSticky.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts`
|
||||||
|
- `frontend/types/table-options.ts`
|
||||||
|
- `frontend/components/screen/table-options/ColumnVisibilityPanel.tsx`
|
||||||
|
- `frontend/components/screen/table-options/TableSettingsModal.tsx`
|
||||||
|
|
||||||
|
Do not modify old runtime bodies:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/SingleTableWithSticky.tsx`
|
||||||
|
|
||||||
|
Also do not touch unrelated current dirty files:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
|
||||||
|
## Required Type Work
|
||||||
|
|
||||||
|
In `frontend/lib/registry/components/table/types.ts`, add these canonical config fields to `TableConfig`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
autoWidth?: boolean;
|
||||||
|
stickyHeader?: boolean;
|
||||||
|
horizontalScroll?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
maxVisibleColumns?: number;
|
||||||
|
minColumnWidth?: number;
|
||||||
|
maxColumnWidth?: number;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Docstrings must say:
|
||||||
|
- These are legacy `TableListConfig` column/runtime options absorbed into canonical config.
|
||||||
|
- D.1 wires runtime behavior.
|
||||||
|
- `horizontalScroll.maxVisibleColumns` is a layout threshold, not a hard column visibility cap.
|
||||||
|
|
||||||
|
Do not add ConfigPanel UI in D.1 unless unavoidable. Old layout JSON compatibility and runtime support are the goal.
|
||||||
|
|
||||||
|
## Required TableComponent Work
|
||||||
|
|
||||||
|
### 1. Separate Source Columns From Runtime Columns
|
||||||
|
|
||||||
|
Currently `columns` is used as both config source and render list. For D.1, separate:
|
||||||
|
|
||||||
|
- source/config columns: all configured columns after field adapter overlay, without dropping `visible === false`
|
||||||
|
- runtime/render columns: derived from source columns + user option overrides
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- `visible === false`: hide in both design and runtime.
|
||||||
|
- `hidden === true`: in design mode keep visible but dimmed; in runtime hide.
|
||||||
|
- TableOptions visibility callback can hide/show/reorder/resize/freeze columns without mutating props.
|
||||||
|
|
||||||
|
Avoid losing ConfigPanel-only metadata (`fixed`, `hidden`, `fixedOrder`, `inputType`, `editable`, `thousandSeparator`, entity join metadata).
|
||||||
|
|
||||||
|
### 2. TableOptions Column Visibility Callback
|
||||||
|
|
||||||
|
Use `ColumnVisibility` from `@/types/table-options`.
|
||||||
|
|
||||||
|
Add local state similar to:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [columnOptionsState, setColumnOptionsState] = useState<ColumnVisibility[]>([]);
|
||||||
|
const [frozenColumnCount, setFrozenColumnCount] = useState(0);
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire registration callbacks:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
onColumnVisibilityChange: setColumnOptionsState
|
||||||
|
onFrozenColumnCountChange: (count, updatedColumns) => { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
If `updatedColumns` is passed, use it to update visible state too.
|
||||||
|
|
||||||
|
Important:
|
||||||
|
- Existing TableOptions UI does not persist column visibility in localStorage; do not claim DB persistence.
|
||||||
|
- This phase only needs mounted runtime state + `tableDisplayStore` metadata sync.
|
||||||
|
- Do not change `TableOptionsContext` unless a type mismatch forces a narrow fix.
|
||||||
|
|
||||||
|
Registration columns should include all configurable source columns, with `visible` reflecting current runtime state.
|
||||||
|
|
||||||
|
### 3. Runtime Column Derivation
|
||||||
|
|
||||||
|
Create helpers inside `TableComponent.tsx` or near top-level constants:
|
||||||
|
|
||||||
|
- normalize option overrides by `column_name`
|
||||||
|
- apply visible/order/width/fixed
|
||||||
|
- derive `renderColumns`
|
||||||
|
- derive `tableOptionsColumns`
|
||||||
|
|
||||||
|
Ordering policy:
|
||||||
|
1. TableOptions callback order, if present.
|
||||||
|
2. `column.order`, if present.
|
||||||
|
3. original source array order.
|
||||||
|
4. fixed groups:
|
||||||
|
- fixed left first
|
||||||
|
- non-fixed middle
|
||||||
|
- fixed right last
|
||||||
|
- within left/right, `fixedOrder` wins if present; otherwise current order.
|
||||||
|
|
||||||
|
`ColumnVisibility.fixed` is boolean only. Treat `true` as `fixed: "left"` because the existing options UI represents frozen left columns only.
|
||||||
|
|
||||||
|
`onFrozenColumnCountChange(count)` should mark the first `count` visible columns as `fixed: "left"` for runtime state. Do not convert right-fixed columns to left unless the callback explicitly affects them through visibility state.
|
||||||
|
|
||||||
|
### 4. Sticky Left/Right Runtime CSS
|
||||||
|
|
||||||
|
Do not import `SingleTableWithSticky` wholesale. Inspect it as a reference for offset math and port only the small sticky offset logic into canonical table if needed.
|
||||||
|
|
||||||
|
Implement sticky styles for `<th>` and `<td>`:
|
||||||
|
- `position: "sticky"`
|
||||||
|
- `left` offset for left fixed columns
|
||||||
|
- `right` offset for right fixed columns
|
||||||
|
- `zIndex` higher for header, lower for cells
|
||||||
|
- stable background color so sticky cells do not become transparent over scrolled content
|
||||||
|
- light border/shadow to distinguish frozen edge
|
||||||
|
|
||||||
|
Offset is the sum of widths of preceding left fixed columns or following right fixed columns. Use the same effective column width helper that rendering uses.
|
||||||
|
|
||||||
|
### 5. Width / Auto Width / Horizontal Scroll
|
||||||
|
|
||||||
|
Add helper:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const getColumnWidth = (col: TableColumn): number => ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- explicit `col.width` wins
|
||||||
|
- `horizontalScroll.minColumnWidth` default around 120
|
||||||
|
- `horizontalScroll.maxColumnWidth` clamps explicit/default width if present
|
||||||
|
- checkbox column stays stable at 32px
|
||||||
|
- if `autoWidth === true`, allow browser natural width by not forcing `width`, but still use numeric fallback for sticky offset math
|
||||||
|
|
||||||
|
For table/container:
|
||||||
|
- existing scroll container remains the wrapper.
|
||||||
|
- `horizontalScroll.enabled` should produce a `minWidth` for the `<table>` so horizontal scrolling actually appears.
|
||||||
|
- If `maxVisibleColumns` is set and render column count is greater than it, use at least `maxVisibleColumns * minColumnWidth` or sum of effective widths, whichever is appropriate to keep columns readable.
|
||||||
|
- `horizontalScroll.maxVisibleColumns` must not drop columns.
|
||||||
|
|
||||||
|
### 6. Sticky Header
|
||||||
|
|
||||||
|
Current canonical header is always sticky. Make it configurable:
|
||||||
|
- default preserve current behavior: `stickyHeader !== false`
|
||||||
|
- if `stickyHeader === false`, header should not be sticky.
|
||||||
|
|
||||||
|
### 7. Store Sync
|
||||||
|
|
||||||
|
Update `tableDisplayStore.setTableDataForComponent` metadata to use runtime visible columns:
|
||||||
|
- `column_labels` should include source columns or render columns consistently.
|
||||||
|
- `visible_columns` should be runtime render column keys.
|
||||||
|
- `page_size` remains existing logic.
|
||||||
|
|
||||||
|
### 8. Rendering Places To Update
|
||||||
|
|
||||||
|
Use runtime render columns consistently in:
|
||||||
|
- header rendering
|
||||||
|
- row cell rendering
|
||||||
|
- empty-state colspan
|
||||||
|
- split detail panel
|
||||||
|
- `GroupedView` columns prop
|
||||||
|
- TableOptions registration
|
||||||
|
- tableDisplayStore metadata
|
||||||
|
|
||||||
|
`CardView` and `PivotView` may continue to use their existing mapping/config fields unless the current basic table columns are directly passed.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify `_shared/TableListComponent.tsx`, `_shared/V2TableListComponent.tsx`, or `_shared/SingleTableWithSticky.tsx`.
|
||||||
|
- Do not delete old table-list/v2-table-list code.
|
||||||
|
- Do not remove early delegation in canonical `TableComponent.tsx`.
|
||||||
|
- Do not touch schema, alias routing, template migration, component registry, or input/select/entity components.
|
||||||
|
- Do not import or revive `v2-input`, `v2-select`, `EntityPicker`, `EntitySearchModal`.
|
||||||
|
- Do not implement Phase D.2 filters.
|
||||||
|
- Do not implement Phase D.3 inline edit.
|
||||||
|
- Do not implement Phase D.4 action rendering/execution.
|
||||||
|
- Do not implement Phase D.6 toolbar/export/pagination UI.
|
||||||
|
- Do not change unrelated `TabBar.tsx` or `v5-layout.css` dirty files.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n "autoWidth|stickyHeader|horizontalScroll|onColumnVisibilityChange|onFrozenColumnCountChange|fixedOrder|hidden" frontend/lib/registry/components/table/{types.ts,TableComponent.tsx}
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "lib/registry/components/table/(TableComponent|types)|types/table-options|TableOptionsContext"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` has no output.
|
||||||
|
- Targeted `tsc` grep shows no new errors for changed table files.
|
||||||
|
- Existing unrelated errors elsewhere may still appear if running full `tsc`; report them separately.
|
||||||
|
|
||||||
|
## Report Format
|
||||||
|
|
||||||
|
After finishing, report:
|
||||||
|
- changed files and line-count summary
|
||||||
|
- fields added to `TableConfig`
|
||||||
|
- how source columns vs render columns are derived
|
||||||
|
- how hidden/visible/fixed/fixedOrder are applied
|
||||||
|
- how TableOptions callbacks are wired
|
||||||
|
- what was intentionally deferred to later phases
|
||||||
|
- validation command results
|
||||||
|
- unrelated dirty files left untouched
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Table Canonical Cleanup — Phase D.10 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.10
|
||||||
|
- Completed phases: B.1, B.2, B.3, B.4, C.1~C.5, D.1~D.9.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- Do not edit `_shared/TableListComponent.tsx` or `_shared/V2TableListComponent.tsx`; read them only as parity references.
|
||||||
|
- Preserve canonical TableComponent early delegation for legacy `table-list` / `v2-table-list`.
|
||||||
|
- Do not start E-phase migration in this prompt.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
Finish Phase D.10 conservatively: canonical table should preserve and safely use table column `autoGeneration` metadata where there is a real row-creation path. If no authoritative create/new-row path exists in canonical `TableComponent`, do **not** invent one. In that case, implement only safe type/adapter preservation and report the runtime gap clearly.
|
||||||
|
|
||||||
|
Why this caution matters:
|
||||||
|
- `AutoGenerationUtils.generateValue()` supports `numbering_rule`, which can allocate real codes.
|
||||||
|
- Canonical `TableComponent` currently has inline cell update and DataReceivable local data override, but no clear “create new persisted row” UI/API path.
|
||||||
|
- Do not consume numbering rules on render, search, receive-only preview, or unrelated local state updates.
|
||||||
|
|
||||||
|
Read first:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- `frontend/lib/fieldConfig/adapters.ts`
|
||||||
|
- `frontend/lib/utils/autoGeneration.ts`
|
||||||
|
- `frontend/types/screen.ts` (`AutoGenerationConfig`)
|
||||||
|
- `frontend/lib/registry/components/input/InputComponent.tsx` autoGeneration behavior
|
||||||
|
- legacy references only:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts`
|
||||||
|
- `_shared/TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListComponent.tsx`
|
||||||
|
|
||||||
|
Implementation rules:
|
||||||
|
|
||||||
|
## 1. Audit first
|
||||||
|
|
||||||
|
Before coding, find whether canonical `TableComponent` has any persisted new-row/create path:
|
||||||
|
- add row button
|
||||||
|
- insert API
|
||||||
|
- create API
|
||||||
|
- paste-to-backend write
|
||||||
|
- batch edit save
|
||||||
|
- DataReceivable receive path that is explicitly persisted
|
||||||
|
|
||||||
|
If there is no persisted create path, say so in the report and keep runtime work minimal.
|
||||||
|
|
||||||
|
## 2. Type preservation
|
||||||
|
|
||||||
|
Add canonical type support if missing:
|
||||||
|
- `TableColumn.autoGeneration?: AutoGenerationConfig`
|
||||||
|
|
||||||
|
Use the existing `AutoGenerationConfig` from `@/types/screen` unless that causes import cycles. If import risk exists, use a structurally compatible type-only alias and explain.
|
||||||
|
|
||||||
|
Do not remove or rename legacy config keys.
|
||||||
|
|
||||||
|
## 3. Adapter preservation
|
||||||
|
|
||||||
|
Update `fieldsToCanonicalColumns()` in `frontend/lib/fieldConfig/adapters.ts` to preserve auto-generation metadata if it exists on field objects:
|
||||||
|
- `(f as any).autoGeneration`
|
||||||
|
- `(f as any).auto_generation`
|
||||||
|
|
||||||
|
Do not change `FieldConfig` itself unless absolutely necessary. It currently does not expose `autoGeneration`; this phase should tolerate metadata from legacy layouts without forcing a global schema change.
|
||||||
|
|
||||||
|
Optionally preserve the same metadata in `fieldsToColumns()` for legacy snake output if the mapping is obviously missing and low-risk:
|
||||||
|
- `autoGeneration`
|
||||||
|
- `auto_generation`
|
||||||
|
|
||||||
|
But do not touch legacy table runtime files.
|
||||||
|
|
||||||
|
## 4. Runtime behavior
|
||||||
|
|
||||||
|
Only implement runtime auto-generation if there is a real and narrow row creation path.
|
||||||
|
|
||||||
|
Allowed safe runtime surfaces:
|
||||||
|
- If canonical has a real create/new-row handler, fill empty generated columns immediately before create API call.
|
||||||
|
- If receiveData is explicitly used as a new-row input and persists rows, apply generation only to newly appended rows, not to existing merge targets.
|
||||||
|
|
||||||
|
Forbidden runtime surfaces:
|
||||||
|
- Do not generate values during render.
|
||||||
|
- Do not generate values during normal fetch.
|
||||||
|
- Do not generate values during inline edit commit unless the user is explicitly editing an auto-generated column and the value is empty.
|
||||||
|
- Do not allocate `numbering_rule` values for local-only `receiveData` overrides.
|
||||||
|
- Do not add a new “Add row” UI in this phase.
|
||||||
|
- Do not make paste write to backend.
|
||||||
|
|
||||||
|
If no safe persisted create path exists, runtime change should be **zero** except helper code that is unused is also discouraged. Prefer no unused helper.
|
||||||
|
|
||||||
|
## 5. Numbering rule policy
|
||||||
|
|
||||||
|
For `numbering_rule`:
|
||||||
|
- Only call `AutoGenerationUtils.generateValue()` from an explicit persisted create/save path.
|
||||||
|
- Pass useful form/row context if available.
|
||||||
|
- If path is not persisted, skip and report deferred.
|
||||||
|
|
||||||
|
For non-allocating types (`uuid`, `current_user`, `current_time`, `sequence`, `random_string`, `random_number`, `company_code`, `department`):
|
||||||
|
- They may be generated in a create path when target value is empty.
|
||||||
|
- Still do not generate on render/fetch.
|
||||||
|
|
||||||
|
## 6. Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n "autoGeneration|auto_generation|AutoGenerationConfig|AutoGenerationUtils" frontend/lib/registry/components/table frontend/lib/fieldConfig frontend/types/screen.ts frontend/lib/utils/autoGeneration.ts
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(lib/registry/components/table/(TableComponent|types)|lib/fieldConfig/adapters|lib/utils/autoGeneration|types/screen)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` passes.
|
||||||
|
- Targeted tsc grep has no output.
|
||||||
|
- If full tsc has unrelated baseline errors, list only scoped-file errors.
|
||||||
|
|
||||||
|
Report format:
|
||||||
|
|
||||||
|
1. Files changed.
|
||||||
|
2. Audit result:
|
||||||
|
- persisted create path exists: yes/no
|
||||||
|
- if yes, where
|
||||||
|
- if no, runtime generation deferred
|
||||||
|
3. Metadata preservation summary.
|
||||||
|
4. Runtime behavior summary:
|
||||||
|
- implemented, or intentionally zero runtime changes
|
||||||
|
5. Numbering rule policy result.
|
||||||
|
6. Verification results.
|
||||||
|
7. Remaining gap before E.1, if any.
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# Table Canonical Cleanup — Phase D.2 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.2
|
||||||
|
- Prior completed phases: B.1/B.4, C.1/C.2/C.3/C.4/C.5, D.1
|
||||||
|
- D.1 includes Codex follow-up fixes already present in the worktree:
|
||||||
|
- `fieldsToCanonicalColumns()` preserves `visible === false` source columns.
|
||||||
|
- canonical `sourceColumns` overlays FieldConfig columns with `componentConfig.columns` and appends config-only extra columns.
|
||||||
|
- fixed group ordering treats missing `fixedOrder` as last and stable.
|
||||||
|
- TableOptions registration uses stable callback deps, runtime column state, and `frozen_column_count`.
|
||||||
|
- `TableOptionsContext.updateTableDataCount()` writes `data_count`.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement Phase D.2 filter runtime parity for canonical
|
||||||
|
`frontend/lib/registry/components/table/TableComponent.tsx`.
|
||||||
|
|
||||||
|
Canonical `TableComponent` must now handle:
|
||||||
|
- `filter` / `filter.filters[]` via `AdvancedSearchFilters`
|
||||||
|
- `linkedFilters[]` from `ScreenContext` data providers and/or explicit props/search params
|
||||||
|
- `excludeFilter` by passing the legacy-compatible API payload to `entityJoinApi.getTableDataWithJoins`
|
||||||
|
- `dataFilter` by passing the configured `DataFilterConfig` to `entityJoinApi.getTableDataWithJoins`
|
||||||
|
- TableOptions `onFilterChange` callback, replacing the current D.2 placeholder
|
||||||
|
|
||||||
|
This phase is runtime wiring only. Do not expand the ConfigPanel and do not implement D.3/D.4/D.6/D.8 behavior.
|
||||||
|
|
||||||
|
## File Scope
|
||||||
|
|
||||||
|
Allowed to edit:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/useTableData.ts`
|
||||||
|
|
||||||
|
Read-only references:
|
||||||
|
- `frontend/components/screen/filters/AdvancedSearchFilters.tsx`
|
||||||
|
- `frontend/lib/api/entityJoin.ts`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/contexts/ScreenContext.tsx`
|
||||||
|
- `frontend/types/table-options.ts`
|
||||||
|
|
||||||
|
Avoid changing `AdvancedSearchFilters.tsx` unless TypeScript makes it unavoidable. If you must touch it, keep the edit tiny and explain why.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify old bodies:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- Do not remove early legacy/v2 delegation in canonical `TableComponent`.
|
||||||
|
- Do not touch schema, registry aliases, template migration, component lists, `V2List`, or split-panel files in this phase.
|
||||||
|
- Do not reintroduce `v2-input`, `v2-select`, EntityPicker, EntitySearchModal, or any deleted input/select path.
|
||||||
|
- Do not touch unrelated dirty files:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-clean-v5.html`
|
||||||
|
|
||||||
|
## Current Relevant State
|
||||||
|
|
||||||
|
`TableComponent.tsx` already merges these C.3 fields into `componentConfig` and filters them from DOM props:
|
||||||
|
- `filter`
|
||||||
|
- `linkedFilters`
|
||||||
|
- `excludeFilter`
|
||||||
|
- `dataFilter`
|
||||||
|
|
||||||
|
`useTableData.ts` currently accepts:
|
||||||
|
- `tableName`
|
||||||
|
- `page/pageSize`
|
||||||
|
- `sortBy/sortOrder`
|
||||||
|
- `search`
|
||||||
|
- `enabled`
|
||||||
|
|
||||||
|
`entityJoinApi.getTableDataWithJoins()` already supports:
|
||||||
|
- `search`
|
||||||
|
- `dataFilter`
|
||||||
|
- `excludeFilter`
|
||||||
|
|
||||||
|
See `frontend/lib/api/entityJoin.ts` lines around `getTableDataWithJoins`.
|
||||||
|
|
||||||
|
## Implementation Requirements
|
||||||
|
|
||||||
|
### 1. Extend `useTableData`
|
||||||
|
|
||||||
|
Add params:
|
||||||
|
- `dataFilter?: DataFilterConfig`
|
||||||
|
- `excludeFilter?: { enabled: boolean; referenceTable: string; referenceColumn: string; sourceColumn: string; filterColumn?: string; filterValue?: any }`
|
||||||
|
|
||||||
|
Use type-only import for `DataFilterConfig`.
|
||||||
|
|
||||||
|
Pass both through to `entityJoinApi.getTableDataWithJoins()` exactly as supported by `frontend/lib/api/entityJoin.ts`.
|
||||||
|
|
||||||
|
Dependency correctness matters:
|
||||||
|
- `fetchData` must refetch when effective `search`, `dataFilter`, or `excludeFilter` changes.
|
||||||
|
- Avoid object identity refetch loops. In `TableComponent`, memoize effective payloads before passing to `useTableData`.
|
||||||
|
- Preserve C.5 behavior: `defaultSort` must still seed initial sorting and user header sort must still override.
|
||||||
|
- Preserve C.1 behavior: `autoLoad === false` still disables initial fetch.
|
||||||
|
|
||||||
|
### 2. Advanced Search Filter UI
|
||||||
|
|
||||||
|
Import and render `AdvancedSearchFilters` in canonical `TableComponent` when:
|
||||||
|
- `componentConfig.filter?.enabled === true`
|
||||||
|
- and there is at least one configured filter, or table columns can be supplied for auto-generation.
|
||||||
|
|
||||||
|
Place the filter area above the table body and below the toolbar. Respect:
|
||||||
|
- `componentConfig.filter.bottomSpacing` as margin-bottom, default small spacing.
|
||||||
|
|
||||||
|
Convert canonical `TableFilterConfig.filters[]` and `renderColumns/sourceColumns` to the shape expected by `AdvancedSearchFilters`:
|
||||||
|
- `columnName`
|
||||||
|
- `widgetType`
|
||||||
|
- `label`
|
||||||
|
- `gridColumns`
|
||||||
|
- `numberFilterMode`
|
||||||
|
- `codeInfo`
|
||||||
|
- `referenceTable`
|
||||||
|
- `referenceColumn`
|
||||||
|
- `displayColumn`
|
||||||
|
|
||||||
|
For `tableColumns`, pass normalized column metadata from `sourceColumns`, not only visible columns, so auto-generation can see searchable hidden columns when needed. Include web type fields that `AdvancedSearchFilters` checks (`webType` / `web_type`, `columnName` / `column_name`, labels, visibility).
|
||||||
|
|
||||||
|
Search value handling:
|
||||||
|
- Keep draft/applied search state so text filters do not rely on stale React state.
|
||||||
|
- `AdvancedSearchFilters` calls `onSearchValueChange()` then `onSearch()` in the same tick for several widget types. Use a ref or functional next-value helper so `onSearch()` applies the latest value, not the previous render's value.
|
||||||
|
- `onClearFilters()` clears both draft and applied values.
|
||||||
|
- Strip empty values and `"__ALL__"` before passing to `useTableData`.
|
||||||
|
|
||||||
|
### 3. Effective Search Merge
|
||||||
|
|
||||||
|
Create one memoized `effectiveSearch` object that merges, in this order:
|
||||||
|
1. external `props.searchParams` (existing behavior)
|
||||||
|
2. TableOptions modal filters from `onFilterChange`
|
||||||
|
3. linked filter values
|
||||||
|
4. applied AdvancedSearchFilters values
|
||||||
|
|
||||||
|
Later sources win if keys collide.
|
||||||
|
|
||||||
|
Sanitize empty values:
|
||||||
|
- empty string
|
||||||
|
- `null` / `undefined`
|
||||||
|
- `"__ALL__"`
|
||||||
|
- empty arrays
|
||||||
|
- range objects with all empty members
|
||||||
|
|
||||||
|
For number/date range objects, preserve the existing `AdvancedSearchFilters` shape; do not invent a new backend contract unless an existing one is clearly used elsewhere.
|
||||||
|
|
||||||
|
### 4. Linked Filters
|
||||||
|
|
||||||
|
Use `useScreenContextOptional()` from `frontend/contexts/ScreenContext.tsx`.
|
||||||
|
|
||||||
|
For each enabled `componentConfig.linkedFilters[]`:
|
||||||
|
- read source provider via `screenContext.getDataProvider(sourceComponentId)`
|
||||||
|
- use `getSelectedData()`
|
||||||
|
- use `sourceField || "value"`
|
||||||
|
- write the value to `targetColumn`
|
||||||
|
|
||||||
|
Match legacy behavior from `_shared/TableListComponent.tsx` / `_shared/V2TableListComponent.tsx`, but keep implementation smaller:
|
||||||
|
- initial check on mount/config change
|
||||||
|
- lightweight polling is acceptable if there is no event bus; use the same 500ms interval style as legacy
|
||||||
|
- cleanup interval
|
||||||
|
- do not throw if `ScreenContext` or provider is missing
|
||||||
|
|
||||||
|
If an operator other than equality is configured and no backend `search` contract exists for it, preserve legacy-compatible plain search value and leave a short comment. Do not create a speculative query DSL.
|
||||||
|
|
||||||
|
### 5. Exclude Filter
|
||||||
|
|
||||||
|
Build `excludeFilterParam` in `TableComponent` and pass it to `useTableData`.
|
||||||
|
|
||||||
|
Legacy-compatible payload:
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
referenceTable,
|
||||||
|
referenceColumn,
|
||||||
|
sourceColumn,
|
||||||
|
filterColumn,
|
||||||
|
filterValue,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolve `filterValue` when `filterColumn` and `filterValueField` are present. Priority:
|
||||||
|
1. explicit props form data (`props.formData`)
|
||||||
|
2. `screenContext.form_data`
|
||||||
|
3. URL query param (`window.location.search`)
|
||||||
|
4. parent/split data if present on props (`splitPanelParentData`, `parentData`, or `selectedParentData`)
|
||||||
|
|
||||||
|
If `filterColumn` is absent, pass the exclude filter without a `filterValue`, same as legacy.
|
||||||
|
|
||||||
|
Memoize the resulting object to avoid unnecessary fetch loops.
|
||||||
|
|
||||||
|
### 6. Data Filter
|
||||||
|
|
||||||
|
Pass `componentConfig.dataFilter` to `useTableData` when enabled.
|
||||||
|
|
||||||
|
Do not implement client-side filtering in this phase unless the API contract is demonstrably unavailable. In this codebase it is available in `entityJoinApi`, so prefer server-side pass-through to keep total/page counts correct.
|
||||||
|
|
||||||
|
### 7. TableOptions Filter Callback
|
||||||
|
|
||||||
|
Replace the D.2 placeholder:
|
||||||
|
```ts
|
||||||
|
onFilterChange: () => undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
with state wiring:
|
||||||
|
- keep `tableOptionsFilters` state
|
||||||
|
- convert `TableFilter[]` to an object merged into `effectiveSearch`
|
||||||
|
- support at least equality-style values
|
||||||
|
- keep unsupported operators as simple values unless an existing backend shape is confirmed
|
||||||
|
|
||||||
|
Do not modify TableOptions UI files.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "lib/registry/components/table/(TableComponent|useTableData|types)|components/screen/filters/AdvancedSearchFilters|types/table-options|contexts/ScreenContext"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` has no output.
|
||||||
|
- targeted `tsc` grep has no new errors for changed files.
|
||||||
|
|
||||||
|
Also run:
|
||||||
|
```bash
|
||||||
|
rg -n "onFilterChange|AdvancedSearchFilters|linkedFilters|excludeFilter|dataFilter" frontend/lib/registry/components/table/{TableComponent.tsx,useTableData.ts}
|
||||||
|
```
|
||||||
|
|
||||||
|
Report:
|
||||||
|
- changed files and line ranges
|
||||||
|
- which filter features are runtime-wired now
|
||||||
|
- which items are intentionally deferred
|
||||||
|
- verification output
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Table Canonical Cleanup — Phase D.3 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.3
|
||||||
|
- Prior completed phases: B.1/B.4, C.1/C.2/C.3/C.4/C.5, D.1, D.2
|
||||||
|
- D.2 includes Codex follow-up fixes already present in the worktree:
|
||||||
|
- `useTableData` clears stale internal search when filters become empty.
|
||||||
|
- canonical linked filters handle `getSelectedData()` array results.
|
||||||
|
- `excludeFilter` is skipped when `filterColumn` exists but no filter value can be resolved.
|
||||||
|
- `AdvancedSearchFilters` table column metadata now includes `isVisible`.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement Phase D.3 inline cell editing in canonical
|
||||||
|
`frontend/lib/registry/components/table/TableComponent.tsx`.
|
||||||
|
|
||||||
|
Canonical table cells should enter edit mode on double-click and support input behavior by column type:
|
||||||
|
- `number` / `decimal` → number input
|
||||||
|
- `date` / `datetime` → `InlineCellDatePicker` if practical, otherwise safe text/date fallback
|
||||||
|
- `category` / `code` / `select` → select when options are already available on the column, otherwise text fallback
|
||||||
|
- default → text input
|
||||||
|
|
||||||
|
This phase should implement immediate-save inline editing only. Keep batch editing, edit-mode toolbar UX, paste behavior, and richer validation for later phases unless a tiny helper is required.
|
||||||
|
|
||||||
|
## File Scope
|
||||||
|
|
||||||
|
Allowed to edit:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
|
||||||
|
Allowed only if a tiny type or helper is necessary:
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
|
||||||
|
Read-only references:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/SingleTableWithSticky.tsx`
|
||||||
|
- `frontend/components/screen/filters/InlineCellDatePicker.tsx`
|
||||||
|
- `frontend/lib/api/screen.ts`
|
||||||
|
|
||||||
|
Do not edit old body files. They are references only.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify:
|
||||||
|
- `_shared/TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- `_shared/SingleTableWithSticky.tsx`
|
||||||
|
- Do not remove early legacy/v2 delegation in canonical `TableComponent`.
|
||||||
|
- Do not implement D.4 row/bulk actions.
|
||||||
|
- Do not implement D.5 special cell rendering except what is strictly needed for edit input choice.
|
||||||
|
- Do not implement D.6 toolbar/export/pagination/paste.
|
||||||
|
- Do not touch schema, registry aliases, template migration, V2List, split-panel files, or unrelated dirty files.
|
||||||
|
- Do not reintroduce `v2-input`, `v2-select`, EntityPicker, EntitySearchModal, or deleted input/select paths.
|
||||||
|
|
||||||
|
Unrelated dirty files to leave untouched:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
|
||||||
|
## Current Relevant State
|
||||||
|
|
||||||
|
Canonical `TableComponent.tsx` already has:
|
||||||
|
- `sourceColumns` / `renderColumns`
|
||||||
|
- `TableColumn.editable`
|
||||||
|
- `TableColumn.inputType`
|
||||||
|
- `TableConfig.isReadOnly`
|
||||||
|
- `componentConfig.toolbar?.showEditMode` config field, but D.6 owns toolbar button behavior
|
||||||
|
- `tableData.refresh()`
|
||||||
|
- `rows` derived from `tableData.data`
|
||||||
|
- `renderRows()` with `<td>` rendering in table mode
|
||||||
|
|
||||||
|
Old reference behavior:
|
||||||
|
- `_shared/TableListComponent.tsx` around lines 625-648: edit state shape
|
||||||
|
- `_shared/TableListComponent.tsx` around lines 2213-2231: double-click start edit
|
||||||
|
- `_shared/TableListComponent.tsx` around lines 2366-2458: immediate save to `/dynamic-form/update-field`
|
||||||
|
- `_shared/SingleTableWithSticky.tsx` around lines 463-553: input type branching for select/date/number/text
|
||||||
|
|
||||||
|
## Implementation Requirements
|
||||||
|
|
||||||
|
### 1. Edit State
|
||||||
|
|
||||||
|
Add minimal state to canonical `TableComponent`:
|
||||||
|
- `editingCell: { rowIndex, columnKey, originalValue } | null`
|
||||||
|
- `editingValue: string`
|
||||||
|
- `editInputRef`
|
||||||
|
- optional `savingCellKey` / `editError`
|
||||||
|
|
||||||
|
Focus/select the input when edit starts.
|
||||||
|
|
||||||
|
### 2. Edit Entry Rules
|
||||||
|
|
||||||
|
Start editing on double-click of a data cell only when all are true:
|
||||||
|
- not design mode
|
||||||
|
- not `componentConfig.isReadOnly`
|
||||||
|
- column `editable !== false`
|
||||||
|
- column key is not checkbox/internal
|
||||||
|
- `tableName` is known
|
||||||
|
- row exists
|
||||||
|
|
||||||
|
Do not start edit for grouped/card/pivot/split detail panels unless table-mode cells naturally reuse the same render path. Keep the scope to cells rendered by canonical basic table.
|
||||||
|
|
||||||
|
### 3. Save Rules
|
||||||
|
|
||||||
|
Save on:
|
||||||
|
- Enter
|
||||||
|
- blur
|
||||||
|
|
||||||
|
Cancel on:
|
||||||
|
- Escape
|
||||||
|
|
||||||
|
If value did not change, close without API call.
|
||||||
|
|
||||||
|
Persist using the existing endpoint pattern from old body:
|
||||||
|
```ts
|
||||||
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
|
await apiClient.put("/dynamic-form/update-field", {
|
||||||
|
tableName,
|
||||||
|
keyField,
|
||||||
|
keyValue,
|
||||||
|
updateField: columnKey,
|
||||||
|
updateValue,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Primary key:
|
||||||
|
- use `(componentConfig as any).primaryKey` if present
|
||||||
|
- else use `"id"`
|
||||||
|
- else try `${tableName}_id`
|
||||||
|
- if no key value exists, close edit and surface a non-crashing console warning
|
||||||
|
|
||||||
|
After successful save:
|
||||||
|
- call `tableData.refresh()`
|
||||||
|
- clear edit state
|
||||||
|
|
||||||
|
On failure:
|
||||||
|
- keep edit state open
|
||||||
|
- store a short error string or console.error
|
||||||
|
- do not corrupt local table data
|
||||||
|
|
||||||
|
### 4. Value Normalization
|
||||||
|
|
||||||
|
Normalize before save:
|
||||||
|
- empty string → `null`
|
||||||
|
- number/decimal → `Number(value)` if value is non-empty and numeric
|
||||||
|
- checkbox/boolean may be deferred unless trivial
|
||||||
|
- date/datetime should pass string value from picker/input
|
||||||
|
|
||||||
|
### 5. Input Rendering
|
||||||
|
|
||||||
|
When cell is editing, render an input inside the cell:
|
||||||
|
- text input for default
|
||||||
|
- number input for number/decimal
|
||||||
|
- date input or `InlineCellDatePicker` for date/datetime
|
||||||
|
- select for category/code/select only when options are already available in `column.options` or `column` has an obvious array option field; otherwise text fallback
|
||||||
|
|
||||||
|
Use inline styles consistent with existing canonical table (not Tailwind-only styling unless already in this file).
|
||||||
|
|
||||||
|
Stop propagation for input click/key events so row selection does not fire unexpectedly.
|
||||||
|
|
||||||
|
### 6. Avoid Scope Creep
|
||||||
|
|
||||||
|
Do not implement:
|
||||||
|
- batch edit mode
|
||||||
|
- pending changes map
|
||||||
|
- validation framework
|
||||||
|
- category/code option fetching or code cache
|
||||||
|
- cascading lookup loading
|
||||||
|
- toolbar edit mode button
|
||||||
|
- toast dependency
|
||||||
|
|
||||||
|
Leave comments for deferred parts only where they help future phases.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/registry/components/table/(TableComponent|types)"
|
||||||
|
rg -n "editingCell|editingValue|update-field|InlineCellDatePicker|onDoubleClick" frontend/lib/registry/components/table/TableComponent.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` has no output.
|
||||||
|
- targeted `tsc` grep has no new errors for changed files.
|
||||||
|
|
||||||
|
Report:
|
||||||
|
- changed files
|
||||||
|
- edit entry/save/cancel behavior
|
||||||
|
- input type branches implemented
|
||||||
|
- intentionally deferred items
|
||||||
|
- verification output
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Table Canonical Cleanup — Phase D.4 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.4
|
||||||
|
- Prior completed phases: B.1/B.4, C.1/C.2/C.3/C.4/C.5, D.1, D.2, D.3
|
||||||
|
- C.4 already added `TableActionType`, `TableActionItemConfig`, `TableActionConfig`, and `TableConfig.actions`.
|
||||||
|
- D.3 includes Codex follow-up fixes already present in the worktree:
|
||||||
|
- checkbox/boolean columns are not editable in canonical inline edit.
|
||||||
|
- inline edit uses a draft ref so date picker save sees the latest value.
|
||||||
|
- Enter/blur duplicate save is guarded by a commit ref.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement Phase D.4 row and bulk action runtime wiring for canonical
|
||||||
|
`frontend/lib/registry/components/table/TableComponent.tsx`.
|
||||||
|
|
||||||
|
This phase should make `componentConfig.actions` usable at runtime:
|
||||||
|
- render a row action column for basic table mode
|
||||||
|
- execute `view` / `edit` / `delete` / `custom` row actions
|
||||||
|
- render a small bulk action bar when row selection + bulk actions are enabled
|
||||||
|
- execute configured bulk actions conservatively
|
||||||
|
|
||||||
|
Keep this as runtime wiring. Do not expand ConfigPanel.
|
||||||
|
|
||||||
|
## File Scope
|
||||||
|
|
||||||
|
Allowed to edit:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
|
||||||
|
Allowed only if a tiny type fix is necessary:
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
|
||||||
|
Read-only references:
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- `frontend/lib/registry/components/table/views/CardView.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/lib/api/screen.ts`
|
||||||
|
- `frontend/types/component.ts`
|
||||||
|
|
||||||
|
Do not edit old body files or `CardView.tsx` unless TypeScript makes it unavoidable. Prefer wiring existing `CardView` props from `TableComponent` if you support card mode.
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify:
|
||||||
|
- `_shared/TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- `_shared/SingleTableWithSticky.tsx`
|
||||||
|
- Do not remove early legacy/v2 delegation in canonical `TableComponent`.
|
||||||
|
- Do not implement D.5 special cell rendering.
|
||||||
|
- Do not implement D.6 toolbar/export/pagination/paste.
|
||||||
|
- Do not implement schema/alias/V2List/split-panel cleanup.
|
||||||
|
- Do not reintroduce deleted v2 input/select/entity modal paths.
|
||||||
|
- Do not touch unrelated dirty files:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
|
||||||
|
## Current Relevant State
|
||||||
|
|
||||||
|
`TableComponent.tsx` already has:
|
||||||
|
- `componentConfig.actions` merged from props/config
|
||||||
|
- `selectedRows: Set<number>` where selected values are row indexes
|
||||||
|
- `rows` for current visible data
|
||||||
|
- `tableName`
|
||||||
|
- `tableData.refresh()`
|
||||||
|
- D.3 helper `resolveKeyField(row)` for primary key resolution
|
||||||
|
- checkbox column / selection handlers
|
||||||
|
- `renderHeader()` and `renderRows()` for basic table mode
|
||||||
|
|
||||||
|
Action config shape:
|
||||||
|
```ts
|
||||||
|
interface TableActionConfig {
|
||||||
|
showActions: boolean;
|
||||||
|
actions: TableActionItemConfig[];
|
||||||
|
bulkActions: boolean;
|
||||||
|
bulkActionList: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableActionItemConfig {
|
||||||
|
type: "view" | "edit" | "delete" | "custom";
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
|
confirmMessage?: string;
|
||||||
|
targetScreen?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Requirements
|
||||||
|
|
||||||
|
### 1. Normalize Active Actions
|
||||||
|
|
||||||
|
Create memoized helpers:
|
||||||
|
- `rowActions`: enabled when `componentConfig.actions?.showActions === true` and actions array non-empty
|
||||||
|
- `bulkActionNames`: enabled when `componentConfig.actions?.bulkActions === true` and `bulkActionList` non-empty
|
||||||
|
|
||||||
|
Filter out malformed row actions with no `type`.
|
||||||
|
|
||||||
|
### 2. Row Action Column
|
||||||
|
|
||||||
|
In basic table mode only:
|
||||||
|
- add an "actions" `<th>` at the right side when row actions are enabled
|
||||||
|
- add matching `<td>` per row
|
||||||
|
- update empty-row `colSpan`
|
||||||
|
- make action cell compact and stable width
|
||||||
|
- stop event propagation so action clicks do not select rows or start inline edit
|
||||||
|
|
||||||
|
Use lucide icons for known action types if already importing is acceptable:
|
||||||
|
- `view` → `Eye`
|
||||||
|
- `edit` → `Pencil`
|
||||||
|
- `delete` → `Trash2`
|
||||||
|
- `custom` → `MoreHorizontal` or `Play`
|
||||||
|
|
||||||
|
If the configured `icon` string is unknown, ignore it or show the label; do not build a risky dynamic import.
|
||||||
|
|
||||||
|
### 3. Row Action Execution
|
||||||
|
|
||||||
|
Create a single handler:
|
||||||
|
```ts
|
||||||
|
handleRowAction(action, row, rowIndex)
|
||||||
|
```
|
||||||
|
|
||||||
|
Order:
|
||||||
|
1. stop propagation at button level
|
||||||
|
2. if `action.confirmMessage` exists, `window.confirm()` before proceeding
|
||||||
|
3. call optional external callbacks if present:
|
||||||
|
- `props.onRowAction?.(action, row, context)`
|
||||||
|
- `props.onTableAction?.(action, row, context)`
|
||||||
|
- `props.onAction?.(action.type, row, context)`
|
||||||
|
4. perform built-in fallback by `action.type`
|
||||||
|
|
||||||
|
Context should include:
|
||||||
|
- `tableName`
|
||||||
|
- `rowIndex`
|
||||||
|
- `keyField`
|
||||||
|
- `keyValue`
|
||||||
|
- `componentId`
|
||||||
|
|
||||||
|
Built-in fallback:
|
||||||
|
- `view` / `edit`:
|
||||||
|
- if `action.targetScreen` is a numeric string, dispatch:
|
||||||
|
```ts
|
||||||
|
window.dispatchEvent(new CustomEvent("openScreenModal", {
|
||||||
|
detail: {
|
||||||
|
screenId: Number(action.targetScreen),
|
||||||
|
urlParams: {
|
||||||
|
mode: action.type,
|
||||||
|
editId: keyValue,
|
||||||
|
tableName,
|
||||||
|
primaryKeyColumn: keyField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
- if `targetScreen` starts with `/`, use `window.location.assign(targetScreen)`.
|
||||||
|
- otherwise do not crash; console.info a short message.
|
||||||
|
- `delete`:
|
||||||
|
- resolve row id using D.3 `resolveKeyField(row)` / `keyValue`
|
||||||
|
- confirm with `action.confirmMessage || "이 행을 삭제하시겠습니까?"`
|
||||||
|
- call `apiClient.delete(`/table-management/tables/${tableName}/delete`, { data: { ids: [String(keyValue)] } })`
|
||||||
|
- on success: clear selection for deleted index if needed and `tableData.refresh()`
|
||||||
|
- `custom`:
|
||||||
|
- dispatch `window.dispatchEvent(new CustomEvent("tableRowAction", { detail }))` after callbacks
|
||||||
|
- no built-in mutation
|
||||||
|
|
||||||
|
Do not add toast dependency.
|
||||||
|
|
||||||
|
### 4. Bulk Action Bar
|
||||||
|
|
||||||
|
When all are true:
|
||||||
|
- not design mode
|
||||||
|
- `bulkActionNames.length > 0`
|
||||||
|
- `selectedRows.size > 0`
|
||||||
|
|
||||||
|
Render a compact bar between filters and body, or just above the table body:
|
||||||
|
- selected count
|
||||||
|
- one button per bulk action name
|
||||||
|
|
||||||
|
Button click:
|
||||||
|
```ts
|
||||||
|
handleBulkAction(actionName)
|
||||||
|
```
|
||||||
|
|
||||||
|
Rows:
|
||||||
|
- selected row indexes are in `selectedRows`
|
||||||
|
- selected data is `Array.from(selectedRows).map(i => rows[i]).filter(Boolean)`
|
||||||
|
|
||||||
|
Built-in bulk behavior:
|
||||||
|
- `delete`: confirm, collect ids via `resolveKeyField(row)`, call the same delete endpoint with all ids, clear selection, refresh
|
||||||
|
- `export` / `copy` / unknown: call optional `props.onBulkAction?.(actionName, selectedRowsData, context)` and dispatch `tableBulkAction`; no built-in mutation
|
||||||
|
|
||||||
|
Do not implement Excel export here; D.6 owns export/copy toolbar behavior.
|
||||||
|
|
||||||
|
### 5. Card View Optional Wiring
|
||||||
|
|
||||||
|
If simple and safe, wire existing `CardView` callbacks:
|
||||||
|
- map first configured `view` action to `onView`
|
||||||
|
- first `edit` to `onEdit`
|
||||||
|
- first `delete` to `onDelete`
|
||||||
|
|
||||||
|
Do not edit `CardView.tsx`. If card mode conflicts with `cardStyle.showActions`, leave it untouched and report that row action column is table-mode only for this phase.
|
||||||
|
|
||||||
|
### 6. Styling
|
||||||
|
|
||||||
|
Use the existing inline style pattern in `TableComponent.tsx`.
|
||||||
|
- no nested cards
|
||||||
|
- no layout shifts
|
||||||
|
- compact icon buttons
|
||||||
|
- action header/cell fixed width
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/registry/components/table/(TableComponent|types)"
|
||||||
|
rg -n "handleRowAction|handleBulkAction|tableRowAction|tableBulkAction|actions\\?|bulkAction" frontend/lib/registry/components/table/TableComponent.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` has no output.
|
||||||
|
- targeted `tsc` grep has no new errors for changed files.
|
||||||
|
|
||||||
|
Report:
|
||||||
|
- changed files
|
||||||
|
- row action rendering and handlers
|
||||||
|
- bulk action rendering and handlers
|
||||||
|
- built-in fallback behavior
|
||||||
|
- intentionally deferred items
|
||||||
|
- verification output
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# Table Canonical Cleanup — Phase D.5 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.5
|
||||||
|
- Prior completed phases: B.1/B.4, C.1/C.2/C.3/C.4/C.5, D.1, D.2, D.3, D.4
|
||||||
|
- D.3 already implemented basic-table inline editing.
|
||||||
|
- D.4 already implemented row/bulk action runtime wiring.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement Phase D.5 special cell rendering for canonical table:
|
||||||
|
- image cells
|
||||||
|
- file / attachment cells
|
||||||
|
- entity display cells using `entityDisplayConfig`
|
||||||
|
- number/date/boolean display formatting
|
||||||
|
- column `langKey` label translation support
|
||||||
|
|
||||||
|
This phase should replace the canonical table's current plain
|
||||||
|
`String(row[col.key])` display path with a safe canonical cell renderer. Keep the
|
||||||
|
scope to display rendering. Do not change ConfigPanel.
|
||||||
|
|
||||||
|
## File Scope
|
||||||
|
|
||||||
|
Allowed to edit:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
|
||||||
|
Preferred if the implementation grows beyond a small helper:
|
||||||
|
- create `frontend/lib/registry/components/table/cell-renderers.tsx`
|
||||||
|
|
||||||
|
Allowed only if absolutely required for a type-only compatibility fix:
|
||||||
|
- `frontend/lib/fieldConfig/adapters.ts`
|
||||||
|
|
||||||
|
Read-only references:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `TableCellImage` lines near 21-124
|
||||||
|
- `TableCellFile` lines near 128-314
|
||||||
|
- `formatCellValue` lines near 4489-4700
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `formatCellValue` lines near 4171-4445
|
||||||
|
- `frontend/contexts/ScreenMultiLangContext.tsx`
|
||||||
|
- `frontend/lib/api/client.ts`
|
||||||
|
- `frontend/lib/api/file.ts`
|
||||||
|
- `frontend/lib/formatting.ts`
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify:
|
||||||
|
- `_shared/TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- `_shared/SingleTableWithSticky.tsx`
|
||||||
|
- Do not remove early legacy/v2 delegation in canonical `TableComponent`.
|
||||||
|
- Do not implement D.6 toolbar/export/pagination/paste.
|
||||||
|
- Do not implement D.7 card-mode full parity.
|
||||||
|
- Do not implement schema/alias/V2List/split-panel cleanup.
|
||||||
|
- Do not reintroduce deleted v2 input/select/entity modal paths.
|
||||||
|
- Do not add toast dependencies.
|
||||||
|
- Do not use `require()` inside React render paths. Use static imports or dynamic
|
||||||
|
imports inside effects/handlers.
|
||||||
|
- Do not touch unrelated dirty files:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
|
||||||
|
## Current Relevant State
|
||||||
|
|
||||||
|
`TableComponent.tsx` currently renders basic-table cells as plain strings:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{isEditingThisCell ? (
|
||||||
|
renderEditInput(col)
|
||||||
|
) : isDesignMode ? (
|
||||||
|
<span style={{ color: "hsl(var(--muted-foreground))" }}>
|
||||||
|
{row[col.key] != null ? String(row[col.key]) : "..."}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{row[col.key] != null ? String(row[col.key]) : ""}</span>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
`TableColumn` already has these Phase C.2 fields:
|
||||||
|
- `inputType`
|
||||||
|
- `dataType`
|
||||||
|
- `format`
|
||||||
|
- `thousandSeparator`
|
||||||
|
- `isEntityJoin`
|
||||||
|
- `entityJoinInfo`
|
||||||
|
- `entityDisplayConfig`
|
||||||
|
- `additionalJoinInfo`
|
||||||
|
|
||||||
|
It does not yet explicitly type `langKey` / `langKeyId`; add them if needed.
|
||||||
|
|
||||||
|
## Implementation Requirements
|
||||||
|
|
||||||
|
### 1. Add Canonical Cell Renderer
|
||||||
|
|
||||||
|
Create a helper that can be used from `TableComponent`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
renderCellValue(value, column, row, options)
|
||||||
|
```
|
||||||
|
|
||||||
|
or, if placed in a new file:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function renderTableCellValue(args): React.ReactNode
|
||||||
|
```
|
||||||
|
|
||||||
|
Inputs should include:
|
||||||
|
- raw `value`
|
||||||
|
- `column: TableColumn`
|
||||||
|
- `row: Record<string, any>`
|
||||||
|
- `isDesignMode`
|
||||||
|
- optional `getTranslatedText`
|
||||||
|
|
||||||
|
Use this helper in the basic table body display path. Keep D.3 inline edit
|
||||||
|
unchanged: if a cell is editing, `renderEditInput(col)` still wins.
|
||||||
|
|
||||||
|
### 2. Entity Display Config
|
||||||
|
|
||||||
|
Apply `column.entityDisplayConfig` before null/empty fallback, because joined
|
||||||
|
display values can live elsewhere in the row even when `row[col.key]` is empty.
|
||||||
|
|
||||||
|
Support both forms:
|
||||||
|
- `displayColumns`
|
||||||
|
- legacy `selectedColumns`
|
||||||
|
|
||||||
|
For each display column, try these row keys in order:
|
||||||
|
- `${column.key}_${displayColumn}`
|
||||||
|
- `${column.entityJoinInfo?.sourceColumn}_${displayColumn}`
|
||||||
|
- `${column.entityDisplayConfig?.joinTable}_${displayColumn}`
|
||||||
|
- `${column.entityJoinInfo?.joinAlias}_${displayColumn}`
|
||||||
|
- direct `displayColumn`
|
||||||
|
- if `displayColumn` contains `.`, also try the last segment
|
||||||
|
|
||||||
|
Join non-empty values using `separator || " - "`. If the result is non-empty,
|
||||||
|
return it. If not, fall through to normal value formatting.
|
||||||
|
|
||||||
|
Do not add new entity fetches in this phase. Use data already returned in the row.
|
||||||
|
|
||||||
|
### 3. Image Cell
|
||||||
|
|
||||||
|
Render image cells when:
|
||||||
|
- `column.inputType === "image"`
|
||||||
|
- or `column.format === "image"`
|
||||||
|
|
||||||
|
Value formats to support:
|
||||||
|
- numeric objid string
|
||||||
|
- comma-separated objid/path string
|
||||||
|
- JSON array of objects/ids if trivial to support
|
||||||
|
- direct URL or `/uploads/...` path
|
||||||
|
|
||||||
|
Use existing APIs:
|
||||||
|
- `getFullImageUrl` from `@/lib/api/client`
|
||||||
|
- `getFilePreviewUrl` from `@/lib/api/file`
|
||||||
|
- optionally dynamic `getFileInfoByObjid` only to choose representative image for
|
||||||
|
comma-separated numeric objids. Keep this conservative.
|
||||||
|
|
||||||
|
Rendering:
|
||||||
|
- compact thumbnail, stable 32-40px size
|
||||||
|
- click opens preview/new tab and stops row click propagation
|
||||||
|
- safe loading/error fallback
|
||||||
|
- clean up blob/object URLs if you create any
|
||||||
|
- do not start remote image/file lookup in design mode; show a muted placeholder
|
||||||
|
or direct path preview only.
|
||||||
|
|
||||||
|
Prefer a small `TableCellImage` component in `cell-renderers.tsx` if the logic is
|
||||||
|
not trivial.
|
||||||
|
|
||||||
|
### 4. File / Attachment Cell
|
||||||
|
|
||||||
|
Render file cells when:
|
||||||
|
- `column.inputType === "file"` or `"attachment"`
|
||||||
|
- or `column.format === "file"` or `"attachment"`
|
||||||
|
- or column key contains `attachment` / `file` case-insensitively
|
||||||
|
|
||||||
|
Value formats to support:
|
||||||
|
- objid string
|
||||||
|
- comma-separated objids
|
||||||
|
- JSON array objects with `objid` / `id` / `realFileName` / `real_file_name` /
|
||||||
|
`name` / `fileExt` / `file_ext` / `fileSize` / `file_size`
|
||||||
|
|
||||||
|
Use existing APIs:
|
||||||
|
- dynamic `getFileInfoByObjid` from `@/lib/api/file`
|
||||||
|
- `getFilePreviewUrl` from `@/lib/api/file`
|
||||||
|
- dynamic `apiClient` only inside a download handler if you implement download
|
||||||
|
|
||||||
|
Rendering:
|
||||||
|
- compact icon + filename text
|
||||||
|
- if multiple files, show count or joined names without forcing row height growth
|
||||||
|
- click stops row selection and opens preview/list modal or preview URL
|
||||||
|
- no upload/edit/delete behavior; D.5 is display-only
|
||||||
|
- no shadcn modal dependency required. If you need a modal, use a minimal inline
|
||||||
|
overlay like old V2 did, but keep it small.
|
||||||
|
|
||||||
|
Prefer a small `TableCellFile` component in `cell-renderers.tsx` if the logic is
|
||||||
|
not trivial.
|
||||||
|
|
||||||
|
### 5. Number / Date / Boolean Formatting
|
||||||
|
|
||||||
|
Implement display formatting for basic cells:
|
||||||
|
- `inputType === "number" | "decimal"` or `format === "number"`:
|
||||||
|
- if `column.thousandSeparator !== false`, use central formatting from
|
||||||
|
`@/lib/formatting` if available, otherwise `toLocaleString("ko-KR")`.
|
||||||
|
- if disabled, show numeric string without separators.
|
||||||
|
- `format === "currency"`:
|
||||||
|
- use central currency formatting if available, otherwise `₩` + localized number.
|
||||||
|
- `inputType === "date" | "datetime"` or `format === "date"`:
|
||||||
|
- render `YYYY-MM-DD` for valid values.
|
||||||
|
- `format === "boolean"` or `inputType === "checkbox"`:
|
||||||
|
- render `예` / `아니오` for boolean-like values.
|
||||||
|
|
||||||
|
Do not change sort/filter semantics. This is display only.
|
||||||
|
|
||||||
|
### 6. Column Label Translation
|
||||||
|
|
||||||
|
Add optional `langKey?: string` and `langKeyId?: number` to `TableColumn` if not
|
||||||
|
present.
|
||||||
|
|
||||||
|
Use `useScreenMultiLang()` in `TableComponent` to translate header labels:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { getTranslatedText } = useScreenMultiLang();
|
||||||
|
const label = getTranslatedText(col.langKey, col.label);
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply the translated label in:
|
||||||
|
- basic table header
|
||||||
|
- `tableDisplayStore.setTableDataForComponent` `column_labels` if that data is
|
||||||
|
already being synced
|
||||||
|
|
||||||
|
Do not edit `ScreenMultiLangContext.tsx`; it already collects `componentConfig.columns[].langKey`.
|
||||||
|
|
||||||
|
### 7. Keep Existing Phase Behavior
|
||||||
|
|
||||||
|
Do not regress:
|
||||||
|
- D.1 visible/hidden/fixed/sticky column rendering
|
||||||
|
- D.2 filter runtime
|
||||||
|
- D.3 inline edit entry/save/cancel
|
||||||
|
- D.4 row action column and bulk action bar
|
||||||
|
|
||||||
|
Action cells and checkbox cells should not use the special cell renderer.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/registry/components/table/(TableComponent|types|cell-renderers)|^lib/fieldConfig/adapters"
|
||||||
|
rg -n "TableCellImage|TableCellFile|renderTableCellValue|entityDisplayConfig|thousandSeparator|useScreenMultiLang|langKey" frontend/lib/registry/components/table
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` prints nothing.
|
||||||
|
- targeted `tsc | rg ...` prints nothing.
|
||||||
|
- grep shows canonical table files only, plus old `_shared` references if your
|
||||||
|
grep includes `_shared`.
|
||||||
|
|
||||||
|
Known unrelated baseline errors may still appear if you run full `tsc` without a
|
||||||
|
targeted grep:
|
||||||
|
- `components/screen/filters/AdvancedSearchFilters.tsx` import of
|
||||||
|
`@/types/screen-legacy-backup`
|
||||||
|
- `lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx`
|
||||||
|
`DataReceivable` import from `ScreenContext`
|
||||||
|
|
||||||
|
## Final Report
|
||||||
|
|
||||||
|
Report:
|
||||||
|
- changed files
|
||||||
|
- whether a new `cell-renderers.tsx` was created
|
||||||
|
- exact cell types now handled
|
||||||
|
- entity display fallback key order
|
||||||
|
- whether `langKey` translation was wired
|
||||||
|
- validation command results
|
||||||
|
- any intentionally deferred items
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
# Table Canonical Cleanup — Phase D.6 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.6
|
||||||
|
- Prior completed phases: B.1/B.4, C.1/C.2/C.3/C.4/C.5, D.1, D.2, D.3, D.4, D.5
|
||||||
|
- C.5 already added `tableStyle`, `toolbar`, `defaultSort`, `refreshInterval`, and
|
||||||
|
expanded `pagination`.
|
||||||
|
- D.5 added `cell-renderers.tsx` and Codex follow-up fixes already present:
|
||||||
|
- `profile`-like keys are no longer treated as file columns just because they contain `file`.
|
||||||
|
- file cells reset loading state when value changes.
|
||||||
|
- translated column labels are memoized and synced into `tableDisplayStore`.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement Phase D.6 runtime wiring for canonical `TableComponent.tsx`:
|
||||||
|
- toolbar buttons (`showEditMode`, `showExcel`, `showPdf`, `showCopy`,
|
||||||
|
`showSearch`, `showFilter`, `showRefresh`)
|
||||||
|
- footer pagination controls (`showSizeSelector`, `showPageInfo`,
|
||||||
|
`pageSizeOptions`, `showPaginationRefresh`, `position`)
|
||||||
|
- lazy XLSX export
|
||||||
|
- clipboard copy
|
||||||
|
- conservative paste handling
|
||||||
|
- basic runtime styling for `tableStyle.theme`, `headerStyle`, `borderStyle`
|
||||||
|
|
||||||
|
Keep this as canonical runtime wiring. Do not expand ConfigPanel.
|
||||||
|
|
||||||
|
## File Scope
|
||||||
|
|
||||||
|
Allowed to edit:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
|
||||||
|
Allowed only if absolutely required for a tiny type fix:
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- `frontend/lib/registry/components/table/cell-renderers.tsx`
|
||||||
|
|
||||||
|
Read-only references:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `exportToExcel` near 2618-2715
|
||||||
|
- copy helpers near 3161-3184 and 6554-6568
|
||||||
|
- toolbar/footer rendering near 5087-5520
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `exportToExcel` near 2972-3042
|
||||||
|
- copy helpers near 3466-3489 and 6930-6944
|
||||||
|
- toolbar/footer rendering near 5503-5865
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/stores/tableDisplayStore.ts`
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify:
|
||||||
|
- `_shared/TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- `_shared/SingleTableWithSticky.tsx`
|
||||||
|
- Do not remove early legacy/v2 delegation in canonical `TableComponent`.
|
||||||
|
- Do not implement D.7 card-mode full parity.
|
||||||
|
- Do not implement D.8 GroupSum / table options advanced summaries.
|
||||||
|
- Do not implement schema/alias/V2List/split-panel cleanup.
|
||||||
|
- Do not add static `import * as XLSX from "xlsx"` at module top. Use dynamic
|
||||||
|
`await import("xlsx")` inside the export handler.
|
||||||
|
- Do not add PDF libraries. PDF button should use a conservative fallback
|
||||||
|
(`window.print()` or `tablePdfExport` CustomEvent) unless a local existing helper
|
||||||
|
is already available without adding dependencies.
|
||||||
|
- Do not add toast dependencies.
|
||||||
|
- Do not touch unrelated dirty files:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
|
||||||
|
## Current Relevant State
|
||||||
|
|
||||||
|
`TableComponent.tsx` currently has:
|
||||||
|
- `renderToolbar()` with refresh and disabled Excel only.
|
||||||
|
- `renderFooter()` with simple prev/page/next only.
|
||||||
|
- `renderColumns` from D.1 for visible ordered columns.
|
||||||
|
- `rows` for current page/display data.
|
||||||
|
- `selectedRows: Set<number>` where values are current `rows` indexes.
|
||||||
|
- `tableData.refresh()`, `tableData.setPage()`, `tableData.totalPages`.
|
||||||
|
- D.2 `AdvancedSearchFilters` always renders when `filter.enabled`.
|
||||||
|
- D.3 inline editing.
|
||||||
|
- D.4 row/bulk actions.
|
||||||
|
- D.5 `renderTableCellValue()` for display cells.
|
||||||
|
|
||||||
|
Important: preserve all previous behavior. Toolbar controls should not break
|
||||||
|
filtering, inline edit, row actions, or sticky columns.
|
||||||
|
|
||||||
|
## Implementation Requirements
|
||||||
|
|
||||||
|
### 1. Toolbar Visibility Aliases
|
||||||
|
|
||||||
|
Normalize toolbar flags near the existing `_showRefreshBtn` / `_showExcelBtn`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const toolbar = componentConfig.toolbar ?? {};
|
||||||
|
const _showRefreshBtn = toolbar.showRefresh ?? componentConfig.showRefresh;
|
||||||
|
const _showExcelBtn = toolbar.showExcel ?? componentConfig.showExcel;
|
||||||
|
const _showEditModeBtn = toolbar.showEditMode === true;
|
||||||
|
const _showPdfBtn = toolbar.showPdf === true;
|
||||||
|
const _showCopyBtn = toolbar.showCopy === true;
|
||||||
|
const _showSearchBtn = toolbar.showSearch === true;
|
||||||
|
const _showFilterBtn = toolbar.showFilter === true;
|
||||||
|
const _showPaginationRefreshBtn = toolbar.showPaginationRefresh === true;
|
||||||
|
```
|
||||||
|
|
||||||
|
Use existing inline style patterns and lucide icons if already imported. Prefer
|
||||||
|
icon buttons with `title`.
|
||||||
|
|
||||||
|
### 2. Edit Mode Toggle
|
||||||
|
|
||||||
|
Add local state:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [editModeEnabled, setEditModeEnabled] = useState(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
When `toolbar.showEditMode` is true:
|
||||||
|
- render an edit-mode toggle button.
|
||||||
|
- `canEditCell()` should require `editModeEnabled`.
|
||||||
|
|
||||||
|
When `toolbar.showEditMode` is false or undefined:
|
||||||
|
- preserve D.3 behavior: editable cells can still enter edit mode by double-click.
|
||||||
|
|
||||||
|
Do not implement batch-edit pending changes in D.6.
|
||||||
|
|
||||||
|
### 3. XLSX Export
|
||||||
|
|
||||||
|
Implement `handleExportExcel()` with dynamic import:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const XLSX = await import("xlsx");
|
||||||
|
```
|
||||||
|
|
||||||
|
Export rows:
|
||||||
|
- if `selectedRows.size > 0`, export selected visible rows.
|
||||||
|
- otherwise export current visible `rows`.
|
||||||
|
|
||||||
|
Columns:
|
||||||
|
- use `renderColumns` only.
|
||||||
|
- header labels should use translated `getColumnLabel(col)`.
|
||||||
|
- values should be raw values from `row[col.key]`, but if
|
||||||
|
`entityDisplayConfig` would display a joined value and you can reuse a pure
|
||||||
|
helper safely, use display value. Do not mount React cell renderers into XLSX.
|
||||||
|
|
||||||
|
File name:
|
||||||
|
- `${tableName || "table"}_YYYY-MM-DD.xlsx`
|
||||||
|
|
||||||
|
Bundle:
|
||||||
|
- no static XLSX import.
|
||||||
|
- handler should fail gracefully with `console.error`.
|
||||||
|
|
||||||
|
### 4. Clipboard Copy
|
||||||
|
|
||||||
|
Implement `handleCopyTable()`:
|
||||||
|
- if selected rows exist, copy selected visible rows.
|
||||||
|
- otherwise copy current visible `rows`.
|
||||||
|
- format as TSV: header row + data rows.
|
||||||
|
- use translated column labels for headers.
|
||||||
|
- use raw text-safe values, not React nodes.
|
||||||
|
- call `navigator.clipboard.writeText(tsv)` if available.
|
||||||
|
- fallback to a temporary `<textarea>` + `document.execCommand("copy")`.
|
||||||
|
- stop propagation from toolbar button.
|
||||||
|
|
||||||
|
Do not add toast. Use `console.info` / `console.error` only.
|
||||||
|
|
||||||
|
### 5. PDF Button
|
||||||
|
|
||||||
|
Implement a conservative `handleExportPdf()`:
|
||||||
|
- first dispatch:
|
||||||
|
```ts
|
||||||
|
window.dispatchEvent(new CustomEvent("tablePdfExport", { detail }))
|
||||||
|
```
|
||||||
|
with `{ componentId, tableName, rows, columns }`.
|
||||||
|
- if no custom handler is expected is impossible to know, so also support a
|
||||||
|
simple fallback button behavior:
|
||||||
|
- either call `window.print()`, or
|
||||||
|
- only dispatch and log `console.info`.
|
||||||
|
|
||||||
|
Do not add jsPDF/html2canvas dependencies in this phase.
|
||||||
|
|
||||||
|
### 6. Search / Filter Toolbar Buttons
|
||||||
|
|
||||||
|
D.2 already renders `AdvancedSearchFilters` when `componentConfig.filter?.enabled`.
|
||||||
|
Add local UI toggles:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [searchPanelOpen, setSearchPanelOpen] = useState(true);
|
||||||
|
const [filterPanelOpen, setFilterPanelOpen] = useState(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- If `toolbar.showSearch === true`, render a button that toggles the D.2
|
||||||
|
`AdvancedSearchFilters` area.
|
||||||
|
- If `toolbar.showFilter === true`, render a button that toggles the same area
|
||||||
|
for now. Keep a comment that D.8 may split filter/table-options panels.
|
||||||
|
- If neither is shown, preserve current D.2 behavior: filters render whenever
|
||||||
|
`filter.enabled`.
|
||||||
|
|
||||||
|
Do not invent a new global search API unless a local existing helper is obvious.
|
||||||
|
|
||||||
|
### 7. Footer Pagination Runtime
|
||||||
|
|
||||||
|
Enhance `renderFooter()`:
|
||||||
|
- `pagination.position`:
|
||||||
|
- `"top"` renders footer controls above body.
|
||||||
|
- `"bottom"` or undefined renders below body as today.
|
||||||
|
- `pagination.showPageInfo`:
|
||||||
|
- render page range text, e.g. `1-20 / 총 132건`.
|
||||||
|
- `pagination.showSizeSelector`:
|
||||||
|
- render a compact `<select>` using `pagination.pageSizeOptions || [10, 20, 50, 100]`.
|
||||||
|
- when changed, update page size in the most conservative available way.
|
||||||
|
|
||||||
|
Important: inspect `useTableData.ts` before coding. If it already exposes
|
||||||
|
`setPageSize`, use it. If not, add a small local state in `TableComponent` and
|
||||||
|
pass that page size into `useTableData`, preserving `componentConfig.pagination?.pageSize`
|
||||||
|
as the initial value. Do not rewrite `useTableData` unless absolutely needed.
|
||||||
|
|
||||||
|
Footer refresh:
|
||||||
|
- if `_showPaginationRefreshBtn`, render a footer refresh button that calls
|
||||||
|
`tableData.refresh()`.
|
||||||
|
|
||||||
|
Keep prev/next behavior.
|
||||||
|
|
||||||
|
### 8. Paste Handling
|
||||||
|
|
||||||
|
Add conservative paste handling for basic table mode only:
|
||||||
|
- attach `onPaste` to the table wrapper or `<table>`.
|
||||||
|
- parse TSV/CSV-ish clipboard text into `rows x columns` matrix.
|
||||||
|
- do not write to backend in D.6.
|
||||||
|
- dispatch:
|
||||||
|
```ts
|
||||||
|
window.dispatchEvent(new CustomEvent("tablePaste", { detail }))
|
||||||
|
```
|
||||||
|
with `{ componentId, tableName, startRowIndex, startColumnKey, data }`.
|
||||||
|
- If a future handler wants to consume it, it can listen externally.
|
||||||
|
- Do not interfere with active inline edit inputs. If `editingCell` is not null,
|
||||||
|
let the edit input handle paste normally.
|
||||||
|
|
||||||
|
### 9. Basic Table Style Runtime
|
||||||
|
|
||||||
|
Apply C.5 style options in basic table mode:
|
||||||
|
- `tableStyle.theme`
|
||||||
|
- `striped`: already mostly covered by `alternateRows`; ensure it enables row striping.
|
||||||
|
- `bordered`: stronger cell borders.
|
||||||
|
- `minimal`: lighter borders/background.
|
||||||
|
- `tableStyle.headerStyle`
|
||||||
|
- `dark`: dark header background and light text.
|
||||||
|
- `light`: light/muted header.
|
||||||
|
- `default`: existing.
|
||||||
|
- `tableStyle.borderStyle`
|
||||||
|
- `none`: no cell borders.
|
||||||
|
- `light`: current border.
|
||||||
|
- `heavy`: stronger border.
|
||||||
|
|
||||||
|
Keep this inline and small. Do not create broad CSS files.
|
||||||
|
|
||||||
|
### 10. Preserve Existing Runtime
|
||||||
|
|
||||||
|
Do not regress:
|
||||||
|
- C.1 autoLoad behavior.
|
||||||
|
- C.5 defaultSort and refreshInterval behavior already wired.
|
||||||
|
- D.1 sticky/fixed columns.
|
||||||
|
- D.2 filters and linkedFilters.
|
||||||
|
- D.3 inline edit.
|
||||||
|
- D.4 actions.
|
||||||
|
- D.5 special cell rendering.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/registry/components/table/(TableComponent|types|cell-renderers|useTableData)"
|
||||||
|
rg -n "handleExportExcel|handleCopyTable|handleExportPdf|tablePaste|showPageInfo|showSizeSelector|showPaginationRefresh|editModeEnabled" frontend/lib/registry/components/table/TableComponent.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` prints nothing.
|
||||||
|
- targeted `tsc | rg ...` prints nothing.
|
||||||
|
- grep shows D.6 handlers and footer/toolbar wiring.
|
||||||
|
|
||||||
|
Known unrelated baseline errors may still appear if you run full `tsc` without a
|
||||||
|
targeted grep:
|
||||||
|
- `components/screen/filters/AdvancedSearchFilters.tsx` import of
|
||||||
|
`@/types/screen-legacy-backup`
|
||||||
|
- `lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx`
|
||||||
|
`DataReceivable` import from `ScreenContext`
|
||||||
|
|
||||||
|
## Final Report
|
||||||
|
|
||||||
|
Report:
|
||||||
|
- changed files
|
||||||
|
- toolbar buttons now rendered and their behavior
|
||||||
|
- Excel export rows/columns policy
|
||||||
|
- copy policy
|
||||||
|
- PDF fallback behavior
|
||||||
|
- pagination behavior and whether `useTableData` was changed
|
||||||
|
- paste event detail shape
|
||||||
|
- tableStyle options wired
|
||||||
|
- validation command results
|
||||||
|
- intentionally deferred items
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# Table Canonical Cleanup — Phase D.7 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.7
|
||||||
|
- Prior completed phases: B.1/B.4, C.1/C.2/C.3/C.4/C.5, D.1, D.2, D.3, D.4, D.5, D.6
|
||||||
|
- D.6 Codex follow-up fix already present:
|
||||||
|
- table paste handling now ignores normal inputs/contenteditable and only dispatches
|
||||||
|
`tablePaste` from cells with `data-row-idx` / `data-col-key`.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement Phase D.7 card-mode parity for canonical table:
|
||||||
|
- expand canonical `CardView` to cover legacy card config options
|
||||||
|
- normalize image URLs like the old v2 card renderer
|
||||||
|
- use D.5 cell formatting for additional/display fields
|
||||||
|
- add ResizeObserver-based narrow/wide automatic card fallback in canonical `TableComponent`
|
||||||
|
|
||||||
|
Keep the work scoped. Do not expand ConfigPanel.
|
||||||
|
|
||||||
|
## File Scope
|
||||||
|
|
||||||
|
Allowed to edit:
|
||||||
|
- `frontend/lib/registry/components/table/views/CardView.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
|
||||||
|
Allowed only if a tiny type compatibility fix is needed:
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
|
||||||
|
Read-only references:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/CardModeRenderer.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts`
|
||||||
|
- `frontend/lib/registry/components/table/cell-renderers.tsx`
|
||||||
|
- `frontend/lib/api/client.ts`
|
||||||
|
- `frontend/lib/api/file.ts`
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify:
|
||||||
|
- `_shared/TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- `_shared/SingleTableWithSticky.tsx`
|
||||||
|
- `_shared/CardModeRenderer.tsx`
|
||||||
|
- Do not remove early legacy/v2 delegation in canonical `TableComponent`.
|
||||||
|
- Do not implement D.8 GroupSum / TableOptions advanced summaries.
|
||||||
|
- Do not implement D.9 SplitPanel / DataProvidable / DataReceivable cleanup.
|
||||||
|
- Do not implement schema/alias/V2List cleanup.
|
||||||
|
- Do not add shadcn card/button dependencies to canonical `CardView`; keep its
|
||||||
|
current inline-style approach.
|
||||||
|
- Do not add toast dependencies.
|
||||||
|
- Do not touch unrelated dirty files:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
|
||||||
|
## Current Relevant State
|
||||||
|
|
||||||
|
Canonical `CardView.tsx` already supports:
|
||||||
|
- `cardsPerRow`
|
||||||
|
- `cardSpacing`
|
||||||
|
- `cardStyle.showTitle/showSubtitle/showDescription/showImage`
|
||||||
|
- `cardStyle.maxDescriptionLength`
|
||||||
|
- `cardStyle.imagePosition`
|
||||||
|
- `cardStyle.imageSize`
|
||||||
|
- `cardStyle.showActions`
|
||||||
|
- `cardStyle.showViewButton/showEditButton/showDeleteButton`
|
||||||
|
- `cardColumnMapping.titleColumn/subtitleColumn/descriptionColumn/imageColumn/displayColumns`
|
||||||
|
- D.4 action callback wiring from `TableComponent`.
|
||||||
|
|
||||||
|
Gaps:
|
||||||
|
- no `ResizeObserver` narrow/wide fallback in canonical table.
|
||||||
|
- no v2-style image URL normalization (`objid` → `getFilePreviewUrl`, path → `getFullImageUrl`).
|
||||||
|
- no `idColumn` / `cardHeight` legacy config shape compatibility.
|
||||||
|
- display columns render raw `String(row[col])`, not D.5 special formatting.
|
||||||
|
- no mapping inference when `cardColumnMapping` is empty.
|
||||||
|
|
||||||
|
## Implementation Requirements
|
||||||
|
|
||||||
|
### 1. Type Compatibility
|
||||||
|
|
||||||
|
If needed, extend canonical card types conservatively:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface TableCardStyleConfig {
|
||||||
|
cardHeight?: number | "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableCardColumnMapping {
|
||||||
|
idColumn?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableConfig {
|
||||||
|
/** narrow auto-card breakpoint; default 600. false/0 disables auto-card if you add this. */
|
||||||
|
responsiveCardBreakpoint?: number | false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only add fields you actually use. Keep them optional.
|
||||||
|
|
||||||
|
### 2. CardView Mapping Compatibility
|
||||||
|
|
||||||
|
Support both canonical and legacy-ish sources:
|
||||||
|
- `config.cardColumnMapping.titleColumn`
|
||||||
|
- fallback to `(config as any).cardConfig?.titleColumn`
|
||||||
|
- fallback to inferred visible/source columns if mapping is empty
|
||||||
|
|
||||||
|
Inference guideline:
|
||||||
|
- title: first non-image, non-file column
|
||||||
|
- subtitle: second non-image, non-file column
|
||||||
|
- description: third non-image, non-file column
|
||||||
|
- image: first column whose key includes `image` / `photo` / `thumbnail` or whose
|
||||||
|
`inputType` / `format` is `image`
|
||||||
|
|
||||||
|
Do not require inference to be perfect; it should avoid blank cards.
|
||||||
|
|
||||||
|
### 3. Pass Columns Into CardView
|
||||||
|
|
||||||
|
Update `CardViewProps` to accept optional canonical columns:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
columns?: TableColumn[];
|
||||||
|
getColumnLabel?: (col: TableColumn) => string;
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `renderColumns` and `getColumnLabel` from `TableComponent`.
|
||||||
|
|
||||||
|
Use these columns for:
|
||||||
|
- mapping inference
|
||||||
|
- display field labels
|
||||||
|
- D.5 cell formatting
|
||||||
|
|
||||||
|
### 4. D.5 Cell Formatting In Cards
|
||||||
|
|
||||||
|
For `cardColumnMapping.displayColumns`, render each row using:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
renderTableCellValue({ value: row[col.key], column: col, row, isDesignMode })
|
||||||
|
```
|
||||||
|
|
||||||
|
If the display column is not present in `columns`, fall back to raw string.
|
||||||
|
|
||||||
|
For title/subtitle/description, raw string is acceptable, but image must be
|
||||||
|
normalized.
|
||||||
|
|
||||||
|
### 5. Image URL Normalization
|
||||||
|
|
||||||
|
For card image:
|
||||||
|
- numeric string objid → `getFilePreviewUrl(value)`
|
||||||
|
- `/uploads/...` or other path → `getFullImageUrl(value)`
|
||||||
|
- full `http://` / `https://` URL → unchanged via `getFullImageUrl`
|
||||||
|
|
||||||
|
Use existing APIs:
|
||||||
|
- `getFullImageUrl` from `@/lib/api/client`
|
||||||
|
- `getFilePreviewUrl` from `@/lib/api/file`
|
||||||
|
|
||||||
|
Add a small fallback block if image fails to load. Do not imperatively append DOM
|
||||||
|
nodes like old `_shared/CardModeRenderer`; use React state/component rendering.
|
||||||
|
|
||||||
|
### 6. Legacy Card Height / ID
|
||||||
|
|
||||||
|
Support:
|
||||||
|
- `cardStyle.cardHeight` or `(config as any).cardConfig?.cardHeight`
|
||||||
|
- `cardColumnMapping.idColumn` or `(config as any).cardConfig?.idColumn`
|
||||||
|
|
||||||
|
If `idColumn` is configured and the row has a value, show a compact ID badge in
|
||||||
|
the card header or top-right area.
|
||||||
|
|
||||||
|
### 7. ResizeObserver Narrow/Wide Fallback
|
||||||
|
|
||||||
|
In canonical `TableComponent`, add a root ref or body wrapper ref and observe width:
|
||||||
|
- default breakpoint: `600`
|
||||||
|
- if `componentConfig.responsiveCardBreakpoint` is a positive number, use it.
|
||||||
|
- if it is `false` or `0`, disable auto-card.
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- If explicit `displayMode === "card"`, always render card.
|
||||||
|
- If explicit `displayMode === "table"` and container width is below breakpoint,
|
||||||
|
render `CardView` instead of basic table.
|
||||||
|
- Do not auto-switch `split`, `grouped`, or `pivot`.
|
||||||
|
- In design mode, keep the same behavior as runtime unless it makes the builder
|
||||||
|
unstable. If you choose to disable auto-card in design mode, state that in the
|
||||||
|
report.
|
||||||
|
|
||||||
|
Important: do not delete or alter the early legacy/v2 delegation block.
|
||||||
|
|
||||||
|
### 8. Preserve D.4 Actions
|
||||||
|
|
||||||
|
`TableComponent` already maps first configured view/edit/delete action to
|
||||||
|
`CardView` callbacks. Preserve that behavior.
|
||||||
|
|
||||||
|
CardView should continue honoring:
|
||||||
|
- `cardStyle.showActions`
|
||||||
|
- `cardStyle.showViewButton`
|
||||||
|
- `cardStyle.showEditButton`
|
||||||
|
- `cardStyle.showDeleteButton`
|
||||||
|
|
||||||
|
Do not introduce table-level row action columns in card mode.
|
||||||
|
|
||||||
|
### 9. Styling
|
||||||
|
|
||||||
|
Keep canonical `CardView` inline styles:
|
||||||
|
- no nested cards
|
||||||
|
- stable card height if `cardHeight` configured
|
||||||
|
- responsive grid should not overflow horizontally
|
||||||
|
- narrow fallback should produce 1 column or a sensible reduced column count
|
||||||
|
|
||||||
|
Suggested card count:
|
||||||
|
- explicit card mode: use configured `cardsPerRow`
|
||||||
|
- responsive narrow fallback: clamp to `1` or `2` based on width; do not blindly
|
||||||
|
use `cardsPerRow=3` on a 400px container.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/registry/components/table/(TableComponent|types|cell-renderers|views/CardView)"
|
||||||
|
rg -n "ResizeObserver|responsiveCardBreakpoint|renderTableCellValue|getFilePreviewUrl|getFullImageUrl|cardHeight|idColumn" frontend/lib/registry/components/table
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` prints nothing.
|
||||||
|
- targeted `tsc | rg ...` prints nothing.
|
||||||
|
- grep shows canonical `TableComponent` / `CardView` / optional `types` only,
|
||||||
|
plus old `_shared` references if your grep includes `_shared`.
|
||||||
|
|
||||||
|
Known unrelated baseline errors may still appear if you run full `tsc` without a
|
||||||
|
targeted grep:
|
||||||
|
- `components/screen/filters/AdvancedSearchFilters.tsx` import of
|
||||||
|
`@/types/screen-legacy-backup`
|
||||||
|
- `lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx`
|
||||||
|
`DataReceivable` import from `ScreenContext`
|
||||||
|
|
||||||
|
## Final Report
|
||||||
|
|
||||||
|
Report:
|
||||||
|
- changed files
|
||||||
|
- CardView options now supported
|
||||||
|
- image URL normalization policy
|
||||||
|
- mapping inference policy
|
||||||
|
- ResizeObserver breakpoint and exact display-mode switching rules
|
||||||
|
- whether types were changed
|
||||||
|
- validation command results
|
||||||
|
- intentionally deferred items
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
# Table Canonical Cleanup — Phase D.8 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase D.8
|
||||||
|
- Prior completed phases: B.1/B.4, C.1/C.2/C.3/C.4/C.5, D.1, D.2, D.3, D.4, D.5, D.6, D.7
|
||||||
|
- D.7 Codex follow-up fixes already present:
|
||||||
|
- `CardView._isFileColumn()` no longer treats `profile`-like keys as file columns.
|
||||||
|
- card image error state resets when the actual image value changes.
|
||||||
|
- `responsiveCardBreakpoint` direct prop is merged into `componentConfig` and filtered from DOM props.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement Phase D.8 for canonical table:
|
||||||
|
- wire `TableOptionsContext` grouping and group-sum callbacks into canonical `TableComponent`
|
||||||
|
- render group/subtotal summaries in canonical `GroupedView`
|
||||||
|
- expose a settings/modal entry point from canonical table toolbar when a
|
||||||
|
`TableOptionsProvider` exists
|
||||||
|
- ensure the existing table-options UI actually calls the canonical callbacks for
|
||||||
|
filter / grouping / group-sum / column visibility
|
||||||
|
|
||||||
|
Keep this scoped. Do not rewrite all table-options UI.
|
||||||
|
|
||||||
|
## File Scope
|
||||||
|
|
||||||
|
Allowed to edit:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/views/GroupedView.tsx`
|
||||||
|
|
||||||
|
Allowed if required for callback wiring:
|
||||||
|
- `frontend/components/screen/table-options/TableSettingsModal.tsx`
|
||||||
|
|
||||||
|
Read-only references:
|
||||||
|
- `frontend/components/common/TableOptionsModal.tsx`
|
||||||
|
- `frontend/components/screen/table-options/TableOptionsToolbar.tsx`
|
||||||
|
- `frontend/components/screen/table-options/ColumnVisibilityPanel.tsx`
|
||||||
|
- `frontend/components/screen/table-options/FilterPanel.tsx`
|
||||||
|
- `frontend/components/screen/table-options/GroupingPanel.tsx`
|
||||||
|
- `frontend/types/table-options.ts`
|
||||||
|
- `frontend/contexts/TableOptionsContext.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `summaryData` near 2295-2357
|
||||||
|
- `handleTableOptionsSave` near 4574
|
||||||
|
- grouped summary rendering near 6110-6145
|
||||||
|
- total summary rendering near 6382-6445
|
||||||
|
- `TableOptionsModal` rendering near 6818
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `groupSumConfig` / `summedData` near 4978-5055
|
||||||
|
- `TableOptionsModal` rendering near 7194
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not modify:
|
||||||
|
- `_shared/TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListComponent.tsx`
|
||||||
|
- `_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- `_shared/SingleTableWithSticky.tsx`
|
||||||
|
- Do not remove early legacy/v2 delegation in canonical `TableComponent`.
|
||||||
|
- Do not implement D.9 SplitPanel / DataProvidable / DataReceivable cleanup.
|
||||||
|
- Do not implement schema/alias/V2List cleanup.
|
||||||
|
- Do not add new dependencies or toast dependencies.
|
||||||
|
- Do not make `TableComponent` require `TableOptionsProvider`; it must continue
|
||||||
|
mounting safely without a Provider.
|
||||||
|
- Do not touch unrelated dirty files:
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
|
||||||
|
## Current Relevant State
|
||||||
|
|
||||||
|
Canonical `TableComponent` already:
|
||||||
|
- uses `useTableOptionsOptional()`
|
||||||
|
- registers `TableRegistration` with:
|
||||||
|
- `onFilterChange` wired to `setTableOptionsFilters`
|
||||||
|
- `onColumnVisibilityChange` wired
|
||||||
|
- `onFrozenColumnCountChange` wired
|
||||||
|
- `onGroupChange: () => undefined`
|
||||||
|
- no real `onGroupSumChange`
|
||||||
|
- has D.6 toolbar buttons but no settings modal entry point.
|
||||||
|
- has D.2 filters, D.1 render columns, D.5 cell renderer.
|
||||||
|
|
||||||
|
`TableSettingsModal.tsx` is the preferred integrated UI because it already contains:
|
||||||
|
- columns tab
|
||||||
|
- filters tab
|
||||||
|
- grouping tab
|
||||||
|
- group-sum settings in filters tab
|
||||||
|
|
||||||
|
However, before coding, verify whether it actually invokes:
|
||||||
|
- `table.onFilterChange(activeFilters)`
|
||||||
|
- `table.onGroupChange(selectedGroupColumns)`
|
||||||
|
- `table.onGroupSumChange(groupSumConfig | null)`
|
||||||
|
- `table.onColumnVisibilityChange(localColumns)`
|
||||||
|
- `table.onFrozenColumnCountChange(...)`
|
||||||
|
|
||||||
|
If any callback is missing, add the smallest fix in `TableSettingsModal.tsx`.
|
||||||
|
|
||||||
|
## Implementation Requirements
|
||||||
|
|
||||||
|
### 1. Canonical Table State
|
||||||
|
|
||||||
|
In `TableComponent.tsx`, add local runtime state:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const [tableOptionsGroups, setTableOptionsGroups] = useState<string[]>([]);
|
||||||
|
const [groupSumConfig, setGroupSumConfig] = useState<GroupSumConfig | null>(null);
|
||||||
|
const [isTableSettingsOpen, setIsTableSettingsOpen] = useState(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
Import `GroupSumConfig` type from `@/types/table-options`.
|
||||||
|
|
||||||
|
Register callbacks:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
onGroupChange: (groups) => setTableOptionsGroups(Array.isArray(groups) ? groups : [])
|
||||||
|
onGroupSumChange: (config) => setGroupSumConfig(config && config.enabled ? config : null)
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the optional-provider guard. If `_tableOptions` is undefined, no modal should render and the table should still work.
|
||||||
|
|
||||||
|
### 2. Effective Grouping
|
||||||
|
|
||||||
|
Compute:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const effectiveGroupByColumns =
|
||||||
|
tableOptionsGroups.length > 0 ? tableOptionsGroups :
|
||||||
|
groupSumConfig?.enabled && groupSumConfig.group_by_column ? [groupSumConfig.group_by_column] :
|
||||||
|
componentConfig.groupBy ? [componentConfig.groupBy] :
|
||||||
|
[];
|
||||||
|
```
|
||||||
|
|
||||||
|
For `GroupedView`, use the first group column as the current minimal canonical behavior.
|
||||||
|
Do not implement deep nested grouping in this phase unless it is very small.
|
||||||
|
|
||||||
|
If `groupSumConfig.enabled` is true and the current configured display mode is `"table"`,
|
||||||
|
it is acceptable to render `GroupedView` as the body so users see group subtotals.
|
||||||
|
State this clearly in the report.
|
||||||
|
|
||||||
|
Do not mutate `componentConfig` permanently for grouping state.
|
||||||
|
|
||||||
|
### 3. GroupedView Group Summary
|
||||||
|
|
||||||
|
Extend `GroupedViewProps`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
groupByColumns?: string[];
|
||||||
|
groupSumConfig?: GroupSumConfig | null;
|
||||||
|
getColumnLabel?: (col: TableColumn) => string;
|
||||||
|
renderCellValue?: (value, column, row) => React.ReactNode; // optional if useful
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
- group by the first column in `groupByColumns` or `config.groupBy`.
|
||||||
|
- compute numeric summaries per group for visible numeric columns:
|
||||||
|
- numeric column if `inputType` or `dataType` is `number` / `decimal`, or `format` is `number` / `currency`
|
||||||
|
- summary object: `{ sum, avg, count }`
|
||||||
|
- render a compact summary line in the group header or a summary row directly after the header.
|
||||||
|
- Use `getColumnLabel(col)` for labels when provided.
|
||||||
|
- Keep existing expand/collapse behavior.
|
||||||
|
- For normal group rows, use D.5 `renderTableCellValue` if you import it, otherwise at least preserve current formatting.
|
||||||
|
|
||||||
|
Do not add expensive recomputation outside `useMemo`.
|
||||||
|
|
||||||
|
### 4. Settings Modal Entry Point
|
||||||
|
|
||||||
|
Add a compact settings button to canonical toolbar when all are true:
|
||||||
|
- not design mode
|
||||||
|
- `_tableOptions` exists
|
||||||
|
- `_canonicalTableId` exists
|
||||||
|
|
||||||
|
Button behavior:
|
||||||
|
- call `_tableOptions.setSelectedTableId(_canonicalTableId)`
|
||||||
|
- `setIsTableSettingsOpen(true)`
|
||||||
|
|
||||||
|
Use an icon button (`Settings` from `lucide-react`) and existing `btnStyle`.
|
||||||
|
|
||||||
|
Render `TableSettingsModal` only when Provider exists:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{_tableOptions && isTableSettingsOpen && (
|
||||||
|
<TableSettingsModal
|
||||||
|
isOpen={isTableSettingsOpen}
|
||||||
|
onClose={() => setIsTableSettingsOpen(false)}
|
||||||
|
onFiltersApplied={(filters) => setTableOptionsFilters(filters)}
|
||||||
|
screenId={...optional if available...}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not render it in design mode unless you have a clear reason. Prefer runtime only.
|
||||||
|
|
||||||
|
### 5. TableSettingsModal Callback Fixes
|
||||||
|
|
||||||
|
If `TableSettingsModal` currently saves a group-sum config but does not call
|
||||||
|
`table.onGroupSumChange`, fix that.
|
||||||
|
|
||||||
|
Expected save behavior:
|
||||||
|
- column tab:
|
||||||
|
- `table.onColumnVisibilityChange(localColumns)`
|
||||||
|
- `table.onFrozenColumnCountChange(frozenColumnCount, updatedColumns)`
|
||||||
|
- filters tab:
|
||||||
|
- `table.onFilterChange(activeFilters)`
|
||||||
|
- `onFiltersApplied?.(activeFilters)`
|
||||||
|
- group-sum:
|
||||||
|
- if enabled and group column exists: `table.onGroupSumChange?.(groupSumConfig)`
|
||||||
|
- otherwise: `table.onGroupSumChange?.(null)`
|
||||||
|
- grouping:
|
||||||
|
- `table.onGroupChange(selectedGroupColumns)`
|
||||||
|
|
||||||
|
Keep localStorage behavior intact.
|
||||||
|
|
||||||
|
### 6. Preserve Existing Runtime
|
||||||
|
|
||||||
|
Do not regress:
|
||||||
|
- D.1 column visibility/fixed/sticky
|
||||||
|
- D.2 filters
|
||||||
|
- D.3 inline edit
|
||||||
|
- D.4 row/bulk actions
|
||||||
|
- D.5 special cell rendering
|
||||||
|
- D.6 toolbar/export/paste/pagination
|
||||||
|
- D.7 card mode / ResizeObserver
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/registry/components/table/(TableComponent|types|cell-renderers|views/GroupedView|views/CardView)|^components/screen/table-options/TableSettingsModal|^types/table-options|^contexts/TableOptionsContext"
|
||||||
|
rg -n "GroupSumConfig|onGroupSumChange|isTableSettingsOpen|TableSettingsModal|tableOptionsGroups|groupSumConfig|groupByColumns" frontend/lib/registry/components/table frontend/components/screen/table-options/TableSettingsModal.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` prints nothing.
|
||||||
|
- targeted `tsc | rg ...` prints nothing.
|
||||||
|
- grep shows canonical table + optional `TableSettingsModal` wiring.
|
||||||
|
|
||||||
|
Known unrelated baseline errors may still appear if you run full `tsc` without a
|
||||||
|
targeted grep:
|
||||||
|
- `components/screen/filters/AdvancedSearchFilters.tsx` import of
|
||||||
|
`@/types/screen-legacy-backup`
|
||||||
|
- `lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx`
|
||||||
|
`DataReceivable` import from `ScreenContext`
|
||||||
|
|
||||||
|
## Final Report
|
||||||
|
|
||||||
|
Report:
|
||||||
|
- changed files
|
||||||
|
- TableOptions callbacks now wired
|
||||||
|
- whether `TableSettingsModal.tsx` was changed and why
|
||||||
|
- GroupedView summary rendering behavior
|
||||||
|
- group-sum behavior in normal table display mode
|
||||||
|
- settings button visibility rules
|
||||||
|
- validation command results
|
||||||
|
- intentionally deferred items
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Table Canonical Cleanup — Phase E.1 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase E.1 + §3.3
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- Do not delete `_shared/TableListComponent.tsx` or `_shared/V2TableListComponent.tsx`.
|
||||||
|
- Do not remove canonical `TableComponent` early delegation yet. That is Phase F.1.
|
||||||
|
- Do not start E.2 / E.3 unless a tiny compile-only adjustment is required.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
`frontend/components/v2/V2List.tsx` must stop wrapping legacy `_shared/TableListComponent` and render canonical `TableComponent` directly.
|
||||||
|
|
||||||
|
Hard requirements:
|
||||||
|
- `V2List.tsx` must not import `TableListComponent`.
|
||||||
|
- `V2List.tsx` must not create a component object with `type: "table-list"`.
|
||||||
|
- `rg -n "table-list|TableListComponent" frontend/components/v2/V2List.tsx` should return 0 lines.
|
||||||
|
- Do not break old `V2ListProps` layouts that use either snake_case or camelCase config keys.
|
||||||
|
|
||||||
|
Primary scope:
|
||||||
|
- `frontend/components/v2/V2List.tsx`
|
||||||
|
|
||||||
|
Allowed if needed:
|
||||||
|
- `frontend/types/v2-components.ts`
|
||||||
|
- only for comments/type cleanup related to `V2List`.
|
||||||
|
- preserve `LEGACY_TO_V2_MAP` mappings for:
|
||||||
|
- `table-search-widget`
|
||||||
|
- `modal-repeater-table`
|
||||||
|
- `repeater-field-group`
|
||||||
|
- `card-display`
|
||||||
|
|
||||||
|
Do not edit:
|
||||||
|
- `frontend/components/v2/config-panels/InvDataConfigPanel.tsx`
|
||||||
|
- `frontend/components/v2/config-panels/V2ListConfigPanel.tsx`
|
||||||
|
- `frontend/components/v2/config-panels/V2TableListConfigPanel.tsx`
|
||||||
|
- registry/schema/alias files
|
||||||
|
|
||||||
|
Read first:
|
||||||
|
- `frontend/components/v2/V2List.tsx`
|
||||||
|
- `frontend/types/v2-components.ts`
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- `frontend/lib/registry/components/table/views/CardView.tsx`
|
||||||
|
|
||||||
|
Implementation guidance:
|
||||||
|
|
||||||
|
## 1. Replace legacy import
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
- `TableListComponent` import from `_shared/TableListComponent`
|
||||||
|
|
||||||
|
With:
|
||||||
|
- canonical `TableComponent` import from `@/lib/registry/components/table/TableComponent`
|
||||||
|
|
||||||
|
Render `<TableComponent />` inside the existing wrapper div that owns `ref`, `id`, width, and height.
|
||||||
|
|
||||||
|
## 2. Build a canonical component object
|
||||||
|
|
||||||
|
Create a memoized object that cannot trigger early delegation:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
id: id || "v2-list",
|
||||||
|
type: "table",
|
||||||
|
componentType: "table",
|
||||||
|
config: canonicalConfig,
|
||||||
|
componentConfig: canonicalConfig,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not include `type: "table-list"` anywhere in this file.
|
||||||
|
|
||||||
|
## 3. Normalize old V2List config keys
|
||||||
|
|
||||||
|
Current code and types are mixed:
|
||||||
|
- old runtime code reads camelCase:
|
||||||
|
- `dataSource`
|
||||||
|
- `viewMode`
|
||||||
|
- `pageSize`
|
||||||
|
- `cardConfig`
|
||||||
|
- `V2ListConfig` type exposes snake_case:
|
||||||
|
- `data_source`
|
||||||
|
- `view_mode`
|
||||||
|
- `page_size`
|
||||||
|
- `card_config`
|
||||||
|
|
||||||
|
Support both.
|
||||||
|
|
||||||
|
Normalization should include:
|
||||||
|
- `tableName = config.data_source?.table ?? config.dataSource?.table ?? config.tableName ?? props.tableName`
|
||||||
|
- `viewMode = config.view_mode ?? config.viewMode ?? "table"`
|
||||||
|
- `pageSize = config.page_size ?? config.pageSize ?? 10`
|
||||||
|
- `cardConfig = config.card_config ?? config.cardConfig`
|
||||||
|
- `pagination = config.pagination ?? config.pageable ?? true`
|
||||||
|
|
||||||
|
Use defensive `any` only at the normalization boundary.
|
||||||
|
|
||||||
|
## 4. Convert columns to canonical TableColumn
|
||||||
|
|
||||||
|
Input column variants may include:
|
||||||
|
- typed `ListColumn`: `field`, `header`, `width`, `sortable`, `filterable`, `editable`, `format`
|
||||||
|
- legacy/camel variants: `key`, `title`, `align`, `isJoinColumn`, `thousandSeparator`, `inputType`
|
||||||
|
|
||||||
|
Output canonical columns:
|
||||||
|
- `key`
|
||||||
|
- `label`
|
||||||
|
- `width` as number if parseable
|
||||||
|
- `visible: true` unless explicit false
|
||||||
|
- `sortable`
|
||||||
|
- `searchable`
|
||||||
|
- `editable`
|
||||||
|
- `align`
|
||||||
|
- `order`
|
||||||
|
- `format`
|
||||||
|
- `inputType`
|
||||||
|
- `dataType`
|
||||||
|
- `isEntityJoin`
|
||||||
|
- `thousandSeparator`
|
||||||
|
- preserve `autoGeneration` if present
|
||||||
|
|
||||||
|
Avoid dropping unknown useful metadata if already present on the column object; spread it first or last carefully, but ensure `key`/`label` are canonical and non-empty.
|
||||||
|
|
||||||
|
## 5. Map config to canonical TableConfig
|
||||||
|
|
||||||
|
Canonical config should use actual `TableComponent` keys:
|
||||||
|
- `selectedTable`
|
||||||
|
- `tableName`
|
||||||
|
- `columns`
|
||||||
|
- `displayMode`: `"card"` for card view, otherwise `"table"` for unsupported V2 modes (`kanban`/`list` fallback to table)
|
||||||
|
- `cardColumnMapping`:
|
||||||
|
- `idColumn`
|
||||||
|
- `titleColumn`
|
||||||
|
- `subtitleColumn`
|
||||||
|
- `descriptionColumn`
|
||||||
|
- `imageColumn`
|
||||||
|
- `cardStyle`:
|
||||||
|
- `cardsPerRow`
|
||||||
|
- `gap`
|
||||||
|
- `showActions`
|
||||||
|
- `showHeader`
|
||||||
|
- `showFooter`
|
||||||
|
- `showCheckbox: true`
|
||||||
|
- `selectionMode: "multiple"`
|
||||||
|
- `autoWidth`
|
||||||
|
- `stickyHeader`
|
||||||
|
- `autoLoad`
|
||||||
|
- `horizontalScroll`
|
||||||
|
- `pagination.enabled`
|
||||||
|
- `pagination.pageSize`
|
||||||
|
- `pagination.position`
|
||||||
|
- `pagination.showSizeSelector`
|
||||||
|
- `pagination.pageSizeOptions`
|
||||||
|
- `tableStyle`:
|
||||||
|
- use canonical keys (`theme`, `borderStyle`, `alternateRows`, `hoverEffect`)
|
||||||
|
- `toolbar`:
|
||||||
|
- use canonical keys (`showRefresh`, `showExcel`, etc.)
|
||||||
|
|
||||||
|
Do not pass old `checkbox`, `cardConfig`, `tableStyle.striped`, `toolbar.showExport`, `pagination.showPageSize` as the only source of truth. If keeping aliases for compatibility, make canonical keys authoritative.
|
||||||
|
|
||||||
|
## 6. Preserve callbacks
|
||||||
|
|
||||||
|
Existing V2List API:
|
||||||
|
- `onRowSelect?: (rows: Record<string, unknown>[]) => void`
|
||||||
|
- `onRowClick?: (row: Record<string, unknown>) => void`
|
||||||
|
|
||||||
|
Map to canonical:
|
||||||
|
- `onSelectedRowsChange={(_, selectedData) => onRowSelect?.(selectedData)}`
|
||||||
|
- `onRowSelect={(row) => onRowClick?.(row)}`
|
||||||
|
|
||||||
|
If both callbacks exist, both should still work.
|
||||||
|
|
||||||
|
## 7. Empty table state
|
||||||
|
|
||||||
|
Keep the current “테이블이 설정되지 않았습니다.” fallback when no table name is resolved.
|
||||||
|
|
||||||
|
## 8. Forbidden
|
||||||
|
|
||||||
|
- Do not edit config panels in this phase.
|
||||||
|
- Do not remove `v2-table-list` schema/registry/alias entries.
|
||||||
|
- Do not delete old shared table bodies.
|
||||||
|
- Do not touch `TableComponent` unless a tiny compile-only change is absolutely required.
|
||||||
|
- Do not broaden this into E.2/E.3 cleanup.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n "table-list|TableListComponent" frontend/components/v2/V2List.tsx
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(components/v2/V2List|types/v2-components|lib/registry/components/table/(TableComponent|types))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` passes.
|
||||||
|
- `rg` for `table-list|TableListComponent` in `V2List.tsx` returns 0 lines.
|
||||||
|
- Targeted tsc grep has no output.
|
||||||
|
- Full tsc may still have unrelated baseline errors; report only scoped-file errors.
|
||||||
|
|
||||||
|
Report format:
|
||||||
|
|
||||||
|
1. Files changed.
|
||||||
|
2. Import/render change summary.
|
||||||
|
3. Config normalization summary:
|
||||||
|
- snake/camel support
|
||||||
|
- tableName
|
||||||
|
- viewMode/displayMode
|
||||||
|
- columns
|
||||||
|
4. Callback compatibility summary.
|
||||||
|
5. Verification results.
|
||||||
|
6. Any remaining E.1 gap.
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Table Canonical Cleanup — Phase E.2 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase E.2 + §3.3
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10, E.1.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- Do not delete old files yet. In particular, do not delete `V2TableListConfigPanel.tsx`, `_shared/TableListComponent.tsx`, or `_shared/TableListConfigPanel.tsx`.
|
||||||
|
- Do not start E.3/F cleanup in this prompt.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
1. `InvDataConfigPanel` should no longer expose the hidden legacy `v2-table-list` option/branch.
|
||||||
|
2. `V2ListConfigPanel` should stop delegating to legacy `_shared/TableListConfigPanel` and use canonical `InvTableConfigPanel` instead.
|
||||||
|
|
||||||
|
Primary scope:
|
||||||
|
- `frontend/components/v2/config-panels/InvDataConfigPanel.tsx`
|
||||||
|
- `frontend/components/v2/config-panels/V2ListConfigPanel.tsx`
|
||||||
|
|
||||||
|
Read first:
|
||||||
|
- `frontend/components/v2/config-panels/InvDataConfigPanel.tsx`
|
||||||
|
- `frontend/components/v2/config-panels/V2ListConfigPanel.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/InvTableConfigPanel.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- `frontend/components/v2/V2List.tsx` from E.1
|
||||||
|
|
||||||
|
## Part A — InvDataConfigPanel cleanup
|
||||||
|
|
||||||
|
Remove from `InvDataConfigPanel.tsx`:
|
||||||
|
- `V2TableListConfigPanel` import.
|
||||||
|
- `v2-table-list` from `DataComponentType`.
|
||||||
|
- `v2-table-list` item from `TYPES_BY_KIND.read`.
|
||||||
|
- `v2-table-list` from `KIND_OF_TYPE`.
|
||||||
|
- render branch for `componentType === "v2-table-list"`.
|
||||||
|
- stale doc/comments that advertise `v2-table-list`.
|
||||||
|
|
||||||
|
Do not remove `v2-repeater`.
|
||||||
|
Do not remove `v2-list`.
|
||||||
|
|
||||||
|
Compatibility rule:
|
||||||
|
- If `componentType` somehow arrives as `"v2-table-list"` from old saved data, normalize it to `"v2-list"` for UI selection rather than crashing.
|
||||||
|
- Do not call `onComponentTypeChange` automatically on mount unless the existing code already does. This panel should be display-safe.
|
||||||
|
|
||||||
|
## Part B — V2ListConfigPanel canonical panel delegation
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
- `TableListConfigPanel` import
|
||||||
|
- `TableListConfig` type import
|
||||||
|
- `tableListConfig` conversion
|
||||||
|
- `handleConfigChange(partialConfig: Partial<TableListConfig>)`
|
||||||
|
- `<TableListConfigPanel ... />`
|
||||||
|
|
||||||
|
With:
|
||||||
|
- `InvTableConfigPanel` import from `@/lib/registry/components/table/InvTableConfigPanel`
|
||||||
|
- `TableConfig`, `TableColumn` type imports from `@/lib/registry/components/table/types`
|
||||||
|
- canonical conversion helpers local to `V2ListConfigPanel`.
|
||||||
|
|
||||||
|
Keep the existing outer UX:
|
||||||
|
- the top data-source summary card
|
||||||
|
- read-only switch
|
||||||
|
- pagination switch + page size select
|
||||||
|
- collapsible details wrapper
|
||||||
|
|
||||||
|
Inside the collapsible details, render `InvTableConfigPanel`.
|
||||||
|
|
||||||
|
## Config conversion requirements
|
||||||
|
|
||||||
|
`V2ListConfigPanel` must support both snake_case and camelCase keys, same as E.1 runtime:
|
||||||
|
- table:
|
||||||
|
- `config.tableName`
|
||||||
|
- `config.dataSource?.table`
|
||||||
|
- `config.data_source?.table`
|
||||||
|
- fallback `currentTableName`
|
||||||
|
- view:
|
||||||
|
- `config.view_mode`
|
||||||
|
- `config.viewMode`
|
||||||
|
- page size:
|
||||||
|
- `config.page_size`
|
||||||
|
- `config.pageSize`
|
||||||
|
- card config:
|
||||||
|
- `config.card_config`
|
||||||
|
- `config.cardConfig`
|
||||||
|
|
||||||
|
Build a canonical `TableConfig` for `InvTableConfigPanel`:
|
||||||
|
- `selectedTable`
|
||||||
|
- `tableName`
|
||||||
|
- `columns`
|
||||||
|
- `displayMode`
|
||||||
|
- `useCustomTable`
|
||||||
|
- `customTableName`
|
||||||
|
- `isReadOnly`
|
||||||
|
- `pagination`
|
||||||
|
- `filter`
|
||||||
|
- `dataFilter`
|
||||||
|
- `linkedFilters`
|
||||||
|
- `excludeFilter`
|
||||||
|
- `actions`
|
||||||
|
- `tableStyle`
|
||||||
|
- `toolbar`
|
||||||
|
- `defaultSort`
|
||||||
|
- `cardColumnMapping`
|
||||||
|
- `cardStyle`
|
||||||
|
- `cardsPerRow`
|
||||||
|
- `cardSpacing`
|
||||||
|
- `showHeader`
|
||||||
|
- `showFooter`
|
||||||
|
- `showCheckbox`
|
||||||
|
- `selectionMode`
|
||||||
|
- `autoWidth`
|
||||||
|
- `stickyHeader`
|
||||||
|
- `autoLoad`
|
||||||
|
- `horizontalScroll`
|
||||||
|
|
||||||
|
Column conversion into canonical `TableColumn[]`:
|
||||||
|
- `key = col.key || col.field || col.column_name || col.columnName`
|
||||||
|
- `label = col.label || col.title || col.header || col.column_label || col.columnLabel || col.displayName || key`
|
||||||
|
- preserve:
|
||||||
|
- `visible`
|
||||||
|
- `sortable`
|
||||||
|
- `searchable` or `filterable`
|
||||||
|
- `editable`
|
||||||
|
- `align`
|
||||||
|
- `order`
|
||||||
|
- `format`
|
||||||
|
- `inputType` / `input_type`
|
||||||
|
- `dataType` / `data_type`
|
||||||
|
- `isJoinColumn` / `isEntityJoin`
|
||||||
|
- `thousandSeparator` / `thousand_separator`
|
||||||
|
- `entityDisplayConfig`
|
||||||
|
- `entityJoinInfo`
|
||||||
|
- `additionalJoinInfo`
|
||||||
|
- `autoGeneration` / `auto_generation`
|
||||||
|
|
||||||
|
When `InvTableConfigPanel` calls `onChange(nextTableConfig)`, convert back to the V2List config shape without losing existing V2 fields:
|
||||||
|
- preserve existing `config` object keys.
|
||||||
|
- write both canonical-friendly and current V2 runtime-friendly keys:
|
||||||
|
- `tableName`
|
||||||
|
- `dataSource.table`
|
||||||
|
- `data_source.table`
|
||||||
|
- `viewMode`
|
||||||
|
- `view_mode`
|
||||||
|
- `pageSize`
|
||||||
|
- `page_size`
|
||||||
|
- `cardConfig`
|
||||||
|
- `card_config`
|
||||||
|
- write `columns` in V2-friendly shape:
|
||||||
|
- `key`
|
||||||
|
- `field`
|
||||||
|
- `title`
|
||||||
|
- `header`
|
||||||
|
- plus preserved canonical metadata.
|
||||||
|
- preserve `filter`, `dataFilter`, `linkedFilters`, `excludeFilter`, `actions`, `tableStyle`, `toolbar`, `defaultSort`.
|
||||||
|
- preserve D.10 `autoGeneration`.
|
||||||
|
|
||||||
|
Important:
|
||||||
|
- Avoid adding `table-list` or `TableListConfigPanel` strings to the two scoped files.
|
||||||
|
- `V2TableListConfigPanel.tsx` can remain on disk but should not be imported or selected by `InvDataConfigPanel`.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n "v2-table-list|V2TableListConfigPanel|TableListConfigPanel" frontend/components/v2/config-panels/InvDataConfigPanel.tsx frontend/components/v2/config-panels/V2ListConfigPanel.tsx
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(components/v2/config-panels/(InvDataConfigPanel|V2ListConfigPanel)|lib/registry/components/table/(InvTableConfigPanel|types))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` passes.
|
||||||
|
- The `rg` command returns 0 lines for the two scoped files.
|
||||||
|
- Targeted tsc grep has no output.
|
||||||
|
- Full tsc may still have unrelated baseline errors; report only scoped-file errors.
|
||||||
|
|
||||||
|
Report format:
|
||||||
|
|
||||||
|
1. Files changed.
|
||||||
|
2. InvDataConfigPanel cleanup summary.
|
||||||
|
3. V2ListConfigPanel delegation summary.
|
||||||
|
4. Config conversion summary:
|
||||||
|
- table name
|
||||||
|
- columns
|
||||||
|
- pagination
|
||||||
|
- card config
|
||||||
|
- advanced options preservation
|
||||||
|
5. Verification results.
|
||||||
|
6. Remaining E.2 gaps, if any.
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Table Canonical Cleanup — Phase E.3 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase E.3 + §3.4
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10, E.1, E.2.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- Do not delete old files yet.
|
||||||
|
- Do not remove `TableComponent` early delegation.
|
||||||
|
- Do not edit registry/schema/alias cleanup targets in this phase:
|
||||||
|
- `frontend/lib/registry/DynamicComponentRenderer.tsx`
|
||||||
|
- `frontend/lib/utils/getComponentConfigPanel.tsx`
|
||||||
|
- `frontend/lib/schemas/componentConfig.ts`
|
||||||
|
- `frontend/lib/utils/templateMigrate.ts`
|
||||||
|
- `frontend/lib/utils/componentTypeUtils.ts`
|
||||||
|
- Do not touch domain enum usages where `"table-list"` means a data-source mode, not a component type:
|
||||||
|
- `frontend/lib/registry/components/repeat-container/**`
|
||||||
|
- `frontend/lib/registry/components/v2-repeat-container/**`
|
||||||
|
- `frontend/components/v2/config-panels/V2RepeatContainerConfigPanel.tsx`
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
Remove stale hard-coded table legacy IDs from external active UI/config branches after E.1/E.2. Keep helper-based compatibility (`isTableLikeComponentType`, `isTableLikeComponent`, `getTableNameFromTableLikeComponent`) intact. This phase is a narrow cleanup of stale fallback branches and stale palette/default literals, not the final alias/schema deletion.
|
||||||
|
|
||||||
|
Primary scope:
|
||||||
|
- `frontend/components/screen/modals/MultilangSettingsModal.tsx`
|
||||||
|
- `frontend/components/screen/panels/ComponentsPanel.tsx`
|
||||||
|
- `frontend/components/screen/InvyoneStudio.tsx`
|
||||||
|
- `frontend/lib/utils/responsiveDefaults.ts`
|
||||||
|
|
||||||
|
Optional stale-comment-only scope if obvious and low risk:
|
||||||
|
- `frontend/components/screen/RealtimePreviewDynamic.tsx`
|
||||||
|
- `frontend/components/screen/ScreenNode.tsx`
|
||||||
|
- `frontend/components/screen/panels/V2PropertiesPanel.tsx`
|
||||||
|
- `frontend/components/screen/config-panels/button/DataTab.tsx`
|
||||||
|
- `frontend/components/screen/config-panels/button-config/ActionTab.tsx`
|
||||||
|
- files that already use `isTableLikeComponent*` and only mention `table-list` / `v2-table-list` in comments.
|
||||||
|
|
||||||
|
## Current known cleanup points
|
||||||
|
|
||||||
|
### 1. `MultilangSettingsModal.tsx`
|
||||||
|
|
||||||
|
Current stale code:
|
||||||
|
- `getTypeIcon()` has:
|
||||||
|
- `case "table":`
|
||||||
|
- `case "table-list":`
|
||||||
|
- `case "v2-table-list":`
|
||||||
|
- `NON_INPUT_COMPONENT_TYPES` contains:
|
||||||
|
- `"table"`
|
||||||
|
- `"table-list"`
|
||||||
|
- `"v2-table-list"`
|
||||||
|
|
||||||
|
Required cleanup:
|
||||||
|
- Import and use `isTableLikeComponentType` from `@/lib/utils/componentTypeUtils`.
|
||||||
|
- Make `getTypeIcon(type)` return `Table2` when `isTableLikeComponentType(type)` is true.
|
||||||
|
- Remove the `table-list` / `v2-table-list` switch cases.
|
||||||
|
- Keep `"table"` in the explicit non-input set if useful, but add an early `if (isTableLikeComponentType(compType)) return false;` in `isInputComponent`.
|
||||||
|
- Remove `"table-list"` / `"v2-table-list"` from `NON_INPUT_COMPONENT_TYPES`.
|
||||||
|
- Update comments to say "table-like" instead of naming old IDs.
|
||||||
|
|
||||||
|
### 2. `ComponentsPanel.tsx`
|
||||||
|
|
||||||
|
Current stale code:
|
||||||
|
- `hiddenComponents` contains `"table-list"` and `"v2-table-list"`.
|
||||||
|
- `getComponentIcon()` icon map contains `"v2-table-list": <Table2 ... />`.
|
||||||
|
- Several comments still describe old table shell IDs as active palette fallbacks.
|
||||||
|
|
||||||
|
Required cleanup:
|
||||||
|
- Remove `"table-list"` and `"v2-table-list"` from `hiddenComponents`.
|
||||||
|
- Remove `"v2-table-list"` from icon map.
|
||||||
|
- Keep canonical `"table"` icon.
|
||||||
|
- Keep `v2-repeater` and other non-table cleanup entries unchanged.
|
||||||
|
- Update comments so the palette describes canonical `table` as the only new creation path.
|
||||||
|
|
||||||
|
Do not add new logic that reintroduces old IDs. If registry does not expose those IDs, the hidden list does not need to guard them anymore.
|
||||||
|
|
||||||
|
### 3. `InvyoneStudio.tsx`
|
||||||
|
|
||||||
|
Current stale code around the component drop grid ratio map:
|
||||||
|
- `"table": 1`
|
||||||
|
- `"table-list": 1`
|
||||||
|
- `"v2-table-list": 1`
|
||||||
|
|
||||||
|
Required cleanup:
|
||||||
|
- Keep `"table": 1`.
|
||||||
|
- Remove `"table-list"` and `"v2-table-list"` from the ratio map.
|
||||||
|
- Do not remove `isTableLikeComponentType(component.id)`; it is the compatibility helper and remains until F.7.
|
||||||
|
|
||||||
|
### 4. `responsiveDefaults.ts`
|
||||||
|
|
||||||
|
Current stale code:
|
||||||
|
- comment says `datatable, table-list 등`
|
||||||
|
- `fullWidthComponents` contains `"table-list"`.
|
||||||
|
|
||||||
|
Required cleanup:
|
||||||
|
- Remove `"table-list"` from `fullWidthComponents`.
|
||||||
|
- Update comment to `datatable, table 등` or similar.
|
||||||
|
- Keep `"table"`, `"datatable"`, `"data-table"`, `"grouped-table"`, `"card-list"` as-is.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
Do not change these in E.3:
|
||||||
|
- `TABLE_LIKE_COMPONENT_TYPES` contents. That is F.7.
|
||||||
|
- `LEGACY_TO_UNIFIED` / alias maps. That is F.6.
|
||||||
|
- schema/default registry entries. That is F.4/F.5.
|
||||||
|
- `TableComponent.tsx` early delegation. That is F.1.
|
||||||
|
- old `_shared/*TableList*` files. That is F.2/F.3/F.8.
|
||||||
|
- `v2-table-list` references in docs/types that explicitly describe legacy alias compatibility unless they are in the primary active UI scope above.
|
||||||
|
- repeat-container `DataSourceType = "table-list"`; it is a domain mode, not a component type.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n 'componentType === "table-list"|componentType === "v2-table-list"|case "table-list"|case "v2-table-list"' frontend/
|
||||||
|
rg -n '"table-list"|"v2-table-list"|V2TableListConfigPanel|TableListConfigPanel' frontend/components/screen/modals/MultilangSettingsModal.tsx frontend/components/screen/panels/ComponentsPanel.tsx frontend/components/screen/InvyoneStudio.tsx frontend/lib/utils/responsiveDefaults.ts
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(components/screen/(modals/MultilangSettingsModal|panels/ComponentsPanel|InvyoneStudio)|lib/utils/responsiveDefaults|lib/utils/componentTypeUtils)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` passes.
|
||||||
|
- First `rg` has 0 matches.
|
||||||
|
- Second scoped `rg` has 0 matches.
|
||||||
|
- Targeted tsc grep has no output.
|
||||||
|
- Full tsc may still have unrelated baseline errors; report only scoped-file errors.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
1. Files changed.
|
||||||
|
2. Removed hard-coded stale IDs by file.
|
||||||
|
3. Helper compatibility preserved:
|
||||||
|
- where `isTableLikeComponentType` remains in use
|
||||||
|
- confirmation that `componentTypeUtils.ts` was not changed
|
||||||
|
4. Verification results.
|
||||||
|
5. Remaining gaps for F phase.
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# Table Canonical Cleanup — Phase F.1 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase F.1 + §3.2
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10, E.1~E.3.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- This is F.1 only. Do not start F.2/F.3 deletion work.
|
||||||
|
- Do not delete or edit old `_shared/*TableList*` files in this phase.
|
||||||
|
- Do not edit registry/schema/alias cleanup targets in this phase:
|
||||||
|
- `frontend/lib/registry/DynamicComponentRenderer.tsx`
|
||||||
|
- `frontend/lib/utils/getComponentConfigPanel.tsx`
|
||||||
|
- `frontend/lib/schemas/componentConfig.ts`
|
||||||
|
- `frontend/lib/utils/templateMigrate.ts`
|
||||||
|
- `frontend/lib/utils/componentTypeUtils.ts`
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
`frontend/lib/registry/components/table/TableComponent.tsx` should no longer early-delegate to legacy/v2 shared table runtimes. Remove the old wrapper imports, raw type resolver, meaningful/generic type sets, and the early return block. The canonical `TableComponent` body must become the only runtime path for the table component.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/index.ts` (docstring only)
|
||||||
|
|
||||||
|
## Current code to remove from `TableComponent.tsx`
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- the "Legacy / v2 table-list delegation" doc block near the imports.
|
||||||
|
- imports:
|
||||||
|
- `TableListWrapper as LegacyTableListWrapper` from `./_shared/TableListComponent`
|
||||||
|
- `TableListContainerWrapper as V2TableListContainerWrapper` from `./_shared/V2TableListContainerWrapper`
|
||||||
|
- `_MEANINGFUL_TABLE_TYPES`
|
||||||
|
- `_GENERIC_COMPONENT_TYPES`
|
||||||
|
- `_resolveRawComponentType(...)`
|
||||||
|
- the early block at the start of `TableComponent`:
|
||||||
|
- `const _rawType = _resolveRawComponentType(...)`
|
||||||
|
- `if (_rawType === ...) return <LegacyTableListWrapper ... />`
|
||||||
|
- `if (_rawType === ...) return <V2TableListContainerWrapper ... />`
|
||||||
|
|
||||||
|
Also remove stale comments inside `TableComponent.tsx` that still name old table IDs or old shared wrapper names. Current known extra match:
|
||||||
|
- store sync comment around `tableDisplayStore.setTableDataForComponent` mentions old key convention; rewrite it without old ID literals.
|
||||||
|
|
||||||
|
Do not remove or alter canonical runtime logic added in B/C/D/E:
|
||||||
|
- TableOptions registration and callbacks
|
||||||
|
- tableDisplayStore sync
|
||||||
|
- data source fallback chain
|
||||||
|
- `fieldsToCanonicalColumns`
|
||||||
|
- source/render columns, sticky, hidden/visible, fixed columns
|
||||||
|
- D.2 filters / AdvancedSearchFilters / dataFilter / excludeFilter
|
||||||
|
- D.3 inline edit
|
||||||
|
- D.4 row/bulk actions
|
||||||
|
- D.5 cell renderers
|
||||||
|
- D.6 toolbar/footer/paste/style
|
||||||
|
- D.7 CardView responsive fallback
|
||||||
|
- D.8 TableSettingsModal/GroupedView group sum
|
||||||
|
- B.2/B.3/D.9 DataProvider/DataReceiver/SplitPanel wiring
|
||||||
|
- D.10 autoGeneration metadata preservation
|
||||||
|
|
||||||
|
## `index.ts` docstring cleanup
|
||||||
|
|
||||||
|
Update the top docstring so it describes canonical `table` and its supported display modes, without listing old table runtime IDs as active absorbed targets. Keep the component definition itself unchanged.
|
||||||
|
|
||||||
|
Suggested shape:
|
||||||
|
- "Table — canonical data table component"
|
||||||
|
- "supports table/split/grouped/pivot/card display modes"
|
||||||
|
- avoid naming old table IDs in this file.
|
||||||
|
|
||||||
|
## Important behavior expectation
|
||||||
|
|
||||||
|
After this phase, any component reaching `TableComponent` is rendered by the canonical body. Old layout compatibility is now expected to be handled by the already-completed canonical parity work plus existing alias/migration layers. Do not add a new alternate wrapper path.
|
||||||
|
|
||||||
|
If you find an obvious canonical fallback gap while removing delegation, keep it tiny and local to `TableComponent.tsx`. Examples of acceptable small fallback checks:
|
||||||
|
- table name fallback from existing component config/root props
|
||||||
|
- columns/fields normalization already present
|
||||||
|
|
||||||
|
Do not reintroduce old ID string literals in `TableComponent.tsx`.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
rg -n "table-list|v2-table-list|LegacyTableListWrapper|V2TableListContainerWrapper|_resolveRawComponentType|_MEANINGFUL_TABLE_TYPES|_GENERIC_COMPONENT_TYPES" frontend/lib/registry/components/table/TableComponent.tsx
|
||||||
|
rg -n "table-list|v2-table-list|TableListComponent|V2TableListContainerWrapper" frontend/lib/registry/components/table/index.ts
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/registry/components/table/(TableComponent|index|types|useTableData|cell-renderers|views/(CardView|GroupedView))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` passes.
|
||||||
|
- Both `rg` commands return 0 lines.
|
||||||
|
- Targeted tsc grep has no output.
|
||||||
|
- Full tsc may still have unrelated baseline errors; report only scoped-file errors.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
1. Files changed.
|
||||||
|
2. Removed early delegation items:
|
||||||
|
- imports
|
||||||
|
- resolver/helper sets
|
||||||
|
- early return block
|
||||||
|
- stale comments
|
||||||
|
3. Canonical runtime preserved:
|
||||||
|
- table options
|
||||||
|
- data source/tableName fallback
|
||||||
|
- filters/actions/edit/cell-render/card/group/split data transfer
|
||||||
|
4. Verification results.
|
||||||
|
5. Remaining F phase gaps:
|
||||||
|
- F.2 old `_shared` file deletion
|
||||||
|
- F.3 shared helper variant cleanup
|
||||||
|
- F.4 schema
|
||||||
|
- F.6 alias
|
||||||
|
- F.7 `TABLE_LIKE_COMPONENT_TYPES`
|
||||||
|
- F.8 V2 shim/config-panel cleanup
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Table Canonical Cleanup — Phase F.2 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase F.2
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10, E.1~E.3, F.1.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- This is F.2 only. Do not start F.3/F.4/F.6/F.7/F.8.
|
||||||
|
- Do not edit registry/schema/alias cleanup targets in this phase:
|
||||||
|
- `frontend/lib/registry/DynamicComponentRenderer.tsx`
|
||||||
|
- `frontend/lib/utils/getComponentConfigPanel.tsx`
|
||||||
|
- `frontend/lib/schemas/componentConfig.ts`
|
||||||
|
- `frontend/lib/utils/templateMigrate.ts`
|
||||||
|
- `frontend/lib/utils/componentTypeUtils.ts`
|
||||||
|
- Do not touch domain enum usages where `"table-list"` means a data-source mode.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
Delete the four old shared table runtime/config-panel files after F.1 removed `TableComponent` early delegation.
|
||||||
|
|
||||||
|
Delete exactly these files:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/V2TableListContainerWrapper.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/TableListConfigPanel.tsx`
|
||||||
|
|
||||||
|
Do not delete or edit these in F.2:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/SingleTableWithSticky.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/CardModeRenderer.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts`
|
||||||
|
- `frontend/components/v2/config-panels/V2TableListConfigPanel.tsx`
|
||||||
|
- `frontend/components/v2/V2List.tsx`
|
||||||
|
- `frontend/components/v2/config-panels/V2ListConfigPanel.tsx`
|
||||||
|
|
||||||
|
Those are F.3/F.8 or later.
|
||||||
|
|
||||||
|
## Precondition
|
||||||
|
|
||||||
|
Before deleting, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n 'from .*_shared/(TableListComponent|V2TableListComponent|V2TableListContainerWrapper|TableListConfigPanel)|from "\./_shared/(TableListComponent|V2TableListComponent|V2TableListContainerWrapper|TableListConfigPanel)"|from "./_shared/(TableListComponent|V2TableListComponent|V2TableListContainerWrapper|TableListConfigPanel)"|from "\.\/_shared\/(TableListComponent|V2TableListComponent|V2TableListContainerWrapper|TableListConfigPanel)"' frontend/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 0 matches outside the four files being deleted. If this command finds an import in a file that is not one of the delete targets, stop and report it. Do not delete.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- Broad `rg -l "TableListComponent|V2TableListComponent|V2TableListContainerWrapper|TableListConfigPanel" frontend/` currently catches stale comments, local function names such as `findTableListComponent`, and F.8-owned `V2TableListConfigPanel.tsx`. Do **not** chase those in F.2.
|
||||||
|
- F.2 is deletion-only plus validation.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Delete the four target files. Prefer `git rm` if available; otherwise remove them with the normal file deletion mechanism.
|
||||||
|
|
||||||
|
Do not edit comments in unrelated files just because they mention old names. Stale comments/types are F.5/F.8 or later.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
test ! -e frontend/lib/registry/components/table/_shared/TableListComponent.tsx
|
||||||
|
test ! -e frontend/lib/registry/components/table/_shared/V2TableListComponent.tsx
|
||||||
|
test ! -e frontend/lib/registry/components/table/_shared/V2TableListContainerWrapper.tsx
|
||||||
|
test ! -e frontend/lib/registry/components/table/_shared/TableListConfigPanel.tsx
|
||||||
|
rg -n '_shared/(TableListComponent|V2TableListComponent|V2TableListContainerWrapper|TableListConfigPanel)' frontend/
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/registry/components/table/(_shared/(SingleTableWithSticky|CardModeRenderer|tableListConfigTypes)|TableComponent|index|types|useTableData|cell-renderers|views/(CardView|GroupedView))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `git diff --check` passes.
|
||||||
|
- all four `test ! -e` commands pass.
|
||||||
|
- `_shared/...` import/path grep has 0 matches.
|
||||||
|
- Targeted tsc grep has no output.
|
||||||
|
- Full tsc may still have unrelated baseline errors; report only scoped-file errors.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
1. Precondition result.
|
||||||
|
2. Files deleted.
|
||||||
|
3. Files explicitly not touched:
|
||||||
|
- `SingleTableWithSticky.tsx`
|
||||||
|
- `CardModeRenderer.tsx`
|
||||||
|
- `tableListConfigTypes.ts`
|
||||||
|
- `V2TableListConfigPanel.tsx`
|
||||||
|
- `V2List.tsx`
|
||||||
|
- `V2ListConfigPanel.tsx`
|
||||||
|
4. Verification results.
|
||||||
|
5. Remaining F phase gaps:
|
||||||
|
- F.3 shared helper/variant cleanup
|
||||||
|
- F.4 schema
|
||||||
|
- F.5 stale type/comment cleanup
|
||||||
|
- F.6 alias
|
||||||
|
- F.7 `TABLE_LIKE_COMPONENT_TYPES`
|
||||||
|
- F.8 V2 shim/config-panel cleanup
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# Phase F.3 Claude Prompt — shared helper variant cleanup
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Canonical table cleanup is near the end.
|
||||||
|
|
||||||
|
Completed:
|
||||||
|
- F.1: `TableComponent.tsx` legacy/v2 table-list early delegation removed.
|
||||||
|
- F.2: `_shared/{TableListComponent,V2TableListComponent,V2TableListContainerWrapper,TableListConfigPanel}` deleted.
|
||||||
|
- F.4: `componentConfig.ts` `v2-table-list` schema/default removed.
|
||||||
|
- F.6: alias maps removed `table-list` / `v2-table-list` → `table`.
|
||||||
|
- F.7: `TABLE_LIKE_COMPONENT_TYPES` now keeps only canonical table IDs.
|
||||||
|
- F.8: `V2List`, `V2ListConfigPanel`, `V2TableListConfigPanel`, `v2-list` registry/schema/renderer/demo/type paths removed.
|
||||||
|
|
||||||
|
User policy: this is solution development. Old layout DB/runtime compatibility is not required. Do not preserve old `table-list`, `v2-table-list`, or `v2-list` behavior.
|
||||||
|
|
||||||
|
Unrelated dirty files exist. Do not touch them unless explicitly listed in this prompt.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Clean the remaining `_shared` table helpers after old table-list/v2-list deletion:
|
||||||
|
|
||||||
|
1. Keep `SingleTableWithSticky.tsx` because `FlowWidget` still imports it.
|
||||||
|
2. Remove the `variant="v2"` branch from `SingleTableWithSticky.tsx`.
|
||||||
|
3. Delete unused card helper/type files if precondition confirms no external import:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/CardModeRenderer.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts`
|
||||||
|
|
||||||
|
## Required precondition checks
|
||||||
|
|
||||||
|
Run these before editing and include the result in the report:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "SingleTableWithSticky|CardModeRenderer|tableListConfigTypes" frontend/
|
||||||
|
rg -n "variant=\"v2\"|variant: \"v2\"|variant = \"v2\"|variant === \"v2\"" frontend/lib/registry/components/table/_shared frontend/components/screen/widgets/FlowWidget.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected current shape:
|
||||||
|
- `SingleTableWithSticky` is imported by `frontend/components/screen/widgets/FlowWidget.tsx`.
|
||||||
|
- `CardModeRenderer` has no external import after F.2/F.8.
|
||||||
|
- `tableListConfigTypes` is only used by `CardModeRenderer`.
|
||||||
|
|
||||||
|
If the expected shape differs, stop and report before deleting files.
|
||||||
|
|
||||||
|
## Allowed file scope
|
||||||
|
|
||||||
|
Primary:
|
||||||
|
- `frontend/lib/registry/components/table/_shared/SingleTableWithSticky.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/CardModeRenderer.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts`
|
||||||
|
|
||||||
|
Conditional:
|
||||||
|
- `frontend/components/screen/widgets/FlowWidget.tsx`
|
||||||
|
- Only if its `SingleTableWithSticky` call passes a variant prop or needs type adjustment after the helper prop removal.
|
||||||
|
|
||||||
|
Do not touch canonical table runtime files:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- `frontend/lib/registry/components/table/views/CardView.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/views/GroupedView.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/useTableData.ts`
|
||||||
|
|
||||||
|
Do not touch F.8 files unless a direct import break requires it.
|
||||||
|
|
||||||
|
## Implementation details
|
||||||
|
|
||||||
|
### 1. `SingleTableWithSticky.tsx`
|
||||||
|
|
||||||
|
Remove v2-specific compatibility:
|
||||||
|
- Delete `SingleTableVariant`.
|
||||||
|
- Remove `variant?: SingleTableVariant` prop.
|
||||||
|
- Remove `variant = "default"` from the component parameters.
|
||||||
|
- Remove `const isV2 = variant === "v2"`.
|
||||||
|
- Collapse all `isV2 ? A : B` branches to the existing default / FlowWidget behavior.
|
||||||
|
- Remove v2-only props if they become unused:
|
||||||
|
- `onEditSave`
|
||||||
|
- `columnMeta`
|
||||||
|
- `categoryMappings`
|
||||||
|
- Remove v2-only inline editing branches:
|
||||||
|
- category/code `<select>`
|
||||||
|
- date/datetime `require("@/components/screen/filters/InlineCellDatePicker")`
|
||||||
|
- number input branch
|
||||||
|
- Keep FlowWidget behavior intact:
|
||||||
|
- normal text input editing
|
||||||
|
- sticky left/right columns
|
||||||
|
- checkbox column
|
||||||
|
- search highlight
|
||||||
|
- langKey translation
|
||||||
|
- sort header
|
||||||
|
- empty state
|
||||||
|
|
||||||
|
Also update the file header comments so they no longer mention:
|
||||||
|
- `table-list`
|
||||||
|
- `v2-table-list`
|
||||||
|
- `variant="v2"`
|
||||||
|
|
||||||
|
The helper may remain named `SingleTableWithSticky`; do not rename it.
|
||||||
|
|
||||||
|
### 2. `CardModeRenderer.tsx` and `tableListConfigTypes.ts`
|
||||||
|
|
||||||
|
If precondition confirms no external import:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rm frontend/lib/registry/components/table/_shared/CardModeRenderer.tsx
|
||||||
|
git rm frontend/lib/registry/components/table/_shared/tableListConfigTypes.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not keep unused files just for old layout compatibility.
|
||||||
|
|
||||||
|
If a real external import exists, do not delete. Instead remove only `variant="v2"` from `CardModeRenderer` and report the remaining consumer.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
git diff --cached --check
|
||||||
|
rg -n "variant=\"v2\"|variant: \"v2\"|variant = \"v2\"|variant === \"v2\"" frontend/lib/registry/components/table/_shared frontend/components/screen/widgets/FlowWidget.tsx
|
||||||
|
rg -n "CardModeRenderer|tableListConfigTypes" frontend/
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(lib/registry/components/table/_shared|components/screen/widgets/FlowWidget)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- diff checks pass.
|
||||||
|
- `variant="v2"` search returns 0.
|
||||||
|
- `CardModeRenderer|tableListConfigTypes` search returns 0 if deleted.
|
||||||
|
- scoped tsc output is empty, or only baseline errors clearly unrelated to changed lines.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
Report:
|
||||||
|
1. Precondition results.
|
||||||
|
2. Files changed/deleted.
|
||||||
|
3. Exact v2-variant branches removed from `SingleTableWithSticky`.
|
||||||
|
4. Whether `CardModeRenderer` / `tableListConfigTypes` were deleted or preserved, and why.
|
||||||
|
5. Validation command results.
|
||||||
|
6. Remaining gap: F.5 stale comment/type cleanup only.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Table Canonical Cleanup — Phase F.4 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase F.4
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10, E.1~E.3, F.1, F.2.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- F.2 deleted four old `_shared` files, and those deletions may be staged. Do not unstage, restore, or recreate them.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- This is F.4 only. Do not start F.3/F.5/F.6/F.7/F.8.
|
||||||
|
- Do not edit alias/migration/helper cleanup targets in this phase:
|
||||||
|
- `frontend/lib/registry/DynamicComponentRenderer.tsx`
|
||||||
|
- `frontend/lib/utils/getComponentConfigPanel.tsx`
|
||||||
|
- `frontend/lib/utils/templateMigrate.ts`
|
||||||
|
- `frontend/lib/utils/componentTypeUtils.ts`
|
||||||
|
- Do not touch domain enum usages where `"table-list"` means a data-source mode.
|
||||||
|
|
||||||
|
User policy:
|
||||||
|
- This is solution development; old data formats and migration for old table layout JSON are not required.
|
||||||
|
- Therefore it is acceptable to remove the old `v2-table-list` schema/default now.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
Remove the `v2-table-list` schema/default entries from `frontend/lib/schemas/componentConfig.ts`.
|
||||||
|
|
||||||
|
File scope:
|
||||||
|
- `frontend/lib/schemas/componentConfig.ts`
|
||||||
|
|
||||||
|
## Current removal targets
|
||||||
|
|
||||||
|
Remove exactly these `v2-table-list` pieces:
|
||||||
|
- the `v2TableListOverridesSchema` constant block.
|
||||||
|
- `"v2-table-list": v2TableListOverridesSchema,` from `componentOverridesSchemaRegistry`.
|
||||||
|
- `"v2-table-list": { ... }` from `componentDefaultsRegistry`.
|
||||||
|
- any stale comment in this file that says `v2-table-list` is an active schema/default entry.
|
||||||
|
|
||||||
|
Do not remove:
|
||||||
|
- `v2-list` schema/defaults.
|
||||||
|
- `v2-split-panel-layout`, `v2-table-search-widget`, `v2-repeater`, or other V2 schemas.
|
||||||
|
- generic component schema logic.
|
||||||
|
- alias/migration maps in other files.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
git diff --cached --check
|
||||||
|
rg -n "v2-table-list|v2TableListOverridesSchema" frontend/lib/schemas/componentConfig.ts
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/schemas/componentConfig"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- both diff checks pass.
|
||||||
|
- scoped `rg` has 0 matches.
|
||||||
|
- targeted tsc grep has no output.
|
||||||
|
- Full tsc may still have unrelated baseline errors; report only scoped-file errors.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
1. Files changed.
|
||||||
|
2. Removed schema/default items:
|
||||||
|
- schema const
|
||||||
|
- overrides registry entry
|
||||||
|
- defaults registry entry
|
||||||
|
3. Confirm not touched:
|
||||||
|
- aliases/migration/helper files
|
||||||
|
- F.2 deleted files
|
||||||
|
- V2 schemas other than `v2-table-list`
|
||||||
|
4. Verification results.
|
||||||
|
5. Remaining F phase gaps:
|
||||||
|
- F.3 shared helper/variant cleanup
|
||||||
|
- F.5 stale type/comment cleanup
|
||||||
|
- F.6 alias
|
||||||
|
- F.7 `TABLE_LIKE_COMPONENT_TYPES`
|
||||||
|
- F.8 V2 shim/config-panel cleanup
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# Phase F.5 Claude Prompt — stale table-list/v2-list comment cleanup
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Canonical table cleanup is almost complete.
|
||||||
|
|
||||||
|
Already completed:
|
||||||
|
- F.1: `TableComponent.tsx` legacy/v2 table-list early delegation removed.
|
||||||
|
- F.2: old `_shared` table-list bodies deleted.
|
||||||
|
- F.3: `_shared/SingleTableWithSticky.tsx` kept only for `FlowWidget`; `variant="v2"` removed; `CardModeRenderer.tsx` and `tableListConfigTypes.ts` deleted.
|
||||||
|
- F.4: `componentConfig.ts` `v2-table-list` schema/default removed.
|
||||||
|
- F.6: alias maps no longer map `table-list` / `v2-table-list` to canonical `table`.
|
||||||
|
- F.7: `TABLE_LIKE_COMPONENT_TYPES` no longer includes old table IDs.
|
||||||
|
- F.8: `V2List`, `V2ListConfigPanel`, `V2TableListConfigPanel`, active `v2-list` registry/renderer/schema/type paths removed.
|
||||||
|
|
||||||
|
User policy: old layout DB/runtime compatibility is not required. However F.5 is a low-risk stale-comment/type-doc cleanup phase, not a broad runtime refactor.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Remove stale comments/docstrings/type examples that still imply active support for:
|
||||||
|
- `table-list`
|
||||||
|
- `v2-table-list`
|
||||||
|
- `v2-list`
|
||||||
|
- `V2List`
|
||||||
|
- deleted `_shared/{Table,V2}TableListComponent`
|
||||||
|
- deleted `CardModeRenderer`
|
||||||
|
- deleted `tableListConfigTypes`
|
||||||
|
|
||||||
|
Do not change runtime behavior unless a line is provably just an obsolete alias/comment with no behavior.
|
||||||
|
|
||||||
|
## Important guardrails
|
||||||
|
|
||||||
|
Do not blindly replace every `table-list` string.
|
||||||
|
|
||||||
|
Some strings may still be real runtime concepts unrelated to deleted component types:
|
||||||
|
- `table-list-${componentId}` in `tableDisplayStore` is a store key convention. Do not change this key unless you also audit every consumer and prove it is unused. Prefer updating comments around it or explicitly reporting that it remains as a historical fallback key.
|
||||||
|
- `dataSourceType === "table-list"` in repeater/repeat-container config may mean "data source is a table/list", not old component type. Do not change it in F.5 unless you verify it is only stale wording.
|
||||||
|
|
||||||
|
Do not edit canonical table runtime files for behavior:
|
||||||
|
- `frontend/lib/registry/components/table/TableComponent.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/useTableData.ts`
|
||||||
|
- `frontend/lib/registry/components/table/views/CardView.tsx`
|
||||||
|
- `frontend/lib/registry/components/table/views/GroupedView.tsx`
|
||||||
|
|
||||||
|
## Suggested search
|
||||||
|
|
||||||
|
Start with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "table-list|v2-table-list|v2-list|V2List|TableListComponent|V2TableList|CardModeRenderer|tableListConfigTypes" \
|
||||||
|
frontend/contexts/TableOptionsContext.tsx \
|
||||||
|
frontend/stores/tableDisplayStore.ts \
|
||||||
|
frontend/lib/registry/components/table/cell-renderers.tsx \
|
||||||
|
frontend/lib/fieldConfig/adapters.ts \
|
||||||
|
frontend/lib/api/screenGroup.ts \
|
||||||
|
frontend/types \
|
||||||
|
frontend/components/screen \
|
||||||
|
frontend/components/v2 \
|
||||||
|
frontend/lib/registry/components/table
|
||||||
|
```
|
||||||
|
|
||||||
|
Then classify each match:
|
||||||
|
- stale comment/docstring/example: update or remove
|
||||||
|
- real runtime key/value: preserve and report
|
||||||
|
- unrelated business data source wording: preserve and report
|
||||||
|
|
||||||
|
## Current known stale targets
|
||||||
|
|
||||||
|
High confidence comment/doc cleanup:
|
||||||
|
- `frontend/contexts/TableOptionsContext.tsx`
|
||||||
|
- comments referencing legacy `_shared/{Table,V2}TableListComponent`
|
||||||
|
- `frontend/stores/tableDisplayStore.ts`
|
||||||
|
- comments/docstrings referencing deleted old bodies
|
||||||
|
- keep the runtime key functions unless you audit all consumers
|
||||||
|
- `frontend/lib/registry/components/table/cell-renderers.tsx`
|
||||||
|
- docstring referencing `_shared/V2TableListComponent.tsx`
|
||||||
|
- `frontend/lib/fieldConfig/adapters.ts`
|
||||||
|
- docstring still says fields feed legacy `table-list` / hidden `v2-table-list` bodies
|
||||||
|
- `frontend/lib/api/screenGroup.ts`
|
||||||
|
- example comment mentioning old table component IDs
|
||||||
|
- `frontend/types/invyone-component.ts`
|
||||||
|
- example comment mentioning old table component IDs
|
||||||
|
- `frontend/types/screen-management.ts`
|
||||||
|
- example comment mentioning old table component IDs
|
||||||
|
- `frontend/types/component-events.ts`
|
||||||
|
- publisher/subscriber comments mentioning `v2-table-list`
|
||||||
|
- `frontend/types/table-options.ts`
|
||||||
|
- `table_id` example can use canonical `table-123`
|
||||||
|
- `frontend/lib/registry/components/table/types.ts`
|
||||||
|
- top/doc comments and C.5 alias docs referencing old table IDs/bodies
|
||||||
|
- `frontend/lib/registry/components/table/InvTableConfigPanel.tsx`
|
||||||
|
- hint text/comment referencing deleted `_shared` bodies
|
||||||
|
- F.8 disposal comments in:
|
||||||
|
- `frontend/components/v2/registerV2Components.ts`
|
||||||
|
- `frontend/components/v2/V2ComponentRenderer.tsx`
|
||||||
|
- `frontend/components/v2/V2ComponentsDemo.tsx`
|
||||||
|
- `frontend/components/v2/index.ts`
|
||||||
|
- `frontend/components/v2/config-panels/index.ts`
|
||||||
|
- `frontend/components/v2/config-panels/InvDataConfigPanel.tsx`
|
||||||
|
- `frontend/types/v2-components.ts`
|
||||||
|
- `frontend/lib/schemas/componentConfig.ts`
|
||||||
|
- `frontend/lib/utils/getComponentConfigPanel.tsx`
|
||||||
|
|
||||||
|
Likely preserve / report, do not edit blindly:
|
||||||
|
- `frontend/components/v2/config-panels/V2RepeatContainerConfigPanel.tsx`
|
||||||
|
- `dataSourceType` option value `"table-list"` may be a business mode, not deleted component ID.
|
||||||
|
- `frontend/stores/tableDisplayStore.ts`
|
||||||
|
- `table-list-${componentId}` runtime key fallback may still be referenced by button/export code. If not removed, update comments so it is described as a historical fallback key, not an active deleted body integration.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
git diff --cached --check
|
||||||
|
rg -n "v2-table-list|V2TableList|V2ListConfigPanel|V2TableListConfigPanel|CardModeRenderer|tableListConfigTypes" frontend/
|
||||||
|
rg -n "table-list|v2-list|V2List|TableListComponent" frontend/ | head -200
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(contexts/TableOptionsContext|stores/tableDisplayStore|lib/registry/components/table/(cell-renderers|types|InvTableConfigPanel)|lib/fieldConfig/adapters|lib/api/screenGroup|types/|components/v2|components/screen/(RealtimePreviewDynamic|ScreenNode|config-panels/button|config-panels/button-config|widgets/TabsWidget))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- diff checks pass.
|
||||||
|
- Deleted file/type names should be 0 or only in this new note file if grep includes notes. The validation above only scans `frontend/`.
|
||||||
|
- Remaining `table-list` / `v2-list` matches, if any, must be explicitly classified as runtime key/business wording, not stale support.
|
||||||
|
- scoped tsc should show no new errors. Existing baseline errors in unrelated changed files may remain; classify them.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
Report:
|
||||||
|
1. Files changed.
|
||||||
|
2. Stale comments/examples removed or rewritten.
|
||||||
|
3. Runtime strings intentionally preserved, with reason.
|
||||||
|
4. Remaining grep matches and classification.
|
||||||
|
5. Validation results.
|
||||||
|
6. Final remaining gap, if any. If none, say F phase cleanup is complete except unrelated baseline TypeScript errors.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Table Canonical Cleanup — Phase F.6 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase F.6
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10, E.1~E.3, F.1, F.2, F.4.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- F.2 deleted four old `_shared` files, and those deletions may be staged. Do not unstage, restore, or recreate them.
|
||||||
|
- F.4 edited `frontend/lib/schemas/componentConfig.ts`. Do not modify it in this phase.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- This is F.6 only. Do not start F.3/F.5/F.7/F.8.
|
||||||
|
- Do not touch domain enum usages where `"table-list"` means a data-source mode.
|
||||||
|
|
||||||
|
User policy:
|
||||||
|
- This is solution development; old table layout data formats and migration compatibility are not required.
|
||||||
|
- Therefore it is acceptable to remove old table alias mappings now.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
Remove old table alias routing entries that map `v2-table-list` / `table-list` to canonical `table`.
|
||||||
|
|
||||||
|
File scope:
|
||||||
|
- `frontend/lib/registry/DynamicComponentRenderer.tsx`
|
||||||
|
- `frontend/lib/utils/templateMigrate.ts`
|
||||||
|
- `frontend/lib/utils/getComponentConfigPanel.tsx`
|
||||||
|
|
||||||
|
## Current removal targets
|
||||||
|
|
||||||
|
### 1. `DynamicComponentRenderer.tsx`
|
||||||
|
|
||||||
|
In local `LEGACY_TO_UNIFIED`, remove only:
|
||||||
|
- `"v2-table-list": "table"`
|
||||||
|
- `"table-list": "table"`
|
||||||
|
|
||||||
|
If there is a table-specific comment that exists only for those two entries, remove that comment too.
|
||||||
|
|
||||||
|
Do not remove:
|
||||||
|
- `v2-split-panel-layout` or split-panel aliases.
|
||||||
|
- `v2-table-search-widget` / `table-search-widget`.
|
||||||
|
- data-table/datatable handling if present elsewhere.
|
||||||
|
- any non-table alias.
|
||||||
|
|
||||||
|
### 2. `templateMigrate.ts`
|
||||||
|
|
||||||
|
In `LEGACY_TO_UNIFIED`, remove only:
|
||||||
|
- `'v2-table-list': 'table'`
|
||||||
|
- `'table-list': 'table'`
|
||||||
|
|
||||||
|
Do not remove other migration aliases.
|
||||||
|
|
||||||
|
### 3. `getComponentConfigPanel.tsx`
|
||||||
|
|
||||||
|
In `CONFIG_PANEL_ALIAS`, remove only:
|
||||||
|
- `"v2-table-list": "table"`
|
||||||
|
- `"table-list": "table"`
|
||||||
|
|
||||||
|
Clean up nearby stale comments if they specifically advertise these table aliases.
|
||||||
|
|
||||||
|
Do not remove aliases for search/stats/container/divider/title/button.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
Do not edit:
|
||||||
|
- `frontend/lib/utils/componentTypeUtils.ts` — F.7 owns `TABLE_LIKE_COMPONENT_TYPES`.
|
||||||
|
- `frontend/lib/schemas/componentConfig.ts` — F.4 already handled schema/default.
|
||||||
|
- `frontend/lib/registry/components/table/**` — F.1/F.2 already handled runtime deletion.
|
||||||
|
- `frontend/types/**` stale comments — F.5 owns those.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
git diff --cached --check
|
||||||
|
rg -n '"v2-table-list": "table"|"table-list": "table"' frontend/lib/registry/DynamicComponentRenderer.tsx frontend/lib/utils/getComponentConfigPanel.tsx
|
||||||
|
rg -n "'v2-table-list': 'table'|'table-list': 'table'" frontend/lib/utils/templateMigrate.ts
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(lib/registry/DynamicComponentRenderer|lib/utils/(templateMigrate|getComponentConfigPanel))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- both diff checks pass.
|
||||||
|
- scoped alias grep has 0 matches.
|
||||||
|
- targeted tsc grep has no output.
|
||||||
|
- Full tsc may still have unrelated baseline errors; report only scoped-file errors.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
1. Files changed.
|
||||||
|
2. Removed alias entries by file.
|
||||||
|
3. Confirm not touched:
|
||||||
|
- `componentTypeUtils.ts`
|
||||||
|
- `componentConfig.ts`
|
||||||
|
- table runtime files / F.2 deleted files
|
||||||
|
- other non-table aliases
|
||||||
|
4. Verification results.
|
||||||
|
5. Remaining F phase gaps:
|
||||||
|
- F.3 shared helper/variant cleanup
|
||||||
|
- F.5 stale type/comment cleanup
|
||||||
|
- F.7 `TABLE_LIKE_COMPONENT_TYPES`
|
||||||
|
- F.8 V2 shim/config-panel cleanup
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Table Canonical Cleanup — Phase F.7 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase F.7
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10, E.1~E.3, F.1, F.2, F.4, F.6.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- F.2 deleted four old `_shared` files, and those deletions may be staged. Do not unstage, restore, or recreate them.
|
||||||
|
- F.4 edited `frontend/lib/schemas/componentConfig.ts`; F.6 edited alias/migration files. Do not modify those in this phase.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- This is F.7 only. Do not start F.3/F.5/F.8.
|
||||||
|
- Do not touch domain enum usages where `"table-list"` means a data-source mode.
|
||||||
|
|
||||||
|
User policy:
|
||||||
|
- This is solution development; old table layout data formats and migration compatibility are not required.
|
||||||
|
- F.1/F.2/F.4/F.6 already removed runtime delegation, old bodies, old schema/defaults, and old aliases.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
Update `TABLE_LIKE_COMPONENT_TYPES` so table-like helper recognition no longer includes old table IDs.
|
||||||
|
|
||||||
|
File scope:
|
||||||
|
- `frontend/lib/utils/componentTypeUtils.ts`
|
||||||
|
|
||||||
|
## Required changes
|
||||||
|
|
||||||
|
In `componentTypeUtils.ts`:
|
||||||
|
- Remove `"table-list"` from `TABLE_LIKE_COMPONENT_TYPES`.
|
||||||
|
- Remove `"v2-table-list"` from `TABLE_LIKE_COMPONENT_TYPES`.
|
||||||
|
- Keep:
|
||||||
|
- `"table"`
|
||||||
|
- `"data-table"`
|
||||||
|
- `"datatable"`
|
||||||
|
- Update the surrounding docstring/comments so they describe the new state:
|
||||||
|
- canonical table is `"table"`.
|
||||||
|
- external old non-canonical data-table names `"data-table"` / `"datatable"` remain recognized.
|
||||||
|
- do not claim `table-list` or `v2-table-list` are still recognized.
|
||||||
|
|
||||||
|
Do not change:
|
||||||
|
- `isTableLikeComponentType` function behavior other than the set contents.
|
||||||
|
- `isTableLikeComponent`.
|
||||||
|
- `getTableNameFromTableLikeComponent`.
|
||||||
|
- older unrelated helpers in the same file unless a comment directly contradicts F.7.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
Do not edit:
|
||||||
|
- callers in `components/` or `registry/components/`; stale caller comments are F.5.
|
||||||
|
- `DynamicComponentRenderer.tsx`, `templateMigrate.ts`, `getComponentConfigPanel.tsx`; F.6 already handled those.
|
||||||
|
- `componentConfig.ts`; F.4 already handled it.
|
||||||
|
- table runtime files.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
git diff --cached --check
|
||||||
|
rg -n 'table-list|v2-table-list' frontend/lib/utils/componentTypeUtils.ts
|
||||||
|
rg -n 'TABLE_LIKE_COMPONENT_TYPES|\"table\"|\"data-table\"|\"datatable\"' frontend/lib/utils/componentTypeUtils.ts
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^lib/utils/componentTypeUtils"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- both diff checks pass.
|
||||||
|
- first scoped grep has 0 matches.
|
||||||
|
- second grep shows the set still contains `table`, `data-table`, and `datatable`.
|
||||||
|
- targeted tsc grep has no output.
|
||||||
|
- Full tsc may still have unrelated baseline errors; report only scoped-file errors.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
1. Files changed.
|
||||||
|
2. Set change summary:
|
||||||
|
- removed old IDs
|
||||||
|
- preserved canonical/external IDs
|
||||||
|
3. Confirm not touched:
|
||||||
|
- callers
|
||||||
|
- aliases/migration files
|
||||||
|
- schema/default files
|
||||||
|
- F.2 deleted files
|
||||||
|
4. Verification results.
|
||||||
|
5. Remaining F phase gaps:
|
||||||
|
- F.3 shared helper/variant cleanup
|
||||||
|
- F.5 stale type/comment cleanup
|
||||||
|
- F.8 V2 shim/config-panel cleanup
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Table Canonical Cleanup — Phase F.8 Claude Prompt
|
||||||
|
|
||||||
|
You are working in `/Users/gbpark/invyone`.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `notes/gbpark/2026-05-20-table-canonical-cleanup-plan.md` §8 Phase F.8
|
||||||
|
- Completed phases: B.1~B.4, C.1~C.5, D.1~D.10, E.1~E.3, F.1, F.2, F.4, F.6, F.7.
|
||||||
|
|
||||||
|
Important workspace rule:
|
||||||
|
- Do not revert or touch unrelated dirty files.
|
||||||
|
- F.2 deleted four old `_shared` files, and those deletions may be staged. Do not unstage, restore, or recreate them.
|
||||||
|
- F.4/F.6/F.7 already edited schema/alias/helper files. Work with those changes; do not revert them.
|
||||||
|
- Known unrelated dirty files include:
|
||||||
|
- `frontend/app/(main)/admin/systemMng/numberingRuleList/page.tsx`
|
||||||
|
- `frontend/components/layout/TabBar.tsx`
|
||||||
|
- `frontend/styles/v5-layout.css`
|
||||||
|
- `notes/gbpark/2026-05-20-control-ide-refactor.md`
|
||||||
|
- `notes/gbpark/2026-05-20-numbering-rule-*.html`
|
||||||
|
- `open-design/`
|
||||||
|
- This is F.8 only. Do not start F.3 or F.5.
|
||||||
|
- Do not touch domain enum usages where `"table-list"` means a data-source mode.
|
||||||
|
|
||||||
|
User policy:
|
||||||
|
- This is solution development; old table/V2 list data formats and migration compatibility are not required.
|
||||||
|
- `v2-list` shim/config panel can now be removed. Canonical `table` is the only list/table creation path.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
Remove the `V2List` shim and its config panels, then remove active `v2-list` registration/schema/renderer/type paths. Keep `v2-repeater` and other V2 components.
|
||||||
|
|
||||||
|
## Delete files
|
||||||
|
|
||||||
|
Delete exactly:
|
||||||
|
- `frontend/components/v2/V2List.tsx`
|
||||||
|
- `frontend/components/v2/config-panels/V2ListConfigPanel.tsx`
|
||||||
|
- `frontend/components/v2/config-panels/V2TableListConfigPanel.tsx`
|
||||||
|
|
||||||
|
Do not delete:
|
||||||
|
- `frontend/components/v2/V2Repeater.tsx`
|
||||||
|
- `frontend/components/v2/config-panels/InvDataConfigPanel.tsx`
|
||||||
|
- any v2 layout/group/biz/hierarchy files.
|
||||||
|
|
||||||
|
## Update active imports/registries
|
||||||
|
|
||||||
|
### `frontend/components/v2/registerV2Components.ts`
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- `V2List` import.
|
||||||
|
- `V2ListConfigPanel` import if still present.
|
||||||
|
- entire `v2-list` component definition from `v2ComponentDefinitions`.
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
- `v2-repeater` definition and `InvDataConfigPanel`.
|
||||||
|
- all other V2 components.
|
||||||
|
|
||||||
|
### `frontend/components/v2/config-panels/InvDataConfigPanel.tsx`
|
||||||
|
|
||||||
|
This panel should no longer import or render `V2ListConfigPanel`.
|
||||||
|
|
||||||
|
Required:
|
||||||
|
- remove `V2ListConfigPanel` import.
|
||||||
|
- remove `v2-list` from `DataComponentType`.
|
||||||
|
- remove read kind / v2-list crumb option, or otherwise make the only usable type `v2-repeater`.
|
||||||
|
- remove branch `normalizedType === "v2-list"`.
|
||||||
|
- update `_normalizeComponentType` fallback to `"v2-repeater"`.
|
||||||
|
- keep `InvRepeaterConfigPanel` rendering for `v2-repeater`.
|
||||||
|
- update comments/docstring so they no longer advertise `v2-list`.
|
||||||
|
|
||||||
|
### `frontend/components/v2/config-panels/index.ts`
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- `export { V2ListConfigPanel } from "./V2ListConfigPanel";`
|
||||||
|
|
||||||
|
### `frontend/lib/utils/getComponentConfigPanel.tsx`
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- `"v2-list": () => import("@/components/v2/config-panels/InvDataConfigPanel"),`
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
- `"v2-repeater": () => import("@/components/v2/config-panels/InvDataConfigPanel"),`
|
||||||
|
|
||||||
|
### `frontend/components/v2/V2ComponentRenderer.tsx`
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- `isV2List` import.
|
||||||
|
- `V2List` import.
|
||||||
|
- `if (isV2List(props)) return <V2List ... />` branch.
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
- V2Layout / V2Group / V2Biz / V2Hierarchy.
|
||||||
|
|
||||||
|
### `frontend/components/v2/V2ComponentsDemo.tsx`
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- `V2List` import.
|
||||||
|
- the List tab trigger/content and any V2List demo-only sample data/state.
|
||||||
|
- default `activeTab` should point to an existing tab, e.g. `"layout"`.
|
||||||
|
|
||||||
|
Keep the rest of the demo compiling.
|
||||||
|
|
||||||
|
### `frontend/components/v2/index.ts`
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- `export { V2List } from "./V2List";`
|
||||||
|
- V2List type re-exports (`V2ListViewMode`, `ListColumn`, `V2ListConfig`, `V2ListProps`).
|
||||||
|
- comments that advertise V2List.
|
||||||
|
|
||||||
|
### `frontend/types/v2-components.ts`
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
- `"V2List"` from `V2ComponentType`.
|
||||||
|
- V2List section:
|
||||||
|
- `V2ListViewMode`
|
||||||
|
- `ListColumn`
|
||||||
|
- `V2ListCardConfig`
|
||||||
|
- `V2ListConfig`
|
||||||
|
- `V2ListProps`
|
||||||
|
- `V2ListProps` from `V2ComponentProps`.
|
||||||
|
- `isV2List(...)`.
|
||||||
|
- `LEGACY_TO_V2_MAP` entries that map old list-ish components to `"V2List"`:
|
||||||
|
- `"table-search-widget": "V2List"`
|
||||||
|
- `"modal-repeater-table": "V2List"`
|
||||||
|
- `"repeater-field-group": "V2List"`
|
||||||
|
- `"card-display": "V2List"`
|
||||||
|
|
||||||
|
Do not remove layout/group/biz/hierarchy/repeater types.
|
||||||
|
|
||||||
|
### `frontend/lib/schemas/componentConfig.ts`
|
||||||
|
|
||||||
|
Remove active `v2-list` schema/default:
|
||||||
|
- `v2ListOverridesSchema` const.
|
||||||
|
- `"v2-list": v2ListOverridesSchema` from `componentOverridesSchemaRegistry`.
|
||||||
|
- `"v2-list": { ... }` from `componentDefaultsRegistry`.
|
||||||
|
|
||||||
|
Do not remove `v2-repeater`, `v2-layout`, `v2-group`, `v2-biz`, `v2-hierarchy`.
|
||||||
|
|
||||||
|
### Active `v2-list` fallback branches
|
||||||
|
|
||||||
|
Remove active `v2-list` hard branches from:
|
||||||
|
- `frontend/lib/utils/buttonActions.ts`
|
||||||
|
- remove the fallback that searches `comp.componentType === "v2-list"`.
|
||||||
|
- table-like helper path should remain.
|
||||||
|
- `frontend/components/screen/config-panels/button-config/ActionTab.tsx`
|
||||||
|
- remove `compType === "v2-list"` special case.
|
||||||
|
- table-like helper path should remain.
|
||||||
|
- `frontend/components/screen/panels/V2PropertiesPanel.tsx`
|
||||||
|
- remove `componentId === "v2-list"` special extraProps branch.
|
||||||
|
|
||||||
|
Only remove active `v2-list` logic. Stale broad comments in unrelated files can be left for F.5 unless they block validation.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
git diff --cached --check
|
||||||
|
test ! -e frontend/components/v2/V2List.tsx
|
||||||
|
test ! -e frontend/components/v2/config-panels/V2ListConfigPanel.tsx
|
||||||
|
test ! -e frontend/components/v2/config-panels/V2TableListConfigPanel.tsx
|
||||||
|
rg -n "V2List|V2ListConfigPanel|V2TableListConfigPanel|v2-list" \
|
||||||
|
frontend/components/v2 \
|
||||||
|
frontend/types/v2-components.ts \
|
||||||
|
frontend/lib/schemas/componentConfig.ts \
|
||||||
|
frontend/lib/utils/getComponentConfigPanel.tsx \
|
||||||
|
frontend/lib/utils/buttonActions.ts \
|
||||||
|
frontend/components/screen/config-panels/button-config/ActionTab.tsx \
|
||||||
|
frontend/components/screen/panels/V2PropertiesPanel.tsx
|
||||||
|
cd frontend && npx tsc --noEmit --pretty false 2>&1 | rg "^(components/v2|types/v2-components|lib/schemas/componentConfig|lib/utils/(getComponentConfigPanel|buttonActions)|components/screen/(config-panels/button-config/ActionTab|panels/V2PropertiesPanel))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- both diff checks pass.
|
||||||
|
- all three `test ! -e` commands pass.
|
||||||
|
- scoped `rg` has 0 matches, except if a remaining match is clearly a stale comment in a file not edited by F.8; report it explicitly.
|
||||||
|
- targeted tsc grep has no new scoped errors. If baseline errors in these large files already exist, classify them.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
1. Files deleted.
|
||||||
|
2. Files changed.
|
||||||
|
3. Removed active `v2-list` paths:
|
||||||
|
- registry
|
||||||
|
- config panel
|
||||||
|
- renderer/demo/index
|
||||||
|
- types
|
||||||
|
- schema/default
|
||||||
|
- active fallback branches
|
||||||
|
4. Confirm preserved:
|
||||||
|
- `v2-repeater`
|
||||||
|
- v2-layout/group/biz/hierarchy
|
||||||
|
- table canonical runtime
|
||||||
|
- F.2 deleted files remain deleted
|
||||||
|
5. Verification results.
|
||||||
|
6. Remaining F phase gaps:
|
||||||
|
- F.3 shared helper/variant cleanup
|
||||||
|
- F.5 stale type/comment cleanup
|
||||||
Submodule
+1
Submodule open-design added at 96b6db14c2
Reference in New Issue
Block a user