diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml index a4058fe..512dff3 100644 --- a/.github/workflows/pr-policy.yml +++ b/.github/workflows/pr-policy.yml @@ -73,7 +73,6 @@ jobs: /^\.github\/PULL_REQUEST_TEMPLATE\.md$/, /^\.github\/workflows\/pr-policy\.yml$/, /^CONTRIBUTING\.md$/, - /^README\.md$/, ]; const isDocsPolicyOnlyPr = @@ -81,15 +80,13 @@ jobs: changedFiles.every((file) => docsPolicyOnlyPatterns.some((pattern) => pattern.test(file.filename)) ); - const isDocsAreaOnly = selectedAreas.length === 1 && selectedAreas[0] === "Docs"; - const issuePattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|refs?)\s+#\d+\b/i; const hasIssueReference = issuePattern.test(relatedIssueSection); const usesDocsOnlyException = noIssuePattern.test(relatedIssueSection); if (!hasIssueReference) { - if (!(usesDocsOnlyException && isDocsAreaOnly && isDocsPolicyOnlyPr)) { - errors.push("`## Related Issue` must include an issue reference such as `Closes #12`, unless this is a Docs-only docs/policy PR and the section says `None (docs/policy-only PR)`."); + if (!(usesDocsOnlyException && isDocsPolicyOnlyPr)) { + errors.push("`## Related Issue` must include an issue reference such as `Closes #12`, unless changed files are limited to the docs/policy paths and the section says `None (docs/policy-only PR)`."); } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf49625..5b5264a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,6 +67,7 @@ PR 제목은 아래 형식을 따릅니다. - 아키텍처 규칙 점검 결과 - 빌드/테스트 검증 결과 또는 미실행 사유 - 남은 리스크나 후속 작업 +- docs/policy-only PR이고 변경 경로가 `docs/`, `uml/`, `CONTRIBUTING.md`, PR/issue template, `pr-policy.yml`에만 한정되면 `Related Issue`에는 `None (docs/policy-only PR)`를 사용합니다. ## 아키텍처 체크 diff --git a/docs/process/GitHub Project.md b/docs/process/GitHub Project.md index b98c4f9..8c952cb 100644 --- a/docs/process/GitHub Project.md +++ b/docs/process/GitHub Project.md @@ -10,25 +10,35 @@ - 선행 작업 관계는 GitHub native issue dependency `blocked by`를 사용한다. - Task 제목은 `Task-short title` 형식을 사용하고, 순서는 제목 번호가 아니라 GitHub issue 번호, Sprint, Parent issue, dependency로 관리한다. -## 현재 필드 -- `Title` -- `Status` - - `In Progress` - - `Done` -- `Area` - - `⚙️Engine` - - `🏃Domain` - - `🖥️Application` - - `🔎Analysis` - - `📄Docs` - - 메모: issue form은 `Build`도 받지만, 현재 Project `Area` 필드에는 `Build` 옵션이 없다. -- `Sprint` - - `Sprint 1` - - `Sprint 2` - - `Sprint 3` - - `Later` -- `Parent issue` -- `Sub-issues progress` +## 실제 필드 +- 기본 컨텍스트 필드 + - `Title` + - `Assignees` + - `Labels` + - `Linked pull requests` + - `Milestone` + - `Repository` + - `Reviewers` +- 작업 추적 필드 + - `Status` + - `In Progress` + - `Done` + - `Area` + - `⚙️Engine` + - `🏃Domain` + - `🖥️Application` + - `🔎Analysis` + - `📄Docs` + - `Sprint` + - `Sprint 1` + - `Sprint 2` + - `Sprint 3` + - `Later` + - `Parent issue` + - `Sub-issues progress` +- 메모 + - issue form은 `Build`도 받지만, 현재 Project `Area` 필드에는 `Build` 옵션이 없다. + - `Build` 성격 작업은 issue 본문에는 `Build`로 남기고, 보드에서는 가장 가까운 기존 영역으로 배치한다. ## 현재 뷰 - 뷰 수: 1개 @@ -44,19 +54,18 @@ - `Parent issue` - `Sub-issues progress` -## 현재 구조 +## 현재 상위 구조 - `Sprint 1` - Epic: `#1 EPIC-1 Engine Foundation` - Epic: `#2 EPIC-2 Sprint 1 Demo Vertical Slice` - - Engine foundation tasks: `#6 ~ #13`, `#47` - - Demo vertical slice tasks: `#14 ~ #20`, `#52 ~ #55` + - Epic: `#3 EPIC-3 MVP 코어 구조 정렬` - `Sprint 2` - - Epic: `#3 EPIC-3 Product Completion for Sprint 2` - Epic: `#4 EPIC-4 Compare and Presentation Readiness` - - Task: `#21 ~ #30` (`Task-...` 형식) - `Sprint 3` - - Epic: `#5 EPIC-5 Finish and Optional Extensions` - - Task: `#31 ~ #35` (`Task-...` 형식) + - Epic: `#5 EPIC-5 1차 확장 기능` +- `Later` + - Epic: `#97 EPIC-중기 확장 기능` + - Epic: `#98 EPIC-연구 후보 검토` ## 메모 - 작업을 시작하기 전에 먼저 관련 issue가 이미 있는지 확인한다. @@ -65,11 +74,10 @@ - Project view에서 `Parent issue`, `Sub-issues progress` 컬럼으로 연결 결과가 보이는지 확인한다. - `Docs`, `Chore`, `Analysis`는 `Lightweight Task` form으로 가볍게 등록한다. - `Engine`, `Domain`, `Application`, `Build`는 `Implementation Task` form으로 범위와 검증 계획까지 남긴다. -- 현재 Project 보드의 `Area` 필드에는 `Build` 옵션이 없으므로, Build 성격 task는 issue form과 본문에는 `Build`로 남기고 보드에서는 임시로 가장 가까운 기존 영역에 배치한다. +- docs/policy-only 예외는 변경 경로가 `docs/`, `uml/`, `CONTRIBUTING.md`, PR/issue template, `pr-policy.yml`에만 한정될 때만 사용한다. +- docs/policy-only PR의 `Related Issue` 섹션은 `None (docs/policy-only PR)`로 남긴다. - 세부 작업명, 부모-자식 관계, dependency는 GitHub Project와 issue 자체를 기준으로 관리한다. - `blocked by`는 실제로 선행 해결이 필요한 hard dependency에만 건다. 단순한 권장 순서나 같은 Epic 안의 묶음 관계 때문에 불필요하게 직렬화하지 않는다. - 하나의 Task가 서로 다른 관심사를 함께 묶어 병렬 진행을 막으면, 별도 Task로 분리해서 dependency를 다시 연결한다. - 문서 또는 기여 정책만 다루는 변경은 별도 issue 없이 진행할 수 있다. -- 변경 범위가 `docs/`, `uml/`, `CONTRIBUTING.md`, PR/issue template, PR 정책 워크플로에만 한정되면 유지보수자는 PR 없이 `main`에 직접 push할 수 있다. - Task의 순서는 제목 접두사 뒤 숫자로 관리하지 않는다. 중간 작업이 생기면 새 issue를 추가하고 `Sprint`, `Parent issue`, `blocked by`로 위치를 표현한다. -- `#23 Task-Implement drawing import or preprocessing to FacilityLayout2D flow`는 범위가 넓어서 삭제했고, Sprint 1 데모용 import 흐름은 `#52 ~ #55`로 분리했다. diff --git a/src/application/MainWindow.cpp b/src/application/MainWindow.cpp index d0820c8..92c3272 100644 --- a/src/application/MainWindow.cpp +++ b/src/application/MainWindow.cpp @@ -1,5 +1,8 @@ #include "application/MainWindow.h" +#include +#include +#include #include #include #include @@ -28,6 +31,19 @@ QString stateToString(safecrowd::engine::EngineState state) { return "Unknown"; } +QLabel* createBodyLabel(const QString& text, QWidget* parent) { + auto* label = new QLabel(text, parent); + label->setWordWrap(true); + label->setTextFormat(Qt::RichText); + return label; +} + +QLabel* createValueLabel(QWidget* parent) { + auto* label = new QLabel("-", parent); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + return label; +} + } // namespace namespace safecrowd::application { @@ -36,64 +52,159 @@ MainWindow::MainWindow(safecrowd::domain::SafeCrowdDomain& domain, QWidget* pare : QMainWindow(parent), domain_(domain) { auto* centralWidget = new QWidget(this); - auto* layout = new QVBoxLayout(centralWidget); - statusLabel_ = new QLabel(this); - - auto* startButton = new QPushButton("Start", this); - auto* pauseButton = new QPushButton("Pause", this); - auto* stopButton = new QPushButton("Stop", this); - - layout->addWidget(statusLabel_); - layout->addWidget(startButton); - layout->addWidget(pauseButton); - layout->addWidget(stopButton); + auto* rootLayout = new QHBoxLayout(centralWidget); + rootLayout->setContentsMargins(18, 18, 18, 18); + rootLayout->setSpacing(16); + + auto* workspaceGroup = new QGroupBox("Project Workspace", centralWidget); + auto* workspaceLayout = new QVBoxLayout(workspaceGroup); + workspaceLayout->setSpacing(12); + workspaceLayout->addWidget(createBodyLabel( + "1. Import & Validate
" + "DXF and facility topology import, review, and manual correction will surface here.", + workspaceGroup)); + workspaceLayout->addWidget(createBodyLabel( + "2. Scenario Editor
" + "Baseline and variation authoring stay in the same workspace but outside the run panel.", + workspaceGroup)); + workspaceLayout->addWidget(createBodyLabel( + "3. Results & Recommendation
" + "Run summaries, comparison, export, and recommendation remain downstream of persisted artifacts.", + workspaceGroup)); + workspaceLayout->addStretch(); + + auto* workspaceColumn = new QVBoxLayout(); + workspaceColumn->setSpacing(16); + workspaceColumn->addWidget(createBodyLabel( + "SafeCrowd Workspace Prototype
" + "This shell now mirrors the documented workflow. Only playback control is wired live today; " + "the rest of the workspace is reserved so future application features land in explicit sections.", + centralWidget)); + + auto* runControlGroup = new QGroupBox("Run Control Panel", centralWidget); + auto* runControlLayout = new QVBoxLayout(runControlGroup); + runControlLayout->setSpacing(10); + runControlLayout->addWidget(createBodyLabel( + "Playback control remains the active path into the current runtime prototype.", + runControlGroup)); + + auto* buttonLayout = new QHBoxLayout(); + startButton_ = new QPushButton("Start Playback", runControlGroup); + pauseButton_ = new QPushButton("Pause Playback", runControlGroup); + stopButton_ = new QPushButton("Stop Playback", runControlGroup); + buttonLayout->addWidget(startButton_); + buttonLayout->addWidget(pauseButton_); + buttonLayout->addWidget(stopButton_); + runControlLayout->addLayout(buttonLayout); + runControlLayout->addWidget(createBodyLabel( + "Planned next: execution readiness checks, repeat runs, and variation selection.", + runControlGroup)); + + auto* runtimeStatusGroup = new QGroupBox("Runtime Status", centralWidget); + auto* runtimeStatusLayout = new QFormLayout(runtimeStatusGroup); + runtimeStatusLayout->setLabelAlignment(Qt::AlignLeft); + runtimeStatusLayout->setFormAlignment(Qt::AlignTop | Qt::AlignLeft); + + runtimeStateValue_ = createValueLabel(runtimeStatusGroup); + frameValue_ = createValueLabel(runtimeStatusGroup); + fixedStepValue_ = createValueLabel(runtimeStatusGroup); + alphaValue_ = createValueLabel(runtimeStatusGroup); + runValue_ = createValueLabel(runtimeStatusGroup); + variationValue_ = createValueLabel(runtimeStatusGroup); + + runtimeStatusLayout->addRow("Engine state", runtimeStateValue_); + runtimeStatusLayout->addRow("Rendered frames", frameValue_); + runtimeStatusLayout->addRow("Fixed steps", fixedStepValue_); + runtimeStatusLayout->addRow("Interpolation alpha", alphaValue_); + runtimeStatusLayout->addRow("Current run", runValue_); + runtimeStatusLayout->addRow("Variation", variationValue_); + + auto* resultsGroup = new QGroupBox("Results Pipeline", centralWidget); + auto* resultsLayout = new QVBoxLayout(resultsGroup); + resultsLayout->setSpacing(12); + resultsLayout->addWidget(createBodyLabel( + "Run Results Panel
" + "Single-run and variation summaries will read persisted artifacts first.", + resultsGroup)); + resultsLayout->addWidget(createBodyLabel( + "Comparison View
" + "Baseline versus alternative comparisons stay separate from live runtime state.", + resultsGroup)); + resultsLayout->addWidget(createBodyLabel( + "Export & Recommendation
" + "Artifact export and recommendation evidence remain downstream consumers of saved results.", + resultsGroup)); + + workspaceColumn->addWidget(runControlGroup); + workspaceColumn->addWidget(runtimeStatusGroup); + workspaceColumn->addWidget(resultsGroup); + workspaceColumn->addStretch(); + + rootLayout->addWidget(workspaceGroup, 5); + rootLayout->addLayout(workspaceColumn, 7); tickTimer_ = new QTimer(this); tickTimer_->setInterval(16); - connect(startButton, &QPushButton::clicked, this, [this]() { startSimulation(); }); - connect(pauseButton, &QPushButton::clicked, this, [this]() { pauseSimulation(); }); - connect(stopButton, &QPushButton::clicked, this, [this]() { stopSimulation(); }); + connect(startButton_, &QPushButton::clicked, this, [this]() { startSimulation(); }); + connect(pauseButton_, &QPushButton::clicked, this, [this]() { pauseSimulation(); }); + connect(stopButton_, &QPushButton::clicked, this, [this]() { stopSimulation(); }); connect(tickTimer_, &QTimer::timeout, this, [this]() { tickSimulation(); }); setCentralWidget(centralWidget); - setWindowTitle("SafeCrowd"); - resize(420, 220); + setWindowTitle("SafeCrowd Workspace"); + resize(980, 560); - refreshStatusLabel(); + refreshRuntimePanel(); } void MainWindow::startSimulation() { domain_.start(); tickTimer_->start(); - refreshStatusLabel(); + refreshRuntimePanel(); } void MainWindow::pauseSimulation() { domain_.pause(); tickTimer_->stop(); - refreshStatusLabel(); + refreshRuntimePanel(); } void MainWindow::stopSimulation() { domain_.stop(); tickTimer_->stop(); - refreshStatusLabel(); + refreshRuntimePanel(); } void MainWindow::tickSimulation() { domain_.update(1.0 / 60.0); - refreshStatusLabel(); + refreshRuntimePanel(); } -void MainWindow::refreshStatusLabel() { +void MainWindow::refreshRuntimePanel() { + using safecrowd::engine::EngineState; + const auto summary = domain_.summary(); - statusLabel_->setText( - QString("State: %1\nFrames: %2\nFixed Steps: %3\nAlpha: %4") - .arg(stateToString(summary.state)) - .arg(summary.frameIndex) - .arg(summary.fixedStepIndex) - .arg(summary.alpha, 0, 'f', 2)); + runtimeStateValue_->setText(stateToString(summary.state)); + frameValue_->setText(QString::number(summary.frameIndex)); + fixedStepValue_->setText(QString::number(summary.fixedStepIndex)); + alphaValue_->setText(QString::number(summary.alpha, 'f', 2)); + + if (summary.state == EngineState::Running || summary.state == EngineState::Paused) { + runValue_->setText("Prototype run 1 / 1"); + } else if (summary.frameIndex > 0 || summary.fixedStepIndex > 0) { + runValue_->setText("Last prototype run retained"); + } else { + runValue_->setText("Ready for first run"); + } + + variationValue_->setText("Baseline placeholder (domain wiring pending)"); + + const bool isRunning = summary.state == EngineState::Running; + const bool isPaused = summary.state == EngineState::Paused; + startButton_->setEnabled(!isRunning); + pauseButton_->setEnabled(isRunning); + stopButton_->setEnabled(isRunning || isPaused || summary.frameIndex > 0 || summary.fixedStepIndex > 0); } } // namespace safecrowd::application diff --git a/src/application/MainWindow.h b/src/application/MainWindow.h index d2c5465..e9e8acc 100644 --- a/src/application/MainWindow.h +++ b/src/application/MainWindow.h @@ -7,6 +7,7 @@ class SafeCrowdDomain; } class QLabel; +class QPushButton; class QTimer; namespace safecrowd::application { @@ -20,10 +21,18 @@ class MainWindow : public QMainWindow { void pauseSimulation(); void stopSimulation(); void tickSimulation(); - void refreshStatusLabel(); + void refreshRuntimePanel(); safecrowd::domain::SafeCrowdDomain& domain_; - QLabel* statusLabel_{nullptr}; + QPushButton* startButton_{nullptr}; + QPushButton* pauseButton_{nullptr}; + QPushButton* stopButton_{nullptr}; + QLabel* runtimeStateValue_{nullptr}; + QLabel* frameValue_{nullptr}; + QLabel* fixedStepValue_{nullptr}; + QLabel* alphaValue_{nullptr}; + QLabel* runValue_{nullptr}; + QLabel* variationValue_{nullptr}; QTimer* tickTimer_{nullptr}; };