Files
gtsit/design/post-5.html
2026-08-10 21:19:10 +09:00

1941 lines
149 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CCTV 화소수와 저장기간, 적정 기준은? | GTS 정보통신</title>
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml">
<script src="assets/5b13e7f6-dae3-4ac7-8ea5-3b76448f1cfb.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>/* ============================================================
MONA Family Design System — global entry point
------------------------------------------------------------
Consuming sites link THIS one file. It is an @import manifest
only — every token, font and base rule lives in tokens/*.css.
Add a new family site by linking styles.css and toggling `.dark`
on the root element; the family look follows automatically.
============================================================ */
/* ============================================================
MONA Family — Webfont loading
------------------------------------------------------------
The #1 family-wide fix: all three sites declared a font in CSS
but never LOADED one, so every site rendered in whatever the OS
provided. We load a single Korean+Latin webfont for real.
Family font: Pretendard (OFL, open-source, optimised for Korean
alongside Latin). One variable woff2 covers weights 400900.
Served from jsDelivr (pinned). ⚠️ For production you may want to
vendor this file into the repo / serve from an internal CDN —
see readme.md "Webfont" note.
============================================================ */
@font-face {
font-family: 'Pretendard Variable';
font-weight: 400 900;
font-style: normal;
font-display: swap;
src: url("assets/cb6787c5-c940-4770-a3a6-e0331204618a.woff2") format('woff2-variations');
}
/* ============================================================
MONA Family — Color tokens
------------------------------------------------------------
Single source of truth for color across every MONA family site.
Values reverse-engineered from the three live products and
converged: Pass/Files already shared primary #2563eb, so that is
the fixed brand color. Conflicts (warning hue, role colors,
neutral blacks) are resolved here into ONE family standard.
Structure:
:root → light theme (default)
.dark → dark theme overrides (new for Files & AiRSFlow)
Always consume the SEMANTIC aliases (--surface-card, --text-body)
in components — not raw palette steps — so a new family site
inherits the family look automatically.
============================================================ */
:root {
/* ---- Brand ---- */
--color-primary: #2563eb; /* blue-600 — fixed family brand color */
--color-primary-hover: #1d4ed8; /* blue-700 */
--color-primary-active: #1e40af; /* blue-800 */
--color-primary-subtle: #eff6ff; /* blue-50 — selected/info background */
--color-primary-contrast: #ffffff;
/* ---- Status / semantic ---- */
--color-success: #16a34a; /* green-600 */
--color-success-subtle: #dcfce7;
--color-warning: #d97706; /* amber-600 — family-standard warning (was amber/yellow mix) */
--color-warning-subtle: #fef3c7;
--color-danger: #dc2626; /* red-600 */
--color-danger-subtle: #fee2e2;
--color-info: #2563eb;
--color-info-subtle: #dbeafe;
/* ---- Role colors (family standard — unified across all sites) ---- */
--color-role-admin: #d97706; /* amber — "elevated = caution"; avoids clash with danger red */
--color-role-admin-subtle: #fef3c7;
--color-role-developer: #2563eb; /* blue — already shared */
--color-role-developer-subtle: #dbeafe;
--color-role-viewer: #64748b; /* slate — neutral */
--color-role-viewer-subtle: #f1f5f9;
--color-role-platform: #7e22ce; /* purple — global platform-* roles, "P-" prefix */
--color-role-platform-subtle: #f3e8ff;
/* ---- Environment accents (dev / stage / prod chips) ---- */
--color-env-dev: #047857; /* emerald-700 */
--color-env-dev-subtle: #d1fae5;
--color-env-stage: #7e22ce; /* purple-700 */
--color-env-stage-subtle: #f3e8ff;
--color-env-prod: #c2410c; /* orange-700 */
--color-env-prod-subtle: #ffedd5;
/* ---- File-type accents (Files product) ---- */
--color-file-folder: #2563eb;
--color-file-doc: #b07a12; /* json / yaml / csv — amber */
--color-file-text: #5e6672; /* slate */
/* ---- Neutral palette (zinc) ---- */
--zinc-50: #fafafa;
--zinc-100: #f4f4f5;
--zinc-200: #e4e4e7;
--zinc-300: #d4d4d8;
--zinc-400: #a1a1aa;
--zinc-500: #71717a;
--zinc-600: #52525b;
--zinc-700: #3f3f46;
--zinc-800: #27272a;
--zinc-900: #18181b;
--zinc-950: #09090b;
/* ---- Semantic surfaces & text (consume THESE) ---- */
--color-bg: #fafafa; /* page background */
--surface-card: #ffffff; /* cards, modals, header */
--surface-subtle: #fafbfc; /* side panels, table heads */
--surface-sunken: #f4f5f7; /* inset fills: search bars, segmented tracks */
--surface-hover: #f4f4f5; /* row / item hover */
--text-body: #18181b; /* zinc-900 */
--text-secondary: #52525b; /* zinc-600 */
--text-muted: #71717a; /* zinc-500 */
--text-disabled: #a1a1aa; /* zinc-400 / placeholders */
--text-on-primary: #ffffff;
--color-border: #e4e4e7; /* zinc-200 — default dividers/cards */
--color-border-strong: #d4d9df; /* input borders */
--color-overlay: rgba(9, 9, 11, 0.55); /* modal scrim */
--focus-ring: 0 0 0 3px rgba(37, 99, 235, 0.32);
}
.dark {
/* ---- Brand ---- */
--color-primary: #3b82f6; /* blue-500 — lifts on dark */
--color-primary-hover: #60a5fa; /* blue-400 */
--color-primary-active: #93c5fd;
--color-primary-subtle: rgba(59, 130, 246, 0.14);
--color-primary-contrast: #ffffff;
/* ---- Status / semantic ---- */
--color-success: #4ade80;
--color-success-subtle: rgba(34, 197, 94, 0.14);
--color-warning: #fbbf24;
--color-warning-subtle: rgba(217, 119, 6, 0.18);
--color-danger: #f87171;
--color-danger-subtle: rgba(239, 68, 68, 0.14);
--color-info: #60a5fa;
--color-info-subtle: rgba(59, 130, 246, 0.14);
/* ---- Role colors ---- */
--color-role-admin: #fbbf24;
--color-role-admin-subtle: rgba(217, 119, 6, 0.18);
--color-role-developer: #60a5fa;
--color-role-developer-subtle: rgba(59, 130, 246, 0.14);
--color-role-viewer: #94a3b8;
--color-role-viewer-subtle: rgba(148, 163, 184, 0.14);
--color-role-platform: #c084fc;
--color-role-platform-subtle: rgba(168, 85, 247, 0.16);
/* ---- Environment accents ---- */
--color-env-dev: #34d399;
--color-env-dev-subtle: rgba(16, 185, 129, 0.14);
--color-env-stage: #c084fc;
--color-env-stage-subtle: rgba(168, 85, 247, 0.16);
--color-env-prod: #fb923c;
--color-env-prod-subtle: rgba(249, 115, 22, 0.16);
/* ---- File-type accents ---- */
--color-file-folder: #60a5fa;
--color-file-doc: #fbbf24;
--color-file-text: #94a3b8;
/* ---- Semantic surfaces & text ---- */
--color-bg: #09090b; /* zinc-950 */
--surface-card: #18181b; /* zinc-900 */
--surface-subtle: #1c1c1f;
--surface-sunken: #27272a;
--surface-hover: rgba(255, 255, 255, 0.05);
--text-body: #f4f4f5; /* zinc-100 */
--text-secondary: #a1a1aa; /* zinc-400 */
--text-muted: #71717a; /* zinc-500 */
--text-disabled: #52525b; /* zinc-600 */
--text-on-primary: #ffffff;
--color-border: rgba(255, 255, 255, 0.08);
--color-border-strong: rgba(255, 255, 255, 0.16);
--color-overlay: rgba(0, 0, 0, 0.65);
--focus-ring: 0 0 0 3px rgba(96, 165, 250, 0.38);
}
/* ============================================================
MONA Family — Typography tokens
------------------------------------------------------------
Family font = Pretendard (loaded in tokens/fonts.css). The
stack keeps Korean system fallbacks so text never breaks if the
webfont is blocked. Mono stays a system stack (matches all three
sites today — code / IDs / bucket paths / kubeconfig).
Scale is compact-but-readable (14px base), tuned for dense admin
tables and file lists. Weights are capped at 400700 (Files'
800/900 overuse is intentionally relaxed for a calmer hierarchy).
============================================================ */
:root {
/* ---- Families ---- */
--font-sans: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont,
'Apple SD Gothic Neo', 'Malgun Gothic', 'Segoe UI', Roboto,
system-ui, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
'Liberation Mono', monospace;
/* ---- Size scale (px) ---- */
--text-2xs: 0.6875rem; /* 11 — micro labels, table heads, badges */
--text-xs: 0.75rem; /* 12 — meta, secondary captions */
--text-sm: 0.8125rem; /* 13 — dense rows, inputs (Files base) */
--text-base:0.875rem; /* 14 — DEFAULT body */
--text-md: 1rem; /* 16 — emphasised body, small headings */
--text-lg: 1.125rem; /* 18 — modal titles, section headings */
--text-xl: 1.25rem; /* 20 — page sub-headings, wordmark */
--text-2xl: 1.5rem; /* 24 — page titles */
--text-3xl: 1.875rem; /* 30 — hero / login headlines */
/* ---- Weights ---- */
--font-regular: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* ---- Line heights ---- */
--leading-tight: 1.2; /* headings, wordmark */
--leading-snug: 1.35;
--leading-normal: 1.5; /* body default */
--leading-relaxed: 1.6; /* long-form help / prose */
/* ---- Letter spacing ---- */
--tracking-tight: -0.01em; /* large headings / wordmark */
--tracking-normal: 0;
--tracking-wide: 0.04em; /* uppercase micro-labels & badges */
}
/* ============================================================
MONA Family — Spacing, radius & layout tokens
------------------------------------------------------------
4px base grid (Pass/Tailwind heritage). Header height resolves
the three-way conflict (Pass 64 / Files 60 / AiRSFlow 48) to a
single family value of 56px.
Button shape: default family radius is --radius-md (8px squared,
Pass heritage) — neutral and extensible. Pills (--radius-pill)
are reserved for badges, avatars and chips.
============================================================ */
:root {
/* ---- Spacing scale (4px base) ---- */
--space-0: 0;
--space-px: 1px;
--space-0_5: 0.125rem; /* 2 */
--space-1: 0.25rem; /* 4 */
--space-1_5: 0.375rem; /* 6 */
--space-2: 0.5rem; /* 8 */
--space-2_5: 0.625rem; /* 10 */
--space-3: 0.75rem; /* 12 */
--space-4: 1rem; /* 16 */
--space-5: 1.25rem; /* 20 */
--space-6: 1.5rem; /* 24 */
--space-7: 1.75rem; /* 28 */
--space-8: 2rem; /* 32 */
--space-10: 2.5rem; /* 40 */
--space-12: 3rem; /* 48 */
--space-16: 4rem; /* 64 */
/* ---- Border radius ---- */
--radius-sm: 6px; /* small code chips, nav links */
--radius-md: 8px; /* DEFAULT — buttons, inputs, small cards */
--radius-lg: 12px; /* cards, panels, modals */
--radius-xl: 16px; /* login card, large surfaces */
--radius-pill: 9999px; /* badges, avatars, segmented chips */
/* ---- Border width ---- */
--border-width: 1px;
--border-width-strong: 2px;
/* ---- Layout ---- */
--container-max: 1280px; /* max-w-7xl — header / main / footer */
--container-pad: 1.5rem; /* page gutter (24px; tighten to 16 on mobile) */
--header-height: 56px; /* family-standard app header */
--banner-height: 36px; /* dev environment banner */
--panel-pad: 1.375rem; /* 22px — standard panel inner padding */
}
/* ============================================================
MONA Family — Elevation, z-index & motion tokens
------------------------------------------------------------
Shadow scale averages Pass + Files elevation. On dark, shadows
deepen (light theme shadows disappear on near-black surfaces).
z-index layers are taken from real stacking conflicts across the
three sites and normalised into one ladder.
============================================================ */
:root {
/* ---- Shadow / elevation ---- */
--shadow-xs: 0 1px 2px rgba(20, 30, 45, 0.06);
--shadow-sm: 0 1px 3px rgba(20, 30, 45, 0.10);
--shadow-md: 0 2px 10px rgba(20, 30, 45, 0.08);
--shadow-lg: 0 12px 28px rgba(20, 30, 45, 0.12);
--shadow-xl: 0 18px 50px rgba(20, 30, 45, 0.16);
--shadow-2xl: 0 26px 70px rgba(20, 30, 45, 0.20);
/* ---- z-index ladder ---- */
--z-base: 1; /* @kind other */
/* sticky table heads */
--z-sticky: 30; /* @kind other */
/* app header */
--z-header: 50; /* @kind other */
/* dev environment banner (above header) */
--z-banner: 60; /* @kind other */
/* menus, project switcher, popovers */
--z-dropdown: 1000; /* @kind other */
/* dialog scrim */
--z-overlay: 1080; /* @kind other */
/* modal / upload queue */
--z-modal: 1100; /* @kind other */
/* toast stack (always on top) */
--z-toast: 1200; /* @kind other */
/* ---- Motion ---- */
--duration-fast: 150ms; /* @kind other */
--duration: 200ms; /* @kind other */
--duration-slow: 300ms; /* @kind other */
/* light/dark cross-fade */
--duration-theme: 400ms; /* @kind other */
--ease: cubic-bezier(0.4, 0, 0.2, 1); /* @kind other */
--ease-out: cubic-bezier(0, 0, 0.2, 1); /* @kind other */
--ease-in: cubic-bezier(0.4, 0, 1, 1); /* @kind other */
}
.dark {
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.40);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.50);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
--shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.55);
--shadow-xl: 0 24px 60px rgba(0, 0, 0, 0.60);
--shadow-2xl: 0 30px 80px rgba(0, 0, 0, 0.65);
}
/* ---- Reusable keyframes (loaders / progress) ---- */
@keyframes mona-spin { to { transform: rotate(360deg); } }
@keyframes mona-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@keyframes mona-loading-bar {
0% { transform: translateX(-100%); }
50% { transform: translateX(0); }
100% { transform: translateX(100%); }
}
@keyframes mona-fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* ============================================================
MONA Family — Base / reset
------------------------------------------------------------
Minimal, opinionated reset that applies the family font and
surface tokens to the document. Consuming sites get correct
typography + light/dark background simply by linking styles.css
and toggling the `.dark` class on <html> or <body>.
============================================================ */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: var(--font-sans);
font-size: var(--text-base);
font-weight: var(--font-regular);
line-height: var(--leading-normal);
color: var(--text-body);
background: var(--color-bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-synthesis: none;
transition: background-color var(--duration-theme) var(--ease),
color var(--duration-theme) var(--ease);
}
h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: var(--font-bold); line-height: var(--leading-tight); }
p { margin: 0; }
a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }
code, pre, kbd, samp { font-family: var(--font-mono); }
::selection { background: var(--color-primary); color: var(--color-primary-contrast); }
/* Thin, themed scrollbars (WebKit) */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb {
background: var(--color-border-strong);
border-radius: var(--radius-pill);
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--text-disabled); background-clip: content-box; }
::-webkit-scrollbar-track { background: transparent; }
/* Respect reduced-motion globally */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
<style>/* ============================================================
MONA Family — Base / reset
------------------------------------------------------------
Minimal, opinionated reset that applies the family font and
surface tokens to the document. Consuming sites get correct
typography + light/dark background simply by linking styles.css
and toggling the `.dark` class on <html> or <body>.
============================================================ */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: var(--font-sans);
font-size: var(--text-base);
font-weight: var(--font-regular);
line-height: var(--leading-normal);
color: var(--text-body);
background: var(--color-bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-synthesis: none;
transition: background-color var(--duration-theme) var(--ease),
color var(--duration-theme) var(--ease);
}
h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: var(--font-bold); line-height: var(--leading-tight); }
p { margin: 0; }
a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }
code, pre, kbd, samp { font-family: var(--font-mono); }
::selection { background: var(--color-primary); color: var(--color-primary-contrast); }
/* Thin, themed scrollbars (WebKit) */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb {
background: var(--color-border-strong);
border-radius: var(--radius-pill);
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--text-disabled); background-clip: content-box; }
::-webkit-scrollbar-track { background: transparent; }
/* Respect reduced-motion globally */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
<style>/* ============================================================
MONA Family — Color tokens
------------------------------------------------------------
Single source of truth for color across every MONA family site.
Values reverse-engineered from the three live products and
converged: Pass/Files already shared primary #2563eb, so that is
the fixed brand color. Conflicts (warning hue, role colors,
neutral blacks) are resolved here into ONE family standard.
Structure:
:root → light theme (default)
.dark → dark theme overrides (new for Files & AiRSFlow)
Always consume the SEMANTIC aliases (--surface-card, --text-body)
in components — not raw palette steps — so a new family site
inherits the family look automatically.
============================================================ */
:root {
/* ---- Brand ---- */
--color-primary: #2563eb; /* blue-600 — fixed family brand color */
--color-primary-hover: #1d4ed8; /* blue-700 */
--color-primary-active: #1e40af; /* blue-800 */
--color-primary-subtle: #eff6ff; /* blue-50 — selected/info background */
--color-primary-contrast: #ffffff;
/* ---- Status / semantic ---- */
--color-success: #16a34a; /* green-600 */
--color-success-subtle: #dcfce7;
--color-warning: #d97706; /* amber-600 — family-standard warning (was amber/yellow mix) */
--color-warning-subtle: #fef3c7;
--color-danger: #dc2626; /* red-600 */
--color-danger-subtle: #fee2e2;
--color-info: #2563eb;
--color-info-subtle: #dbeafe;
/* ---- Role colors (family standard — unified across all sites) ---- */
--color-role-admin: #d97706; /* amber — "elevated = caution"; avoids clash with danger red */
--color-role-admin-subtle: #fef3c7;
--color-role-developer: #2563eb; /* blue — already shared */
--color-role-developer-subtle: #dbeafe;
--color-role-viewer: #64748b; /* slate — neutral */
--color-role-viewer-subtle: #f1f5f9;
--color-role-platform: #7e22ce; /* purple — global platform-* roles, "P-" prefix */
--color-role-platform-subtle: #f3e8ff;
/* ---- Environment accents (dev / stage / prod chips) ---- */
--color-env-dev: #047857; /* emerald-700 */
--color-env-dev-subtle: #d1fae5;
--color-env-stage: #7e22ce; /* purple-700 */
--color-env-stage-subtle: #f3e8ff;
--color-env-prod: #c2410c; /* orange-700 */
--color-env-prod-subtle: #ffedd5;
/* ---- File-type accents (Files product) ---- */
--color-file-folder: #2563eb;
--color-file-doc: #b07a12; /* json / yaml / csv — amber */
--color-file-text: #5e6672; /* slate */
/* ---- Neutral palette (zinc) ---- */
--zinc-50: #fafafa;
--zinc-100: #f4f4f5;
--zinc-200: #e4e4e7;
--zinc-300: #d4d4d8;
--zinc-400: #a1a1aa;
--zinc-500: #71717a;
--zinc-600: #52525b;
--zinc-700: #3f3f46;
--zinc-800: #27272a;
--zinc-900: #18181b;
--zinc-950: #09090b;
/* ---- Semantic surfaces & text (consume THESE) ---- */
--color-bg: #fafafa; /* page background */
--surface-card: #ffffff; /* cards, modals, header */
--surface-subtle: #fafbfc; /* side panels, table heads */
--surface-sunken: #f4f5f7; /* inset fills: search bars, segmented tracks */
--surface-hover: #f4f4f5; /* row / item hover */
--text-body: #18181b; /* zinc-900 */
--text-secondary: #52525b; /* zinc-600 */
--text-muted: #71717a; /* zinc-500 */
--text-disabled: #a1a1aa; /* zinc-400 / placeholders */
--text-on-primary: #ffffff;
--color-border: #e4e4e7; /* zinc-200 — default dividers/cards */
--color-border-strong: #d4d9df; /* input borders */
--color-overlay: rgba(9, 9, 11, 0.55); /* modal scrim */
--focus-ring: 0 0 0 3px rgba(37, 99, 235, 0.32);
}
.dark {
/* ---- Brand ---- */
--color-primary: #3b82f6; /* blue-500 — lifts on dark */
--color-primary-hover: #60a5fa; /* blue-400 */
--color-primary-active: #93c5fd;
--color-primary-subtle: rgba(59, 130, 246, 0.14);
--color-primary-contrast: #ffffff;
/* ---- Status / semantic ---- */
--color-success: #4ade80;
--color-success-subtle: rgba(34, 197, 94, 0.14);
--color-warning: #fbbf24;
--color-warning-subtle: rgba(217, 119, 6, 0.18);
--color-danger: #f87171;
--color-danger-subtle: rgba(239, 68, 68, 0.14);
--color-info: #60a5fa;
--color-info-subtle: rgba(59, 130, 246, 0.14);
/* ---- Role colors ---- */
--color-role-admin: #fbbf24;
--color-role-admin-subtle: rgba(217, 119, 6, 0.18);
--color-role-developer: #60a5fa;
--color-role-developer-subtle: rgba(59, 130, 246, 0.14);
--color-role-viewer: #94a3b8;
--color-role-viewer-subtle: rgba(148, 163, 184, 0.14);
--color-role-platform: #c084fc;
--color-role-platform-subtle: rgba(168, 85, 247, 0.16);
/* ---- Environment accents ---- */
--color-env-dev: #34d399;
--color-env-dev-subtle: rgba(16, 185, 129, 0.14);
--color-env-stage: #c084fc;
--color-env-stage-subtle: rgba(168, 85, 247, 0.16);
--color-env-prod: #fb923c;
--color-env-prod-subtle: rgba(249, 115, 22, 0.16);
/* ---- File-type accents ---- */
--color-file-folder: #60a5fa;
--color-file-doc: #fbbf24;
--color-file-text: #94a3b8;
/* ---- Semantic surfaces & text ---- */
--color-bg: #09090b; /* zinc-950 */
--surface-card: #18181b; /* zinc-900 */
--surface-subtle: #1c1c1f;
--surface-sunken: #27272a;
--surface-hover: rgba(255, 255, 255, 0.05);
--text-body: #f4f4f5; /* zinc-100 */
--text-secondary: #a1a1aa; /* zinc-400 */
--text-muted: #71717a; /* zinc-500 */
--text-disabled: #52525b; /* zinc-600 */
--text-on-primary: #ffffff;
--color-border: rgba(255, 255, 255, 0.08);
--color-border-strong: rgba(255, 255, 255, 0.16);
--color-overlay: rgba(0, 0, 0, 0.65);
--focus-ring: 0 0 0 3px rgba(96, 165, 250, 0.38);
}
</style>
<style>/* ============================================================
MONA Family — Elevation, z-index & motion tokens
------------------------------------------------------------
Shadow scale averages Pass + Files elevation. On dark, shadows
deepen (light theme shadows disappear on near-black surfaces).
z-index layers are taken from real stacking conflicts across the
three sites and normalised into one ladder.
============================================================ */
:root {
/* ---- Shadow / elevation ---- */
--shadow-xs: 0 1px 2px rgba(20, 30, 45, 0.06);
--shadow-sm: 0 1px 3px rgba(20, 30, 45, 0.10);
--shadow-md: 0 2px 10px rgba(20, 30, 45, 0.08);
--shadow-lg: 0 12px 28px rgba(20, 30, 45, 0.12);
--shadow-xl: 0 18px 50px rgba(20, 30, 45, 0.16);
--shadow-2xl: 0 26px 70px rgba(20, 30, 45, 0.20);
/* ---- z-index ladder ---- */
--z-base: 1; /* @kind other */
/* sticky table heads */
--z-sticky: 30; /* @kind other */
/* app header */
--z-header: 50; /* @kind other */
/* dev environment banner (above header) */
--z-banner: 60; /* @kind other */
/* menus, project switcher, popovers */
--z-dropdown: 1000; /* @kind other */
/* dialog scrim */
--z-overlay: 1080; /* @kind other */
/* modal / upload queue */
--z-modal: 1100; /* @kind other */
/* toast stack (always on top) */
--z-toast: 1200; /* @kind other */
/* ---- Motion ---- */
--duration-fast: 150ms; /* @kind other */
--duration: 200ms; /* @kind other */
--duration-slow: 300ms; /* @kind other */
/* light/dark cross-fade */
--duration-theme: 400ms; /* @kind other */
--ease: cubic-bezier(0.4, 0, 0.2, 1); /* @kind other */
--ease-out: cubic-bezier(0, 0, 0.2, 1); /* @kind other */
--ease-in: cubic-bezier(0.4, 0, 1, 1); /* @kind other */
}
.dark {
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.40);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.50);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
--shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.55);
--shadow-xl: 0 24px 60px rgba(0, 0, 0, 0.60);
--shadow-2xl: 0 30px 80px rgba(0, 0, 0, 0.65);
}
/* ---- Reusable keyframes (loaders / progress) ---- */
@keyframes mona-spin { to { transform: rotate(360deg); } }
@keyframes mona-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@keyframes mona-loading-bar {
0% { transform: translateX(-100%); }
50% { transform: translateX(0); }
100% { transform: translateX(100%); }
}
@keyframes mona-fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
<style>/* ============================================================
MONA Family — Webfont loading
------------------------------------------------------------
The #1 family-wide fix: all three sites declared a font in CSS
but never LOADED one, so every site rendered in whatever the OS
provided. We load a single Korean+Latin webfont for real.
Family font: Pretendard (OFL, open-source, optimised for Korean
alongside Latin). One variable woff2 covers weights 400900.
Served from jsDelivr (pinned). ⚠️ For production you may want to
vendor this file into the repo / serve from an internal CDN —
see readme.md "Webfont" note.
============================================================ */
@font-face {
font-family: 'Pretendard Variable';
font-weight: 400 900;
font-style: normal;
font-display: swap;
src: url("assets/cb6787c5-c940-4770-a3a6-e0331204618a.woff2") format('woff2-variations');
}
</style>
<style>/* ============================================================
MONA Family — Spacing, radius & layout tokens
------------------------------------------------------------
4px base grid (Pass/Tailwind heritage). Header height resolves
the three-way conflict (Pass 64 / Files 60 / AiRSFlow 48) to a
single family value of 56px.
Button shape: default family radius is --radius-md (8px squared,
Pass heritage) — neutral and extensible. Pills (--radius-pill)
are reserved for badges, avatars and chips.
============================================================ */
:root {
/* ---- Spacing scale (4px base) ---- */
--space-0: 0;
--space-px: 1px;
--space-0_5: 0.125rem; /* 2 */
--space-1: 0.25rem; /* 4 */
--space-1_5: 0.375rem; /* 6 */
--space-2: 0.5rem; /* 8 */
--space-2_5: 0.625rem; /* 10 */
--space-3: 0.75rem; /* 12 */
--space-4: 1rem; /* 16 */
--space-5: 1.25rem; /* 20 */
--space-6: 1.5rem; /* 24 */
--space-7: 1.75rem; /* 28 */
--space-8: 2rem; /* 32 */
--space-10: 2.5rem; /* 40 */
--space-12: 3rem; /* 48 */
--space-16: 4rem; /* 64 */
/* ---- Border radius ---- */
--radius-sm: 6px; /* small code chips, nav links */
--radius-md: 8px; /* DEFAULT — buttons, inputs, small cards */
--radius-lg: 12px; /* cards, panels, modals */
--radius-xl: 16px; /* login card, large surfaces */
--radius-pill: 9999px; /* badges, avatars, segmented chips */
/* ---- Border width ---- */
--border-width: 1px;
--border-width-strong: 2px;
/* ---- Layout ---- */
--container-max: 1280px; /* max-w-7xl — header / main / footer */
--container-pad: 1.5rem; /* page gutter (24px; tighten to 16 on mobile) */
--header-height: 56px; /* family-standard app header */
--banner-height: 36px; /* dev environment banner */
--panel-pad: 1.375rem; /* 22px — standard panel inner padding */
}
</style>
<style>/* ============================================================
MONA Family — Typography tokens
------------------------------------------------------------
Family font = Pretendard (loaded in tokens/fonts.css). The
stack keeps Korean system fallbacks so text never breaks if the
webfont is blocked. Mono stays a system stack (matches all three
sites today — code / IDs / bucket paths / kubeconfig).
Scale is compact-but-readable (14px base), tuned for dense admin
tables and file lists. Weights are capped at 400700 (Files'
800/900 overuse is intentionally relaxed for a calmer hierarchy).
============================================================ */
:root {
/* ---- Families ---- */
--font-sans: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont,
'Apple SD Gothic Neo', 'Malgun Gothic', 'Segoe UI', Roboto,
system-ui, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
'Liberation Mono', monospace;
/* ---- Size scale (px) ---- */
--text-2xs: 0.6875rem; /* 11 — micro labels, table heads, badges */
--text-xs: 0.75rem; /* 12 — meta, secondary captions */
--text-sm: 0.8125rem; /* 13 — dense rows, inputs (Files base) */
--text-base:0.875rem; /* 14 — DEFAULT body */
--text-md: 1rem; /* 16 — emphasised body, small headings */
--text-lg: 1.125rem; /* 18 — modal titles, section headings */
--text-xl: 1.25rem; /* 20 — page sub-headings, wordmark */
--text-2xl: 1.5rem; /* 24 — page titles */
--text-3xl: 1.875rem; /* 30 — hero / login headlines */
/* ---- Weights ---- */
--font-regular: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* ---- Line heights ---- */
--leading-tight: 1.2; /* headings, wordmark */
--leading-snug: 1.35;
--leading-normal: 1.5; /* body default */
--leading-relaxed: 1.6; /* long-form help / prose */
/* ---- Letter spacing ---- */
--tracking-tight: -0.01em; /* large headings / wordmark */
--tracking-normal: 0;
--tracking-wide: 0.04em; /* uppercase micro-labels & badges */
}
</style>
<script src="assets/2fbd4286-601e-4267-8ad5-83a6f624a684"></script>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-family: var(--font-sans); color: var(--text-body); background: var(--color-bg); -webkit-font-smoothing: antialiased; }
::selection { background: var(--color-primary-subtle); }
</style>
</helmet>
<div style="min-height:100vh;background:var(--color-bg)">
<!-- ===================== HEADER (site only) ===================== -->
<sc-if value="{{ isSite }}" hint-placeholder-val="{{ true }}">
<header style="position:sticky;top:0;z-index:50;background:var(--surface-card);border-bottom:1px solid var(--color-border)">
<div style="max-width:1200px;margin:0 auto;padding:0 24px;height:68px;display:flex;align-items:center;gap:30px">
<button onclick="{{ goHome }}" style="appearance:none;border:0;background:none;cursor:pointer;display:inline-flex;align-items:center;gap:11px;padding:0;font-family:inherit">
{{ headerLogo }}
<span style="display:flex;flex-direction:column;align-items:flex-start;line-height:1.05">
<span style="font-weight:700;font-size:19px;letter-spacing:-0.01em;color:var(--text-body);white-space:nowrap">{{ coBrandA }} <span style="color:var(--color-primary)">{{ coBrandB }}</span></span>
<span style="font-size:10.5px;font-weight:600;letter-spacing:0.14em;color:var(--text-muted)">{{ coSubtitle }}</span>
</span>
</button>
<nav style="display:flex;align-items:center;gap:6px">
<sc-for list="{{ navs }}" as="n" hint-placeholder-count="3">
<sc-if value="{{ n.on }}">
<button onclick="{{ n.go }}" style="position:relative;appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:700;color:var(--text-body);padding:8px 6px">{{ n.name }}<span style="position:absolute;left:6px;right:6px;bottom:-22px;height:3px;border-radius:3px;background:var(--color-primary)"></span></button>
</sc-if>
<sc-if value="{{ n.off }}">
<button onclick="{{ n.go }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:500;color:var(--text-secondary);padding:8px 6px" style-hover="color:var(--text-body)">{{ n.name }}</button>
</sc-if>
</sc-for>
</nav>
<div style="flex:1"></div>
<a href="tel:{{ coPhone }}" style="display:inline-flex;align-items:center;gap:8px;text-decoration:none;color:var(--text-body);font-weight:700;font-size:15px">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
{{ coPhone }}
</a>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" onclick="{{ goContact }}" hint-size="auto,38px">상담문의</x-import>
</div>
</header>
</sc-if>
<!-- ===================== HOME ===================== -->
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}">
<main data-screen-label="홈">
<!-- Hero -->
<section style="position:relative;overflow:hidden;background:var(--surface-card);border-bottom:1px solid var(--color-border)">
<div style="position:absolute;inset:0;background:radial-gradient(1100px 520px at 88% -12%, var(--color-primary-subtle), transparent 62%);pointer-events:none"></div>
<div style="position:relative;max-width:1200px;margin:0 auto;padding:clamp(52px,7vw,92px) 24px;display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:56px;align-items:center">
<div>
<span style="display:inline-flex;align-items:center;gap:7px;padding:6px 12px;border-radius:var(--radius-pill);background:var(--color-primary-subtle);color:var(--color-primary);font-size:13px;font-weight:600">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path></svg>
20년 현장 경험 · 통신 인프라 전문
</span>
<h1 style="font-size:clamp(31px,3.7vw,51px);line-height:1.16;letter-spacing:-0.025em;font-weight:700;margin:20px 0 0;color:var(--text-body)">사무실 네트워크,<br>처음부터 끝까지 책임집니다</h1>
<p style="margin:22px 0 0;font-size:clamp(16px,1.3vw,19px);line-height:1.65;color:var(--text-secondary);max-width:540px">LAN 구축부터 키폰·CCTV 시공과 유지보수까지 — 직접 시공팀을 운영하는 GTS 정보통신이 안정적인 통신 환경을 끝까지 책임집니다.</p>
<div style="margin-top:34px;display:flex;flex-wrap:wrap;align-items:center;gap:13px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" as="a" href="tel:{{ coPhone }}" size="lg" hint-size="auto,46px">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
전화상담 {{ coPhone }}
</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="secondary" size="lg" onclick="{{ goContact }}" hint-size="auto,46px">1:1 온라인 문의</x-import>
</div>
<p style="margin:16px 0 0;font-size:13px;color:var(--text-muted)">평일 09:0018:00 · 경기지점 {{ coSub }} · 견적 상담 무료</p>
</div>
<div style="position:relative">
<div style="aspect-ratio:4/3;border-radius:var(--radius-xl);border:1px solid var(--color-border);background:var(--surface-sunken);display:grid;place-items:center;box-shadow:var(--shadow-md);overflow:hidden">
<div style="text-align:center;color:var(--text-disabled)">
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path></svg>
<div style="margin-top:10px;font-size:13px;font-weight:600">현장 시공 이미지 영역</div>
</div>
</div>
<div style="position:absolute;left:-14px;bottom:-18px;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);padding:14px 18px;display:flex;align-items:center;gap:12px">
<span style="width:40px;height:40px;border-radius:var(--radius-md);background:var(--color-success-subtle);display:grid;place-items:center;flex:none">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg>
</span>
<div style="line-height:1.25"><div style="font-size:20px;font-weight:700;color:var(--text-body)">1,200<span style="color:var(--color-primary)">+</span></div><div style="font-size:12px;color:var(--text-muted)">누적 시공 현장</div></div>
</div>
</div>
</div>
</section>
<!-- Stats band -->
<section style="background:var(--surface-card)">
<div style="max-width:1200px;margin:0 auto;padding:0 24px">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-sm);transform:translateY(28px)">
<div style="padding:26px 24px;text-align:center;border-right:1px solid var(--color-border)"><div style="font-size:30px;font-weight:700;color:var(--text-body)">20<span style="font-size:18px"></span></div><div style="margin-top:4px;font-size:13px;color:var(--text-muted)">통신 인프라 업력</div></div>
<div style="padding:26px 24px;text-align:center;border-right:1px solid var(--color-border)"><div style="font-size:30px;font-weight:700;color:var(--text-body)">1,200<span style="color:var(--color-primary)">+</span></div><div style="margin-top:4px;font-size:13px;color:var(--text-muted)">누적 시공 현장</div></div>
<div style="padding:26px 24px;text-align:center;border-right:1px solid var(--color-border)"><div style="font-size:30px;font-weight:700;color:var(--text-body)">24<span style="font-size:18px">시간</span></div><div style="margin-top:4px;font-size:13px;color:var(--text-muted)">견적 회신 기준</div></div>
<div style="padding:26px 24px;text-align:center"><div style="font-size:30px;font-weight:700;color:var(--text-body)">A/S</div><div style="margin-top:4px;font-size:13px;color:var(--text-muted)">시공 후 정기 사후관리</div></div>
</div>
</div>
</section>
<!-- Services -->
<section style="max-width:1200px;margin:0 auto;padding:clamp(72px,8vw,108px) 24px clamp(56px,7vw,88px)">
<div style="text-align:center;max-width:640px;margin:0 auto 48px">
<span style="font-size:13px;font-weight:700;letter-spacing:0.1em;color:var(--color-primary)">SERVICES</span>
<h2 style="font-size:clamp(26px,2.6vw,37px);font-weight:700;letter-spacing:-0.02em;margin:12px 0 0;color:var(--text-body)">전문 시공 서비스</h2>
<p style="margin:14px 0 0;font-size:16px;line-height:1.6;color:var(--text-secondary)">네트워크부터 키폰·CCTV까지, 한 곳에서 설계·시공·유지보수를 책임집니다.</p>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(248px,1fr));gap:22px">
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:28px 24px;transition:box-shadow .2s var(--ease),transform .2s var(--ease)" style-hover="box-shadow:var(--shadow-lg);transform:translateY(-3px)">
<span style="width:48px;height:48px;border-radius:var(--radius-md);background:var(--color-primary-subtle);display:grid;place-items:center"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="14" rx="2"></rect><path d="M6.01 18H6.01"></path><path d="M10.01 18H10.01"></path><path d="M15 10v4"></path><path d="M17.84 7.17a4 4 0 0 0-5.66 0"></path><path d="M20.66 4.34a8 8 0 0 0-11.31 0"></path></svg></span>
<h3 style="margin:18px 0 0;font-size:18px;font-weight:700;color:var(--text-body)">네트워크(LAN) 구축</h3>
<p style="margin:9px 0 0;font-size:14.5px;line-height:1.6;color:var(--text-secondary)">사무실·건물의 유선/무선 네트워크를 설계·시공해 끊김 없는 인터넷 환경을 구성합니다.</p>
</div>
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:28px 24px;transition:box-shadow .2s var(--ease),transform .2s var(--ease)" style-hover="box-shadow:var(--shadow-lg);transform:translateY(-3px)">
<span style="width:48px;height:48px;border-radius:var(--radius-md);background:var(--color-role-platform-subtle);display:grid;place-items:center"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-role-platform)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"></path></svg></span>
<h3 style="margin:18px 0 0;font-size:18px;font-weight:700;color:var(--text-body)">키폰시스템</h3>
<p style="margin:9px 0 0;font-size:14.5px;line-height:1.6;color:var(--text-secondary)">LG·삼성 디지털/아날로그 키폰 주장치 설치와 내선 구성, 하이브리드 구성까지 지원합니다.</p>
</div>
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:28px 24px;transition:box-shadow .2s var(--ease),transform .2s var(--ease)" style-hover="box-shadow:var(--shadow-lg);transform:translateY(-3px)">
<span style="width:48px;height:48px;border-radius:var(--radius-md);background:var(--color-success-subtle);display:grid;place-items:center"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-success)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8Z"></path><rect width="14" height="12" x="2" y="6" rx="2"></rect></svg></span>
<h3 style="margin:18px 0 0;font-size:18px;font-weight:700;color:var(--text-body)">CCTV 보안</h3>
<p style="margin:9px 0 0;font-size:14.5px;line-height:1.6;color:var(--text-secondary)">매장·사무실 CCTV 설치와 다지점 통합 관제 환경을 구축해 보안을 강화합니다.</p>
</div>
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:28px 24px;transition:box-shadow .2s var(--ease),transform .2s var(--ease)" style-hover="box-shadow:var(--shadow-lg);transform:translateY(-3px)">
<span style="width:48px;height:48px;border-radius:var(--radius-md);background:var(--color-warning-subtle);display:grid;place-items:center"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-warning)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
<h3 style="margin:18px 0 0;font-size:18px;font-weight:700;color:var(--text-body)">시공·유지보수</h3>
<p style="margin:9px 0 0;font-size:14.5px;line-height:1.6;color:var(--text-secondary)">통신 인프라 시공부터 정기 점검, 장애 발생 시 신속한 A/S까지 책임집니다.</p>
</div>
</div>
</section>
<!-- Why GTS -->
<section style="background:var(--surface-card);border-top:1px solid var(--color-border);border-bottom:1px solid var(--color-border)">
<div style="max-width:1200px;margin:0 auto;padding:clamp(56px,7vw,84px) 24px;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:40px;align-items:start">
<div style="display:flex;gap:16px">
<span style="width:46px;height:46px;flex:none;border-radius:var(--radius-md);background:var(--color-primary-subtle);display:grid;place-items:center"><svg width="23" height="23" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path></svg></span>
<div><h3 style="margin:2px 0 0;font-size:17px;font-weight:700;color:var(--text-body)">직접 시공팀 운영</h3><p style="margin:8px 0 0;font-size:14.5px;line-height:1.6;color:var(--text-secondary)">외주 없이 자사 인력이 시공해 품질이 일정하고 책임 소재가 명확합니다.</p></div>
</div>
<div style="display:flex;gap:16px">
<span style="width:46px;height:46px;flex:none;border-radius:var(--radius-md);background:var(--color-primary-subtle);display:grid;place-items:center"><svg width="23" height="23" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg></span>
<div><h3 style="margin:2px 0 0;font-size:17px;font-weight:700;color:var(--text-body)">24시간 내 견적 회신</h3><p style="margin:8px 0 0;font-size:14.5px;line-height:1.6;color:var(--text-secondary)">문의 접수 후 빠르게 현장을 확인하고 항목별 견적을 회신해 드립니다.</p></div>
</div>
<div style="display:flex;gap:16px">
<span style="width:46px;height:46px;flex:none;border-radius:var(--radius-md);background:var(--color-primary-subtle);display:grid;place-items:center"><svg width="23" height="23" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="m9 12 2 2 4-4"></path></svg></span>
<div><h3 style="margin:2px 0 0;font-size:17px;font-weight:700;color:var(--text-body)">투명한 비용</h3><p style="margin:8px 0 0;font-size:14.5px;line-height:1.6;color:var(--text-secondary)">불필요한 장비 없이 환경에 맞는 합리적 구성으로 비용을 제안합니다.</p></div>
</div>
</div>
</section>
<!-- Process -->
<sc-if value="{{ showProcess }}" hint-placeholder-val="{{ true }}">
<section style="max-width:1200px;margin:0 auto;padding:clamp(72px,8vw,104px) 24px">
<div style="text-align:center;max-width:640px;margin:0 auto 52px">
<span style="font-size:13px;font-weight:700;letter-spacing:0.1em;color:var(--color-primary)">PROCESS</span>
<h2 style="font-size:clamp(26px,2.6vw,37px);font-weight:700;letter-spacing:-0.02em;margin:12px 0 0;color:var(--text-body)">상담부터 사후관리까지</h2>
<p style="margin:14px 0 0;font-size:16px;line-height:1.6;color:var(--text-secondary)">투명하고 예측 가능한 5단계 진행으로 안심하고 맡기실 수 있습니다.</p>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:18px">
<div style="position:relative;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:24px 20px"><span style="display:inline-grid;place-items:center;width:34px;height:34px;border-radius:var(--radius-pill);background:var(--color-primary);color:#fff;font-weight:700;font-size:15px">1</span><h3 style="margin:16px 0 0;font-size:16px;font-weight:700;color:var(--text-body)">상담 신청</h3><p style="margin:7px 0 0;font-size:13.5px;line-height:1.55;color:var(--text-secondary)">전화 또는 온라인으로 간편하게 요청</p></div>
<div style="position:relative;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:24px 20px"><span style="display:inline-grid;place-items:center;width:34px;height:34px;border-radius:var(--radius-pill);background:var(--color-primary);color:#fff;font-weight:700;font-size:15px">2</span><h3 style="margin:16px 0 0;font-size:16px;font-weight:700;color:var(--text-body)">현장 조사</h3><p style="margin:7px 0 0;font-size:13.5px;line-height:1.55;color:var(--text-secondary)">환경 확인 및 요구사항 파악</p></div>
<div style="position:relative;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:24px 20px"><span style="display:inline-grid;place-items:center;width:34px;height:34px;border-radius:var(--radius-pill);background:var(--color-primary);color:#fff;font-weight:700;font-size:15px">3</span><h3 style="margin:16px 0 0;font-size:16px;font-weight:700;color:var(--text-body)">견적 제안</h3><p style="margin:7px 0 0;font-size:13.5px;line-height:1.55;color:var(--text-secondary)">투명한 항목별 견적 회신</p></div>
<div style="position:relative;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:24px 20px"><span style="display:inline-grid;place-items:center;width:34px;height:34px;border-radius:var(--radius-pill);background:var(--color-primary);color:#fff;font-weight:700;font-size:15px">4</span><h3 style="margin:16px 0 0;font-size:16px;font-weight:700;color:var(--text-body)">시공</h3><p style="margin:7px 0 0;font-size:13.5px;line-height:1.55;color:var(--text-secondary)">직접 시공팀이 안전하게 설치</p></div>
<div style="position:relative;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:24px 20px"><span style="display:inline-grid;place-items:center;width:34px;height:34px;border-radius:var(--radius-pill);background:var(--color-success);color:#fff;font-weight:700;font-size:15px">5</span><h3 style="margin:16px 0 0;font-size:16px;font-weight:700;color:var(--text-body)">사후관리</h3><p style="margin:7px 0 0;font-size:13.5px;line-height:1.55;color:var(--text-secondary)">정기 점검과 신속한 A/S</p></div>
</div>
</section>
</sc-if>
<!-- Blog preview -->
<sc-if value="{{ showBlogPreview }}" hint-placeholder-val="{{ true }}">
<section style="background:var(--surface-card);border-top:1px solid var(--color-border)">
<div style="max-width:1200px;margin:0 auto;padding:clamp(64px,7vw,92px) 24px">
<div style="display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap;margin-bottom:32px">
<div>
<span style="font-size:13px;font-weight:700;letter-spacing:0.1em;color:var(--color-primary)">BLOG</span>
<h2 style="font-size:clamp(24px,2.4vw,33px);font-weight:700;letter-spacing:-0.02em;margin:10px 0 0;color:var(--text-body)">시공 이야기 · 알아두면 좋은 정보</h2>
</div>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="secondary" onclick="{{ goBlog }}" hint-size="auto,38px">블로그 더보기</x-import>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:22px">
<sc-for list="{{ latest }}" as="p" hint-placeholder-count="3">
<article onclick="{{ p.open }}" style="cursor:pointer;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);overflow:hidden;transition:box-shadow .2s var(--ease),transform .2s var(--ease)" style-hover="box-shadow:var(--shadow-lg);transform:translateY(-3px)">
<div style="position:relative;aspect-ratio:16/9;background:var(--surface-sunken);display:grid;place-items:center;border-bottom:1px solid var(--color-border)">
<sc-if value="{{ p.hasImage }}" hint-placeholder-val="{{ false }}">{{ p.thumb }}</sc-if><sc-if value="{{ p.noImage }}" hint-placeholder-val="{{ true }}"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="var(--text-disabled)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path></svg></sc-if>
<span style="position:absolute;top:12px;left:12px"><x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="{{ p.variant }}" shape="pill" hint-size="auto,22px">{{ p.cat }}</x-import></span>
</div>
<div style="padding:18px 20px 20px">
<h3 style="margin:0;font-size:16.5px;font-weight:700;line-height:1.4;color:var(--text-body)">{{ p.title }}</h3>
<p style="margin:9px 0 0;font-size:14px;line-height:1.55;color:var(--text-secondary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">{{ p.excerpt }}</p>
<div style="margin-top:14px;font-size:12.5px;color:var(--text-muted)">{{ p.date }}</div>
</div>
</article>
</sc-for>
</div>
</div>
</section>
</sc-if>
<!-- CTA band -->
<section style="background:var(--color-primary);position:relative;overflow:hidden">
<div style="position:absolute;inset:0;background:radial-gradient(700px 360px at 12% 120%, rgba(255,255,255,0.16), transparent 60%);pointer-events:none"></div>
<div style="position:relative;max-width:1100px;margin:0 auto;padding:clamp(52px,6vw,76px) 24px;text-align:center">
<h2 style="margin:0;font-size:clamp(24px,2.6vw,34px);font-weight:700;letter-spacing:-0.02em;color:#fff">네트워크 구축, 어디서부터 시작할지 고민되세요?</h2>
<p style="margin:14px 0 0;font-size:16px;line-height:1.6;color:rgba(255,255,255,0.86)">전화 한 통이면 현장 상황에 맞는 무료 견적을 받아보실 수 있습니다.</p>
<div style="margin-top:30px;display:flex;flex-wrap:wrap;gap:13px;justify-content:center">
<a href="tel:{{ coPhone }}" style="display:inline-flex;align-items:center;gap:8px;height:48px;padding:0 24px;border-radius:var(--radius-md);background:#fff;color:var(--color-primary);font-weight:700;font-size:15px;text-decoration:none;box-shadow:var(--shadow-md)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
전화상담 {{ coPhone }}
</a>
<button onclick="{{ goContact }}" style="display:inline-flex;align-items:center;height:48px;padding:0 24px;border-radius:var(--radius-md);background:transparent;color:#fff;font-weight:700;font-size:15px;border:1px solid rgba(255,255,255,0.6);cursor:pointer;font-family:inherit" style-hover="background:rgba(255,255,255,0.12)">1:1 온라인 문의</button>
</div>
</div>
</section>
</main>
</sc-if>
<!-- ===================== BLOG ===================== -->
<sc-if value="{{ isBlog }}">
<main data-screen-label="블로그" style="max-width:1200px;margin:0 auto;padding:clamp(44px,5vw,64px) 24px clamp(64px,7vw,88px)">
<div style="margin-bottom:8px"><span style="font-size:13px;font-weight:700;letter-spacing:0.1em;color:var(--color-primary)">BLOG · 자료실</span></div>
<h1 style="margin:0;font-size:clamp(27px,3vw,40px);font-weight:700;letter-spacing:-0.02em;color:var(--text-body)">시공 이야기 &amp; 알아두면 좋은 정보</h1>
<p style="margin:14px 0 0;font-size:16px;line-height:1.6;color:var(--text-secondary);max-width:620px">네트워크·키폰·CCTV 시공 사례와 장비 선택 가이드를 정리했습니다. 궁금한 점은 언제든 상담으로 문의해 주세요.</p>
<div style="display:flex;flex-wrap:wrap;gap:9px;margin:30px 0 28px">
<sc-for list="{{ cats }}" as="c" hint-placeholder-count="5">
<sc-if value="{{ c.on }}"><button onclick="{{ c.set }}" style="appearance:none;cursor:pointer;font-family:inherit;font-size:14px;font-weight:600;padding:8px 16px;border-radius:var(--radius-pill);border:1px solid var(--color-primary);background:var(--color-primary);color:#fff">{{ c.name }}</button></sc-if>
<sc-if value="{{ c.off }}"><button onclick="{{ c.set }}" style="appearance:none;cursor:pointer;font-family:inherit;font-size:14px;font-weight:600;padding:8px 16px;border-radius:var(--radius-pill);border:1px solid var(--color-border-strong);background:var(--surface-card);color:var(--text-secondary)" style-hover="border-color:var(--color-primary);color:var(--color-primary)">{{ c.name }}</button></sc-if>
</sc-for>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:22px">
<sc-for list="{{ posts }}" as="p" hint-placeholder-count="6">
<article onclick="{{ p.open }}" style="cursor:pointer;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);overflow:hidden;transition:box-shadow .2s var(--ease),transform .2s var(--ease)" style-hover="box-shadow:var(--shadow-lg);transform:translateY(-3px)">
<div style="position:relative;aspect-ratio:16/9;background:var(--surface-sunken);display:grid;place-items:center;border-bottom:1px solid var(--color-border)">
<sc-if value="{{ p.hasImage }}" hint-placeholder-val="{{ false }}">{{ p.thumb }}</sc-if><sc-if value="{{ p.noImage }}" hint-placeholder-val="{{ true }}"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="var(--text-disabled)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path></svg></sc-if>
<span style="position:absolute;top:12px;left:12px"><x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="{{ p.variant }}" shape="pill" hint-size="auto,22px">{{ p.cat }}</x-import></span>
</div>
<div style="padding:18px 20px 20px">
<h3 style="margin:0;font-size:16.5px;font-weight:700;line-height:1.4;color:var(--text-body)">{{ p.title }}</h3>
<p style="margin:9px 0 0;font-size:14px;line-height:1.55;color:var(--text-secondary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">{{ p.excerpt }}</p>
<div style="margin-top:14px;display:flex;align-items:center;justify-content:space-between">
<span style="font-size:12.5px;color:var(--text-muted)">{{ p.date }}</span>
<span style="font-size:13px;font-weight:600;color:var(--color-primary);display:inline-flex;align-items:center;gap:4px">자세히 보기<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></span>
</div>
</div>
</article>
</sc-for>
</div>
<div style="margin-top:40px;display:flex;justify-content:center">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Pagination" page="{{ page }}" page-count="{{ pageCount }}" total="{{ filteredTotal }}" page-size="{{ pageSize }}" onchange="{{ onPage }}" hint-size="auto,40px"></x-import>
</div>
</main>
</sc-if>
<!-- ===================== POST DETAIL ===================== -->
<sc-if value="{{ isPost }}">
<main data-screen-label="블로그 상세" style="max-width:820px;margin:0 auto;padding:clamp(32px,4vw,52px) 24px clamp(64px,7vw,88px)">
<button onclick="{{ goBlog }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:6px;font-size:14px;font-weight:600;color:var(--text-secondary);padding:0;margin-bottom:24px" style-hover="color:var(--color-primary)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg>블로그 목록</button>
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="{{ dVariant }}" shape="pill" hint-size="auto,22px">{{ dCat }}</x-import>
<span style="font-size:13px;color:var(--text-muted)">{{ dDate }}</span>
</div>
<h1 style="margin:16px 0 0;font-size:clamp(26px,3vw,38px);line-height:1.25;letter-spacing:-0.02em;font-weight:700;color:var(--text-body)">{{ dTitle }}</h1>
<div style="margin:18px 0 0;display:flex;align-items:center;gap:10px;padding-bottom:24px;border-bottom:1px solid var(--color-border)">
<span style="width:34px;height:34px;border-radius:var(--radius-pill);background:var(--color-primary);display:grid;place-items:center;color:#fff;font-weight:700;font-size:13px">G</span>
<div style="line-height:1.3"><div style="font-size:14px;font-weight:600;color:var(--text-body)">{{ dAuthor }}</div><div style="font-size:12px;color:var(--text-muted)">GTS 정보통신 기술팀</div></div>
</div>
<sc-if value="{{ galNone }}" hint-placeholder-val="{{ true }}">
<div style="margin-top:28px;aspect-ratio:16/9;border-radius:var(--radius-lg);background:var(--surface-sunken);border:1px solid var(--color-border);display:grid;place-items:center;color:var(--text-disabled)">
<div style="text-align:center"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"></rect><circle cx="9" cy="9" r="2"></circle><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path></svg><div style="margin-top:8px;font-size:13px;font-weight:600">대표 이미지 영역</div></div>
</div>
</sc-if>
<sc-if value="{{ galSingle }}" hint-placeholder-val="{{ false }}">
<div style="margin-top:28px;aspect-ratio:16/9;border-radius:var(--radius-lg);overflow:hidden;border:1px solid var(--color-border)">{{ galFirstNode }}</div>
</sc-if>
<sc-if value="{{ galHasMany }}" hint-placeholder-val="{{ false }}">
<div style="margin-top:28px">
<div style="position:relative;aspect-ratio:16/9;border-radius:var(--radius-lg);overflow:hidden;border:1px solid var(--color-border);background:var(--surface-sunken);touch-action:pan-y;user-select:none" onpointerdown="{{ onGalPointerDown }}" onpointerup="{{ onGalPointerUp }}">
<div style="{{ galTrackStyle }}">
<sc-for list="{{ galImages }}" as="g" hint-placeholder-count="3">
<div style="flex:0 0 100%;height:100%">{{ g.node }}</div>
</sc-for>
</div>
<button onclick="{{ galPrev }}" aria-label="이전 이미지" style="position:absolute;top:50%;left:12px;transform:translateY(-50%);width:40px;height:40px;border-radius:var(--radius-pill);border:0;cursor:pointer;background:rgba(255,255,255,0.92);box-shadow:var(--shadow-md);display:grid;place-items:center"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--text-body)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"></path></svg></button>
<button onclick="{{ galNext }}" aria-label="다음 이미지" style="position:absolute;top:50%;right:12px;transform:translateY(-50%);width:40px;height:40px;border-radius:var(--radius-pill);border:0;cursor:pointer;background:rgba(255,255,255,0.92);box-shadow:var(--shadow-md);display:grid;place-items:center"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--text-body)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"></path></svg></button>
<div style="position:absolute;top:12px;right:12px;padding:4px 11px;border-radius:var(--radius-pill);background:rgba(9,9,11,0.62);color:#fff;font-size:12px;font-weight:700;font-family:var(--font-mono)">{{ galCounter }}</div>
</div>
<div style="margin-top:12px;display:flex;gap:7px;justify-content:center">
<sc-for list="{{ dots }}" as="d" hint-placeholder-count="3">
<sc-if value="{{ d.on }}"><button onclick="{{ d.go }}" aria-label="이미지 이동" style="width:24px;height:8px;border-radius:var(--radius-pill);border:0;cursor:pointer;background:var(--color-primary);transition:width .2s var(--ease)"></button></sc-if>
<sc-if value="{{ d.off }}"><button onclick="{{ d.go }}" aria-label="이미지 이동" style="width:8px;height:8px;border-radius:var(--radius-pill);border:0;cursor:pointer;background:var(--color-border-strong);transition:width .2s var(--ease)"></button></sc-if>
</sc-for>
</div>
</div>
</sc-if>
<div style="margin-top:32px">
<sc-for list="{{ dParas }}" as="para" hint-placeholder-count="4">
<p style="margin:0 0 20px;font-size:16.5px;line-height:1.8;color:var(--text-body)">{{ para }}</p>
</sc-for>
</div>
<div style="margin-top:40px;padding:24px 26px;border-radius:var(--radius-lg);background:var(--color-primary-subtle);border:1px solid var(--color-border);display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap">
<div><div style="font-size:16px;font-weight:700;color:var(--text-body)">비슷한 시공이 필요하신가요?</div><div style="margin-top:4px;font-size:14px;color:var(--text-secondary)">현장에 맞는 무료 견적을 받아보세요.</div></div>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" as="a" href="tel:{{ coPhone }}" hint-size="auto,38px">전화상담 {{ coPhone }}</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="secondary" onclick="{{ goContact }}" hint-size="auto,38px">1:1 문의</x-import>
</div>
</div>
<div style="margin-top:48px">
<h2 style="margin:0 0 18px;font-size:19px;font-weight:700;color:var(--text-body)">다른 글</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px">
<sc-for list="{{ related }}" as="r" hint-placeholder-count="3">
<article onclick="{{ r.open }}" style="cursor:pointer;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:18px 20px;transition:box-shadow .2s var(--ease),transform .2s var(--ease)" style-hover="box-shadow:var(--shadow-lg);transform:translateY(-3px)">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="{{ r.variant }}" shape="pill" size="sm" hint-size="auto,20px">{{ r.cat }}</x-import>
<h3 style="margin:10px 0 0;font-size:15px;font-weight:700;line-height:1.4;color:var(--text-body)">{{ r.title }}</h3>
<div style="margin-top:10px;font-size:12px;color:var(--text-muted)">{{ r.date }}</div>
</article>
</sc-for>
</div>
</div>
</main>
</sc-if>
<!-- ===================== CONTACT ===================== -->
<sc-if value="{{ isContact }}">
<main data-screen-label="1:1 문의" style="max-width:1200px;margin:0 auto;padding:clamp(44px,5vw,64px) 24px clamp(64px,7vw,88px)">
<div style="margin-bottom:8px"><span style="font-size:13px;font-weight:700;letter-spacing:0.1em;color:var(--color-primary)">CONTACT</span></div>
<h1 style="margin:0;font-size:clamp(27px,3vw,40px);font-weight:700;letter-spacing:-0.02em;color:var(--text-body)">1:1 비공개 상담 신청</h1>
<p style="margin:14px 0 0;font-size:16px;line-height:1.6;color:var(--text-secondary);max-width:640px">남겨주신 내용은 비공개로 접수되며, 담당자가 확인 후 연락처로 회신드립니다. 급하신 경우 대표번호로 전화 주시면 바로 상담해 드립니다.</p>
<div style="margin-top:36px;display:flex;flex-wrap:wrap;gap:26px;align-items:flex-start">
<!-- Form -->
<div style="flex:1 1 440px;min-width:0;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:clamp(24px,3vw,34px)">
<sc-if value="{{ submitted }}">
<div style="text-align:center;padding:24px 8px 12px">
<span style="width:64px;height:64px;border-radius:var(--radius-pill);background:var(--color-success-subtle);display:inline-grid;place-items:center"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--color-success)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg></span>
<h2 style="margin:20px 0 0;font-size:22px;font-weight:700;color:var(--text-body)">문의가 접수되었습니다</h2>
<p style="margin:10px 0 0;font-size:15px;line-height:1.6;color:var(--text-secondary)">담당자가 확인 후 빠르게 연락드리겠습니다.<br>접수 내용은 입력하신 비밀번호로 확인하실 수 있습니다.</p>
<div style="margin:22px auto 0;display:inline-flex;align-items:center;gap:10px;padding:12px 18px;border-radius:var(--radius-md);background:var(--surface-sunken);border:1px solid var(--color-border)"><span style="font-size:13px;color:var(--text-muted)">접수번호</span><span style="font-family:var(--font-mono);font-size:15px;font-weight:700;color:var(--text-body)">{{ ticket }}</span></div>
<div style="margin-top:26px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="secondary" onclick="{{ resetForm }}" hint-size="auto,38px">새 문의 작성</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" onclick="{{ goHome }}" hint-size="auto,38px">홈으로</x-import>
</div>
</div>
</sc-if>
<sc-if value="{{ notSubmitted }}">
<h2 style="margin:0 0 4px;font-size:19px;font-weight:700;color:var(--text-body)">상담 신청서</h2>
<p style="margin:0 0 22px;font-size:13.5px;color:var(--text-muted)"><span style="color:var(--color-danger)">*</span> 표시는 필수 입력 항목입니다.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="이름 / 회사명" required="{{ true }}" placeholder="홍길동 / (주)○○" value="{{ fName }}" onchange="{{ onName }}" hint-size="100%,66px"></x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="연락처" required="{{ true }}" placeholder="010-0000-0000" value="{{ fPhone }}" onchange="{{ onPhone }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="이메일 (선택)" type="email" placeholder="name@example.com" value="{{ fEmail }}" onchange="{{ onEmail }}" hint-size="100%,66px"></x-import>
<div>
<label style="display:block;font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:6px">문의 유형</label>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Select" options="{{ typeOptions }}" value="{{ fType }}" onchange="{{ onType }}" hint-size="100%,40px"></x-import>
</div>
</div>
<div style="margin-top:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="제목" required="{{ true }}" placeholder="예) 사무실 이전 네트워크 + 키폰 견적 문의" value="{{ fTitle }}" onchange="{{ onTitle }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Textarea" label="문의 내용" required="{{ true }}" rows="6" placeholder="현장 위치, 규모(평수/회선 수), 희망 일정 등을 적어주시면 더 정확한 견적이 가능합니다." value="{{ fBody }}" onchange="{{ onBody }}" hint-size="100%,150px"></x-import>
</div>
<div style="margin-top:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="비공개 확인 비밀번호" type="password" placeholder="답변 확인용 비밀번호" hint="접수 내용 확인 시 사용됩니다." value="{{ fPw }}" onchange="{{ onPw }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:18px;padding:14px 16px;border-radius:var(--radius-md);background:var(--surface-sunken);border:1px solid var(--color-border)">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Checkbox" checked="{{ fAgree }}" onchange="{{ onAgree }}" hint-size="100%,24px">개인정보 수집·이용에 동의합니다. (상담 답변 목적, 보관 후 파기)</x-import>
</div>
<div style="margin-top:22px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" size="lg" block="{{ true }}" onclick="{{ submit }}" hint-size="100%,46px">1:1 문의 접수하기</x-import>
</div>
</sc-if>
</div>
<!-- Info aside -->
<aside style="flex:1 1 300px;min-width:0;display:flex;flex-direction:column;gap:16px">
<div style="background:var(--color-primary);border-radius:var(--radius-lg);box-shadow:var(--shadow-md);padding:24px;color:#fff">
<div style="font-size:13px;font-weight:600;color:rgba(255,255,255,0.82)">전화로 바로 상담</div>
<a href="tel:{{ coPhone }}" style="display:flex;align-items:center;gap:10px;margin-top:8px;color:#fff;text-decoration:none;font-size:28px;font-weight:700;letter-spacing:-0.01em">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
{{ coPhone }}
</a>
<div style="margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.22);font-size:14px;color:rgba(255,255,255,0.9)">경기지점 {{ coSub }}<br>FAX {{ coFax }}</div>
</div>
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:22px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px"><span style="width:34px;height:34px;border-radius:var(--radius-md);background:var(--color-primary-subtle);display:grid;place-items:center"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg></span><span style="font-size:15px;font-weight:700;color:var(--text-body)">영업시간</span></div>
<p style="margin:0;font-size:14px;line-height:1.7;color:var(--text-secondary)">평일 09:00 18:00<br>주말 · 공휴일 휴무</p>
</div>
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:22px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px"><span style="width:34px;height:34px;border-radius:var(--radius-md);background:var(--color-primary-subtle);display:grid;place-items:center"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"></path><circle cx="12" cy="10" r="3"></circle></svg></span><span style="font-size:15px;font-weight:700;color:var(--text-body)">오시는 길</span></div>
<p style="margin:0;font-size:14px;line-height:1.7;color:var(--text-secondary)">{{ coAddr1 }}<br>{{ coAddr2 }}</p>
<div style="margin-top:14px;aspect-ratio:16/9;border-radius:var(--radius-md);background:var(--surface-sunken);border:1px dashed var(--color-border-strong);display:grid;place-items:center;color:var(--text-disabled);font-size:13px;font-weight:600">지도 영역</div>
</div>
<div style="display:flex;gap:11px;padding:15px 16px;border-radius:var(--radius-md);background:var(--color-primary-subtle);border:1px solid var(--color-border)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex:none;margin-top:1px"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>
<p style="margin:0;font-size:13px;line-height:1.6;color:var(--text-secondary)">1:1 비공개 문의는 작성하신 비밀번호로 본인만 답변을 확인할 수 있습니다.</p>
</div>
</aside>
</div>
</main>
</sc-if>
<!-- ===================== ADMIN CONSOLE ===================== -->
<sc-if value="{{ isAdmin }}">
<sc-if value="{{ adminLocked }}" hint-placeholder-val="{{ false }}">
<div data-screen-label="관리자 로그인" style="min-height:100vh;background:var(--surface-card);position:relative;overflow:hidden;display:grid;place-items:center;padding:24px">
<div style="position:absolute;inset:0;background:radial-gradient(600px 360px at 18% 0%, var(--color-primary-subtle), transparent 60%),radial-gradient(600px 360px at 100% 100%, var(--color-primary-subtle), transparent 62%);pointer-events:none"></div>
<div style="position:relative;width:100%;max-width:400px;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-xl);box-shadow:var(--shadow-2xl);padding:36px 32px">
<div style="display:flex;flex-direction:column;align-items:center;text-align:center">
<span style="width:50px;height:50px;border-radius:var(--radius-md);background:var(--color-primary);display:grid;place-items:center;box-shadow:var(--shadow-sm)"><svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
<h1 style="margin:18px 0 0;font-size:21px;font-weight:700;letter-spacing:-0.01em;color:var(--text-body)">관리자 로그인</h1>
<p style="margin:8px 0 0;font-size:14px;line-height:1.6;color:var(--text-secondary)">콘텐츠 · 문의 · 회사 정보 관리를 위해<br>관리자 비밀번호를 입력하세요.</p>
</div>
<div style="margin-top:26px">
<div class="mona-field"><label class="mona-field__label" for="gts-admin-password">비밀번호</label><div class="mona-inputwrap"><input id="gts-admin-password" class="mona-input" type="password" placeholder="관리자 비밀번호" autocomplete="current-password" onkeydown="{{ onAdminKey }}"></div></div>
</div>
<sc-if value="{{ adminPwError }}" hint-placeholder-val="{{ false }}">
<div style="margin-top:10px;display:flex;align-items:center;gap:6px;color:var(--color-danger);font-size:13px;font-weight:500"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg>비밀번호가 올바르지 않습니다.</div>
</sc-if>
<div style="margin-top:18px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" size="lg" block="{{ true }}" onclick="{{ tryLogin }}" hint-size="100%,46px">로그인</x-import>
</div>
<div style="margin-top:18px;padding-top:16px;border-top:1px solid var(--color-border);display:flex;align-items:center;justify-content:space-between;gap:10px">
<button onclick="{{ goHome }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:13px;font-weight:600;color:var(--text-secondary);padding:0;display:inline-flex;align-items:center;gap:5px" style-hover="color:var(--color-primary)"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg>사이트로 돌아가기</button>
<span style="font-size:12px;color:var(--text-muted)">데모 비밀번호 · gts1234</span>
</div>
</div>
</div>
</sc-if>
<sc-if value="{{ adminUnlocked }}" hint-placeholder-val="{{ true }}">
<div data-screen-label="관리자" style="min-height:100vh;background:var(--color-bg)">
<header style="position:sticky;top:0;z-index:50;background:var(--surface-card);border-bottom:1px solid var(--color-border)">
<div style="max-width:1100px;margin:0 auto;padding:0 24px;height:60px;display:flex;align-items:center;gap:12px">
<span style="width:32px;height:32px;border-radius:var(--radius-md);background:var(--color-primary);display:grid;place-items:center;flex:none"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="14" rx="2"></rect><path d="M6.01 18H6.01"></path><path d="M10.01 18H10.01"></path><path d="M15 10v4"></path><path d="M17.84 7.17a4 4 0 0 0-5.66 0"></path><path d="M20.66 4.34a8 8 0 0 0-11.31 0"></path></svg></span>
<span style="font-weight:700;font-size:16px;color:var(--text-body)">GTS 관리자 콘솔</span>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="primary" shape="pill" size="sm" hint-size="auto,20px">admin</x-import>
<div style="flex:1"></div>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="ghost" size="sm" onclick="{{ logout }}" hint-size="auto,30px">로그아웃</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="secondary" size="sm" onclick="{{ goHome }}" hint-size="auto,30px">사이트로 돌아가기</x-import>
</div>
</header>
<div style="max-width:1100px;margin:0 auto;padding:clamp(28px,3vw,40px) 24px clamp(56px,6vw,80px)">
<h1 style="margin:0 0 22px;font-size:clamp(22px,2.4vw,28px);font-weight:700;letter-spacing:-0.02em;color:var(--text-body)">콘텐츠 관리</h1>
<!-- Tabs -->
<div style="display:flex;gap:6px;margin-bottom:26px;border-bottom:1px solid var(--color-border)">
<sc-if value="{{ adminIsPosts }}"><button style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:700;color:var(--color-primary);padding:11px 14px;border-bottom:2px solid var(--color-primary);margin-bottom:-1px">글 관리</button></sc-if>
<sc-if value="{{ adminNotPosts }}"><button onclick="{{ tabPosts }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:500;color:var(--text-secondary);padding:11px 14px;border-bottom:2px solid transparent;margin-bottom:-1px" style-hover="color:var(--text-body)">글 관리</button></sc-if>
<sc-if value="{{ adminIsInquiries }}"><button style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:700;color:var(--color-primary);padding:11px 14px;border-bottom:2px solid var(--color-primary);margin-bottom:-1px">문의 관리</button></sc-if>
<sc-if value="{{ adminNotInquiries }}"><button onclick="{{ tabInquiries }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:500;color:var(--text-secondary);padding:11px 14px;border-bottom:2px solid transparent;margin-bottom:-1px" style-hover="color:var(--text-body)">문의 관리</button></sc-if>
<sc-if value="{{ adminIsSettings }}"><button style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:700;color:var(--color-primary);padding:11px 14px;border-bottom:2px solid var(--color-primary);margin-bottom:-1px">회사 정보</button></sc-if>
<sc-if value="{{ adminNotSettings }}"><button onclick="{{ tabSettings }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:500;color:var(--text-secondary);padding:11px 14px;border-bottom:2px solid transparent;margin-bottom:-1px" style-hover="color:var(--text-body)">회사 정보</button></sc-if>
</div>
<!-- Posts: list -->
<sc-if value="{{ adminPostsList }}">
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:18px">
<div style="font-size:15px;color:var(--text-secondary)">전체 <b style="color:var(--text-body)">{{ postCount }}</b>개의 글</div>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" onclick="{{ newPost }}" hint-size="auto,38px"> 새 글 작성</x-import>
</div>
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);overflow:hidden">
<div style="display:grid;grid-template-columns:1fr 120px 110px 156px;gap:12px;padding:13px 20px;background:var(--surface-subtle);border-bottom:1px solid var(--color-border);font-size:12.5px;font-weight:700;color:var(--text-muted)">
<div>제목</div><div>카테고리</div><div>작성일</div><div style="text-align:right">관리</div>
</div>
<sc-for list="{{ adminPosts }}" as="p" hint-placeholder-count="5">
<div style="display:grid;grid-template-columns:1fr 120px 110px 156px;gap:12px;align-items:center;padding:14px 20px;border-bottom:1px solid var(--color-border)">
<div style="font-size:14.5px;font-weight:600;color:var(--text-body);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ p.title }}</div>
<div><x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="{{ p.variant }}" shape="pill" size="sm" hint-size="auto,20px">{{ p.cat }}</x-import></div>
<div style="font-size:13px;color:var(--text-muted)">{{ p.date }}</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="secondary" size="sm" onclick="{{ p.edit }}" hint-size="auto,30px">수정</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="danger-outline" size="sm" onclick="{{ p.del }}" hint-size="auto,30px">삭제</x-import>
</div>
</div>
</sc-for>
</div>
</sc-if>
<!-- Posts: editor -->
<sc-if value="{{ adminPostsEdit }}">
<div style="max-width:760px">
<sc-if value="{{ editingIsNew }}"><h2 style="margin:0 0 4px;font-size:21px;font-weight:700;color:var(--text-body)">새 글 작성</h2></sc-if>
<sc-if value="{{ editingIsEdit }}"><h2 style="margin:0 0 4px;font-size:21px;font-weight:700;color:var(--text-body)">글 수정</h2></sc-if>
<p style="margin:0 0 22px;font-size:13.5px;color:var(--text-muted)">작성한 글은 이 브라우저에 저장되어 블로그 목록과 상세 화면에 바로 노출됩니다.</p>
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:clamp(22px,3vw,30px)">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="제목" required="{{ true }}" placeholder="글 제목을 입력하세요" value="{{ eTitle }}" onchange="{{ onETitle }}" hint-size="100%,66px"></x-import>
<div style="margin-top:16px;max-width:280px">
<label style="display:block;font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:6px">카테고리</label>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Select" options="{{ categoryOptions }}" value="{{ eCat }}" onchange="{{ onECat }}" hint-size="100%,40px"></x-import>
</div>
<div style="margin-top:16px">
<label style="display:block;font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:6px">이미지 <span style="font-weight:500;color:var(--text-muted)">(여러 장 등록 가능 · 시공사례에 적합)</span></label>
<div style="display:flex;flex-wrap:wrap;gap:10px">
<sc-for list="{{ eImages }}" as="im" hint-placeholder-count="0">
<div style="position:relative;width:108px;height:80px;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--color-border);background:var(--surface-sunken)">
{{ im.node }}
<button onclick="{{ im.remove }}" aria-label="이미지 삭제" style="position:absolute;top:5px;right:5px;width:22px;height:22px;border-radius:var(--radius-pill);border:0;cursor:pointer;background:rgba(9,9,11,0.66);color:#fff;display:grid;place-items:center"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg></button>
<span style="position:absolute;bottom:5px;left:5px;font-size:10px;font-weight:700;color:#fff;background:rgba(9,9,11,0.6);padding:1px 7px;border-radius:var(--radius-pill)">{{ im.num }}</span>
</div>
</sc-for>
<label style="width:108px;height:80px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;border:1.5px dashed var(--color-border-strong);border-radius:var(--radius-md);background:var(--surface-sunken);cursor:pointer" style-hover="border-color:var(--color-primary)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg>
<span style="font-size:11.5px;font-weight:600;color:var(--text-secondary)">사진 추가</span>
<input type="file" accept="image/*" multiple="{{ true }}" onchange="{{ onImageFile }}" style="display:none">
</label>
</div>
<p style="margin:10px 0 0;font-size:12px;color:var(--text-muted)">여러 장을 올리면 글 상세에서 좌우로 넘겨볼 수 있습니다. 업로드 시 자동으로 크기가 최적화됩니다.</p>
</div>
<div style="margin-top:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Textarea" label="요약 (목록 카드에 표시)" rows="2" placeholder="비워두면 본문 앞부분이 자동으로 사용됩니다." value="{{ eExcerpt }}" onchange="{{ onEExcerpt }}" hint-size="100%,84px"></x-import>
</div>
<div style="margin-top:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Textarea" label="본문" required="{{ true }}" rows="12" placeholder="본문을 입력하세요. 빈 줄로 문단을 구분합니다." value="{{ eBody }}" onchange="{{ onEBody }}" hint-size="100%,260px"></x-import>
</div>
<div style="margin-top:24px;display:flex;gap:10px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" onclick="{{ saveEditing }}" hint-size="auto,38px">저장하기</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="ghost" onclick="{{ cancelEdit }}" hint-size="auto,38px">취소</x-import>
</div>
</div>
</div>
</sc-if>
<!-- Inquiries -->
<sc-if value="{{ adminIsInquiries }}">
<sc-if value="{{ adminInqList }}">
<div style="margin-bottom:18px;font-size:15px;color:var(--text-secondary)">접수된 문의 <b style="color:var(--text-body)">{{ inquiryCount }}</b></div>
<sc-if value="{{ noInquiries }}">
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:52px 24px;text-align:center;color:var(--text-muted)">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block"><path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z"></path><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"></path></svg>
<div style="margin-top:12px;font-size:15px;font-weight:600;color:var(--text-secondary)">아직 접수된 문의가 없습니다</div>
<div style="margin-top:6px;font-size:13px">1:1 문의 폼으로 접수된 상담이 여기에 표시됩니다.</div>
</div>
</sc-if>
<div style="display:flex;flex-direction:column;gap:14px">
<sc-for list="{{ adminInquiries }}" as="q" hint-placeholder-count="3">
<div onclick="{{ q.open }}" style="cursor:pointer;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:18px 22px;transition:box-shadow .2s var(--ease)" style-hover="box-shadow:var(--shadow-lg)">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="{{ q.statusVariant }}" shape="pill" size="sm" dot="{{ true }}" hint-size="auto,20px">{{ q.status }}</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="neutral" shape="pill" size="sm" hint-size="auto,20px">{{ q.type }}</x-import>
<span style="font-size:12.5px;color:var(--text-muted);margin-left:auto;font-family:var(--font-mono)">{{ q.ticket }}</span>
</div>
<h3 style="margin:12px 0 0;font-size:16px;font-weight:700;color:var(--text-body)">{{ q.title }}</h3>
<div style="margin:6px 0 0;font-size:13px;color:var(--text-secondary)">{{ q.contact }} · {{ q.date }}</div>
<p style="margin:10px 0 0;font-size:13.5px;line-height:1.6;color:var(--text-secondary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">{{ q.body }}</p>
<div style="margin-top:14px;display:flex;align-items:center;gap:12px">
<span style="font-size:13px;font-weight:600;color:var(--color-primary);display:inline-flex;align-items:center;gap:4px">응대하기<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></span>
<sc-if value="{{ q.hasReply }}"><span style="font-size:12px;color:var(--color-success);font-weight:600;display:inline-flex;align-items:center;gap:4px"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg>답변 완료</span></sc-if>
</div>
</div>
</sc-for>
</div>
</sc-if>
<sc-if value="{{ adminInqDetail }}">
<button onclick="{{ closeInquiry }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:6px;font-size:14px;font-weight:600;color:var(--text-secondary);padding:0;margin-bottom:18px" style-hover="color:var(--color-primary)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg>문의 목록</button>
<div style="display:flex;flex-wrap:wrap;gap:20px;align-items:flex-start">
<div style="flex:1 1 420px;min-width:0;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:24px 26px">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="{{ cqStatusVariant }}" shape="pill" size="sm" dot="{{ true }}" hint-size="auto,20px">{{ cqStatus }}</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Badge" variant="neutral" shape="pill" size="sm" hint-size="auto,20px">{{ cqType }}</x-import>
<span style="font-size:12.5px;color:var(--text-muted);margin-left:auto;font-family:var(--font-mono)">{{ cqTicket }}</span>
</div>
<h2 style="margin:14px 0 0;font-size:19px;font-weight:700;color:var(--text-body)">{{ cqTitle }}</h2>
<div style="margin:14px 0 0;padding:14px 16px;border-radius:var(--radius-md);background:var(--surface-sunken);font-size:13.5px;line-height:1.95;color:var(--text-secondary)">
<div>이름 · <b style="color:var(--text-body);font-weight:600">{{ cqName }}</b></div>
<div>연락처 · <b style="color:var(--text-body);font-weight:600">{{ cqPhone }}</b></div>
<sc-if value="{{ cqHasEmail }}"><div>이메일 · {{ cqEmail }}</div></sc-if>
<div>접수일 · {{ cqDate }}</div>
</div>
<p style="margin:16px 0 0;font-size:14.5px;line-height:1.75;color:var(--text-body);white-space:pre-wrap">{{ cqBody }}</p>
</div>
<div style="flex:1 1 320px;min-width:0;background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:24px 26px">
<h3 style="margin:0 0 4px;font-size:16px;font-weight:700;color:var(--text-body)">답변 작성</h3>
<sc-if value="{{ cqHasReply }}"><p style="margin:0 0 14px;font-size:12.5px;color:var(--color-success);font-weight:600">{{ cqRepliedAt }} 답변 등록됨</p></sc-if>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Textarea" rows="8" placeholder="고객에게 보낼 답변을 작성하세요. 저장하면 문의가 '완료' 상태로 전환됩니다." value="{{ inqReplyDraft }}" onchange="{{ onInqReply }}" hint-size="100%,200px"></x-import>
<div style="margin-top:16px;display:flex;gap:10px;flex-wrap:wrap">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" onclick="{{ saveReply }}" hint-size="auto,38px">답변 등록</x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="secondary" onclick="{{ setInqWaiting }}" hint-size="auto,38px">대기로 변경</x-import>
</div>
</div>
</div>
</sc-if>
</sc-if>
<!-- Settings: company info -->
<sc-if value="{{ adminIsSettings }}">
<div style="max-width:780px">
<h2 style="margin:0 0 4px;font-size:21px;font-weight:700;color:var(--text-body)">회사 정보 설정</h2>
<p style="margin:0 0 22px;font-size:13.5px;color:var(--text-muted)">여기서 수정한 내용은 헤더·푸터·문의 화면 등 사이트 전체에 바로 반영됩니다.</p>
<div style="background:var(--surface-card);border:1px solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:clamp(22px,3vw,30px)">
<label style="display:block;font-size:13px;font-weight:600;color:var(--text-secondary);margin-bottom:8px">로고</label>
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
<sc-if value="{{ cdHasLogo }}" hint-placeholder-val="{{ false }}"><span style="width:54px;height:54px;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--color-border);background:#fff;display:grid;place-items:center">{{ cdLogoImg }}</span></sc-if>
<sc-if value="{{ cdNoLogo }}" hint-placeholder-val="{{ true }}"><span style="width:54px;height:54px;border-radius:var(--radius-md);background:var(--color-primary);display:grid;place-items:center"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="14" rx="2"></rect><path d="M6.01 18H6.01"></path><path d="M10.01 18H10.01"></path><path d="M15 10v4"></path><path d="M17.84 7.17a4 4 0 0 0-5.66 0"></path><path d="M20.66 4.34a8 8 0 0 0-11.31 0"></path></svg></span></sc-if>
<label style="display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 16px;border-radius:var(--radius-md);border:1px solid var(--color-border-strong);background:var(--surface-card);cursor:pointer;font-size:13px;font-weight:600;color:var(--text-body)" style-hover="background:var(--surface-hover)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" x2="12" y1="3" y2="15"></line></svg>로고 업로드<input type="file" accept="image/*" onchange="{{ onLogoFile }}" style="display:none"></label>
<sc-if value="{{ cdHasLogo }}" hint-placeholder-val="{{ false }}"><x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" variant="ghost" size="sm" onclick="{{ clearLogo }}" hint-size="auto,30px">기본 아이콘으로</x-import></sc-if>
</div>
<div style="margin-top:20px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="회사 이름" value="{{ cdName }}" onchange="{{ onCdName }}" hint="첫 단어 뒤는 강조 색으로 표시됩니다." hint-size="100%,66px"></x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="영문 표기 (로고 하단)" value="{{ cdSubtitle }}" onchange="{{ onCdSubtitle }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="대표번호" value="{{ cdPhone }}" onchange="{{ onCdPhone }}" hint-size="100%,66px"></x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="경기지점 번호" value="{{ cdSub }}" onchange="{{ onCdSub }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="FAX" value="{{ cdFax }}" onchange="{{ onCdFax }}" hint-size="100%,66px"></x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="이메일" type="email" value="{{ cdEmail }}" onchange="{{ onCdEmail }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="주소" value="{{ cdAddr1 }}" onchange="{{ onCdAddr1 }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="주소 상세 (2줄)" value="{{ cdAddr2 }}" onchange="{{ onCdAddr2 }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="법인명 (푸터)" value="{{ cdLegal }}" onchange="{{ onCdLegal }}" hint-size="100%,66px"></x-import>
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="대표자" value="{{ cdCeo }}" onchange="{{ onCdCeo }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px;max-width:340px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Input" label="사업자등록번호" value="{{ cdBizno }}" onchange="{{ onCdBizno }}" hint-size="100%,66px"></x-import>
</div>
<div style="margin-top:16px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Textarea" label="푸터 소개 문구" rows="2" value="{{ cdTagline }}" onchange="{{ onCdTagline }}" hint-size="100%,84px"></x-import>
</div>
<div style="margin-top:24px">
<x-import component-from-global-scope="MONAFamilyDesignSystem_68e947.Button" onclick="{{ saveCompany }}" hint-size="auto,38px">회사 정보 저장</x-import>
</div>
</div>
</div>
</sc-if>
</div>
</div>
</sc-if>
</sc-if>
<!-- ===================== FOOTER (site only) ===================== -->
<sc-if value="{{ isSite }}">
<footer style="background:var(--surface-card);border-top:1px solid var(--color-border)">
<div style="max-width:1200px;margin:0 auto;padding:48px 24px 0;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:36px">
<div>
<div style="display:inline-flex;align-items:center;gap:10px">
{{ footerLogo }}
<span style="font-weight:700;font-size:17px;color:var(--text-body)">{{ coBrandA }} <span style="color:var(--color-primary)">{{ coBrandB }}</span></span>
</div>
<p style="margin:14px 0 0;font-size:13.5px;line-height:1.6;color:var(--text-secondary);max-width:280px">{{ coTagline }}</p>
</div>
<div>
<h4 style="margin:0 0 14px;font-size:13px;font-weight:700;letter-spacing:0.04em;color:var(--text-muted)">바로가기</h4>
<div style="display:flex;flex-direction:column;gap:10px;align-items:flex-start">
<button onclick="{{ goHome }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:14px;color:var(--text-secondary);padding:0" style-hover="color:var(--color-primary)"></button>
<button onclick="{{ goBlog }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:14px;color:var(--text-secondary);padding:0" style-hover="color:var(--color-primary)">블로그</button>
<button onclick="{{ goContact }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;font-size:14px;color:var(--text-secondary);padding:0" style-hover="color:var(--color-primary)">1:1 문의</button>
</div>
</div>
<div>
<h4 style="margin:0 0 14px;font-size:13px;font-weight:700;letter-spacing:0.04em;color:var(--text-muted)">연락처</h4>
<div style="font-size:14px;line-height:1.9;color:var(--text-secondary)">대표번호 <a href="tel:{{ coPhone }}" style="color:var(--text-body);font-weight:600;text-decoration:none">{{ coPhone }}</a><br>경기지점 <a href="tel:{{ coSub }}" style="color:var(--text-body);font-weight:600;text-decoration:none">{{ coSub }}</a><br>FAX {{ coFax }}<br>이메일 {{ coEmail }}</div>
</div>
<div>
<h4 style="margin:0 0 14px;font-size:13px;font-weight:700;letter-spacing:0.04em;color:var(--text-muted)">사업자 정보</h4>
<div style="font-size:13px;line-height:1.85;color:var(--text-secondary)">{{ coLegal }}<br>대표 {{ coCeo }}<br>사업자등록번호 {{ coBizno }}<br>{{ coAddr1 }} {{ coAddr2 }}</div>
</div>
</div>
<div style="max-width:1200px;margin:40px auto 0;padding:20px 24px;border-top:1px solid var(--color-border);display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap">
<span style="font-size:12.5px;color:var(--text-muted)">© 2026 GTS Network. All rights reserved.</span>
<button onclick="{{ goAdmin }}" style="appearance:none;border:0;background:none;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--text-muted);padding:0" style-hover="color:var(--color-primary)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"></path><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"></path></svg>관리자</button>
</div>
</footer>
</sc-if>
<!-- ===================== FLOATING CALL ===================== -->
<sc-if value="{{ showFab }}" hint-placeholder-val="{{ true }}">
<a href="tel:{{ coPhone }}" aria-label="전화상담" style="position:fixed;right:22px;bottom:22px;z-index:60;display:inline-flex;align-items:center;gap:9px;height:54px;padding:0 22px 0 18px;border-radius:var(--radius-pill);background:var(--color-primary);color:#fff;font-weight:700;font-size:15px;text-decoration:none;box-shadow:var(--shadow-xl)" style-hover="background:var(--color-primary-hover)">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
전화상담
</a>
</sc-if>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script="" data-props="{&quot;$preview&quot;:{&quot;width&quot;:1200,&quot;height&quot;:860},&quot;showFloatingCall&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;},&quot;showProcess&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;},&quot;showBlogPreview&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;}}">
class Component extends DCLogic {
POSTS_KEY = 'gts_posts_v1';
INQ_KEY = 'gts_inquiries_v1';
COMPANY_KEY = 'gts_company_v1';
ADMIN_PW = 'gts1234';
companyDefault = {
name: 'GTS 정보통신',
subtitle: 'GTS NETWORK',
legalName: '(주)지티에스정보통신',
phoneMain: '1833-4917',
phoneSub: '1833-4918',
fax: '02-6442-0670',
email: 'help@gts-network.co.kr',
addr1: '서울시 송파구 정의로7길 13,',
addr2: '오피스 1114호 (문정동, 힐스테이트에코송파)',
ceo: '서선배',
bizno: '206-86-70582',
tagline: '신뢰할 수 있는 통신 인프라 파트너. 네트워크·키폰·CCTV 설계부터 시공·유지보수까지.',
logo: '',
};
state = {
view: "post",
cat: '전체',
page: 1,
submitted: false,
ticket: '',
form: { name: '', phone: '', email: '', type: '네트워크 구축', title: '', body: '', pw: '', agree: false },
posts: [],
inquiries: [],
currentPostId: 5,
adminTab: "posts",
adminMode: "list",
editing: { id: null, cat: '네트워크', title: '', excerpt: '', body: '', images: [] },
galleryIndex: 0,
company: { ...this.companyDefault },
companyDraft: {},
adminInqMode: "list",
currentInquiryId: null,
inqReplyDraft: "",
adminAuthed: false,
adminPw: '',
adminPwErr: false,
loaded: false,
};
cats = ['전체', '네트워크', '키폰시스템', 'CCTV', '시공사례'];
categoryOptions = [
{ value: '네트워크', label: '네트워크' },
{ value: '키폰시스템', label: '키폰시스템' },
{ value: 'CCTV', label: 'CCTV' },
{ value: '시공사례', label: '시공사례' },
];
typeOptions = [
{ value: '네트워크 구축', label: '네트워크(LAN) 구축' },
{ value: '키폰시스템', label: '키폰시스템' },
{ value: 'CCTV 설치', label: 'CCTV 설치' },
{ value: '시공·유지보수', label: '시공·유지보수' },
{ value: '기타 문의', label: '기타 문의' },
];
defaultPosts = [
{ id: 1, cat: '네트워크', variant: 'info', date: '2026.06.18', author: 'GTS 정보통신', title: '사무실 이전 시 네트워크 구축 체크리스트',
excerpt: '인터넷 회선 이전 신청부터 배선 경로, 스위치·AP 배치까지 — 이사 전에 확인하면 좋은 항목을 정리했습니다.',
body: '사무실 이전은 단순히 책상을 옮기는 일이 아닙니다. 인터넷과 내선 전화가 첫날부터 정상 동작하려면 이전 2~3주 전부터 통신 환경을 미리 준비해야 합니다.\n\n가장 먼저 인터넷 회선 이전(또는 신규 가입)을 통신사에 신청합니다. 회선 개통에는 보통 영업일 기준 3~7일이 소요되므로 입주일을 기준으로 역산해 신청하는 것이 안전합니다.\n\n다음으로 새 사무실의 배선 경로와 정보 콘센트 위치, 통신 단자함 위치를 확인합니다. 좌석 배치도를 기준으로 유선 포트 수와 무선 AP 설치 지점을 미리 설계해 두면 시공 당일 혼선을 줄일 수 있습니다.\n\nGTS 정보통신은 현장 방문 조사 후 회선·배선·키폰·CCTV를 한 번에 설계해 드립니다. 이전 일정이 잡히셨다면 미리 상담을 신청해 주세요.' },
{ id: 2, cat: '키폰시스템', variant: 'purple', date: '2026.06.11', author: 'GTS 정보통신', title: 'LG vs 삼성 키폰 주장치, 어떻게 고를까?',
excerpt: '내선 규모와 확장성, 부가 기능을 기준으로 디지털·아날로그 주장치를 비교해 선택 기준을 안내합니다.',
body: '키폰 주장치는 한 번 설치하면 오래 사용하는 장비입니다. 그래서 현재 회선 수뿐 아니라 향후 증설 가능성까지 고려해 기종을 선택해야 합니다.\n\nLG와 삼성 모두 디지털·아날로그 주장치를 제공하며, 기본 용량과 확장 카드 구성 방식에 차이가 있습니다. 내선이 자주 늘어나는 사업장이라면 최대 수용 용량이 넉넉한 기종을 고르는 것이 장기적으로 유리합니다.\n\n키폰 전화기와 일반 전화기를 함께 쓰는 하이브리드 구성을 지원하는지도 중요한 기준입니다. 통화량이 적은 공간에는 일반 전화기를 배치해 초기 비용을 낮출 수 있습니다.\n\n어떤 기종이 적합한지 고민되신다면, 사용 인원과 통화 패턴을 알려주시면 환경에 맞는 구성을 제안해 드립니다.' },
{ id: 3, cat: '시공사례', variant: 'warning', date: '2026.06.04', author: 'GTS 정보통신', title: '물류창고 100회선 키폰 + 네트워크 통합 시공기',
excerpt: '넓은 현장의 배선 설계와 무선 구간 보강까지, 통합 시공으로 비용과 공기를 줄인 사례를 소개합니다.',
body: '대형 물류창고는 천장이 높고 면적이 넓어 일반 사무실과는 다른 배선 설계가 필요합니다. 이번 현장은 100회선 규모의 키폰과 무선 네트워크를 동시에 구축한 사례입니다.\n\n먼저 메인 통신실을 중심으로 간선을 포설하고, 구역별 분전반에 스위치를 분산 배치해 케이블 길이를 최소화했습니다. 이렇게 하면 신호 손실을 줄이고 향후 유지보수도 쉬워집니다.\n\n천장이 높은 적재 구역에는 지향성 안테나가 적용된 AP를 배치해 무선 음영 지역을 없앴습니다. 키폰과 네트워크 배선을 함께 진행해 공사 기간과 비용을 모두 절감할 수 있었습니다.\n\n넓은 현장일수록 통합 설계의 효과가 큽니다. 유사한 환경의 시공이 필요하시면 현장 조사를 신청해 주세요.' },
{ id: 4, cat: '네트워크', variant: 'info', date: '2026.05.28', author: 'GTS 정보통신', title: '유선랜 vs 무선랜, 사무실 환경별 선택 가이드',
excerpt: '좌석 밀도와 보안 요구에 따라 유선·무선을 어떻게 조합할지, 실무 기준으로 풀어 설명합니다.',
body: '유선랜과 무선랜은 대체재가 아니라 보완재입니다. 안정성이 중요한 자리는 유선으로, 이동성이 중요한 공간은 무선으로 구성하는 것이 일반적입니다.\n\n고정 좌석이 많고 대용량 파일을 자주 다루는 사무실이라면 유선 포트를 충분히 확보하는 것이 좋습니다. 유선은 속도와 지연 시간이 일정해 화상회의나 서버 접속에 유리합니다.\n\n회의실, 라운지, 방문객 공간처럼 자리가 유동적인 곳은 무선 AP로 커버합니다. 동시 접속자가 많은 구역은 AP 한 대에 과부하가 걸리지 않도록 수량과 채널을 분산 설계합니다.\n\n좌석 배치도와 인원수를 알려주시면 유선·무선 비율을 최적화한 설계안을 제안해 드립니다.' },
{ id: 5, cat: 'CCTV', variant: 'success', date: '2026.05.21', author: 'GTS 정보통신', title: 'CCTV 화소수와 저장기간, 적정 기준은?',
excerpt: '현장 목적에 맞는 해상도와 녹화 보관 일수를 정해 과한 비용 없이 효율적으로 구성하는 법.',
body: 'CCTV는 무조건 화소가 높다고 좋은 것이 아닙니다. 설치 목적과 촬영 거리에 맞는 해상도를 선택해야 비용과 저장 용량을 동시에 아낄 수 있습니다.\n\n출입구나 계산대처럼 얼굴·번호판 식별이 중요한 지점은 고해상도 카메라를 배치합니다. 반면 전반적인 동선 확인이 목적인 구역은 적정 해상도로도 충분합니다.\n\n저장 기간은 업종과 관련 규정에 따라 결정합니다. 보관 일수를 늘리면 그만큼 저장장치 용량이 커지므로, 필요한 기간을 먼저 정한 뒤 녹화 화질과 프레임을 조정하는 것이 합리적입니다.\n\n현장 면적과 감시 목적을 알려주시면 카메라 위치와 저장 구성까지 함께 제안해 드립니다.' },
{ id: 6, cat: '시공사례', variant: 'warning', date: '2026.05.14', author: 'GTS 정보통신', title: '인터넷 속도 저하, 스위치 점검으로 해결한 사례',
excerpt: '체감 속도 저하의 원인을 단계별로 진단하고 노후 스위치 교체로 해결한 실제 점검 과정.',
body: '"인터넷이 느려졌다"는 문의는 원인이 매우 다양합니다. 회선 자체보다 내부 네트워크 장비가 원인인 경우도 많습니다.\n\n이번 현장은 특정 시간대마다 속도가 급격히 떨어지는 증상이었습니다. 회선 속도를 측정한 결과 통신사 구간은 정상이었고, 문제는 내부에 있었습니다.\n\n배선과 장비를 단계별로 점검한 끝에, 오래된 스위치 한 대가 과부하 상태에서 패킷을 제대로 처리하지 못하는 것을 확인했습니다. 해당 스위치를 교체하자 속도가 정상으로 회복됐습니다.\n\n원인을 추측해 장비를 통째로 바꾸기보다, 정확한 진단으로 꼭 필요한 부분만 교체하는 것이 비용을 아끼는 길입니다. 속도 저하가 반복된다면 점검을 신청해 주세요.' },
{ id: 7, cat: '네트워크', variant: 'info', date: '2026.05.07', author: 'GTS 정보통신', title: '재택·하이브리드 근무를 위한 사무실 무선랜 설계',
excerpt: '동시 접속자 증가에 대비한 AP 수량 산정과 채널 설계로 안정적인 Wi-Fi 환경을 만드는 방법.',
body: '하이브리드 근무가 늘면서 노트북·휴대폰·태블릿이 동시에 무선에 접속하는 환경이 일반화됐습니다. 기기 수가 늘면 AP 한 대가 감당할 부하도 커집니다.\n\n안정적인 무선 환경의 핵심은 신호 세기보다 "동시 접속 분산"입니다. 넓은 공간에 강한 AP 한 대를 두기보다, 적정 출력의 AP를 여러 대 배치해 부하를 나누는 것이 효과적입니다.\n\n인접한 AP끼리 같은 채널을 쓰면 서로 간섭을 일으키므로, 채널을 겹치지 않게 설계해야 합니다. 회의실처럼 인원이 몰리는 공간은 별도로 용량을 확보합니다.\n\n근무 인원과 평면도를 공유해 주시면 음영 없는 무선 설계안을 제안해 드립니다.' },
{ id: 8, cat: '키폰시스템', variant: 'purple', date: '2026.04.30', author: 'GTS 정보통신', title: '노후 키폰 교체, 비용을 줄이는 하이브리드 구성',
excerpt: '키폰과 일반 전화기를 혼용하는 하이브리드 구성으로 초기 설치 비용을 낮추는 실무 팁.',
body: '오래된 키폰 시스템을 교체할 때 모든 자리를 고급 키폰 전화기로 채울 필요는 없습니다. 사용 패턴에 맞춰 구성하면 초기 비용을 크게 낮출 수 있습니다.\n\n통화량이 많고 내선 전환이 잦은 자리에는 기능 버튼이 많은 키폰 전화기를 배치합니다. 반면 창고·휴게실처럼 통화가 드문 공간은 일반 전화기로도 충분합니다.\n\n이렇게 키폰과 일반 전화기를 함께 쓰는 방식을 하이브리드 키폰이라고 합니다. 요즘 대부분의 제조사가 지원하므로 기종 선택의 폭도 넓습니다.\n\n현재 사용 중인 전화기 수와 자리별 통화량을 알려주시면, 교체 범위를 최소화한 견적을 드립니다.' },
{ id: 9, cat: 'CCTV', variant: 'success', date: '2026.04.23', author: 'GTS 정보통신', title: '다지점 매장 CCTV 통합 관제 구축 사례',
excerpt: '여러 지점의 영상을 한 화면에서 모니터링하는 통합 관제 환경을 어떻게 구성했는지 정리했습니다.',
body: '매장이 여러 곳으로 늘어나면 지점마다 따로 영상을 확인하는 일이 번거로워집니다. 이번 사례는 본사에서 모든 지점을 한 화면으로 모니터링하도록 구축한 통합 관제 사례입니다.\n\n각 지점에는 녹화장치를 설치해 현장 영상을 자체 저장하고, 네트워크를 통해 본사 관제 PC에서 실시간으로 조회할 수 있도록 연동했습니다.\n\n원격 접속 시 보안이 가장 중요하므로, 접근 권한과 비밀번호 정책을 분리하고 외부 노출을 최소화하는 방식으로 설계했습니다.\n\n지점 수가 늘어날 예정이라면 처음부터 확장을 고려한 관제 구조로 설계하는 것이 좋습니다. 다지점 운영을 준비 중이시면 상담해 주세요.' },
];
componentDidMount() {
this.loadData();
}
loadData() {
let posts = null;
let inquiries = [];
let company = null;
try { const r = window.localStorage.getItem(this.POSTS_KEY); if (r) posts = JSON.parse(r); } catch (_) {}
let inqRaw = null;
try { inqRaw = window.localStorage.getItem(this.INQ_KEY); } catch (_) {}
if (inqRaw) { try { inquiries = JSON.parse(inqRaw); } catch (_) {} }
try { const r = window.localStorage.getItem(this.COMPANY_KEY); if (r) company = JSON.parse(r); } catch (_) {}
if (!posts || !Array.isArray(posts) || !posts.length) {
posts = this.defaultPosts.map((p) => ({ ...p }));
this.persistPosts(posts);
}
if (inqRaw === null || !Array.isArray(inquiries) || inquiries.length === 0) { inquiries = this.sampleInquiries(); this.persistInq(inquiries); }
company = { ...this.companyDefault, ...(company && typeof company === 'object' ? company : {}) };
this.setState({ posts, inquiries: Array.isArray(inquiries) ? inquiries : [], company, loaded: true });
}
sampleInquiries() {
return [
{ id: 1001, ticket: 'GTS-2026-1042', name: '김도현 / (주)다온물류', phone: '010-2841-7720', email: 'dh.kim@daon-log.co.kr', type: '네트워크 구축', title: '신규 물류센터 사무동 네트워크 + CCTV 견적 문의', body: '7월 중순 입주 예정인 물류센터 사무동(약 120평, 30석)에 유·무선 네트워크와 출입구 CCTV 6대 설치를 검토 중입니다. 현장 방문 견적 가능할까요?', date: '2026.06.24', status: '대기', reply: '', repliedAt: '' },
{ id: 1002, ticket: 'GTS-2026-1031', name: '이수민 / 메이플치과', phone: '010-5567-2098', email: '', type: '키폰시스템', title: '키폰 내선 4대 추가 증설 문의', body: '현재 LG 키폰을 사용 중인데 진료실 증설로 내선 4대를 추가하려고 합니다. 기존 주장치에서 증설 가능한지, 비용은 어느 정도인지 궤금합니다.', date: '2026.06.21', status: '완료', reply: '안녕하세요, GTS 정보통신입니다. 사용 중이신 주장치 모델 기준 내선 4회선 증설 가능하며, 기능카드 1매 추가로 처리됩니다. 방문 점검 후 정확한 견적 드리겠습니다. 희망 일정 회신 부탁드립니다.', repliedAt: '2026.06.22' },
{ id: 1003, ticket: 'GTS-2026-1018', name: '박정우', phone: '010-3320-8845', email: 'jwpark@gmail.com', type: '시공·유지보수', title: '사무실 인터넷 속도 저하 점검 요청', body: '오후 시간대만 되면 인터넷이 크게 느려집니다. 공유기와 스위치 점검 부탁드립니다. 강남구 역삼동입니다.', date: '2026.06.19', status: '대기', reply: '', repliedAt: '' },
];
}
persistPosts(p) { try { window.localStorage.setItem(this.POSTS_KEY, JSON.stringify(p)); } catch (_) {} }
persistInq(i) { try { window.localStorage.setItem(this.INQ_KEY, JSON.stringify(i)); } catch (_) {} }
catVariant(cat) {
return { '네트워크': 'info', '키폰시스템': 'purple', 'CCTV': 'success', '시공사례': 'warning' }[cat] || 'neutral';
}
today() {
const d = new Date();
const p = (n) => String(n).padStart(2, '0');
return d.getFullYear() + '.' + p(d.getMonth() + 1) + '.' + p(d.getDate());
}
toTop() { try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (_) {} }
go(v) { const map = { home: 'index.html', blog: 'blog.html', contact: 'contact.html', admin: 'admin.html' }; if (map[v]) { window.location.href = map[v]; return; } this.setState({ view: v }); this.toTop(); }
openPost(id) { window.location.href = 'post-' + id + '.html'; }
setCat(c) { this.setState({ cat: c, page: 1 }); }
field(k) {
return (e) => {
let v;
if (e && e.target) { v = e.target.type === 'checkbox' ? e.target.checked : e.target.value; } else { v = e; }
this.setState((s) => ({ form: { ...s.form, [k]: v } }));
};
}
submit() {
const f = this.state.form;
if (!f.name || !f.phone || !f.title || !f.agree) {
try { window.alert('이름, 연락처, 제목, 개인정보 동의는 필수 항목입니다.'); } catch (_) {}
return;
}
const ticket = 'GTS-2026-' + Math.floor(1000 + Math.random() * 9000);
const inq = { id: Date.now(), ticket, name: f.name, phone: f.phone, email: f.email, type: f.type, title: f.title, body: f.body, date: this.today(), status: '대기' };
const inquiries = [inq, ...this.state.inquiries];
this.setState({ submitted: true, ticket, inquiries });
this.persistInq(inquiries);
this.toTop();
}
resetForm() {
this.setState({ submitted: false, ticket: '', form: { name: '', phone: '', email: '', type: '네트워크 구축', title: '', body: '', pw: '', agree: false } });
}
// ---- Admin: posts ----
newPost() { this.setState({ adminMode: 'edit', editing: { id: null, cat: '네트워크', title: '', excerpt: '', body: '', images: [] } }); this.toTop(); }
editPost(id) {
const p = this.state.posts.find((x) => x.id === id);
if (!p) return;
this.setState({ adminMode: 'edit', editing: { id: p.id, cat: p.cat, title: p.title, excerpt: p.excerpt || '', body: p.body || '', images: Array.isArray(p.images) ? p.images.slice() : [] } });
this.toTop();
}
deletePost(id) {
try { if (!window.confirm('이 글을 삭제할까요?')) return; } catch (_) {}
const posts = this.state.posts.filter((p) => p.id !== id);
this.setState({ posts });
this.persistPosts(posts);
}
editField(k) {
return (e) => {
const v = (e && e.target) ? e.target.value : e;
this.setState((s) => ({ editing: { ...s.editing, [k]: v } }));
};
}
saveEditing() {
const ed = this.state.editing;
if (!ed.title.trim() || !ed.body.trim()) {
try { window.alert('제목과 본문을 입력해 주세요.'); } catch (_) {}
return;
}
const excerpt = ed.excerpt.trim() || (ed.body.trim().slice(0, 90) + (ed.body.trim().length > 90 ? '…' : ''));
let posts;
if (ed.id == null) {
const nextId = this.state.posts.reduce((m, p) => Math.max(m, p.id), 0) + 1;
const np = { id: nextId, cat: ed.cat, variant: this.catVariant(ed.cat), title: ed.title.trim(), excerpt, body: ed.body.trim(), images: ed.images ? ed.images.slice() : [], date: this.today(), author: 'GTS 정보통신' };
posts = [np, ...this.state.posts];
} else {
posts = this.state.posts.map((p) => p.id === ed.id ? { ...p, cat: ed.cat, variant: this.catVariant(ed.cat), title: ed.title.trim(), excerpt, body: ed.body.trim(), images: ed.images ? ed.images.slice() : [] } : p);
}
this.setState({ posts, adminMode: 'list' });
this.persistPosts(posts);
}
cancelEdit() { this.setState({ adminMode: 'list' }); }
onImageFile(e) {
const files = e && e.target && e.target.files ? Array.from(e.target.files) : [];
if (e && e.target) { try { e.target.value = ''; } catch (_) {} }
files.forEach((file) => {
if (!/^image\//.test(file.type)) return;
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
const img = new Image();
img.onload = () => {
const maxW = 1280;
const scale = Math.min(1, maxW / img.width);
const w = Math.max(1, Math.round(img.width * scale));
const h = Math.max(1, Math.round(img.height * scale));
const c = document.createElement('canvas');
c.width = w; c.height = h;
let dataUrl = result;
try { c.getContext('2d').drawImage(img, 0, 0, w, h); dataUrl = c.toDataURL('image/jpeg', 0.82); } catch (_) {}
this.setState((s) => ({ editing: { ...s.editing, images: [...s.editing.images, dataUrl] } }));
};
img.onerror = () => { this.setState((s) => ({ editing: { ...s.editing, images: [...s.editing.images, result] } })); };
img.src = result;
};
reader.readAsDataURL(file);
});
}
removeImage(idx) { this.setState((s) => ({ editing: { ...s.editing, images: s.editing.images.filter((_, i) => i !== idx) } })); }
galPrev() { this.setState((s) => { const n = this._galCount || 1; return { galleryIndex: (s.galleryIndex - 1 + n) % n }; }); }
galNext() { this.setState((s) => { const n = this._galCount || 1; return { galleryIndex: (s.galleryIndex + 1) % n }; }); }
galGo(i) { this.setState({ galleryIndex: i }); }
onGalPointerDown(e) { this._px = (e && typeof e.clientX === 'number') ? e.clientX : 0; }
onGalPointerUp(e) { const x = (e && typeof e.clientX === 'number') ? e.clientX : 0; const dx = x - (this._px || 0); if (Math.abs(dx) > 40) { if (dx < 0) this.galNext(); else this.galPrev(); } }
// ---- Admin: inquiries ----
toggleInquiry(id) {
const inquiries = this.state.inquiries.map((q) => q.id === id ? { ...q, status: q.status === '대기' ? '완료' : '대기' } : q);
this.setState({ inquiries });
this.persistInq(inquiries);
}
deleteInquiry(id) {
try { if (!window.confirm('이 문의를 삭제할까요?')) return; } catch (_) {}
const inquiries = this.state.inquiries.filter((q) => q.id !== id);
this.setState({ inquiries });
this.persistInq(inquiries);
}
openInquiry(id) { const q = this.state.inquiries.find((x) => x.id === id); this.setState({ adminInqMode: 'detail', currentInquiryId: id, inqReplyDraft: (q && q.reply) || '' }); this.toTop(); }
closeInquiry() { this.setState({ adminInqMode: 'list' }); }
onInqReply(e) { const v = (e && e.target) ? e.target.value : e; this.setState({ inqReplyDraft: v }); }
saveReply() {
const id = this.state.currentInquiryId;
const reply = (this.state.inqReplyDraft || '').trim();
const inquiries = this.state.inquiries.map((q) => q.id === id ? { ...q, reply, status: reply ? '완료' : '대기', repliedAt: reply ? this.today() : '' } : q);
this.setState({ inquiries });
this.persistInq(inquiries);
try { window.alert(reply ? '답변이 등록되었습니다.' : '답변이 비워져 대기 상태로 전환되었습니다.'); } catch (_) {}
}
setInquiryStatus(id, status) { const inquiries = this.state.inquiries.map((q) => q.id === id ? { ...q, status } : q); this.setState({ inquiries }); this.persistInq(inquiries); }
// ---- Admin: company settings ----
persistCompany(c) { try { window.localStorage.setItem(this.COMPANY_KEY, JSON.stringify(c)); } catch (_) {} }
setCompanyField(k) { return (e) => { const v = (e && e.target) ? e.target.value : e; this.setState((s) => ({ companyDraft: { ...s.companyDraft, [k]: v } })); }; }
saveCompany() {
const c = { ...this.companyDefault, ...this.state.company, ...this.state.companyDraft };
if (!c.name || !c.name.trim()) { try { window.alert('회사 이름을 입력해 주세요.'); } catch (_) {} return; }
this.setState({ company: c });
this.persistCompany(c);
try { window.alert('회사 정보가 저장되었습니다. 사이트 전체에 반영됩니다.'); } catch (_) {}
}
onLogoFile(e) {
const file = e && e.target && e.target.files && e.target.files[0];
if (e && e.target) { try { e.target.value = ''; } catch (_) {} }
if (!file || !/^image\//.test(file.type)) return;
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
const img = new Image();
img.onload = () => {
const max = 160;
const scale = Math.min(1, max / Math.max(img.width, img.height));
const w = Math.max(1, Math.round(img.width * scale));
const h = Math.max(1, Math.round(img.height * scale));
const c = document.createElement('canvas'); c.width = w; c.height = h;
let dataUrl = result;
try { c.getContext('2d').drawImage(img, 0, 0, w, h); dataUrl = c.toDataURL('image/png'); } catch (_) {}
this.setState((s) => ({ companyDraft: { ...s.companyDraft, logo: dataUrl } }));
};
img.onerror = () => { this.setState((s) => ({ companyDraft: { ...s.companyDraft, logo: result } })); };
img.src = result;
};
reader.readAsDataURL(file);
}
clearLogo() { this.setState((s) => ({ companyDraft: { ...s.companyDraft, logo: '' } })); }
// ---- Admin: auth ----
onAdminPw(e) { const v = (e && e.target) ? e.target.value : e; this.setState({ adminPw: v, adminPwErr: false }); }
onAdminKey(e) { if (e && e.key === 'Enter') this.tryLogin(); }
tryLogin() { const typedPw = (this.state.adminPw || (document.getElementById('gts-admin-password') && document.getElementById('gts-admin-password').value) || ''); if (typedPw === this.ADMIN_PW) { this.setState({ adminAuthed: true, adminPw: '', adminPwErr: false }); this.toTop(); } else { this.setState({ adminPwErr: true }); } }
logout() { this.setState({ adminAuthed: false, adminPw: '', adminPwErr: false }); this.toTop(); }
renderVals() {
const s = this.state;
const view = s.view;
const mkImg = (src, style) => src ? React.createElement('img', { src, alt: '', style }) : null;
const mapPost = (p) => { const imgs = Array.isArray(p.images) ? p.images : []; const first = imgs[0] || ''; return { ...p, images: imgs, image: first, hasImage: imgs.length > 0, noImage: imgs.length === 0, thumb: mkImg(first, { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }), open: () => this.openPost(p.id) }; };
const cats = this.cats.map((c) => ({ name: c, on: c === s.cat, off: c !== s.cat, set: () => this.setCat(c) }));
const all = s.posts;
const filtered = s.cat === '전체' ? all : all.filter((p) => p.cat === s.cat);
const pageSize = 6;
const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
const page = Math.min(s.page, pageCount);
const posts = filtered.slice((page - 1) * pageSize, page * pageSize).map(mapPost);
const navs = [
{ name: '홈', on: view === 'home', off: view !== 'home', go: () => this.go('home') },
{ name: '블로그', on: view === 'blog', off: view !== 'blog', go: () => this.go('blog') },
{ name: '1:1 문의', on: view === 'contact', off: view !== 'contact', go: () => this.go('contact') },
];
// detail
const cp = all.find((p) => p.id === s.currentPostId) || all[0] || {};
const dParas = (cp.body || '').split(/\n\s*\n/).map((t) => t.trim()).filter(Boolean);
const related = all.filter((p) => p.id !== cp.id).slice(0, 3).map(mapPost);
const galArr = Array.isArray(cp.images) ? cp.images : [];
const galCount = galArr.length;
this._galCount = galCount;
const galIndex = galCount ? Math.min(s.galleryIndex, galCount - 1) : 0;
const f = s.form;
const ed = s.editing;
const cd = { ...this.companyDefault, ...s.company, ...s.companyDraft };
const cq = s.inquiries.find((q) => q.id === s.currentInquiryId) || {};
const logoMark = (sz, svgSize) => s.company.logo
? React.createElement('span', { style: { width: sz, height: sz, borderRadius: 'var(--radius-md)', overflow: 'hidden', background: '#fff', border: '1px solid var(--color-border)', display: 'grid', placeItems: 'center', flex: 'none' } }, React.createElement('img', { src: s.company.logo, alt: '', style: { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' } }))
: React.createElement('span', { style: { width: sz, height: sz, borderRadius: 'var(--radius-md)', background: 'var(--color-primary)', display: 'grid', placeItems: 'center', boxShadow: 'var(--shadow-sm)', flex: 'none' }, dangerouslySetInnerHTML: { __html: '<svg width="' + svgSize + '" height="' + svgSize + '" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6.01 18H6.01"/><path d="M10.01 18H10.01"/><path d="M15 10v4"/><path d="M17.84 7.17a4 4 0 0 0-5.66 0"/><path d="M20.66 4.34a8 8 0 0 0-11.31 0"/></svg>' } });
return {
isSite: view !== 'admin',
isHome: view === 'home', isBlog: view === 'blog', isContact: view === 'contact', isPost: view === 'post', isAdmin: view === 'admin',
goHome: () => this.go('home'), goBlog: () => this.go('blog'), goContact: () => this.go('contact'), goAdmin: () => this.go('admin'),
navs, cats,
posts, latest: all.slice(0, 3).map(mapPost),
page, pageCount, filteredTotal: filtered.length, pageSize,
onPage: (p) => this.setState({ page: p }),
showFab: (this.props.showFloatingCall ?? true) && view !== 'admin',
showProcess: this.props.showProcess ?? true,
showBlogPreview: this.props.showBlogPreview ?? true,
typeOptions: this.typeOptions,
submitted: s.submitted, notSubmitted: !s.submitted, ticket: s.ticket,
fName: f.name, fPhone: f.phone, fEmail: f.email, fType: f.type, fTitle: f.title, fBody: f.body, fPw: f.pw, fAgree: f.agree,
onName: this.field('name'), onPhone: this.field('phone'), onEmail: this.field('email'),
onType: this.field('type'), onTitle: this.field('title'), onBody: this.field('body'),
onPw: this.field('pw'), onAgree: this.field('agree'),
submit: () => this.submit(), resetForm: () => this.resetForm(),
// detail
dTitle: cp.title, dCat: cp.cat, dVariant: cp.variant || 'neutral', dDate: cp.date, dAuthor: cp.author || 'GTS 정보통신', dParas, related,
galNone: galCount === 0, galSingle: galCount === 1, galHasMany: galCount > 1,
galFirst: galArr[0] || '', galFirstNode: mkImg(galArr[0] || '', { display: 'block', width: '100%', height: '100%', objectFit: 'cover' }),
galImages: galArr.map((src, idx) => ({ src, idx, node: mkImg(src, { display: 'block', width: '100%', height: '100%', objectFit: 'cover', pointerEvents: 'none' }) })),
galCounter: (galIndex + 1) + ' / ' + galCount,
galTrackStyle: { display: 'flex', height: '100%', transition: 'transform .35s cubic-bezier(0.4,0,0.2,1)', transform: 'translateX(' + (-galIndex * 100) + '%)' },
dots: galArr.map((_, idx) => ({ on: idx === galIndex, off: idx !== galIndex, go: () => this.galGo(idx) })),
galPrev: () => this.galPrev(), galNext: () => this.galNext(),
onGalPointerDown: (e) => this.onGalPointerDown(e), onGalPointerUp: (e) => this.onGalPointerUp(e),
// admin
adminIsPosts: s.adminTab === 'posts', adminNotPosts: s.adminTab !== 'posts',
adminIsInquiries: s.adminTab === 'inquiries', adminNotInquiries: s.adminTab !== 'inquiries',
tabPosts: () => this.setState({ adminTab: 'posts', adminMode: 'list' }),
tabInquiries: () => this.setState({ adminTab: 'inquiries', adminInqMode: 'list' }),
adminPostsList: s.adminTab === 'posts' && s.adminMode === 'list',
adminPostsEdit: s.adminTab === 'posts' && s.adminMode === 'edit',
postCount: all.length,
adminPosts: all.map((p) => ({ ...p, edit: () => this.editPost(p.id), del: () => this.deletePost(p.id) })),
newPost: () => this.newPost(),
editingIsNew: ed.id == null, editingIsEdit: ed.id != null,
eTitle: ed.title, eCat: ed.cat, eExcerpt: ed.excerpt, eBody: ed.body,
eImages: (ed.images || []).map((src, idx) => ({ src, num: idx + 1, node: mkImg(src, { display: 'block', width: '100%', height: '100%', objectFit: 'cover' }), remove: () => this.removeImage(idx) })),
onETitle: this.editField('title'), onECat: this.editField('cat'), onEExcerpt: this.editField('excerpt'), onEBody: this.editField('body'),
categoryOptions: this.categoryOptions,
saveEditing: () => this.saveEditing(), cancelEdit: () => this.cancelEdit(), onImageFile: (e) => this.onImageFile(e),
inquiryCount: s.inquiries.length, noInquiries: s.inquiries.length === 0,
adminInquiries: s.inquiries.map((q) => ({
...q,
statusVariant: q.status === '완료' ? 'success' : 'warning',
contact: q.name + ' · ' + q.phone + (q.email ? ' · ' + q.email : ''),
toggleLabel: q.status === '대기' ? '답변 완료로 변경' : '대기로 변경',
toggle: () => this.toggleInquiry(q.id),
del: () => this.deleteInquiry(q.id),
open: () => this.openInquiry(q.id),
hasReply: !!q.reply,
})),
// company (applied site-wide)
coName: s.company.name, coSubtitle: s.company.subtitle, coLegal: s.company.legalName, coPhone: s.company.phoneMain, coSub: s.company.phoneSub, coFax: s.company.fax, coEmail: s.company.email, coAddr1: s.company.addr1, coAddr2: s.company.addr2, coCeo: s.company.ceo, coBizno: s.company.bizno, coTagline: s.company.tagline, coLogo: s.company.logo, coHasLogo: !!s.company.logo, coNoLogo: !s.company.logo,
coBrandA: (s.company.name || '').split(' ')[0] || '', coBrandB: (s.company.name || '').split(' ').slice(1).join(' '),
headerLogo: logoMark('38px', 21), footerLogo: logoMark('34px', 19), cdLogoImg: mkImg(cd.logo, { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }),
// admin: settings tab
adminIsSettings: s.adminTab === 'settings', adminNotSettings: s.adminTab !== 'settings',
tabSettings: () => this.setState({ adminTab: 'settings', companyDraft: { ...this.state.company } }),
cdName: cd.name, cdSubtitle: cd.subtitle, cdLegal: cd.legalName, cdPhone: cd.phoneMain, cdSub: cd.phoneSub, cdFax: cd.fax, cdEmail: cd.email, cdAddr1: cd.addr1, cdAddr2: cd.addr2, cdCeo: cd.ceo, cdBizno: cd.bizno, cdTagline: cd.tagline, cdLogo: cd.logo, cdHasLogo: !!cd.logo, cdNoLogo: !cd.logo,
onCdName: this.setCompanyField('name'), onCdSubtitle: this.setCompanyField('subtitle'), onCdLegal: this.setCompanyField('legalName'), onCdPhone: this.setCompanyField('phoneMain'), onCdSub: this.setCompanyField('phoneSub'), onCdFax: this.setCompanyField('fax'), onCdEmail: this.setCompanyField('email'), onCdAddr1: this.setCompanyField('addr1'), onCdAddr2: this.setCompanyField('addr2'), onCdCeo: this.setCompanyField('ceo'), onCdBizno: this.setCompanyField('bizno'), onCdTagline: this.setCompanyField('tagline'),
onLogoFile: (e) => this.onLogoFile(e), clearLogo: () => this.clearLogo(), saveCompany: () => this.saveCompany(),
// admin: inquiry detail / respond
adminInqList: s.adminTab === 'inquiries' && s.adminInqMode === 'list',
adminInqDetail: s.adminTab === 'inquiries' && s.adminInqMode === 'detail',
cqTitle: cq.title, cqBody: cq.body, cqType: cq.type, cqTicket: cq.ticket, cqDate: cq.date, cqName: cq.name, cqPhone: cq.phone, cqEmail: cq.email || '', cqHasEmail: !!cq.email, cqStatus: cq.status, cqStatusVariant: cq.status === '완료' ? 'success' : 'warning', cqHasReply: !!cq.reply, cqRepliedAt: cq.repliedAt || '',
inqReplyDraft: s.inqReplyDraft, onInqReply: (e) => this.onInqReply(e), saveReply: () => this.saveReply(), closeInquiry: () => this.closeInquiry(), setInqWaiting: () => this.setInquiryStatus(s.currentInquiryId, '대기'),
// admin: auth / login
adminLocked: view === 'admin' && !s.adminAuthed, adminUnlocked: view === 'admin' && s.adminAuthed,
adminPw: s.adminPw, onAdminPw: (e) => this.onAdminPw(e), onAdminKey: (e) => this.onAdminKey(e), tryLogin: () => this.tryLogin(), adminPwError: s.adminPwErr, logout: () => this.logout(),
};
}
}
</script>
</body></html>