Skip to content

Commit baef0f2

Browse files
committed
UI 구현
1 parent 829d419 commit baef0f2

13 files changed

Lines changed: 522 additions & 226 deletions

AGENTS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,24 @@
9393
- Update docs when structure, build rules, or repository workflow changes.
9494
- When changing contribution workflow files, keep `CONTRIBUTING.md` and `.github/` files aligned.
9595

96+
## UI Design Guidelines
97+
- Prefer a calm, minimal desktop UI: light neutral surfaces, restrained accent color, and clear visual hierarchy over heavy borders or high-contrast chrome.
98+
- Centralize reusable Qt widget styling in shared application-layer helpers instead of scattering large inline `setStyleSheet()` blocks across many widgets.
99+
- Reuse a small set of design tokens consistently:
100+
- spacing around 8 / 12 / 16 / 24 / 32 px
101+
- rounded corners around 12-20 px for cards/panels
102+
- one primary accent color with muted supporting grays
103+
- Use typography roles consistently:
104+
- large page title
105+
- section title
106+
- body text
107+
- caption/meta text
108+
- Primary actions should be visually emphasized; secondary actions should stay quieter but still consistent in size, radius, and padding.
109+
- Prefer card/panel composition with generous padding instead of drawing many separator lines.
110+
- Keep dense review/workspace screens readable by separating navigation, canvas, and inspector panels with spacing and surface contrast rather than thick borders.
111+
- Avoid introducing custom UI styling in `domain` or `engine`; all presentation decisions remain in `src/application/`.
112+
- When adding a new screen or widget, align it with the existing shared UI tokens before introducing a new color, radius, or button treatment.
113+
96114
## Docs
97115
- After any changes to the project, ensure the documents remain consistent.
98116
- Architecture notes: `docs/architecture/프로젝트 구조.md`

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ if (SAFECROWD_BUILD_APP)
173173
src/application/ProjectNavigatorActions.h
174174
src/application/ProjectNavigatorWidget.h
175175
src/application/ProjectPersistence.h
176+
src/application/UiStyle.h
176177
src/application/WorkspaceShell.h
177178
src/application/IssueCardWidget.cpp
178179
src/application/LayoutPreviewWidget.cpp
@@ -184,6 +185,7 @@ if (SAFECROWD_BUILD_APP)
184185
src/application/ProjectNavigatorActions.cpp
185186
src/application/ProjectNavigatorWidget.cpp
186187
src/application/ProjectPersistence.cpp
188+
src/application/UiStyle.cpp
187189
src/application/WorkspaceShell.cpp
188190
)
189191

src/application/IssueCardWidget.cpp

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,12 @@
44
#include <QMouseEvent>
55
#include <QVBoxLayout>
66

7+
#include "application/UiStyle.h"
78
#include "domain/ImportIssue.h"
89

