fix: 모바일 앱 화면 섹션 목업 개선

- phone-screen CSS 클래스 누락 버그 수정
- phone-frame border/overflow:hidden 추가
- MobileScreenList: 아이콘+텍스트 → 실제 앱 화면 UI 목업으로 재설계
  (상태바·앱헤더·대시보드/리스트/차트/폼 레이아웃·하단버튼)
- 화면 이름 키워드로 layout 자동 선택(dashboard/chart/list/form)
This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-06-18 00:15:14 +09:00
parent 248d927a00
commit 1f06a27aad
3 changed files with 288 additions and 36 deletions

View File

@ -1,14 +1,45 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { SolutionIcon } from './SolutionIcon';
const TYPE_LABEL = { const TYPE_LABEL = {
customer: '사용자', customer: '사용자',
admin: '관리자', admin: '관리자',
field: '현장', field: '현장',
}; };
function guessLayout(name) {
const n = name;
if (/대시보드|현황|KPI|분석|통계|보드|히트맵|스코어/.test(n))
return 'dashboard';
if (/목록|리스트|조회|紐⑸줉/.test(n))
return 'list';
if (/차트|그래프|추이|예측|리포트|ROI/.test(n))
return 'chart';
if (/등록|작성|입력|설정|수정|관리/.test(n))
return 'form';
if (/칸반|보드|타임라인|스프린트/.test(n))
return 'kanban';
return 'list';
}
const BAR_HEIGHTS = [60, 85, 45, 70, 55, 90, 40, 75];
function PhoneMockup({ screen, color }) {
const layout = guessLayout(screen.name);
const alpha = (a) => `${color}${Math.round(a * 255).toString(16).padStart(2, '0')}`;
const headerBg = `linear-gradient(135deg, ${alpha(0.9)}, ${alpha(0.6)})`;
return (_jsxs("div", { className: "phone-card", children: [_jsxs("div", { className: "phone-frame", style: { borderColor: alpha(0.4) }, children: [_jsxs("div", { className: "phone-status", children: [_jsx("span", { children: "9:41" }), _jsx("div", { className: "phone-status-icons", children: _jsxs("svg", { width: "14", height: "8", viewBox: "0 0 14 8", fill: "none", children: [_jsx("rect", { x: "0.5", y: "0.5", width: "11", height: "7", rx: "1.5", stroke: "rgba(255,255,255,0.5)", strokeWidth: "1" }), _jsx("rect", { x: "1.5", y: "1.5", width: "7", height: "5", rx: "0.5", fill: color }), _jsx("path", { d: "M12 2.5v3", stroke: "rgba(255,255,255,0.5)", strokeWidth: "1", strokeLinecap: "round" })] }) })] }), _jsxs("div", { className: "phone-header", style: { background: headerBg }, children: [_jsx("div", { className: "phone-header-icon", style: { background: alpha(0.3) }, children: _jsxs("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2.5", strokeLinecap: "round", children: [_jsx("rect", { x: "5", y: "2", width: "14", height: "20", rx: "2" }), _jsx("line", { x1: "12", y1: "18", x2: "12.01", y2: "18" })] }) }), _jsx("span", { className: "phone-header-title", children: screen.name }), _jsx("div", { className: "phone-header-action", children: _jsxs("svg", { width: "8", height: "8", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2.5", strokeLinecap: "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "1" }), _jsx("circle", { cx: "19", cy: "12", r: "1" }), _jsx("circle", { cx: "5", cy: "12", r: "1" })] }) })] }), _jsxs("div", { className: "phone-body", children: [layout === 'dashboard' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "phone-stat-row", children: [_jsxs("div", { className: "phone-stat", children: [_jsx("div", { className: "phone-stat-val", style: { color }, children: "24" }), _jsx("div", { className: "phone-stat-lbl", children: "\uC9C4\uD589\uC911" })] }), _jsxs("div", { className: "phone-stat", children: [_jsx("div", { className: "phone-stat-val", style: { color: '#10b981' }, children: "98%" }), _jsx("div", { className: "phone-stat-lbl", children: "\uC644\uB8CC\uC728" })] })] }), _jsxs("div", { className: "phone-stat-row", children: [_jsxs("div", { className: "phone-stat", children: [_jsx("div", { className: "phone-stat-val", style: { color: '#f59e0b' }, children: "3" }), _jsx("div", { className: "phone-stat-lbl", children: "\uACBD\uBCF4" })] }), _jsxs("div", { className: "phone-stat", children: [_jsx("div", { className: "phone-stat-val", style: { color: '#6366f1' }, children: "142" }), _jsx("div", { className: "phone-stat-lbl", children: "\uC11C\uBC84" })] })] }), _jsxs("div", { className: "phone-list-item", children: [_jsx("div", { className: "phone-list-dot", style: { background: color } }), _jsxs("div", { className: "phone-list-lines", children: [_jsx("div", { className: "phone-list-line", style: { width: '75%' } }), _jsx("div", { className: "phone-list-line", style: { width: '50%', opacity: 0.5 } })] })] })] })), layout === 'chart' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "phone-stat-row", children: [_jsxs("div", { className: "phone-stat", children: [_jsx("div", { className: "phone-stat-val", style: { color }, children: "87%" }), _jsx("div", { className: "phone-stat-lbl", children: "\uB2EC\uC131\uB960" })] }), _jsxs("div", { className: "phone-stat", children: [_jsx("div", { className: "phone-stat-val", style: { color: '#10b981' }, children: "\u219112%" }), _jsx("div", { className: "phone-stat-lbl", children: "\uC804\uC6D4\uBE44" })] })] }), _jsx("div", { className: "phone-chart", children: BAR_HEIGHTS.map((h, i) => (_jsx("div", { className: "phone-bar", style: {
height: `${h}%`,
background: i === 5 ? color : alpha(0.35 + (i % 3) * 0.1),
} }, i))) }), _jsxs("div", { className: "phone-list-item", children: [_jsx("div", { className: "phone-list-dot", style: { background: '#10b981' } }), _jsx("div", { className: "phone-list-lines", children: _jsx("div", { className: "phone-list-line", style: { width: '80%' } }) }), _jsx("div", { className: "phone-list-badge", style: { background: alpha(0.2), color }, children: "AI" })] })] })), (layout === 'list' || layout === 'kanban') && (_jsx(_Fragment, { children: [
{ dot: color, w1: '80%', w2: '55%', badge: '완료', bc: '#10b981' },
{ dot: '#f59e0b', w1: '65%', w2: '40%', badge: '진행', bc: '#f59e0b' },
{ dot: '#6366f1', w1: '90%', w2: '70%', badge: '대기', bc: '#6366f1' },
].map((row, i) => (_jsxs("div", { className: "phone-list-item", children: [_jsx("div", { className: "phone-list-dot", style: { background: row.dot } }), _jsxs("div", { className: "phone-list-lines", children: [_jsx("div", { className: "phone-list-line", style: { width: row.w1 } }), _jsx("div", { className: "phone-list-line", style: { width: row.w2, opacity: 0.5 } })] }), _jsx("div", { className: "phone-list-badge", style: { background: `${row.bc}22`, color: row.bc }, children: row.badge })] }, i))) })), layout === 'form' && (_jsx(_Fragment, { children: [70, 100, 85].map((w, i) => (_jsxs("div", { style: {
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 6, padding: '5px 8px',
}, children: [_jsx("div", { style: { height: 4, width: '35%', background: alpha(0.5), borderRadius: 2, marginBottom: 4 } }), _jsx("div", { style: { height: 5, width: `${w}%`, background: 'rgba(255,255,255,0.2)', borderRadius: 2 } })] }, i))) }))] }), _jsxs("div", { className: "phone-footer", children: [_jsx("div", { className: "phone-btn", style: { background: alpha(0.15), border: `1px solid ${alpha(0.3)}`, color }, children: "\uCDE8\uC18C" }), _jsx("div", { className: "phone-btn", style: { background: headerBg }, children: "\uD655\uC778" })] })] }), _jsxs("div", { className: "phone-label", children: [_jsx("span", { className: "phone-name", children: screen.name }), _jsx("span", { className: "phone-type", style: { color, borderColor: alpha(0.4) }, children: TYPE_LABEL[screen.type] || screen.type })] })] }));
}
export const MobileScreenList = ({ screens, color }) => { export const MobileScreenList = ({ screens, color }) => {
if (!screens.length) { if (!screens.length) {
return _jsx("p", { className: "mobile-empty", children: "\uBAA8\uBC14\uC77C \uC571 \uD654\uBA74\uC740 \uC900\uBE44 \uC911\uC785\uB2C8\uB2E4." }); return _jsx("p", { className: "mobile-empty", children: "\uBAA8\uBC14\uC77C \uC571 \uD654\uBA74\uC740 \uC900\uBE44 \uC911\uC785\uB2C8\uB2E4." });
} }
return (_jsx("div", { className: "mobile-screen-list", children: screens.map((s) => (_jsxs("div", { className: "phone-card", children: [_jsxs("div", { className: "phone-frame", style: { borderColor: `${color}55` }, children: [_jsx("div", { className: "phone-notch" }), _jsx("div", { className: "phone-screen", style: { background: `linear-gradient(160deg, ${color}22, #0f172a 70%)` }, children: _jsxs("div", { className: "phone-screen-inner", children: [_jsx("span", { className: "phone-icon", style: { color }, children: _jsx(SolutionIcon, { slug: "mobile", size: 40, color: color }) }), _jsx("span", { className: "phone-screen-name", children: s.name })] }) })] }), _jsxs("div", { className: "phone-label", children: [_jsx("span", { className: "phone-name", children: s.name }), _jsx("span", { className: "phone-type", style: { color, borderColor: `${color}55` }, children: TYPE_LABEL[s.type] || s.type })] })] }, s.name))) })); return (_jsx("div", { className: "mobile-screen-list", children: screens.map((s) => (_jsx(PhoneMockup, { screen: s, color: color }, s.name))) }));
}; };
export default MobileScreenList; export default MobileScreenList;