910
namespace safecrowd::application {
1011
namespace {
1112

12-
QFont makeIssueFont(int pointSize) {
13-
QFont font;
14-
font.setPointSize(pointSize);
15-
font.setWeight(QFont::Normal);
16-
return font;
17-
}
18-
1913
QString issueTitle(const safecrowd::domain::ImportIssue& issue) {
2014
return QString::fromUtf8(safecrowd::domain::toString(issue.code));
2115
}
@@ -41,44 +35,40 @@ IssueCardWidget::IssueCardWidget(
4135
selectIssueHandler_(std::move(selectIssueHandler)) {
4236
setFrameShape(QFrame::StyledPanel);
4337
setLineWidth(1);
44-
setStyleSheet(
45-
"IssueCardWidget {"
46-
" border: 1px solid #888888;"
47-
" background: #ffffff;"
48-
"}"
49-
"IssueCardWidget:hover {"
50-
" background: #f5f5f5;"
51-
"}"
52-
"QLabel { border: 0; background: transparent; }");
38+
setStyleSheet(ui::cardStyleSheet());
39+
setCursor(Qt::PointingHandCursor);
5340

5441
auto* layout = new QVBoxLayout(this);
55-
layout->setContentsMargins(10, 8, 10, 9);
56-
layout->setSpacing(6);
42+
layout->setContentsMargins(14, 12, 14, 12);
43+
layout->setSpacing(8);
5744

5845
auto* title = new QLabel(issueTitle(issue_), this);
59-
title->setFont(makeIssueFont(10));
46+
title->setFont(ui::font(ui::FontRole::Body));
6047
title->setWordWrap(true);
6148
layout->addWidget(title);
6249

6350
auto* severity = new QLabel(QString::fromUtf8(safecrowd::domain::toString(issue_.severity)), this);
64-
severity->setFont(makeIssueFont(8));
65-
severity->setStyleSheet("QLabel { color: #a33a24; }");
51+
severity->setFont(ui::font(ui::FontRole::Caption));
52+
const auto severityColor = issue_.blocksSimulation()
53+
? QColor("#b54708")
54+
: (issue_.severity == safecrowd::domain::ImportIssueSeverity::Warning ? QColor("#c27100") : QColor("#1f5fae"));
55+
severity->setStyleSheet(ui::severityTextStyleSheet(severityColor));
6656
layout->addWidget(severity);
6757

6858
if (!issue_.message.empty()) {
6959
auto* message = new QLabel(QString::fromStdString(issue_.message), this);
70-
message->setFont(makeIssueFont(9));
60+
message->setFont(ui::font(ui::FontRole::Body));
7161
message->setWordWrap(true);
72-
message->setStyleSheet("QLabel { color: #333333; }");
62+
message->setStyleSheet(ui::mutedTextStyleSheet());
7363
layout->addWidget(message);
7464
}
7565

7666
const auto targetText = compactTarget(issue_);
7767
if (!targetText.isEmpty()) {
7868
auto* target = new QLabel(targetText, this);
79-
target->setFont(makeIssueFont(8));
69+
target->setFont(ui::font(ui::FontRole::Caption));
8070
target->setWordWrap(true);
81-
target->setStyleSheet("QLabel { color: #555555; }");
71+
target->setStyleSheet(ui::subtleTextStyleSheet());
8272
layout->addWidget(target);
8373
}
8474
}

src/application/LayoutPreviewWidget.cpp

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
namespace safecrowd::application {
1414
namespace {
1515

16+
QRectF previewViewport(const QRect& widgetRect) {
17+
return QRectF(widgetRect).adjusted(20, 20, -20, -20);
18+
}
19+
1620
struct Bounds2D {
1721
double minX{std::numeric_limits<double>::max()};
1822
double minY{std::numeric_limits<double>::max()};
@@ -277,7 +281,7 @@ void LayoutPreviewWidget::focusIssueTarget(const QString& targetId) {
277281
includeMatchingGeometryBounds(importResult_, targetId, targetBounds);
278282
const auto worldBounds = collectBounds(importResult_);
279283
if (targetBounds.valid() && worldBounds.has_value()) {
280-
const QRectF viewport = rect().adjusted(width() / 12, height() / 12, -width() / 12, -height() / 12);
284+
const QRectF viewport = previewViewport(rect());
281285
const auto targetWidth = std::max(targetBounds.maxX - targetBounds.minX, 1.0);
282286
const auto targetHeight = std::max(targetBounds.maxY - targetBounds.minY, 1.0);
283287
const auto targetZoom = 0.55 * std::min(viewport.width() / targetWidth, viewport.height() / targetHeight)
@@ -344,21 +348,29 @@ void LayoutPreviewWidget::paintEvent(QPaintEvent* event) {
344348

345349
QPainter painter(this);
346350
painter.setRenderHint(QPainter::Antialiasing);
347-
painter.fillRect(rect(), Qt::white);
351+
painter.fillRect(rect(), QColor(250, 252, 255));
348352

349353
const auto bounds = collectBounds(importResult_);
350354
if (!bounds.has_value()) {
351355
painter.setPen(QPen(QColor(80, 80, 80), 1));
352-
painter.setFont(QFont("Arial", 14));
356+
painter.setFont(QFont("Segoe UI", 14, QFont::DemiBold));
353357
painter.drawText(rect(), Qt::AlignCenter, "No layout geometry imported");
354358
return;
355359
}
356360

357-
const QRectF viewport = rect().adjusted(width() / 12, height() / 12, -width() / 12, -height() / 12);
361+
const QRectF viewport = previewViewport(rect());
358362
const LayoutTransform transform(*bounds, viewport, zoom_, panOffset_);
359363

364+
painter.setPen(QPen(QColor(238, 243, 248), 1));
365+
for (int x = static_cast<int>(viewport.left()); x < static_cast<int>(viewport.right()); x += 32) {
366+
painter.drawLine(QPointF(x, viewport.top()), QPointF(x, viewport.bottom()));
367+
}
368+
for (int y = static_cast<int>(viewport.top()); y < static_cast<int>(viewport.bottom()); y += 32) {
369+
painter.drawLine(QPointF(viewport.left(), y), QPointF(viewport.right(), y));
370+
}
371+
360372
painter.setPen(Qt::NoPen);
361-
painter.setBrush(QColor(235, 235, 235));
373+
painter.setBrush(QColor(231, 238, 246));
362374
if (importResult_.layout.has_value()) {
363375
for (const auto& zone : importResult_.layout->zones) {
364376
painter.drawPath(polygonPath(zone.area, transform));
@@ -370,39 +382,39 @@ void LayoutPreviewWidget::paintEvent(QPaintEvent* event) {
370382
}
371383

372384
if (importResult_.canonicalGeometry.has_value()) {
373-
painter.setBrush(QColor(230, 160, 70, 150));
374-
painter.setPen(QPen(QColor(180, 118, 38), 1));
385+
painter.setBrush(QColor(222, 145, 70, 120));
386+
painter.setPen(QPen(QColor(177, 110, 39), 1.5));
375387
for (const auto& obstacle : importResult_.canonicalGeometry->obstacles) {
376388
painter.drawPath(polygonPath(obstacle.footprint, transform));
377389
}
378390

379-
painter.setPen(QPen(QColor(70, 70, 70), 3));
391+
painter.setPen(QPen(QColor(82, 92, 105), 2.5));
380392
for (const auto& wall : importResult_.canonicalGeometry->walls) {
381393
drawLine(painter, wall.segment, transform);
382394
}
383395

384-
painter.setPen(QPen(QColor(58, 174, 74), 3, Qt::DashLine));
396+
painter.setPen(QPen(QColor(66, 156, 96), 2.5, Qt::DashLine));
385397
for (const auto& opening : importResult_.canonicalGeometry->openings) {
386398
drawLine(painter, opening.span, transform);
387399
}
388400
}
389401

390402
if (importResult_.layout.has_value()) {
391-
painter.setPen(QPen(QColor(62, 150, 210), 2));
403+
painter.setPen(QPen(QColor(56, 122, 186), 2.5));
392404
for (const auto& connection : importResult_.layout->connections) {
393405
drawLine(painter, connection.centerSpan, transform);
394406
}
395407

396-
painter.setPen(QPen(QColor(70, 70, 70), 3));
408+
painter.setPen(QPen(QColor(82, 92, 105), 2.5));
397409
painter.setBrush(Qt::NoBrush);
398410
for (const auto& barrier : importResult_.layout->barriers) {
399411
drawPolyline(painter, barrier.geometry, transform);
400412
}
401413
}
402414

403415
if (!focusedTargetId_.isEmpty()) {
404-
painter.setBrush(QColor(255, 217, 64, 90));
405-
painter.setPen(QPen(QColor(210, 55, 55), 4));
416+
painter.setBrush(QColor(255, 219, 102, 96));
417+
painter.setPen(QPen(QColor(194, 74, 44), 3.5));
406418

407419
if (importResult_.layout.has_value()) {
408420
for (const auto& zone : importResult_.layout->zones) {
@@ -450,9 +462,10 @@ void LayoutPreviewWidget::paintEvent(QPaintEvent* event) {
450462
}
451463
}
452464

453-
painter.setPen(QPen(QColor(80, 80, 80), 1));
454-
painter.setFont(QFont("Arial", 10));
455-
painter.drawText(rect().adjusted(8, 8, -8, -8), Qt::AlignTop | Qt::AlignRight, QString("Zoom %1%").arg(static_cast<int>(zoom_ * 100.0)));
465+
painter.setPen(QPen(QColor(115, 128, 140), 1));
466+
painter.setFont(QFont("Segoe UI", 9, QFont::Medium));
467+
painter.drawText(viewport.adjusted(0, -10, 0, 0), Qt::AlignTop | Qt::AlignRight, QString("Zoom %1%").arg(static_cast<int>(zoom_ * 100.0)));
468+
painter.drawText(viewport.adjusted(0, -10, 0, 0), Qt::AlignTop | Qt::AlignLeft, "Layout Preview");
456469
}
457470

458471
void LayoutPreviewWidget::wheelEvent(QWheelEvent* event) {

0 commit comments

Comments
 (0)