View File

@ -1,8 +1,12 @@
import React from 'react'; import React from 'react';
import { SolutionIcon } from './SolutionIcon';
interface Screen {
name: string;
type: string;
}
interface MobileScreenListProps { interface MobileScreenListProps {
screens: { name: string; type: string }[]; screens: Screen[];
color: string; color: string;
} }
@ -12,6 +16,184 @@ const TYPE_LABEL: Record<string, string> = {
field: '현장', field: '현장',
}; };
/* --- 화면 이름 키워드로 UI 레이아웃 결정 --- */
type Layout = 'dashboard' | 'list' | 'chart' | 'form' | 'kanban';
function guessLayout(name: string): Layout {
const n = name;
if (/대시보드|현황|KPI|분석|통계|보드|히트맵|스코어/.test(n)) return 'dashboard';
if (/목록|리스트|조회|紐⑸줉/.test(n)) return 'list';
if (/차트|그래프|추이|예측|리포트|ROI/.test(n)) return 'chart';
if (/등록|작성|입력|설정|수정|관리/.test(n)) return 'form';
if (/칸반|보드|타임라인|스프린트/.test(n)) return 'kanban';
return 'list';
}
const BAR_HEIGHTS = [60, 85, 45, 70, 55, 90, 40, 75];
function PhoneMockup({ screen, color }: { screen: Screen; color: string }) {
const layout = guessLayout(screen.name);
const alpha = (a: number) => `${color}${Math.round(a * 255).toString(16).padStart(2, '0')}`;
const headerBg = `linear-gradient(135deg, ${alpha(0.9)}, ${alpha(0.6)})`;
return (
<div className="phone-card">
<div
className="phone-frame"
style={{ borderColor: alpha(0.4) }}
>
{/* 상태바 */}
<div className="phone-status">
<span>9:41</span>
<div className="phone-status-icons">
{/* 배터리 아이콘 (SVG) */}
<svg width="14" height="8" viewBox="0 0 14 8" fill="none">
<rect x="0.5" y="0.5" width="11" height="7" rx="1.5" stroke="rgba(255,255,255,0.5)" strokeWidth="1"/>
<rect x="1.5" y="1.5" width="7" height="5" rx="0.5" fill={color} />
<path d="M12 2.5v3" stroke="rgba(255,255,255,0.5)" strokeWidth="1" strokeLinecap="round"/>
</svg>
</div>
</div>
{/* 앱 헤더 */}
<div className="phone-header" style={{ background: headerBg }}>
<div className="phone-header-icon" style={{ background: alpha(0.3) }}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round">
<rect x="5" y="2" width="14" height="20" rx="2"/>
<line x1="12" y1="18" x2="12.01" y2="18"/>
</svg>
</div>
<span className="phone-header-title">{screen.name}</span>
<div className="phone-header-action">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round">
<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>
</svg>
</div>
</div>
{/* 본문 */}
<div className="phone-body">
{layout === 'dashboard' && (
<>
<div className="phone-stat-row">
<div className="phone-stat">
<div className="phone-stat-val" style={{ color }}>24</div>
<div className="phone-stat-lbl"></div>
</div>
<div className="phone-stat">
<div className="phone-stat-val" style={{ color: '#10b981' }}>98%</div>
<div className="phone-stat-lbl"></div>
</div>
</div>
<div className="phone-stat-row">
<div className="phone-stat">
<div className="phone-stat-val" style={{ color: '#f59e0b' }}>3</div>
<div className="phone-stat-lbl"></div>
</div>
<div className="phone-stat">
<div className="phone-stat-val" style={{ color: '#6366f1' }}>142</div>
<div className="phone-stat-lbl"></div>
</div>
</div>
<div className="phone-list-item">
<div className="phone-list-dot" style={{ background: color }} />
<div className="phone-list-lines">
<div className="phone-list-line" style={{ width: '75%' }} />
<div className="phone-list-line" style={{ width: '50%', opacity: 0.5 }} />
</div>
</div>
</>
)}
{layout === 'chart' && (
<>
<div className="phone-stat-row">
<div className="phone-stat">
<div className="phone-stat-val" style={{ color }}>87%</div>
<div className="phone-stat-lbl"></div>
</div>
<div className="phone-stat">
<div className="phone-stat-val" style={{ color: '#10b981' }}>12%</div>
<div className="phone-stat-lbl"></div>
</div>
</div>
<div className="phone-chart">
{BAR_HEIGHTS.map((h, i) => (
<div
key={i}
className="phone-bar"
style={{
height: `${h}%`,
background: i === 5 ? color : alpha(0.35 + (i % 3) * 0.1),
}}
/>
))}
</div>
<div className="phone-list-item">
<div className="phone-list-dot" style={{ background: '#10b981' }} />
<div className="phone-list-lines">
<div className="phone-list-line" style={{ width: '80%' }} />
</div>
<div className="phone-list-badge" style={{ background: alpha(0.2), color }}>AI</div>
</div>
</>
)}
{(layout === 'list' || layout === 'kanban') && (
<>
{[
{ dot: color, w1: '80%', w2: '55%', badge: '완료', bc: '#10b981' },
{ dot: '#f59e0b', w1: '65%', w2: '40%', badge: '진행', bc: '#f59e0b' },
{ dot: '#6366f1', w1: '90%', w2: '70%', badge: '대기', bc: '#6366f1' },
].map((row, i) => (
<div className="phone-list-item" key={i}>
<div className="phone-list-dot" style={{ background: row.dot }} />
<div className="phone-list-lines">
<div className="phone-list-line" style={{ width: row.w1 }} />
<div className="phone-list-line" style={{ width: row.w2, opacity: 0.5 }} />
</div>
<div className="phone-list-badge" style={{ background: `${row.bc}22`, color: row.bc }}>
{row.badge}
</div>
</div>
))}
</>
)}
{layout === 'form' && (
<>
{[70, 100, 85].map((w, i) => (
<div key={i} style={{
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 6, padding: '5px 8px',
}}>
<div style={{ height: 4, width: '35%', background: alpha(0.5), borderRadius: 2, marginBottom: 4 }} />
<div style={{ height: 5, width: `${w}%`, background: 'rgba(255,255,255,0.2)', borderRadius: 2 }} />
</div>
))}
</>
)}
</div>
{/* 하단 버튼 */}
<div className="phone-footer">
<div className="phone-btn" style={{ background: alpha(0.15), border: `1px solid ${alpha(0.3)}`, color }}>
</div>
<div className="phone-btn" style={{ background: headerBg }}>
</div>
</div>
</div>
<div className="phone-label">
<span className="phone-name">{screen.name}</span>
<span className="phone-type" style={{ color, borderColor: alpha(0.4) }}>
{TYPE_LABEL[screen.type] || screen.type}
</span>
</div>
</div>
);
}
export const MobileScreenList: React.FC<MobileScreenListProps> = ({ screens, color }) => { export const MobileScreenList: React.FC<MobileScreenListProps> = ({ screens, color }) => {
if (!screens.length) { if (!screens.length) {
return <p className="mobile-empty"> .</p>; return <p className="mobile-empty"> .</p>;
@ -19,25 +201,7 @@ export const MobileScreenList: React.FC<MobileScreenListProps> = ({ screens, col
return ( return (
<div className="mobile-screen-list"> <div className="mobile-screen-list">
{screens.map((s) => ( {screens.map((s) => (
<div className="phone-card" key={s.name}> <PhoneMockup key={s.name} screen={s} color={color} />
<div className="phone-frame" style={{ borderColor: `${color}55` }}>
<div className="phone-notch" />
<div className="phone-screen" style={{ background: `linear-gradient(160deg, ${color}22, #0f172a 70%)` }}>
<div className="phone-screen-inner">
<span className="phone-icon" style={{ color }}>
<SolutionIcon slug="mobile" size={40} color={color} />
</span>
<span className="phone-screen-name">{s.name}</span>
</div>
</div>
</div>
<div className="phone-label">
<span className="phone-name">{s.name}</span>
<span className="phone-type" style={{ color, borderColor: `${color}55` }}>
{TYPE_LABEL[s.type] || s.type}
</span>
</div>
</div>
))} ))}
</div> </div>
); );

View File

@ -187,20 +187,77 @@
.step-guide-body p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; } .step-guide-body p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; }
/* ============ Mobile Screens ============ */ /* ============ Mobile Screens ============ */
.mobile-screen-list { display: flex; gap: 24px; flex-wrap: wrap; } .mobile-screen-list { display: flex; gap: 20px; flex-wrap: wrap; }
.mobile-empty { font-size: 14px; color: var(--text-muted); } .mobile-empty { font-size: 14px; color: var(--text-muted); }
.phone-card { display: flex; flex-direction: column; align-items: center; gap: 12px; } .phone-card { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.phone-frame { width: 150px; height: 300px; border-radius: 26px; } .phone-frame {
.phone-notch { width: 64px; height: 18px; } width: 148px; height: 296px; border-radius: 24px;
.phone-screen-inner { border: 2px solid;
width: 100%; height: 100%; display: flex; flex-direction: column; overflow: hidden;
align-items: center; justify-content: center; gap: 14px; padding: 16px; background: #0a0f1e;
display: flex; flex-direction: column;
box-shadow: 0 12px 40px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.06);
position: relative;
} }
.phone-icon { display: inline-flex; } .phone-status {
.phone-screen-name { font-size: 13px; font-weight: 600; color: var(--text-secondary); text-align: center; } display: flex; align-items: center; justify-content: space-between;
.phone-label { display: flex; flex-direction: column; align-items: center; gap: 6px; } padding: 6px 12px 0; font-size: 9px; font-weight: 700;
.phone-name { font-size: 13px; font-weight: 600; } color: rgba(255,255,255,0.5); flex-shrink: 0;
.phone-type { font-size: 11px; font-weight: 600; border: 1px solid; border-radius: 20px; padding: 1px 8px; } }
.phone-status-icons { display: flex; align-items: center; gap: 3px; }
.phone-header {
padding: 8px 12px 10px; flex-shrink: 0;
display: flex; align-items: center; gap: 8px;
}
.phone-header-icon {
width: 20px; height: 20px; border-radius: 6px;
display: grid; place-items: center; flex-shrink: 0;
}
.phone-header-title {
font-size: 12px; font-weight: 700; color: #fff; flex: 1;
}
.phone-header-action {
width: 18px; height: 18px; border-radius: 50%;
background: rgba(255,255,255,0.12);
display: grid; place-items: center;
}
.phone-body { flex: 1; overflow: hidden; padding: 0 10px 10px; display: flex; flex-direction: column; gap: 6px; }
.phone-stat-row { display: flex; gap: 5px; }
.phone-stat {
flex: 1; border-radius: 8px; background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
padding: 7px 8px;
}
.phone-stat-val { font-size: 13px; font-weight: 800; color: #fff; line-height: 1; }
.phone-stat-lbl { font-size: 8px; color: rgba(255,255,255,0.4); margin-top: 2px; }
.phone-list-item {
display: flex; align-items: center; gap: 7px;
padding: 7px 8px; border-radius: 8px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.06);
}
.phone-list-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.phone-list-lines { flex: 1; display: flex; flex-direction: column; gap: 3px; }
.phone-list-line { height: 5px; border-radius: 3px; background: rgba(255,255,255,0.15); }
.phone-list-badge {
font-size: 8px; font-weight: 700; border-radius: 4px;
padding: 1px 5px; flex-shrink: 0;
}
.phone-chart { display: flex; align-items: flex-end; gap: 4px; height: 40px; padding: 0 2px; }
.phone-bar { flex: 1; border-radius: 3px 3px 0 0; }
.phone-footer {
flex-shrink: 0; padding: 8px 10px;
border-top: 1px solid rgba(255,255,255,0.06);
display: flex; gap: 5px;
}
.phone-btn {
flex: 1; height: 26px; border-radius: 7px;
display: grid; place-items: center;
font-size: 9px; font-weight: 700; color: #fff;
}
.phone-label { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.phone-name { font-size: 12px; font-weight: 600; text-align: center; color: var(--text-primary); }
.phone-type { font-size: 10px; font-weight: 600; border: 1px solid; border-radius: 20px; padding: 1px 8px; }
/* ============ Home Footer ============ */ /* ============ Home Footer ============ */
.home-footer { border-top: 1px solid var(--border); padding: 40px 0; background: var(--bg-surface); } .home-footer { border-top: 1px solid var(--border); padding: 40px 0; background: var(--bg-surface); }