From fcce544af9ecea8fa59da1ad83827e71089ce707 Mon Sep 17 00:00:00 2001 From: DH Park Date: Wed, 19 Aug 2026 19:51:15 +0900 Subject: [PATCH 01/18] =?UTF-8?q?Community=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EB=A9=94=EB=89=B4=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B2=B9?= =?UTF-8?q?=EC=B9=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/flarum/apply-community-theme-update.sh | 76 +++++++++++++++++++ .../resources/less/forum.less | 9 +++ deploy/flarum/tests/test_community_theme.py | 14 ++++ .../community-theme-rollout-rollback.md | 14 ++++ 4 files changed, 113 insertions(+) create mode 100644 deploy/flarum/apply-community-theme-update.sh diff --git a/deploy/flarum/apply-community-theme-update.sh b/deploy/flarum/apply-community-theme-update.sh new file mode 100644 index 0000000..e8d4f36 --- /dev/null +++ b/deploy/flarum/apply-community-theme-update.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${EUID}" -ne 0 ]]; then + echo "run as root" >&2 + exit 1 +fi + +source_root="${1:-}" +change_id="${2:-}" +app_root="/var/www/html" +extension_root="$app_root/extensions/ablecloud-community-theme" +backup_root="/var/backups/techflow-flarum/$change_id" + +if [[ -z "$source_root" || -z "$change_id" ]]; then + echo "usage: $0 /tmp/ablecloud-community-theme- theme-" >&2 + exit 1 +fi + +source_root="$(realpath "$source_root")" +extension_root="$(realpath "$extension_root")" + +[[ "$source_root" == /tmp/ablecloud-community-theme-* ]] || { + echo "unexpected source path: $source_root" >&2 + exit 1 +} +[[ "$extension_root" == "/var/www/html/extensions/ablecloud-community-theme" ]] || { + echo "unexpected extension path: $extension_root" >&2 + exit 1 +} +[[ "$backup_root" == /var/backups/techflow-flarum/theme-* ]] || { + echo "unexpected backup path: $backup_root" >&2 + exit 1 +} +[[ -f "$source_root/resources/less/forum.less" ]] || { + echo "theme source is incomplete" >&2 + exit 1 +} + +install -d -m 0750 "$backup_root" +cp -a "$extension_root" "$backup_root/extension-before" +cp -a "$app_root/vendor/ablecloud/community-theme" "$backup_root/vendor-before" +cp -a "$app_root/composer.json" "$app_root/composer.lock" "$backup_root/" +cp -a "$app_root/public/assets" "$backup_root/assets-before" +if [[ -d "$app_root/storage/locale" ]]; then + cp -a "$app_root/storage/locale" "$backup_root/locale-before" +fi + +rsync -a --delete "$source_root/" "$extension_root/" +cd "$app_root" +sudo -u www-data env COMPOSER_HOME=/tmp/techflow-composer \ + composer require ablecloud/community-theme:@dev --with-dependencies --no-interaction +sudo -u www-data php flarum extension:enable ablecloud-community-theme +sudo -u www-data php flarum cache:clear +sudo -u www-data env TECHFLOW_ALLOWED_FLARUM_ROOT="$app_root" \ + php vendor/ablecloud/community-theme/tools/warm-korean-locale.php "$app_root" +systemctl restart php8.3-fpm nginx + +systemctl is-active --quiet nginx +systemctl is-active --quiet php8.3-fpm +systemctl is-active --quiet mariadb +curl --fail --silent --show-error --output /dev/null \ + -H 'Host: community.ablecloud.io' \ + -H 'X-Forwarded-Proto: https' \ + http://127.0.0.1/ +grep -q 'UserPage-nav > ul.affix' \ + "$app_root/vendor/ablecloud/community-theme/resources/less/forum.less" +grep -q 'ul.affix' "$app_root/public/assets/forum.css" + +printf 'BACKUP_ROOT=%s\n' "$backup_root" +printf 'SOURCE_SHA256=' +sha256sum "$source_root/resources/less/forum.less" | awk '{print $1}' +printf 'VENDOR_SHA256=' +sha256sum "$app_root/vendor/ablecloud/community-theme/resources/less/forum.less" | awk '{print $1}' +printf 'LOCAL_HTTP=200\n' +printf 'RESULT=PASS\n' diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 1fb7ed8..69d03a5 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1932,6 +1932,15 @@ body { @media (min-width: 768px) { body { + // Flarum adds .affix to this list while the user page scrolls. Once the + // list becomes position: fixed, percentage width resolves against the + // viewport instead of the 280px navigation column and covers the content. + .UserPage .UserPage-nav > ul.affix { + width: 280px !important; + max-width: 280px; + box-sizing: border-box; + } + .App--index > .App-navigation { display: none !important; } diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 9191ce3..102dc64 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -216,6 +216,9 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn(".UserHero .UserCard-profile", self.less) self.assertIn(".UserPage .UserPage-nav", self.less) self.assertIn(".UserPage .UserPage-content", self.less) + self.assertIn(".UserPage .UserPage-nav > ul.affix", self.less) + self.assertIn("width: 280px !important", self.less) + self.assertIn("max-width: 280px", self.less) self.assertIn(".UserHero .UserCard-avatar", self.less) self.assertIn("position: absolute", self.less) self.assertIn("likes_link: 좋아요", self.korean) @@ -266,6 +269,17 @@ def test_rehearsal_is_staging_only_and_preserves_content(self) -> None: self.assertIn("flarum-likes.forum.user.likes_link", locale_warmer) self.assertIn("fof-upload.forum.buttons.media", locale_warmer) + def test_production_theme_update_is_path_scoped(self) -> None: + updater = ( + ROOT / "deploy" / "flarum" / "apply-community-theme-update.sh" + ).read_text(encoding="utf-8") + self.assertIn('/var/www/html/extensions/ablecloud-community-theme', updater) + self.assertIn('/tmp/ablecloud-community-theme-*', updater) + self.assertIn('/var/backups/techflow-flarum/theme-*', updater) + self.assertIn('rsync -a --delete "$source_root/" "$extension_root/"', updater) + self.assertIn("UserPage-nav > ul.affix", updater) + self.assertIn("RESULT=PASS", updater) + if __name__ == "__main__": unittest.main() diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 168e101..4cffd98 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -54,12 +54,26 @@ sudo -u www-data env TECHFLOW_ALLOWED_FLARUM_ROOT=/var/www/html \ sudo systemctl restart php8.3-fpm nginx ``` +이미 설치된 테마의 CSS·Locale·JS만 갱신할 때는 변경 자산을 +`/tmp/ablecloud-community-theme-`에 전송한 뒤 경로 제한 배포기를 사용한다. +배포기는 현재 Extension·Vendor·Composer·컴파일 자산·Locale을 +`/var/backups/techflow-flarum/theme-`에 먼저 백업하고, 적용 후 서비스와 +컴파일 CSS를 검증한다. + +```bash +sudo bash deploy/flarum/apply-community-theme-update.sh \ + /tmp/ablecloud-community-theme- \ + theme- +``` + ## 5. 적용 후 검증 - 외부 HTTPS와 서버 로컬 Host/HTTPS 전달 헤더 점검이 모두 200이어야 한다. - `public/assets/forum.css`에 `--ablecloud-brand-primary`가 있어야 한다. - `public/assets/forum-ko.js`가 비어 있지 않고 `core.forum.header.search_placeholder`를 포함해야 한다. - 데스크톱과 모바일에서 홈, 목록, 상세, 검색, 로그인, 글쓰기, 첨부를 확인한다. +- 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 + 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. - 사용자·토의·게시물 수와 첨부 해시가 적용 전과 같아야 한다. From fce08e1df38f7bee6217f5cea3f936022d9b7a23 Mon Sep 17 00:00:00 2001 From: DH Park Date: Wed, 19 Aug 2026 19:52:12 +0900 Subject: [PATCH 02/18] =?UTF-8?q?Community=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EB=A9=94=EB=89=B4=20=EC=88=98=EC=A0=95=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EC=A6=9D=EC=A0=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community-user-nav-affix-validation.json | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/evidence/issue-73/community-user-nav-affix-validation.json diff --git a/docs/evidence/issue-73/community-user-nav-affix-validation.json b/docs/evidence/issue-73/community-user-nav-affix-validation.json new file mode 100644 index 0000000..560b165 --- /dev/null +++ b/docs/evidence/issue-73/community-user-nav-affix-validation.json @@ -0,0 +1,61 @@ +{ + "schema_version": "1.0", + "executed_at": "2026-08-19T10:49:00Z", + "scope": "Flarum user profile and settings navigation scroll overlap", + "root_cause": "When Flarum adds the affix class, the fixed list resolved width: 100% against the viewport instead of the 280px navigation column.", + "change": { + "selector": ".UserPage .UserPage-nav > ul.affix", + "width_px": 280, + "max_width_px": 280 + }, + "wsl": { + "environment": "WSL Ubuntu 24.04", + "url": "http://localhost:18080/u/pdh1974", + "rehearsal_result": "/srv/techflow-flarum-staging/rehearsals/issue-73/issue73-user-nav-affix/result.json", + "cycle": "PASS", + "browser": { + "viewport": { "width": 1280, "height": 720 }, + "scroll_y": 520, + "menu_width_px": 280, + "menu_right_px": 377.5, + "content_left_px": 427.5, + "overlap": false + } + }, + "production": { + "host": "172.16.0.234", + "url": "https://community.ablecloud.io/u/pdh1974", + "backup_root": "/var/backups/techflow-flarum/theme-nav-affix-20260819T104516Z", + "services": { + "nginx": "active", + "php8.3-fpm": "active", + "mariadb": "active" + }, + "local_http_status": 200, + "external_https_status": 200, + "theme_sha256": "376eefd1d1e00898ed8438566824df11ceb137f970b2ee91297f3b131cb8f08d", + "vendor_sha256_matches": true, + "compiled_css_contains_affix_rule": true, + "browser": { + "viewport": { "width": 1150, "height": 881 }, + "scroll_y": 520, + "menu_width_px": 280, + "menu_right_px": 312.5, + "content_left_px": 362.5, + "overlap": false + } + }, + "validation": { + "theme_contract_tests": { "passed": 11, "failed": 0 }, + "deployment_script_shell_syntax": "PASS", + "git_diff_check": "PASS", + "temporary_remote_assets_removed": true + }, + "protected_scope": { + "database_schema_changed": false, + "community_content_changed": false, + "techflow_ai_services_changed": false, + "github_chat_webhook_changed": false + }, + "decision": "PASS" +} From 1474f1807239cfe1eb6e542da655196f0c682924 Mon Sep 17 00:00:00 2001 From: DH Park Date: Wed, 19 Aug 2026 20:35:02 +0900 Subject: [PATCH 03/18] =?UTF-8?q?Community=20=EC=B2=AB=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B3=A0=EC=A0=95=20=EB=A9=94=EB=89=B4=EC=99=80=20?= =?UTF-8?q?=EC=97=B0=EC=86=8D=20=ED=94=BC=EB=93=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/flarum/apply-community-theme-update.sh | 6 + .../resources/less/forum.less | 158 ++++++++++++++++++ deploy/flarum/tests/test_community_theme.py | 13 ++ ...community-index-fixed-feed-validation.json | 68 ++++++++ .../issue-73-community-ui-validation.md | 17 ++ .../community-theme-rollout-rollback.md | 11 +- 6 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 docs/evidence/issue-73/community-index-fixed-feed-validation.json diff --git a/deploy/flarum/apply-community-theme-update.sh b/deploy/flarum/apply-community-theme-update.sh index e8d4f36..1ff648a 100644 --- a/deploy/flarum/apply-community-theme-update.sh +++ b/deploy/flarum/apply-community-theme-update.sh @@ -47,6 +47,12 @@ if [[ -d "$app_root/storage/locale" ]]; then fi rsync -a --delete "$source_root/" "$extension_root/" +# SCP staging directories can be intentionally private (0700). Normalize the +# deployed path so Composer and PHP-FPM can traverse the path repository. +chown -R www-data:www-data "$extension_root" +find "$extension_root" -type d -exec chmod 0755 {} + +find "$extension_root" -type f -exec chmod 0644 {} + +chmod 0755 "$extension_root/tools/warm-korean-locale.php" cd "$app_root" sudo -u www-data env COMPOSER_HOME=/tmp/techflow-composer \ composer require ablecloud/community-theme:@dev --with-dependencies --no-interaction diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 69d03a5..69455cf 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1931,7 +1931,165 @@ body { } @media (min-width: 768px) { + html:has(body > .App--index), + body:has(> .App--index) { + height: 100%; + overflow: hidden; + } + body { + // Keep the desktop index shell anchored between the fixed header and + // footer. Only the discussion list scrolls during normal navigation. + .App--index { + height: 100%; + min-height: 0; + padding-bottom: 0; + overflow: hidden; + } + + .App--index > .App-content { + position: fixed; + top: 52px; + right: 0; + bottom: 56px; + left: 0; + height: auto; + box-sizing: border-box; + padding-bottom: 0; + overflow: hidden; + } + + .App--index .IndexPage { + position: absolute; + inset: 0; + display: flex; + height: auto; + min-height: 0; + flex-direction: column; + overflow: hidden; + } + + .App--index .WelcomeHero { + flex: 0 0 auto; + } + + .App--index .IndexPage > .container { + flex: 1 1 auto; + min-height: 0; + padding-bottom: 0; + overflow: hidden; + } + + .App--index .IndexPage > .container > .sideNavContainer { + height: 100%; + min-height: 0; + overflow: hidden; + } + + .App--index .IndexPage-nav { + height: 100%; + min-height: 0; + box-sizing: border-box; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-color: #91a9c9 transparent; + scrollbar-width: thin; + } + + .App--index .IndexPage-results { + display: flex; + height: auto; + min-height: 0; + flex-direction: column; + overflow: hidden; + } + + .App--index .IndexPage-toolbar { + flex: 0 0 auto; + } + + .App--index .IndexPage-results .DiscussionList { + flex: 1 1 auto; + min-height: 0; + box-sizing: border-box; + padding-right: 6px; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-color: #91a9c9 transparent; + scrollbar-width: thin; + } + + // Present discussions as one continuous, compact feed. The row keeps the + // existing title, tags, reply count and control menu, while individual + // nested cards are replaced by quiet separators and a single hover state. + .App--index .IndexPage-results .DiscussionList-discussions { + margin: 0; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 16px; + background: var(--ablecloud-brand-surface); + box-shadow: 0 8px 24px rgba(24, 71, 142, 0.05); + } + + .App--index .IndexPage-results .DiscussionList-discussions > li:not(:last-child) { + border-bottom: 1px solid #e2eaf5; + } + + .App--index .IndexPage-results .DiscussionListItem { + margin: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + transform: none; + } + + .App--index .IndexPage-results .DiscussionList-discussions > li:first-child .DiscussionListItem { + border-radius: 15px 15px 0 0; + } + + .App--index .IndexPage-results .DiscussionList-discussions > li:last-child .DiscussionListItem { + border-radius: 0 0 15px 15px; + } + + .App--index .IndexPage-results .DiscussionListItem:hover, + .App--index .IndexPage-results .DiscussionListItem:focus-within, + .App--index .IndexPage-results .DiscussionListItem:has(.DiscussionListItem-controls.open) { + border-color: transparent; + background: #f3f7ff; + box-shadow: inset 3px 0 0 var(--ablecloud-brand-primary); + transform: none; + } + + .App--index .IndexPage-results .DiscussionListItem-content { + min-height: 58px; + padding-top: 9px; + padding-bottom: 9px; + } + + .App--index .IndexPage-nav::-webkit-scrollbar, + .App--index .IndexPage-results .DiscussionList::-webkit-scrollbar { + width: 6px; + } + + .App--index .IndexPage-nav::-webkit-scrollbar-track, + .App--index .IndexPage-results .DiscussionList::-webkit-scrollbar-track { + background: transparent; + } + + .App--index .IndexPage-nav::-webkit-scrollbar-button, + .App--index .IndexPage-results .DiscussionList::-webkit-scrollbar-button { + display: none; + width: 0; + height: 0; + } + + .App--index .IndexPage-nav::-webkit-scrollbar-thumb, + .App--index .IndexPage-results .DiscussionList::-webkit-scrollbar-thumb { + border-radius: 999px; + background: #91a9c9; + } + // Flarum adds .affix to this list while the user page scrolls. Once the // list becomes position: fixed, percentage width resolves against the // viewport instead of the 280px navigation column and covers the content. diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 102dc64..48c3c40 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -213,6 +213,19 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn(".App--index > .App-navigation", self.less) self.assertIn("display: none !important", self.less) self.assertNotIn(".App--index > .App-navigation .Navigation-drawer", self.less) + self.assertIn(".App--index > .App-content", self.less) + self.assertIn("html:has(body > .App--index)", self.less) + self.assertIn("body:has(> .App--index)", self.less) + self.assertIn(".App--index .IndexPage-results .DiscussionList", self.less) + self.assertIn(".App--index .IndexPage-results .DiscussionList-discussions", self.less) + self.assertIn("one continuous, compact feed", self.less) + self.assertIn("box-shadow: inset 3px 0 0 var(--ablecloud-brand-primary)", self.less) + self.assertIn("overscroll-behavior: contain", self.less) + self.assertIn("position: fixed", self.less) + self.assertIn("bottom: 56px", self.less) + deploy_script = (ROOT / "deploy" / "flarum" / "apply-community-theme-update.sh").read_text(encoding="utf-8") + self.assertIn('chown -R www-data:www-data "$extension_root"', deploy_script) + self.assertIn('find "$extension_root" -type d -exec chmod 0755 {} +', deploy_script) self.assertIn(".UserHero .UserCard-profile", self.less) self.assertIn(".UserPage .UserPage-nav", self.less) self.assertIn(".UserPage .UserPage-content", self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json new file mode 100644 index 0000000..51aec68 --- /dev/null +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -0,0 +1,68 @@ +{ + "schema_version": "1.0", + "executed_at": "2026-08-19T11:31:00Z", + "scope": "Flarum desktop index fixed navigation and continuous discussion feed", + "design_reference": "https://www.reddit.com/new/?feed=home", + "change": { + "document_scroll": "disabled on desktop index", + "fixed_regions": ["welcome hero", "discussion toolbar", "left navigation", "footer"], + "scroll_region": ".DiscussionList", + "feed": "one shared surface with row separators", + "individual_row_card": false, + "row_height_px": 58, + "left_navigation_width_px": 280 + }, + "wsl": { + "environment": "WSL Ubuntu 24.04", + "url": "http://localhost:18080/", + "theme_sha256": "348fdb7469bc63651ca8a4e298d5be395dd68a1ac81538e2df943c1b01a238b6", + "browser": { + "viewport": { "width": 1280, "height": 720 }, + "document_scroll_y": 0, + "document_scroll_height": 720, + "discussion_list_client_height": 396, + "discussion_list_scroll_height": 1231, + "discussion_list_scrollable": true, + "menu_y_before_after": [179.890625, 179.890625], + "toolbar_y_before_after": [209.890625, 209.890625] + }, + "compiled_css_contains_feed_rule": true + }, + "production": { + "host": "172.16.0.234", + "url": "https://community.ablecloud.io/", + "backup_root": "/var/backups/techflow-flarum/theme-index-fixed-feed-20260819T1128Z", + "theme_sha256": "348fdb7469bc63651ca8a4e298d5be395dd68a1ac81538e2df943c1b01a238b6", + "services": { + "nginx": "active", + "php8.3-fpm": "active", + "mariadb": "active" + }, + "external_https_status": 200, + "browser": { + "viewport": { "width": 1150, "height": 881 }, + "document_scroll_y_before_after": [0, 0], + "discussion_list_scroll_top_before_after": [0, 420], + "menu_y_before_after": [179.890625, 179.890625], + "toolbar_y_before_after": [209.890625, 209.890625], + "discussion_list_client_height": 557, + "discussion_list_scroll_height": 1231, + "discussion_list_scrollable": true + }, + "compiled_css_contains_feed_rule": true + }, + "validation": { + "theme_contract_tests": { "passed": 11, "failed": 0 }, + "deployment_script_normalizes_permissions": true, + "deployment_script_shell_syntax": "PASS", + "git_diff_check": "PASS", + "temporary_remote_assets_removed": true + }, + "protected_scope": { + "database_schema_changed": false, + "community_content_changed": false, + "techflow_ai_services_changed": false, + "github_chat_webhook_changed": false + }, + "decision": "PASS" +} diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index 99c0449..6bf4a06 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -166,3 +166,20 @@ Hero의 위·아래 패딩은 데스크톱 `19px`, 모바일 `17px`로 대칭화 ## 판정 Issue #73의 설계·구현·WSL 기능 검증·롤백·문서화 조건을 충족했다. 운영 적용은 별도 승인 전까지 수행하지 않는다. 검토자가 운영 반영을 승인하면 Runbook에 따라 테마 확장만 적용하고 같은 검증을 반복한다. + +## 2026-08-19 운영 후속 적용 + +운영 승인 후 사용자 프로필·설정 메뉴 겹침 보완과 첫 화면 고정 피드를 WSL과 +운영 Community에 적용했다. 데스크톱 첫 화면은 Welcome Hero, 280px 좌측 메뉴, +목록 도구막대와 Footer를 고정하고 토론 목록만 스크롤한다. Reddit의 Card/Compact +전환에서 확인되는 연속 피드 구조를 참고하되 ABLESTACK의 블루 색상, 태그와 +댓글 수, 더보기 기능을 유지했다. 개별 행의 카드 테두리·그림자·들뜸은 제거하고 +한 개의 흰색 피드 표면, 1px 구분선, 파란 Hover/Focus 표시로 단순화했다. + +WSL 1280x720과 운영 1150x881에서 문서 `scrollY=0`을 유지한 채 목록 +`scrollTop`만 각각 변화했다. 운영 목록을 `420px` 스크롤한 뒤에도 메뉴 Y좌표 +`179.890625`, 도구막대 Y좌표 `209.890625`가 동일했다. 행 높이는 `58px`, +외부 HTTPS는 `200`, Nginx·PHP-FPM·MariaDB는 모두 `active`다. 운영 적용 전 +백업은 `/var/backups/techflow-flarum/theme-index-fixed-feed-20260819T1128Z`에 +보존했다. 상세 증적은 +`docs/evidence/issue-73/community-index-fixed-feed-validation.json`에 기록했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 4cffd98..7b78bf6 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -72,6 +72,13 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - `public/assets/forum.css`에 `--ablecloud-brand-primary`가 있어야 한다. - `public/assets/forum-ko.js`가 비어 있지 않고 `core.forum.header.search_placeholder`를 포함해야 한다. - 데스크톱과 모바일에서 홈, 목록, 상세, 검색, 로그인, 글쓰기, 첨부를 확인한다. +- 데스크톱 첫 화면에서 문서 전체의 `scrollY`가 `0`으로 유지되는지 확인한다. + Welcome Hero, 좌측 메뉴, 목록 도구막대와 Footer는 고정되어야 하며 + `.DiscussionList`의 `scrollTop`만 변해야 한다. 높이가 짧아 좌측 태그가 모두 + 보이지 않을 때만 좌측 메뉴 자체의 얇은 보조 스크롤을 허용한다. +- 토론 목록은 개별 행마다 카드 테두리·그림자를 중첩하지 않는다. 하나의 피드 + 표면 안에서 얇은 구분선으로 행을 나누고, 제목·태그·댓글 수·더보기 기능과 + 파란색 Hover/Focus 상태가 유지되는지 확인한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. @@ -96,4 +103,6 @@ sudo systemctl restart php8.3-fpm nginx - GO: HTTP 200, 한글 원문 키 0건, 핵심 기능 정상, 콘텐츠·첨부 불변. - ROLLBACK: 화면·번역·로그인·작성·첨부 중 하나라도 실패. -- 운영 적용 전 현재 상태: Conditional GO, 승인 대기. +- 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 첫 화면 고정 피드가 + WSL 및 운영에 반영되었으며, 운영 백업은 + `/var/backups/techflow-flarum/theme-index-fixed-feed-20260819T1128Z`에 보존한다. From c5c39acb8a2197f45d204344e7c9f60ec7e15e8c Mon Sep 17 00:00:00 2001 From: DH Park Date: Wed, 19 Aug 2026 20:39:56 +0900 Subject: [PATCH 04/18] =?UTF-8?q?Community=20=ED=85=8C=EB=A7=88=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EA=B6=8C=ED=95=9C=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../issue-73/community-index-fixed-feed-validation.json | 3 ++- docs/reports/issue-73-community-ui-validation.md | 6 ++++-- docs/runbooks/community-theme-rollout-rollback.md | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 51aec68..faf5fab 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -31,7 +31,7 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-index-fixed-feed-20260819T1128Z", + "backup_root": "/var/backups/techflow-flarum/theme-index-fixed-feed-v2-20260819T1140Z", "theme_sha256": "348fdb7469bc63651ca8a4e298d5be395dd68a1ac81538e2df943c1b01a238b6", "services": { "nginx": "active", @@ -54,6 +54,7 @@ "validation": { "theme_contract_tests": { "passed": 11, "failed": 0 }, "deployment_script_normalizes_permissions": true, + "deployment_script_production_result": "PASS", "deployment_script_shell_syntax": "PASS", "git_diff_check": "PASS", "temporary_remote_assets_removed": true diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index 6bf4a06..7222582 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -180,6 +180,8 @@ WSL 1280x720과 운영 1150x881에서 문서 `scrollY=0`을 유지한 채 목록 `scrollTop`만 각각 변화했다. 운영 목록을 `420px` 스크롤한 뒤에도 메뉴 Y좌표 `179.890625`, 도구막대 Y좌표 `209.890625`가 동일했다. 행 높이는 `58px`, 외부 HTTPS는 `200`, Nginx·PHP-FPM·MariaDB는 모두 `active`다. 운영 적용 전 -백업은 `/var/backups/techflow-flarum/theme-index-fixed-feed-20260819T1128Z`에 -보존했다. 상세 증적은 +백업은 `/var/backups/techflow-flarum/theme-index-fixed-feed-v2-20260819T1140Z`에 +보존했다. SCP 임시 경로가 0700인 경우에도 배포 스크립트가 Extension 경로를 +Composer와 PHP-FPM이 읽을 수 있는 권한으로 정규화하는 것을 운영 재적용 +`RESULT=PASS`로 확인했다. 상세 증적은 `docs/evidence/issue-73/community-index-fixed-feed-validation.json`에 기록했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 7b78bf6..3a5c1ee 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -105,4 +105,4 @@ sudo systemctl restart php8.3-fpm nginx - ROLLBACK: 화면·번역·로그인·작성·첨부 중 하나라도 실패. - 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 첫 화면 고정 피드가 WSL 및 운영에 반영되었으며, 운영 백업은 - `/var/backups/techflow-flarum/theme-index-fixed-feed-20260819T1128Z`에 보존한다. + `/var/backups/techflow-flarum/theme-index-fixed-feed-v2-20260819T1140Z`에 보존한다. From 0fff8d8982bbeecdd9ad0089a2da6b65c21ca495 Mon Sep 17 00:00:00 2001 From: DH Park Date: Wed, 19 Aug 2026 20:59:41 +0900 Subject: [PATCH 05/18] =?UTF-8?q?Community=20=EC=B2=AB=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20Reddit=ED=98=95=20=ED=8F=89=EB=A9=B4=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/less/forum.less | 73 +++++++------------ deploy/flarum/tests/test_community_theme.py | 11 ++- ...community-index-fixed-feed-validation.json | 60 ++++++++++----- .../issue-73-community-ui-validation.md | 33 +++++---- .../community-theme-rollout-rollback.md | 21 ++++-- 5 files changed, 110 insertions(+), 88 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 69455cf..23e446f 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1939,7 +1939,8 @@ body { body { // Keep the desktop index shell anchored between the fixed header and - // footer. Only the discussion list scrolls during normal navigation. + // footer. The full right column scrolls, including its sort/filter tools, + // while the navigation follows the fixed User > Settings pattern. .App--index { height: 100%; min-height: 0; @@ -1992,8 +1993,8 @@ body { box-sizing: border-box; overflow-y: auto; overscroll-behavior: contain; - scrollbar-color: #91a9c9 transparent; - scrollbar-width: thin; + -ms-overflow-style: none; + scrollbar-width: none; } .App--index .IndexPage-results { @@ -2001,38 +2002,39 @@ body { height: auto; min-height: 0; flex-direction: column; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + -ms-overflow-style: none; + scrollbar-width: none; } .App--index .IndexPage-toolbar { flex: 0 0 auto; + padding-top: 20px; } .App--index .IndexPage-results .DiscussionList { - flex: 1 1 auto; - min-height: 0; + flex: 0 0 auto; + min-height: auto; + height: auto; box-sizing: border-box; padding-right: 6px; - overflow-x: hidden; - overflow-y: auto; - overscroll-behavior: contain; - scrollbar-color: #91a9c9 transparent; - scrollbar-width: thin; + overflow: visible; } - // Present discussions as one continuous, compact feed. The row keeps the - // existing title, tags, reply count and control menu, while individual - // nested cards are replaced by quiet separators and a single hover state. + // Follow Reddit's flat feed composition: no list container card and no + // nested row cards. ABLESTACK color, type, tags and actions remain intact. .App--index .IndexPage-results .DiscussionList-discussions { margin: 0; - border: 1px solid var(--ablecloud-brand-border); - border-radius: 16px; - background: var(--ablecloud-brand-surface); - box-shadow: 0 8px 24px rgba(24, 71, 142, 0.05); + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; } .App--index .IndexPage-results .DiscussionList-discussions > li:not(:last-child) { - border-bottom: 1px solid #e2eaf5; + border-bottom: 1px solid var(--ablecloud-brand-border); } .App--index .IndexPage-results .DiscussionListItem { @@ -2044,20 +2046,12 @@ body { transform: none; } - .App--index .IndexPage-results .DiscussionList-discussions > li:first-child .DiscussionListItem { - border-radius: 15px 15px 0 0; - } - - .App--index .IndexPage-results .DiscussionList-discussions > li:last-child .DiscussionListItem { - border-radius: 0 0 15px 15px; - } - .App--index .IndexPage-results .DiscussionListItem:hover, .App--index .IndexPage-results .DiscussionListItem:focus-within, .App--index .IndexPage-results .DiscussionListItem:has(.DiscussionListItem-controls.open) { border-color: transparent; - background: #f3f7ff; - box-shadow: inset 3px 0 0 var(--ablecloud-brand-primary); + background: rgba(21, 94, 239, 0.055); + box-shadow: none; transform: none; } @@ -2067,27 +2061,16 @@ body { padding-bottom: 9px; } - .App--index .IndexPage-nav::-webkit-scrollbar, - .App--index .IndexPage-results .DiscussionList::-webkit-scrollbar { - width: 6px; - } - - .App--index .IndexPage-nav::-webkit-scrollbar-track, - .App--index .IndexPage-results .DiscussionList::-webkit-scrollbar-track { - background: transparent; - } - - .App--index .IndexPage-nav::-webkit-scrollbar-button, - .App--index .IndexPage-results .DiscussionList::-webkit-scrollbar-button { + .App--index .IndexPage-nav::-webkit-scrollbar { display: none; width: 0; height: 0; } - .App--index .IndexPage-nav::-webkit-scrollbar-thumb, - .App--index .IndexPage-results .DiscussionList::-webkit-scrollbar-thumb { - border-radius: 999px; - background: #91a9c9; + .App--index .IndexPage-results::-webkit-scrollbar { + display: none; + width: 0; + height: 0; } // Flarum adds .affix to this list while the user page scrolls. Once the diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 48c3c40..e9c671f 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -218,8 +218,15 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("body:has(> .App--index)", self.less) self.assertIn(".App--index .IndexPage-results .DiscussionList", self.less) self.assertIn(".App--index .IndexPage-results .DiscussionList-discussions", self.less) - self.assertIn("one continuous, compact feed", self.less) - self.assertIn("box-shadow: inset 3px 0 0 var(--ablecloud-brand-primary)", self.less) + self.assertIn("full right column scrolls", self.less) + self.assertIn("Follow Reddit's flat feed composition", self.less) + self.assertIn(".App--index .IndexPage-results::-webkit-scrollbar", self.less) + self.assertIn(".App--index .IndexPage-nav::-webkit-scrollbar", self.less) + self.assertIn("scrollbar-width: none", self.less) + self.assertGreaterEqual(self.less.count("scrollbar-width: none"), 2) + self.assertIn("overflow: visible", self.less) + self.assertIn("flex: 0 0 auto", self.less) + self.assertIn("background: rgba(21, 94, 239, 0.055)", self.less) self.assertIn("overscroll-behavior: contain", self.less) self.assertIn("position: fixed", self.less) self.assertIn("bottom: 56px", self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index faf5fab..27c2b65 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,38 +1,48 @@ { - "schema_version": "1.0", - "executed_at": "2026-08-19T11:31:00Z", - "scope": "Flarum desktop index fixed navigation and continuous discussion feed", + "schema_version": "1.1", + "executed_at": "2026-08-19T11:59:00Z", + "scope": "Flarum desktop index fixed navigation and Reddit-style flat discussion feed", "design_reference": "https://www.reddit.com/new/?feed=home", + "navigation_reference": "https://community.ablecloud.io/settings", "change": { "document_scroll": "disabled on desktop index", - "fixed_regions": ["welcome hero", "discussion toolbar", "left navigation", "footer"], - "scroll_region": ".DiscussionList", - "feed": "one shared surface with row separators", + "fixed_regions": ["welcome hero", "left navigation", "footer"], + "scroll_region": ".IndexPage-results including .IndexPage-toolbar and .DiscussionList, scrollbar hidden", + "discussion_list_owns_scroll": false, + "toolbar_scrolls_with_feed": true, + "feed": "flat transparent feed with row separators and no outer container card", "individual_row_card": false, "row_height_px": 58, - "left_navigation_width_px": 280 + "left_navigation_width_px": 280, + "left_navigation_internal_scroll": "enabled when needed, scrollbar hidden" }, "wsl": { "environment": "WSL Ubuntu 24.04", "url": "http://localhost:18080/", - "theme_sha256": "348fdb7469bc63651ca8a4e298d5be395dd68a1ac81538e2df943c1b01a238b6", + "theme_sha256": "1631ae28ae2703e212618b0807eba9d840df408d1a8f62d6224c3206b0d95ca3", "browser": { "viewport": { "width": 1280, "height": 720 }, "document_scroll_y": 0, "document_scroll_height": 720, - "discussion_list_client_height": 396, - "discussion_list_scroll_height": 1231, - "discussion_list_scrollable": true, + "right_column_client_height": 454, + "right_column_scroll_height": 1307, + "right_column_scroll_top_before_after": [0, 360], + "discussion_list_client_height": 1229, + "discussion_list_scroll_height": 1229, + "discussion_list_scroll_top_before_after": [0, 0], + "discussion_list_scrollable": false, "menu_y_before_after": [179.890625, 179.890625], - "toolbar_y_before_after": [209.890625, 209.890625] + "toolbar_y_before_after": [209.890625, -150.109375], + "menu_scrollbar": "hidden", + "right_column_scrollbar": "hidden" }, "compiled_css_contains_feed_rule": true }, "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-index-fixed-feed-v2-20260819T1140Z", - "theme_sha256": "348fdb7469bc63651ca8a4e298d5be395dd68a1ac81538e2df943c1b01a238b6", + "backup_root": "/var/backups/techflow-flarum/theme-reddit-flat-feed-hidden-scroll-20260819T115539Z", + "theme_sha256": "1631ae28ae2703e212618b0807eba9d840df408d1a8f62d6224c3206b0d95ca3", "services": { "nginx": "active", "php8.3-fpm": "active", @@ -40,14 +50,24 @@ }, "external_https_status": 200, "browser": { - "viewport": { "width": 1150, "height": 881 }, + "viewport": { "width": 1280, "height": 720 }, "document_scroll_y_before_after": [0, 0], - "discussion_list_scroll_top_before_after": [0, 420], + "right_column_scroll_top_before_after": [0, 360], + "discussion_list_scroll_top_before_after": [0, 0], "menu_y_before_after": [179.890625, 179.890625], - "toolbar_y_before_after": [209.890625, 209.890625], - "discussion_list_client_height": 557, - "discussion_list_scroll_height": 1231, - "discussion_list_scrollable": true + "toolbar_y_before_after": [209.890625, -150.109375], + "right_column_client_height": 454, + "right_column_scroll_height": 1307, + "discussion_list_client_height": 1229, + "discussion_list_scroll_height": 1229, + "discussion_list_scrollable": false, + "menu_internal_scroll_top_before_after": [0, 260], + "menu_scrollbar": "hidden", + "right_column_scrollbar": "hidden", + "feed_container_border": "none", + "feed_container_background": "transparent", + "row_border": "none", + "row_shadow": "none" }, "compiled_css_contains_feed_rule": true }, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index 7222582..b595256 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -169,19 +169,26 @@ Issue #73의 설계·구현·WSL 기능 검증·롤백·문서화 조건을 충 ## 2026-08-19 운영 후속 적용 -운영 승인 후 사용자 프로필·설정 메뉴 겹침 보완과 첫 화면 고정 피드를 WSL과 -운영 Community에 적용했다. 데스크톱 첫 화면은 Welcome Hero, 280px 좌측 메뉴, -목록 도구막대와 Footer를 고정하고 토론 목록만 스크롤한다. Reddit의 Card/Compact -전환에서 확인되는 연속 피드 구조를 참고하되 ABLESTACK의 블루 색상, 태그와 -댓글 수, 더보기 기능을 유지했다. 개별 행의 카드 테두리·그림자·들뜸은 제거하고 -한 개의 흰색 피드 표면, 1px 구분선, 파란 Hover/Focus 표시로 단순화했다. - -WSL 1280x720과 운영 1150x881에서 문서 `scrollY=0`을 유지한 채 목록 -`scrollTop`만 각각 변화했다. 운영 목록을 `420px` 스크롤한 뒤에도 메뉴 Y좌표 -`179.890625`, 도구막대 Y좌표 `209.890625`가 동일했다. 행 높이는 `58px`, -외부 HTTPS는 `200`, Nginx·PHP-FPM·MariaDB는 모두 `active`다. 운영 적용 전 -백업은 `/var/backups/techflow-flarum/theme-index-fixed-feed-v2-20260819T1140Z`에 -보존했다. SCP 임시 경로가 0700인 경우에도 배포 스크립트가 Extension 경로를 +운영 승인 후 사용자 프로필·설정 메뉴 겹침 보완과 첫 화면 Reddit형 평면 피드를 +WSL과 운영 Community에 적용했다. 데스크톱 첫 화면은 Welcome Hero, 280px 좌측 +메뉴와 Footer를 고정한다. 우측 `.IndexPage-results` 전체가 스크롤되므로 `최신` +선택 콤보가 있는 도구막대와 토론 행이 함께 이동한다. 좌측 메뉴는 높이가 부족할 +때 내부 스크롤할 수 있지만 스크롤바는 표시하지 않는다. 우측 스크롤바 역시 +숨기되 마우스 휠·트랙패드·키보드 스크롤 동작은 유지한다. + +목록 바깥 Container와 개별 행의 Border·Background·Shadow를 모두 제거했다. +Reddit New의 평면 피드처럼 투명한 배경 위에서 1px 구분선만 사용하고, +ABLESTACK의 블루 색상·폰트·태그·댓글 수·더보기 기능은 유지했다. + +WSL과 운영 1280x720에서 문서 `scrollY=0`, 좌측 메뉴 Y좌표 `179.890625`를 +유지했다. 우측 영역 `scrollTop`은 `0→360`, 도구막대 Y좌표는 +`209.890625→-150.109375`로 함께 이동했으며 `.DiscussionList`의 `scrollTop`은 +`0`을 유지했다. 좌측 메뉴는 `0→260`으로 내부 스크롤됐지만 스크롤바 계산값은 +`none`이었다. 행 높이는 `58px`, 외부 HTTPS는 `200`, Nginx·PHP-FPM·MariaDB는 +모두 `active`다. 운영 적용 전 백업은 +`/var/backups/techflow-flarum/theme-reddit-flat-feed-hidden-scroll-20260819T115539Z`에 +보존했다. +SCP 임시 경로가 0700인 경우에도 배포 스크립트가 Extension 경로를 Composer와 PHP-FPM이 읽을 수 있는 권한으로 정규화하는 것을 운영 재적용 `RESULT=PASS`로 확인했다. 상세 증적은 `docs/evidence/issue-73/community-index-fixed-feed-validation.json`에 기록했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 3a5c1ee..3661160 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -73,12 +73,17 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - `public/assets/forum-ko.js`가 비어 있지 않고 `core.forum.header.search_placeholder`를 포함해야 한다. - 데스크톱과 모바일에서 홈, 목록, 상세, 검색, 로그인, 글쓰기, 첨부를 확인한다. - 데스크톱 첫 화면에서 문서 전체의 `scrollY`가 `0`으로 유지되는지 확인한다. - Welcome Hero, 좌측 메뉴, 목록 도구막대와 Footer는 고정되어야 하며 - `.DiscussionList`의 `scrollTop`만 변해야 한다. 높이가 짧아 좌측 태그가 모두 - 보이지 않을 때만 좌측 메뉴 자체의 얇은 보조 스크롤을 허용한다. -- 토론 목록은 개별 행마다 카드 테두리·그림자를 중첩하지 않는다. 하나의 피드 - 표면 안에서 얇은 구분선으로 행을 나누고, 제목·태그·댓글 수·더보기 기능과 - 파란색 Hover/Focus 상태가 유지되는지 확인한다. + Welcome Hero, 좌측 메뉴와 Footer는 고정하고 `.IndexPage-results` 전체가 + 스크롤되어야 한다. `최신` 선택 콤보를 포함한 `.IndexPage-toolbar`와 토론 + 행이 함께 이동해야 하며 `.DiscussionList` 자체의 `scrollTop`은 `0`을 + 유지해야 한다. 우측 영역도 `scrollbar-width: none`과 WebKit 규칙으로 + 스크롤바를 표시하지 않는다. +- 좌측 메뉴는 사용자 설정 화면과 같이 고정한다. 높이가 짧아 태그가 모두 보이지 + 않을 때 내부 스크롤은 허용하지만 `scrollbar-width: none`과 WebKit 규칙으로 + 스크롤바는 표시하지 않는다. +- 토론 목록 바깥 Container와 개별 행에 Card Border·Background·Shadow를 + 만들지 않는다. 투명한 평면 피드에서 1px 구분선으로만 행을 나누고, + 제목·태그·댓글 수·더보기 기능과 파란색 Hover/Focus 상태를 유지한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. @@ -103,6 +108,6 @@ sudo systemctl restart php8.3-fpm nginx - GO: HTTP 200, 한글 원문 키 0건, 핵심 기능 정상, 콘텐츠·첨부 불변. - ROLLBACK: 화면·번역·로그인·작성·첨부 중 하나라도 실패. -- 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 첫 화면 고정 피드가 +- 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드가 WSL 및 운영에 반영되었으며, 운영 백업은 - `/var/backups/techflow-flarum/theme-index-fixed-feed-v2-20260819T1140Z`에 보존한다. + `/var/backups/techflow-flarum/theme-reddit-flat-feed-hidden-scroll-20260819T115539Z`에 보존한다. From 3f92e37d796a5f4125e1dac0118007e52d7b05d3 Mon Sep 17 00:00:00 2001 From: DH Park Date: Wed, 19 Aug 2026 21:31:45 +0900 Subject: [PATCH 06/18] =?UTF-8?q?Community=20=ED=86=A0=EB=A1=A0=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20Reddit=ED=98=95=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EB=AC=BC=20=EA=B5=AC=EC=A1=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/dist/forum.js | 161 ++++++++++ .../resources/less/forum.less | 301 +++++++++++++++++- deploy/flarum/tests/test_community_theme.py | 39 +++ ...community-index-fixed-feed-validation.json | 100 +++--- .../issue-73-community-ui-validation.md | 56 +++- .../community-theme-rollout-rollback.md | 14 +- 6 files changed, 619 insertions(+), 52 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js index 3be1ac9..7eb5804 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js +++ b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js @@ -5,6 +5,166 @@ var forumApp = forumAppModule.default || forumAppModule; var relativeDatePattern = /(?:방금|전$|후$)/; var absoluteDatePattern = /^(\d{4})-(\d{2})-(\d{2})/; + var discussionItemEnhancedAttribute = 'data-ablecloud-post-structure'; + + function formatDiscussionCreatedAt(createdAt) { + if (!(createdAt instanceof Date) || Number.isNaN(createdAt.getTime())) { + return ''; + } + + var elapsed = Math.max(0, Date.now() - createdAt.getTime()); + var minute = 60 * 1000; + var hour = 60 * minute; + var day = 24 * hour; + + if (elapsed < minute) { + return '방금'; + } + + if (elapsed < hour) { + return Math.floor(elapsed / minute) + '분 전'; + } + + if (elapsed < day) { + return Math.floor(elapsed / hour) + '시간 전'; + } + + if (elapsed < 7 * day) { + return Math.floor(elapsed / day) + '일 전'; + } + + return String(createdAt.getFullYear()).slice(-2) + '년 ' + (createdAt.getMonth() + 1) + '월 ' + createdAt.getDate() + '일'; + } + + function appendMetaPart(meta, className, text) { + if (!text) { + return; + } + + if (meta.childElementCount) { + var separator = document.createElement('span'); + separator.className = 'ablecloud-DiscussionMeta-separator'; + separator.setAttribute('aria-hidden', 'true'); + separator.textContent = '·'; + meta.appendChild(separator); + } + + var part = document.createElement('span'); + part.className = className; + part.textContent = text; + meta.appendChild(part); + } + + function buildDiscussionSummary(discussion) { + var firstPost = discussion && typeof discussion.firstPost === 'function' ? discussion.firstPost() : null; + + if (!firstPost || typeof firstPost.contentPlain !== 'function') { + return ''; + } + + return String(firstPost.contentPlain() || '') + .replace(/\s+/g, ' ') + .trim(); + } + + function findDiscussionThumbnail(discussion) { + var firstPost = discussion && typeof discussion.firstPost === 'function' ? discussion.firstPost() : null; + var contentHtml = firstPost && typeof firstPost.contentHtml === 'function' ? firstPost.contentHtml() : ''; + + if (!contentHtml) { + return null; + } + + var template = document.createElement('template'); + template.innerHTML = String(contentHtml); + var sourceImage = template.content.querySelector('img[src]'); + + if (!sourceImage) { + return null; + } + + var resolvedUrl; + + try { + resolvedUrl = new URL(sourceImage.getAttribute('src'), window.location.origin); + } catch (error) { + return null; + } + + if (!['http:', 'https:'].includes(resolvedUrl.protocol)) { + return null; + } + + return { + src: resolvedUrl.href, + alt: sourceImage.getAttribute('alt') || sourceImage.getAttribute('title') || '게시물 첨부 이미지 미리보기', + }; + } + + function enhanceDiscussionListItems(root) { + root.querySelectorAll('.DiscussionList-discussions > li[data-id]').forEach(function (listItem) { + if (listItem.getAttribute(discussionItemEnhancedAttribute) === 'true') { + return; + } + + var discussion = forumApp.store.getById('discussions', listItem.getAttribute('data-id')); + var content = listItem.querySelector('.DiscussionListItem-content'); + var main = content && content.querySelector('.DiscussionListItem-main'); + var title = main && main.querySelector('.DiscussionListItem-title'); + + if (!discussion || !content || !main || !title) { + return; + } + + var user = typeof discussion.user === 'function' ? discussion.user() : null; + var author = user && typeof user.displayName === 'function' ? user.displayName() : ''; + var category = main.querySelector('.TagLabel-name'); + var meta = document.createElement('div'); + meta.className = 'ablecloud-DiscussionMeta'; + meta.setAttribute('aria-label', '토론 작성 정보'); + appendMetaPart(meta, 'ablecloud-DiscussionMeta-category', category && category.textContent.trim()); + appendMetaPart(meta, 'ablecloud-DiscussionMeta-author', author); + appendMetaPart( + meta, + 'ablecloud-DiscussionMeta-time', + formatDiscussionCreatedAt(typeof discussion.createdAt === 'function' ? discussion.createdAt() : null) + ); + content.insertBefore(meta, main); + + var summaryText = buildDiscussionSummary(discussion); + + if (summaryText) { + var summary = document.createElement('p'); + summary.className = 'ablecloud-DiscussionSummary'; + summary.textContent = summaryText; + title.insertAdjacentElement('afterend', summary); + } + + var thumbnail = findDiscussionThumbnail(discussion); + + if (thumbnail) { + var thumbnailFrame = document.createElement('span'); + var thumbnailImage = document.createElement('img'); + thumbnailFrame.className = 'ablecloud-DiscussionThumbnail'; + thumbnailFrame.setAttribute('aria-hidden', 'true'); + thumbnailImage.src = thumbnail.src; + thumbnailImage.alt = thumbnail.alt; + thumbnailImage.loading = 'lazy'; + thumbnailImage.decoding = 'async'; + thumbnailFrame.appendChild(thumbnailImage); + main.appendChild(thumbnailFrame); + main.classList.add('ablecloud-DiscussionMain--withThumbnail'); + } + + var badges = content.querySelector('.DiscussionListItem-badges'); + + if (badges && badges.querySelector('.item-bestAnswer')) { + badges.setAttribute('aria-label', '해결된 토론'); + } + + listItem.setAttribute(discussionItemEnhancedAttribute, 'true'); + }); + } function formatTagDiscussionDates(root) { root.querySelectorAll('.TagTile-lastPostedDiscussion time[datetime]').forEach(function (time) { @@ -64,6 +224,7 @@ pendingFrame = window.requestAnimationFrame(function () { formatTagDiscussionDates(document); normalizeDiscussionListTargets(document); + enhanceDiscussionListItems(document); }); }; diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 23e446f..2e26771 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1208,6 +1208,64 @@ body { color: var(--ablecloud-brand-muted); } + .ablecloud-DiscussionMeta { + display: flex; + min-width: 0; + align-items: center; + gap: 6px; + overflow: hidden; + color: var(--ablecloud-brand-muted); + font-size: 12px; + font-weight: 560; + line-height: 20px; + white-space: nowrap; + } + + .ablecloud-DiscussionMeta-category { + overflow: hidden; + color: var(--ablecloud-brand-primary-deep); + font-weight: 720; + text-overflow: ellipsis; + } + + .ablecloud-DiscussionMeta-separator { + flex: 0 0 auto; + color: #9aacc3; + } + + .ablecloud-DiscussionMeta-author, + .ablecloud-DiscussionMeta-time { + flex: 0 0 auto; + } + + .ablecloud-DiscussionSummary { + display: -webkit-box; + margin: 6px 0 0; + overflow: hidden; + color: #536b89; + font-size: 14px; + font-weight: 430; + line-height: 1.55; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + white-space: normal; + } + + .ablecloud-DiscussionThumbnail { + display: block; + overflow: hidden; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 10px; + background: #eaf1fb; + } + + .ablecloud-DiscussionThumbnail > img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + } + .Badge--answered { background: #078248; color: #fff; @@ -2056,9 +2114,154 @@ body { } .App--index .IndexPage-results .DiscussionListItem-content { - min-height: 58px; - padding-top: 9px; - padding-bottom: 9px; + display: grid; + min-height: 136px; + grid-template-columns: auto minmax(0, 1fr); + column-gap: 8px; + align-content: start; + padding: 14px 64px 14px 64px; + } + + .App--index .IndexPage-results .DiscussionListItem-author { + position: absolute !important; + top: 15px; + left: 12px; + margin-left: 0 !important; + margin-top: 0 !important; + } + + .App--index .IndexPage-results .ablecloud-DiscussionMeta { + grid-row: 1; + grid-column: 2; + min-height: 20px; + padding-right: 0; + } + + .App--index .IndexPage-results .DiscussionListItem-content:not(:has(.item-bestAnswer)) .ablecloud-DiscussionMeta { + grid-column-start: 1; + grid-column-end: -1; + } + + .App--index .IndexPage-results .DiscussionListItem-main { + display: block; + grid-row: 2; + grid-column-start: 1; + grid-column-end: -1; + min-height: 0; + margin-top: 3px; + } + + .App--index .IndexPage-results .DiscussionListItem-title { + display: -webkit-box; + min-width: 0; + margin: 0 !important; + overflow: hidden; + color: var(--ablecloud-brand-ink); + font-size: 18px; + font-weight: 760; + line-height: 1.4; + text-overflow: initial; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + white-space: normal; + } + + .App--index .IndexPage-results .ablecloud-DiscussionMain--withThumbnail { + display: grid; + grid-template-columns: minmax(0, 1fr) 124px; + grid-template-rows: auto auto 1fr; + column-gap: 16px; + min-height: 92px; + } + + .App--index .IndexPage-results .ablecloud-DiscussionMain--withThumbnail .DiscussionListItem-title { + grid-row: 1; + grid-column: 1; + } + + .App--index .IndexPage-results .ablecloud-DiscussionMain--withThumbnail .ablecloud-DiscussionSummary { + grid-row: 2; + grid-column: 1; + } + + .App--index .IndexPage-results .ablecloud-DiscussionMain--withThumbnail .DiscussionListItem-info { + width: 100%; + max-width: 100%; + grid-row: 3; + grid-column: 1; + align-self: end; + } + + .App--index .IndexPage-results .ablecloud-DiscussionThumbnail { + width: 124px; + height: 92px; + grid-column: 2; + grid-row-start: 1; + grid-row-end: 4; + } + + .App--index .IndexPage-results .DiscussionListItem-info { + display: flex; + width: calc(~"100% - 96px"); + max-width: calc(~"100% - 96px"); + margin-top: 10px; + } + + .App--index .IndexPage-results .DiscussionListItem-info > .item-tags { + display: flex; + max-width: 100%; + overflow: hidden; + } + + .App--index .IndexPage-results .DiscussionListItem-badges { + position: static; + z-index: 2; + width: auto; + grid-row: 1; + grid-column: 1; + margin: 0; + } + + .App--index .IndexPage-results .DiscussionListItem-badges:empty { + display: none; + } + + .App--index .IndexPage-results .DiscussionListItem-badges .Badge--bestAnswer { + display: inline-flex; + width: auto; + min-width: 24px; + height: 24px; + align-items: center; + gap: 5px; + padding: 0 9px; + border-radius: 999px; + background: #d9f5e6; + color: #056038; + font-size: 12px; + font-weight: 730; + line-height: 24px; + } + + .App--index .IndexPage-results .DiscussionListItem-badges .Badge--bestAnswer::after { + content: "해결됨"; + } + + .App--index .IndexPage-results .DiscussionListItem-badges .Badge-icon { + font-size: 11px; + } + + .App--index .IndexPage-results .DiscussionListItem-count, + .App--index .IndexPage-results .DiscussionListItem:has(.DiscussionListItem-controls) .DiscussionListItem-count { + top: auto; + right: 14px; + bottom: 16px; + transform: none; + } + + .App--index .IndexPage-results .DiscussionListItem-controls { + top: 8px; + right: 8px; + transform: none; } .App--index .IndexPage-nav::-webkit-scrollbar { @@ -2532,8 +2735,69 @@ body { } .DiscussionListItem-content { - min-height: 88px; - padding: 17px 44px 17px 60px !important; + display: grid; + min-height: 136px; + grid-template-columns: auto minmax(0, 1fr); + column-gap: 7px; + align-content: start; + padding: 14px 44px 14px 60px !important; + } + + .ablecloud-DiscussionMeta { + grid-row: 1; + grid-column: 2; + padding-right: 8px; + font-size: 11px; + } + + .DiscussionListItem-content:not(:has(.item-bestAnswer)) .ablecloud-DiscussionMeta { + grid-column-start: 1; + grid-column-end: -1; + } + + .ablecloud-DiscussionMeta-category { + max-width: 42%; + } + + .ablecloud-DiscussionSummary { + margin-top: 5px; + font-size: 13px; + -webkit-line-clamp: 2; + } + + .ablecloud-DiscussionMain--withThumbnail { + display: grid; + grid-template-columns: minmax(0, 1fr) 88px; + grid-template-rows: auto auto 1fr; + column-gap: 10px; + min-height: 72px; + } + + .ablecloud-DiscussionMain--withThumbnail .DiscussionListItem-title, + .ablecloud-DiscussionMain--withThumbnail .ablecloud-DiscussionSummary, + .ablecloud-DiscussionMain--withThumbnail .DiscussionListItem-info { + grid-column: 1; + } + + .ablecloud-DiscussionMain--withThumbnail .DiscussionListItem-title { + grid-row: 1; + } + + .ablecloud-DiscussionMain--withThumbnail .ablecloud-DiscussionSummary { + grid-row: 2; + } + + .ablecloud-DiscussionMain--withThumbnail .DiscussionListItem-info { + grid-row: 3; + align-self: end; + } + + .ablecloud-DiscussionThumbnail { + width: 88px; + height: 72px; + grid-column: 2; + grid-row-start: 1; + grid-row-end: 4; } .DiscussionListItem-title { @@ -2547,10 +2811,37 @@ body { .DiscussionListItem-main { min-width: 0; width: 100% !important; + grid-row: 2; + grid-column-start: 1; + grid-column-end: -1; margin-right: 0 !important; display: block; } + .DiscussionListItem-info { + margin-top: 9px; + padding-right: 58px; + } + + .DiscussionListItem-count { + top: auto; + right: 12px; + bottom: 15px; + transform: none; + } + + .DiscussionListItem-badges { + position: static; + width: auto; + grid-row: 1; + grid-column: 1; + margin: 0; + } + + .DiscussionListItem-badges:empty { + display: none; + } + .IndexPage .DiscussionListItem-title, .DiscussionListItem-title { width: auto !important; diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index e9c671f..e35e6cb 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -137,6 +137,45 @@ def test_tag_date_format_contract(self) -> None: self.assertIn("window.location.assign(href)", self.forum_js) self.assertIn("document.addEventListener('click', openDiscussionFromTop, true)", self.forum_js) + def test_reddit_style_discussion_item_structure_contract(self) -> None: + for script_contract in ( + "enhanceDiscussionListItems", + "forumApp.store.getById('discussions'", + "discussion.firstPost()", + "firstPost.contentPlain()", + "ablecloud-DiscussionMeta", + "ablecloud-DiscussionSummary", + "formatDiscussionCreatedAt", + "data-ablecloud-post-structure", + "meta.setAttribute('aria-label', '토론 작성 정보')", + "badges.setAttribute('aria-label', '해결된 토론')", + "findDiscussionThumbnail", + "firstPost.contentHtml()", + "template.content.querySelector('img[src]')", + "thumbnailImage.loading = 'lazy'", + "ablecloud-DiscussionMain--withThumbnail", + "ablecloud-DiscussionThumbnail", + ): + with self.subTest(script_contract=script_contract): + self.assertIn(script_contract, self.forum_js) + + for style_contract in ( + ".ablecloud-DiscussionMeta", + ".ablecloud-DiscussionMeta-category", + ".ablecloud-DiscussionSummary", + "-webkit-line-clamp: 2", + ".App--index .IndexPage-results .DiscussionListItem-badges .Badge--bestAnswer::after", + 'content: "해결됨"', + "min-height: 136px", + "font-size: 18px", + ".ablecloud-DiscussionThumbnail", + "object-fit: cover", + "grid-template-columns: minmax(0, 1fr) 124px", + "grid-template-columns: minmax(0, 1fr) 88px", + ): + with self.subTest(style_contract=style_contract): + self.assertIn(style_contract, self.less) + def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn(".WelcomeHero .container", self.less) self.assertIn("font-size: 28px", self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 27c2b65..28b4a81 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,7 +1,7 @@ { - "schema_version": "1.1", - "executed_at": "2026-08-19T11:59:00Z", - "scope": "Flarum desktop index fixed navigation and Reddit-style flat discussion feed", + "schema_version": "1.2", + "executed_at": "2026-08-19T12:25:52Z", + "scope": "Flarum desktop index fixed navigation and Reddit-style discussion item structure", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", "change": { @@ -12,70 +12,92 @@ "toolbar_scrolls_with_feed": true, "feed": "flat transparent feed with row separators and no outer container card", "individual_row_card": false, - "row_height_px": 58, + "discussion_item_structure": [ + "author avatar", + "solved status and primary category, author, created time metadata", + "two-line title", + "first-post summary clamped to two lines", + "optional first-image thumbnail on the right", + "existing tag labels", + "reply count and existing discussion controls" + ], + "summary_source": "already-included Flarum firstPost.contentPlain model; no per-item API request", + "row_height_px": "variable, 137 to 218 in validated samples", "left_navigation_width_px": 280, "left_navigation_internal_scroll": "enabled when needed, scrollbar hidden" }, "wsl": { "environment": "WSL Ubuntu 24.04", "url": "http://localhost:18080/", - "theme_sha256": "1631ae28ae2703e212618b0807eba9d840df408d1a8f62d6224c3206b0d95ca3", "browser": { - "viewport": { "width": 1280, "height": 720 }, - "document_scroll_y": 0, - "document_scroll_height": 720, - "right_column_client_height": 454, - "right_column_scroll_height": 1307, - "right_column_scroll_top_before_after": [0, 360], - "discussion_list_client_height": 1229, - "discussion_list_scroll_height": 1229, - "discussion_list_scroll_top_before_after": [0, 0], - "discussion_list_scrollable": false, - "menu_y_before_after": [179.890625, 179.890625], - "toolbar_y_before_after": [209.890625, -150.109375], - "menu_scrollbar": "hidden", - "right_column_scrollbar": "hidden" + "viewport": { "width": 1680, "height": 881 }, + "enhanced_discussion_count": 20, + "sample_discussion_ids": [144, 140, 139, 138, 137], + "sample_row_heights_px": [137, 137, 137, 156, 156], + "title_line_height_px": 25, + "summary_line_heights_px": [22, 22, 22, 43, 43], + "first_item_tags": ["ABLESTACK VM", "ABLESTACK 오류/장애", "Mold"], + "thumbnail_count": 4, + "thumbnail_sample_discussion_id": 139, + "thumbnail_size_px": { "width": 124, "height": 92 }, + "thumbnail_loaded": true, + "thumbnail_source": "first image in firstPost.contentHtml", + "canonical_discussion_url": "http://localhost:18080/d/144-gasangmeosin-webkonsol-yeongyeol-oryu", + "discussion_open_scroll_y": 0, + "right_column_scrollbar": "hidden", + "left_navigation_scrollbar": "hidden" }, - "compiled_css_contains_feed_rule": true + "compiled_css_grid_contract": "grid-column-start:1;grid-column-end:-1", + "result": "PASS" }, "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-reddit-flat-feed-hidden-scroll-20260819T115539Z", - "theme_sha256": "1631ae28ae2703e212618b0807eba9d840df408d1a8f62d6224c3206b0d95ca3", + "backup_root": "/var/backups/techflow-flarum/theme-reddit-post-thumbnail-20260819T122946Z", + "theme_less_sha256": "f8c28acbe4aa4c38b6fd25dc3d941ebfbb1dd4c664f684ba4196c17aaa674b73", "services": { "nginx": "active", "php8.3-fpm": "active", - "mariadb": "active" + "mariadb": "active", + "techflow-community-monitor.timer": "active" }, + "local_http_status": 200, "external_https_status": 200, "browser": { - "viewport": { "width": 1280, "height": 720 }, - "document_scroll_y_before_after": [0, 0], - "right_column_scroll_top_before_after": [0, 360], - "discussion_list_scroll_top_before_after": [0, 0], - "menu_y_before_after": [179.890625, 179.890625], - "toolbar_y_before_after": [209.890625, -150.109375], - "right_column_client_height": 454, - "right_column_scroll_height": 1307, - "discussion_list_client_height": 1229, - "discussion_list_scroll_height": 1229, - "discussion_list_scrollable": false, - "menu_internal_scroll_top_before_after": [0, 260], + "viewport": { "width": 1680, "height": 881 }, + "enhanced_discussion_count": 20, + "solved_sample_discussion_id": 173, + "solved_sample_row_height_px": 187, + "plain_sample_discussion_id": 144, + "plain_sample_row_height_px": 137, + "thumbnail_count": 5, + "thumbnail_sample_discussion_id": 169, + "thumbnail_sample_row_height_px": 218, + "thumbnail_size_px": { "width": 124, "height": 92 }, + "thumbnail_image_loaded": true, + "thumbnail_natural_size_px": { "width": 1602, "height": 1210 }, + "avatar_position": "absolute at content top-left", + "avatar_inside_content": true, + "solved_badge_metadata_gap_px": 8, + "solved_badge_overlaps_metadata": false, + "metadata_text": "ABLESTACK v4.x Diplo·dhslove·1일 전", + "document_scroll_y": 0, "menu_scrollbar": "hidden", "right_column_scrollbar": "hidden", "feed_container_border": "none", "feed_container_background": "transparent", - "row_border": "none", "row_shadow": "none" }, - "compiled_css_contains_feed_rule": true + "result": "PASS" }, "validation": { - "theme_contract_tests": { "passed": 11, "failed": 0 }, + "theme_contract_tests": { "passed": 12, "failed": 0 }, + "discussion_item_enhancement_is_idempotent": true, + "summary_uses_existing_store_data": true, + "thumbnail_uses_existing_first_post_html": true, + "thumbnail_loading": "lazy", "deployment_script_normalizes_permissions": true, "deployment_script_production_result": "PASS", - "deployment_script_shell_syntax": "PASS", "git_diff_check": "PASS", "temporary_remote_assets_removed": true }, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index b595256..cc9488a 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -4,7 +4,7 @@ ABLESTACK 브랜드 기반 Flarum 전용 테마 확장을 구현하고 WSL Ubuntu 24.04의 Flarum 1.8.18 복제 환경에서 활성화, 기능 검증, 비활성화 롤백, 재활성화를 완료했다. 데스크톱 1440x900과 모바일 390x844에서 홈·목록·태그·검색·로그인·작성·프로필·설정·보안·해결 답변·게시물 작업 버튼을 실제 렌더링했으며 한글 원문 번역 키와 태그·프로필·설정·보안 직접 노출 영문은 모두 0건이었다. -Flarum Core, Vendor 원본, DB Schema와 Community 콘텐츠는 수정하지 않았다. 사용자 39명, 토의 117건, 게시물 305건, 콘텐츠와 첨부 해시는 전체 주기 전후 동일했다. 운영 Community에는 적용하지 않았으며 판정은 **운영 적용 전 Conditional GO**다. +Flarum Core, Vendor 원본, DB Schema와 Community 콘텐츠는 수정하지 않았다. 사용자 39명, 토의 117건, 게시물 305건, 콘텐츠와 첨부 해시는 전체 주기 전후 동일했다. 이후 승인된 후속 개선을 WSL과 운영 Community에 같은 독립 테마 확장으로 적용했으며 최종 판정은 **PASS**다. ## 완료 범위 @@ -36,7 +36,9 @@ Flarum Core, Vendor 원본, DB Schema와 Community 콘텐츠는 수정하지 않 ABLESTACK Primary `#155EEF`, Ink `#15253E`, Canvas `#F3F7FC`를 기준으로 헤더, Hero, 탐색, 토의 카드, 게시물, 검색, 로그인, 작성기와 첨부 버튼을 블루 계열로 통일했다. 배경에는 옅은 블루 Gradient를 사용하고 카드에는 14~16px Radius, 블루 계열 경계와 낮은 그림자를 적용했다. -브라우저 계산값으로 상세 게시물 카드는 데스크톱 `21px 25px 23px`, 모바일 `18px 16px 20px`의 내부 여백을 확보했다. 데스크톱 토론 목록은 제목·카테고리 태그·댓글 수를 같은 줄에 배치하고 카드 높이를 약 110px에서 `62px`로 줄였다. 모바일은 제목 2줄과 마지막 응답 정보를 유지한다. +브라우저 계산값으로 상세 게시물 카드는 데스크톱 `21px 25px 23px`, 모바일 `18px 16px 20px`의 내부 여백을 확보했다. 최종 토론 목록은 Reddit New의 게시물 한 건 구조를 참고해 작성자·분류·작성 시각, 제목, 첫 글 요약, 태그, 댓글 수를 위에서 아래로 읽도록 재구성했다. 제목과 요약은 각각 최대 두 줄이며 행 높이는 콘텐츠와 해결 상태에 따라 `137~187px`로 달라진다. + +첫 글 요약은 Flarum 목록 응답에 이미 포함된 `firstPost.contentPlain()`을 사용하므로 토론마다 별도 API 요청을 만들지 않는다. 해결 배지와 작성 정보는 같은 헤더 Grid의 독립 열에 배치하고 8px 간격을 보장한다. 사용자 아바타는 행 좌측 상단에 고정하며 해결 여부와 무관하게 본문 안에 완전히 표시된다. 운영 토론 #173에서 배지-작성 정보 겹침 `false`, 일반 토론 #144와 해결 토론 #173 모두 아바타가 본문 경계 안에 있음을 브라우저 계산값으로 확인했다. 기존 목록 링크는 `/d/{slug}/4`처럼 마지막 게시물 번호를 포함해 토론을 열자마자 마지막 응답으로 이동했다. 목록의 `.DiscussionListItem-main` 링크에 한해서 게시물 번호를 제거한 토론 기본 주소로 정규화하고, 클릭 시 해당 주소를 새 문서로 열도록 했다. 브라우저 검증에서 경로는 `/d/144-gasangmeosin-webkonsol-yeongyeol-oryu`, `scrollY=0`, 첫 게시물은 화면 안에 표시됐다. 알림이나 상세 내부의 특정 답변 링크는 변경하지 않는다. @@ -123,7 +125,7 @@ Hero의 위·아래 패딩은 데스크톱 `19px`, 모바일 `17px`로 대칭화 | 화면 | 검증 내용 | 결과 | |---|---|---| -| 홈 Desktop | 브랜드 Hero, 좌측 탐색, 62px 단일 행 목록 | PASS | +| 홈 Desktop | 브랜드 Hero, 좌측 탐색, 작성 정보·제목·요약·태그·댓글로 구성한 137~187px 가변 높이 목록 | PASS | | 목록→상세 진입 | 게시물 번호 제거, `scrollY=0`, 첫 게시물 표시 | PASS | | 토론 목록 메뉴 | 테두리·배경·그림자 없는 이클립스 아이콘 상시 표시, 세로 중심 오차 0px, 메뉴 `z-index: 1080`, 뒤쪽 행 가림 없음 | PASS | | 상세 목록 패널 | 400px 카드 목록, Header 겹침 0px, 6px 스크롤바, 카드 간격 8px, 현재 토론 강조 | PASS | @@ -155,7 +157,7 @@ Hero의 위·아래 패딩은 데스크톱 `19px`, 모바일 `17px`로 대칭화 ## 운영 영향과 남은 결정 -- 운영 Community 변경: 없음 +- 운영 Community 변경: 승인된 `ablecloud/community-theme` 화면 자산 적용 - GitHub→Chat 보호 서비스 변경: 없음 - TechFlow AI Gateway·Poller 변경: 없음 - 업로드 1 GiB/10 GiB 정책 변경: 없음 @@ -165,7 +167,7 @@ Hero의 위·아래 패딩은 데스크톱 `19px`, 모바일 `17px`로 대칭화 ## 판정 -Issue #73의 설계·구현·WSL 기능 검증·롤백·문서화 조건을 충족했다. 운영 적용은 별도 승인 전까지 수행하지 않는다. 검토자가 운영 반영을 승인하면 Runbook에 따라 테마 확장만 적용하고 같은 검증을 반복한다. +Issue #73의 설계·구현·WSL 기능 검증·롤백·운영 적용·문서화 조건을 충족했다. 운영 적용은 테마 확장에만 한정했으며 Runbook의 백업·배포·브라우저 검증·서비스 확인 절차를 모두 통과했다. ## 2026-08-19 운영 후속 적용 @@ -192,3 +194,47 @@ SCP 임시 경로가 0700인 경우에도 배포 스크립트가 Extension 경 Composer와 PHP-FPM이 읽을 수 있는 권한으로 정규화하는 것을 운영 재적용 `RESULT=PASS`로 확인했다. 상세 증적은 `docs/evidence/issue-73/community-index-fixed-feed-validation.json`에 기록했다. + +### Reddit형 게시물 한 건 구조 최종 적용 + +평면 피드의 색상·폰트·1px 구분선은 유지하면서 각 토론 한 건의 내부 구조만 +Reddit New에 맞춰 재구성했다. 사용자 아바타 다음에 해결 상태, 대표 카테고리, +작성자와 작성 시각을 한 행으로 표시하고, 제목·첫 게시물 요약·전체 태그·댓글 수를 +차례로 배치한다. 요약은 이미 내려온 Flarum Store 데이터만 사용하며 항목별 추가 +네트워크 요청은 없다. + +초기 운영 확인에서 해결 배지 폭과 기존 배지 Container 폭이 달라 작성 정보를 +가리는 현상이 확인되었다. 배지와 작성 정보를 Grid의 두 열로 분리하고 LESS에서 +나눗셈으로 오해되는 `grid-column: 1 / -1` 대신 `grid-column-start/end`를 명시했다. +사용자 아바타에는 기존 Flarum의 `margin-left: -52px`가 남아 좌측으로 잘렸으므로 +`position: absolute`, `left: 12px`, `margin-left: 0`을 함께 고정했다. + +최종 운영 브라우저 검증 결과는 다음과 같다. + +| 항목 | 결과 | +|---|---| +| 구조가 적용된 현재 목록 | 20건 | +| 해결 토론 #173 행 높이 | 187px | +| 일반 토론 #144 행 높이 | 137px | +| 해결 배지와 작성 정보 간격 | 8px | +| 해결 배지와 작성 정보 겹침 | 없음 | +| 해결·일반 토론 아바타 | 모두 행 좌측 상단, 본문 경계 안 | +| 이미지 포함 토론 | 첫 이미지 124×92px 우측 썸네일, 지연 로딩 | +| 이미지 없는 토론 | 썸네일 열 없이 기존 폭 유지 | +| 문서 `scrollY` | 0 | +| 좌측·우측 스크롤바 | 숨김 유지 | +| 외부 HTTPS | 200 | +| Nginx·PHP-FPM·MariaDB·Monitor Timer | 모두 active | + +첫 게시물 HTML에 이미지가 있으면 첫 이미지를 목록 오른쪽에 Reddit형 썸네일로 +표시한다. Flarum Store에 이미 포함된 `firstPost.contentHtml()`을 읽으므로 토론별 +추가 API 요청은 없고, `loading=lazy`와 `decoding=async`로 화면 가까이에 올 때만 +이미지를 불러온다. 허용 URL은 HTTP·HTTPS로 제한하며 이미지가 없는 글은 별도 +공간을 만들지 않는다. WSL 토론 #139와 운영 토론 #169에서 124×92px 썸네일, +원본 이미지 정상 로드, 사용자 아이콘·해결 배지·제목·요약 비중첩을 확인했다. + +최종 운영 백업은 +`/var/backups/techflow-flarum/theme-reddit-post-thumbnail-20260819T122946Z`에 +보존한다. 최종 LESS SHA-256은 +`f8c28acbe4aa4c38b6fd25dc3d941ebfbb1dd4c664f684ba4196c17aaa674b73`이며, +임시 전송 자산은 삭제했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 3661160..d8da4f8 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -83,7 +83,14 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 스크롤바는 표시하지 않는다. - 토론 목록 바깥 Container와 개별 행에 Card Border·Background·Shadow를 만들지 않는다. 투명한 평면 피드에서 1px 구분선으로만 행을 나누고, - 제목·태그·댓글 수·더보기 기능과 파란색 Hover/Focus 상태를 유지한다. + 작성 정보·제목·첫 글 요약·태그·댓글 수·더보기 기능과 파란색 Hover/Focus + 상태를 유지한다. +- 해결 배지와 대표 카테고리·작성자·작성 시각은 같은 헤더 행에서 서로 겹치지 + 않아야 한다. 사용자 아바타는 해결 여부와 관계없이 행 좌측 상단의 본문 경계 + 안에 있어야 한다. +- 첫 게시물에 이미지가 있으면 첫 이미지를 데스크톱 124×92px, 모바일 88×72px + 우측 썸네일로 표시한다. 이미지가 없는 글에는 빈 썸네일 영역을 만들지 않으며, + 썸네일은 기존 Store 데이터에서 생성하고 지연 로딩해야 한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. @@ -108,6 +115,7 @@ sudo systemctl restart php8.3-fpm nginx - GO: HTTP 200, 한글 원문 키 0건, 핵심 기능 정상, 콘텐츠·첨부 불변. - ROLLBACK: 화면·번역·로그인·작성·첨부 중 하나라도 실패. -- 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드가 +- 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드·게시물 + 정보 계층·이미지 썸네일이 WSL 및 운영에 반영되었으며, 운영 백업은 - `/var/backups/techflow-flarum/theme-reddit-flat-feed-hidden-scroll-20260819T115539Z`에 보존한다. + `/var/backups/techflow-flarum/theme-reddit-post-thumbnail-20260819T122946Z`에 보존한다. From 9a8ac2bc7bdab95ee5931f016b379f93eb21aa1a Mon Sep 17 00:00:00 2001 From: DH Park Date: Wed, 19 Aug 2026 21:50:16 +0900 Subject: [PATCH 07/18] =?UTF-8?q?Community=20=ED=91=B8=ED=84=B0=EB=A5=BC?= =?UTF-8?q?=20=EC=9A=B0=EC=B8=A1=20=EA=B3=A0=EC=A0=95=20=ED=80=B5=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/less/forum.less | 73 +++++++++++++------ deploy/flarum/tests/test_community_theme.py | 14 +++- ...community-index-fixed-feed-validation.json | 51 ++++++++++--- .../issue-73-community-ui-validation.md | 33 ++++++++- .../community-theme-rollout-rollback.md | 21 ++++-- 5 files changed, 145 insertions(+), 47 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 2e26771..ff0244f 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1459,37 +1459,51 @@ body { } .App-content { - padding-bottom: 64px; + padding-bottom: 24px; } + // The legacy footer bar is replaced with a fixed vertical quick-link rail. + // Keep the existing links and accessible names without reserving a bottom row. #console-nav-footer { - height: 56px; - padding: 0; - border-top: 1px solid var(--ablecloud-brand-border); - background: rgba(255, 255, 255, 0.97); - box-shadow: 0 -6px 20px rgba(24, 71, 142, 0.08); + position: fixed; + top: 50%; + right: max(12px, calc(~"(100vw - 1320px) / 2 + 16px")); + bottom: auto; + left: auto; + width: 48px; + height: auto; + padding: 8px 0; + border: 1px solid #c3d5ef; + border-radius: 999px; + background: rgba(255, 255, 255, 0.94); + box-shadow: 0 8px 24px rgba(24, 71, 142, 0.12); + backdrop-filter: blur(12px); + transform: translateY(-50%); z-index: 1030; } #footer-content { display: flex; - width: 100%; - max-width: 1100px; + width: auto; + max-width: none; + flex-direction: column; align-items: center; - justify-content: space-between; - height: 100%; + justify-content: center; + gap: 8px; + height: auto; box-sizing: border-box; - margin: 0 auto; - padding: 0 15px; + margin: 0; + padding: 0; } #floatleft, #floatright { display: flex; float: none; + flex-direction: column; align-items: center; - gap: 9px; - height: 100%; + gap: 8px; + height: auto; margin: 0; } @@ -1996,9 +2010,9 @@ body { } body { - // Keep the desktop index shell anchored between the fixed header and - // footer. The full right column scrolls, including its sort/filter tools, - // while the navigation follows the fixed User > Settings pattern. + // Keep the desktop index shell anchored below the fixed header. The full + // right column scrolls while the vertical quick-link rail stays outside + // the widened content area. .App--index { height: 100%; min-height: 0; @@ -2010,7 +2024,7 @@ body { position: fixed; top: 52px; right: 0; - bottom: 56px; + bottom: 0; left: 0; height: auto; box-sizing: border-box; @@ -2033,8 +2047,12 @@ body { } .App--index .IndexPage > .container { + width: 100%; + max-width: 1320px; flex: 1 1 auto; min-height: 0; + box-sizing: border-box; + padding-right: 80px; padding-bottom: 0; overflow: hidden; } @@ -2869,23 +2887,30 @@ body { } .App-content { - padding-bottom: 58px; + padding-bottom: 16px; } #console-nav-footer { - height: 52px; - padding: 0; + top: auto; + right: 8px; + bottom: 16px; + width: 44px; + height: auto; + padding: 6px 0; + transform: none; } #footer-content { + flex-direction: column; justify-content: center; - gap: 16px; - padding: 0 12px; + gap: 7px; + padding: 0; } #floatleft, #floatright { - gap: 8px; + flex-direction: column; + gap: 7px; } #footer-content .feedback-menu-left, diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index e35e6cb..7c6104c 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -107,7 +107,7 @@ def test_embedded_svg_icon_and_navigation_contract(self) -> None: self.assertIn("font-size: 18px", self.less) self.assertIn(".Header-secondary > ul", self.less) self.assertIn("height: 44px", self.less) - self.assertIn("max-width: 1100px", self.less) + self.assertIn("max-width: 1320px", self.less) self.assertIn("--ablecloud-footer-icon", self.less) self.assertGreaterEqual(len(re.findall(r"min-height:\s*44px", self.less)), 3) self.assertGreaterEqual(contrast("#155eef", "#ffffff"), 4.5) @@ -257,7 +257,7 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("body:has(> .App--index)", self.less) self.assertIn(".App--index .IndexPage-results .DiscussionList", self.less) self.assertIn(".App--index .IndexPage-results .DiscussionList-discussions", self.less) - self.assertIn("full right column scrolls", self.less) + self.assertIn("vertical quick-link rail stays outside", self.less) self.assertIn("Follow Reddit's flat feed composition", self.less) self.assertIn(".App--index .IndexPage-results::-webkit-scrollbar", self.less) self.assertIn(".App--index .IndexPage-nav::-webkit-scrollbar", self.less) @@ -268,7 +268,8 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("background: rgba(21, 94, 239, 0.055)", self.less) self.assertIn("overscroll-behavior: contain", self.less) self.assertIn("position: fixed", self.less) - self.assertIn("bottom: 56px", self.less) + self.assertIn("bottom: 0", self.less) + self.assertNotIn("bottom: 56px", self.less) deploy_script = (ROOT / "deploy" / "flarum" / "apply-community-theme-update.sh").read_text(encoding="utf-8") self.assertIn('chown -R www-data:www-data "$extension_root"', deploy_script) self.assertIn('find "$extension_root" -type d -exec chmod 0755 {} +', deploy_script) @@ -313,6 +314,13 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("#floatright .feedback-menu-right:first-of-type", self.less) self.assertIn("#floatright .feedback-menu-right:last-of-type", self.less) self.assertIn("mask-image: var(--ablecloud-footer-icon)", self.less) + self.assertIn("legacy footer bar is replaced with a fixed vertical quick-link rail", self.less) + self.assertIn('right: max(12px, calc(~"(100vw - 1320px) / 2 + 16px"))', self.less) + self.assertIn("flex-direction: column", self.less) + self.assertIn("max-width: 1320px", self.less) + self.assertIn("padding-right: 80px", self.less) + self.assertIn("transform: translateY(-50%)", self.less) + self.assertIn("bottom: 16px", self.less) def test_rehearsal_is_staging_only_and_preserves_content(self) -> None: self.assertIn('APP_ROOT" == "/srv/techflow-flarum-staging/app"', self.rehearsal) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 28b4a81..542266e 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,12 +1,12 @@ { - "schema_version": "1.2", - "executed_at": "2026-08-19T12:25:52Z", - "scope": "Flarum desktop index fixed navigation and Reddit-style discussion item structure", + "schema_version": "1.3", + "executed_at": "2026-08-19T12:47:28Z", + "scope": "Flarum desktop index fixed navigation, Reddit-style discussion item structure, and right-side quick links", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", "change": { "document_scroll": "disabled on desktop index", - "fixed_regions": ["welcome hero", "left navigation", "footer"], + "fixed_regions": ["welcome hero", "left navigation", "right quick-link rail"], "scroll_region": ".IndexPage-results including .IndexPage-toolbar and .DiscussionList, scrollbar hidden", "discussion_list_owns_scroll": false, "toolbar_scrolls_with_feed": true, @@ -24,13 +24,20 @@ "summary_source": "already-included Flarum firstPost.contentPlain model; no per-item API request", "row_height_px": "variable, 137 to 218 in validated samples", "left_navigation_width_px": 280, - "left_navigation_internal_scroll": "enabled when needed, scrollbar hidden" + "left_navigation_internal_scroll": "enabled when needed, scrollbar hidden", + "bottom_footer": "removed from visual layout", + "quick_links": ["ABLECLOUD Home", "Blog", "Online Docs"], + "quick_link_layout": "fixed vertical rail on the right, independent of feed scroll", + "desktop_container_width_px": 1320, + "desktop_feed_width_px": 913, + "desktop_feed_to_quick_link_gap_px": 200, + "reclaimed_footer_height_px": 56 }, "wsl": { "environment": "WSL Ubuntu 24.04", "url": "http://localhost:18080/", "browser": { - "viewport": { "width": 1680, "height": 881 }, + "viewport": { "width": 1680, "height": 825 }, "enhanced_discussion_count": 20, "sample_discussion_ids": [144, 140, 139, 138, 137], "sample_row_heights_px": [137, 137, 137, 156, 156], @@ -45,7 +52,18 @@ "canonical_discussion_url": "http://localhost:18080/d/144-gasangmeosin-webkonsol-yeongyeol-oryu", "discussion_open_scroll_y": 0, "right_column_scrollbar": "hidden", - "left_navigation_scrollbar": "hidden" + "left_navigation_scrollbar": "hidden", + "container_width_px": 1320, + "feed_width_px": 913, + "feed_height_px": 615, + "quick_link_rail_size_px": { "width": 48, "height": 148 }, + "quick_link_count": 3, + "quick_link_rail_y_before_scroll_px": 339, + "quick_link_rail_y_after_feed_scroll_px": 339, + "feed_scroll_top_px": 560, + "feed_to_quick_link_gap_px": 200, + "mobile_viewport": { "width": 390, "height": 844 }, + "mobile_quick_link_rail_size_px": { "width": 44, "height": 136 } }, "compiled_css_grid_contract": "grid-column-start:1;grid-column-end:-1", "result": "PASS" @@ -53,8 +71,8 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-reddit-post-thumbnail-20260819T122946Z", - "theme_less_sha256": "f8c28acbe4aa4c38b6fd25dc3d941ebfbb1dd4c664f684ba4196c17aaa674b73", + "backup_root": "/var/backups/techflow-flarum/theme-vertical-quick-links-20260819T124728Z", + "theme_less_sha256": "cacc3fbbda3893ad481f6c4f78a406315ce45cfd6a86269ffd7d1a662072ab07", "services": { "nginx": "active", "php8.3-fpm": "active", @@ -64,7 +82,7 @@ "local_http_status": 200, "external_https_status": 200, "browser": { - "viewport": { "width": 1680, "height": 881 }, + "viewport": { "width": 1680, "height": 825 }, "enhanced_discussion_count": 20, "solved_sample_discussion_id": 173, "solved_sample_row_height_px": 187, @@ -86,7 +104,18 @@ "right_column_scrollbar": "hidden", "feed_container_border": "none", "feed_container_background": "transparent", - "row_shadow": "none" + "row_shadow": "none", + "container_width_px": 1320, + "feed_width_px": 913, + "feed_height_px": 615, + "quick_link_rail_size_px": { "width": 48, "height": 148 }, + "quick_link_count": 3, + "quick_link_names": ["ABLECLOUD Home", "Blog", "Online Docs"], + "feed_to_quick_link_gap_px": 200, + "quick_link_rail_y_before_scroll_px": 339, + "quick_link_rail_y_after_feed_scroll_px": 339, + "feed_scroll_top_px": 420, + "bottom_footer_visible": false }, "result": "PASS" }, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index cc9488a..08e58a8 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -146,7 +146,7 @@ Hero의 위·아래 패딩은 데스크톱 `19px`, 모바일 `17px`로 대칭화 | 프로필 | 165px 블루 Hero, 96px 아바타, 280px 메뉴, 모바일 한 열, 직접 영문 0건 | PASS | | 설정 | 780px 콘텐츠 카드, 공통 버튼·알림 표, 직접 영문 0건, 모바일 표 302px·가로 넘침 0px | PASS | | 보안 | 토큰·세션·로그아웃 공통 카드, `새 토큰` 한글화, 모바일 카드 336px·가로 넘침 0px | PASS | -| Header·Footer | 공통 1100px 컨테이너·15px 내부 여백, 원형 SVG 링크, 콘텐츠 겹침 방지 | PASS | +| Header·퀵 링크 | 하단 Footer 제거, 1320px 콘텐츠 컨테이너, 우측 세로형 원형 SVG 링크 3개, 콘텐츠 겹침 방지 | PASS | | 글쓰기·첨부 | Composer·FoF Upload 스타일 계약, Issue #72 정책 불변 | PASS | ## 롤백 결과 @@ -238,3 +238,34 @@ Reddit New에 맞춰 재구성했다. 사용자 아바타 다음에 해결 상 보존한다. 최종 LESS SHA-256은 `f8c28acbe4aa4c38b6fd25dc3d941ebfbb1dd4c664f684ba4196c17aaa674b73`이며, 임시 전송 자산은 삭제했다. + +### 하단 Footer 제거와 우측 고정 퀵 링크 적용 + +화면 하단을 계속 차지하던 56px 고정 Footer는 시각적 레이아웃에서 제거했다. +기존 `ABLECLOUD Home`, `Blog`, `Online Docs` 링크와 접근 가능한 이름은 그대로 +유지하되, 콘텐츠 오른쪽에 원형 아이콘 3개를 세로로 배치한 고정 퀵 링크로 +전환했다. 퀵 링크는 토론 목록의 스크롤 영역 밖에 있으므로 목록을 이동해도 +위치가 변하지 않는다. + +Footer가 차지하던 하단 공간을 회수하고 퀵 링크와 콘텐츠가 겹치지 않도록 +데스크톱 컨테이너를 1100px에서 1320px로 넓혔다. 1680×825 화면에서 실제 토론 +목록 폭은 기존 758px에서 913px로 155px 증가했고, 목록 오른쪽과 퀵 링크 +사이에는 200px 간격이 유지됐다. 목록 높이는 615px이며 화면 하단까지 사용할 +수 있다. + +| 검증 항목 | WSL | 운영 | +|---|---:|---:| +| 컨테이너 폭 | 1320px | 1320px | +| 토론 목록 폭 | 913px | 913px | +| 퀵 링크 크기 | 48×148px | 48×148px | +| 퀵 링크 개수 | 3 | 3 | +| 목록-퀵 링크 간격 | 200px | 200px | +| 목록 스크롤 후 퀵 링크 Y | 339px → 339px | 339px → 339px | +| 하단 고정 Footer | 없음 | 없음 | + +모바일 390×844에서는 44×136px 퀵 링크를 화면 오른쪽 아래에 배치해 본문 폭을 +침범하지 않도록 했다. WSL과 운영의 데스크톱·모바일 브라우저 검증, 테마 계약 +테스트 12건, 서비스 상태와 HTTP 200 검사를 모두 통과했다. 운영 적용 전 백업은 +`/var/backups/techflow-flarum/theme-vertical-quick-links-20260819T124728Z`에 +보존했고 최종 LESS SHA-256은 +`cacc3fbbda3893ad481f6c4f78a406315ce45cfd6a86269ffd7d1a662072ab07`이다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index d8da4f8..0e3897c 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -73,11 +73,16 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - `public/assets/forum-ko.js`가 비어 있지 않고 `core.forum.header.search_placeholder`를 포함해야 한다. - 데스크톱과 모바일에서 홈, 목록, 상세, 검색, 로그인, 글쓰기, 첨부를 확인한다. - 데스크톱 첫 화면에서 문서 전체의 `scrollY`가 `0`으로 유지되는지 확인한다. - Welcome Hero, 좌측 메뉴와 Footer는 고정하고 `.IndexPage-results` 전체가 - 스크롤되어야 한다. `최신` 선택 콤보를 포함한 `.IndexPage-toolbar`와 토론 - 행이 함께 이동해야 하며 `.DiscussionList` 자체의 `scrollTop`은 `0`을 - 유지해야 한다. 우측 영역도 `scrollbar-width: none`과 WebKit 규칙으로 - 스크롤바를 표시하지 않는다. + Welcome Hero와 좌측 메뉴는 고정하고 `.IndexPage-results` 전체가 스크롤되어야 + 한다. `최신` 선택 콤보를 포함한 `.IndexPage-toolbar`와 토론 행이 함께 + 이동해야 하며 `.DiscussionList` 자체의 `scrollTop`은 `0`을 유지해야 한다. + 우측 영역도 `scrollbar-width: none`과 WebKit 규칙으로 스크롤바를 표시하지 + 않는다. 화면 하단에는 Footer가 없어야 한다. +- 기존 Footer의 `ABLECLOUD Home`, `Blog`, `Online Docs` 링크는 콘텐츠 오른쪽의 + 세로형 원형 퀵 링크로 보여야 한다. 데스크톱에서는 48px 폭, 모바일에서는 + 44px 폭이며 토론 목록을 스크롤해도 화면상 Y 좌표가 변하지 않아야 한다. +- 데스크톱 첫 화면의 콘텐츠 컨테이너는 최대 1320px이며, 오른쪽에 80px의 + 안전 여백을 두어 목록과 퀵 링크가 겹치지 않아야 한다. - 좌측 메뉴는 사용자 설정 화면과 같이 고정한다. 높이가 짧아 태그가 모두 보이지 않을 때 내부 스크롤은 허용하지만 `scrollbar-width: none`과 WebKit 규칙으로 스크롤바는 표시하지 않는다. @@ -116,6 +121,6 @@ sudo systemctl restart php8.3-fpm nginx - GO: HTTP 200, 한글 원문 키 0건, 핵심 기능 정상, 콘텐츠·첨부 불변. - ROLLBACK: 화면·번역·로그인·작성·첨부 중 하나라도 실패. - 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드·게시물 - 정보 계층·이미지 썸네일이 - WSL 및 운영에 반영되었으며, 운영 백업은 - `/var/backups/techflow-flarum/theme-reddit-post-thumbnail-20260819T122946Z`에 보존한다. + 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크가 WSL 및 + 운영에 반영되었으며, 최신 운영 백업은 + `/var/backups/techflow-flarum/theme-vertical-quick-links-20260819T124728Z`에 보존한다. From 34553183d4091c00d8671c9de6b7233148af236d Mon Sep 17 00:00:00 2001 From: DH Park Date: Wed, 19 Aug 2026 21:57:21 +0900 Subject: [PATCH 08/18] =?UTF-8?q?Community=20=ED=80=B5=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EB=A5=BC=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=B0=94?= =?UTF-8?q?=EB=A1=9C=20=EC=98=86=EC=97=90=20=EB=B0=B0=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/less/forum.less | 11 +++--- deploy/flarum/tests/test_community_theme.py | 8 ++--- ...community-index-fixed-feed-validation.json | 34 +++++++++++-------- .../issue-73-community-ui-validation.md | 23 +++++++------ .../community-theme-rollout-rollback.md | 8 +++-- 5 files changed, 47 insertions(+), 37 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index ff0244f..275d5dd 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1467,7 +1467,8 @@ body { #console-nav-footer { position: fixed; top: 50%; - right: max(12px, calc(~"(100vw - 1320px) / 2 + 16px")); + // Escape the whole function so LESS does not fold max() to 12px at build time. + right: ~"max(12px, calc((100vw - 1165px) / 2 + 16px))"; bottom: auto; left: auto; width: 48px; @@ -2011,8 +2012,8 @@ body { body { // Keep the desktop index shell anchored below the fixed header. The full - // right column scrolls while the vertical quick-link rail stays outside - // the widened content area. + // right column scrolls while a narrow reserved rail sits immediately next + // to the unchanged 1100px content area. .App--index { height: 100%; min-height: 0; @@ -2048,7 +2049,9 @@ body { .App--index .IndexPage > .container { width: 100%; - max-width: 1320px; + // Preserve the original 1100px content width and add only the 65px + // needed for the 48px quick-link rail and its 17px separation. + max-width: 1165px; flex: 1 1 auto; min-height: 0; box-sizing: border-box; diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 7c6104c..2508bda 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -107,7 +107,7 @@ def test_embedded_svg_icon_and_navigation_contract(self) -> None: self.assertIn("font-size: 18px", self.less) self.assertIn(".Header-secondary > ul", self.less) self.assertIn("height: 44px", self.less) - self.assertIn("max-width: 1320px", self.less) + self.assertIn("max-width: 1165px", self.less) self.assertIn("--ablecloud-footer-icon", self.less) self.assertGreaterEqual(len(re.findall(r"min-height:\s*44px", self.less)), 3) self.assertGreaterEqual(contrast("#155eef", "#ffffff"), 4.5) @@ -257,7 +257,7 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("body:has(> .App--index)", self.less) self.assertIn(".App--index .IndexPage-results .DiscussionList", self.less) self.assertIn(".App--index .IndexPage-results .DiscussionList-discussions", self.less) - self.assertIn("vertical quick-link rail stays outside", self.less) + self.assertIn("reserved rail sits immediately next", self.less) self.assertIn("Follow Reddit's flat feed composition", self.less) self.assertIn(".App--index .IndexPage-results::-webkit-scrollbar", self.less) self.assertIn(".App--index .IndexPage-nav::-webkit-scrollbar", self.less) @@ -315,9 +315,9 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("#floatright .feedback-menu-right:last-of-type", self.less) self.assertIn("mask-image: var(--ablecloud-footer-icon)", self.less) self.assertIn("legacy footer bar is replaced with a fixed vertical quick-link rail", self.less) - self.assertIn('right: max(12px, calc(~"(100vw - 1320px) / 2 + 16px"))', self.less) + self.assertIn('right: ~"max(12px, calc((100vw - 1165px) / 2 + 16px))"', self.less) self.assertIn("flex-direction: column", self.less) - self.assertIn("max-width: 1320px", self.less) + self.assertIn("max-width: 1165px", self.less) self.assertIn("padding-right: 80px", self.less) self.assertIn("transform: translateY(-50%)", self.less) self.assertIn("bottom: 16px", self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 542266e..eaee0a4 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,6 +1,6 @@ { - "schema_version": "1.3", - "executed_at": "2026-08-19T12:47:28Z", + "schema_version": "1.4", + "executed_at": "2026-08-19T12:55:23Z", "scope": "Flarum desktop index fixed navigation, Reddit-style discussion item structure, and right-side quick links", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", @@ -28,9 +28,11 @@ "bottom_footer": "removed from visual layout", "quick_links": ["ABLECLOUD Home", "Blog", "Online Docs"], "quick_link_layout": "fixed vertical rail on the right, independent of feed scroll", - "desktop_container_width_px": 1320, - "desktop_feed_width_px": 913, - "desktop_feed_to_quick_link_gap_px": 200, + "original_content_width_px": 1100, + "desktop_shell_width_px": 1165, + "quick_link_area_expansion_px": 65, + "desktop_feed_width_px": 758, + "desktop_feed_to_quick_link_gap_px": 16, "reclaimed_footer_height_px": 56 }, "wsl": { @@ -53,15 +55,16 @@ "discussion_open_scroll_y": 0, "right_column_scrollbar": "hidden", "left_navigation_scrollbar": "hidden", - "container_width_px": 1320, - "feed_width_px": 913, + "shell_width_px": 1165, + "preserved_content_width_px": 1100, + "feed_width_px": 758, "feed_height_px": 615, "quick_link_rail_size_px": { "width": 48, "height": 148 }, "quick_link_count": 3, "quick_link_rail_y_before_scroll_px": 339, "quick_link_rail_y_after_feed_scroll_px": 339, - "feed_scroll_top_px": 560, - "feed_to_quick_link_gap_px": 200, + "feed_scroll_top_px": 360, + "feed_to_quick_link_gap_px": 16, "mobile_viewport": { "width": 390, "height": 844 }, "mobile_quick_link_rail_size_px": { "width": 44, "height": 136 } }, @@ -71,8 +74,8 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-vertical-quick-links-20260819T124728Z", - "theme_less_sha256": "cacc3fbbda3893ad481f6c4f78a406315ce45cfd6a86269ffd7d1a662072ab07", + "backup_root": "/var/backups/techflow-flarum/theme-adjacent-quick-links-20260819T125523Z", + "theme_less_sha256": "f586c66c079e71d75b26a9f29a54d15bacd2f63d87eb76cfd69c223300df86cf", "services": { "nginx": "active", "php8.3-fpm": "active", @@ -105,16 +108,17 @@ "feed_container_border": "none", "feed_container_background": "transparent", "row_shadow": "none", - "container_width_px": 1320, - "feed_width_px": 913, + "shell_width_px": 1165, + "preserved_content_width_px": 1100, + "feed_width_px": 758, "feed_height_px": 615, "quick_link_rail_size_px": { "width": 48, "height": 148 }, "quick_link_count": 3, "quick_link_names": ["ABLECLOUD Home", "Blog", "Online Docs"], - "feed_to_quick_link_gap_px": 200, + "feed_to_quick_link_gap_px": 16, "quick_link_rail_y_before_scroll_px": 339, "quick_link_rail_y_after_feed_scroll_px": 339, - "feed_scroll_top_px": 420, + "feed_scroll_top_px": 350, "bottom_footer_visible": false }, "result": "PASS" diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index 08e58a8..a47d27b 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -146,7 +146,7 @@ Hero의 위·아래 패딩은 데스크톱 `19px`, 모바일 `17px`로 대칭화 | 프로필 | 165px 블루 Hero, 96px 아바타, 280px 메뉴, 모바일 한 열, 직접 영문 0건 | PASS | | 설정 | 780px 콘텐츠 카드, 공통 버튼·알림 표, 직접 영문 0건, 모바일 표 302px·가로 넘침 0px | PASS | | 보안 | 토큰·세션·로그아웃 공통 카드, `새 토큰` 한글화, 모바일 카드 336px·가로 넘침 0px | PASS | -| Header·퀵 링크 | 하단 Footer 제거, 1320px 콘텐츠 컨테이너, 우측 세로형 원형 SVG 링크 3개, 콘텐츠 겹침 방지 | PASS | +| Header·퀵 링크 | 하단 Footer 제거, 기존 1100px 콘텐츠 폭 유지, 바로 오른쪽 세로형 원형 SVG 링크 3개 | PASS | | 글쓰기·첨부 | Composer·FoF Upload 스타일 계약, Issue #72 정책 불변 | PASS | ## 롤백 결과 @@ -247,25 +247,26 @@ Reddit New에 맞춰 재구성했다. 사용자 아바타 다음에 해결 상 전환했다. 퀵 링크는 토론 목록의 스크롤 영역 밖에 있으므로 목록을 이동해도 위치가 변하지 않는다. -Footer가 차지하던 하단 공간을 회수하고 퀵 링크와 콘텐츠가 겹치지 않도록 -데스크톱 컨테이너를 1100px에서 1320px로 넓혔다. 1680×825 화면에서 실제 토론 -목록 폭은 기존 758px에서 913px로 155px 증가했고, 목록 오른쪽과 퀵 링크 -사이에는 200px 간격이 유지됐다. 목록 높이는 615px이며 화면 하단까지 사용할 -수 있다. +Footer가 차지하던 하단 공간은 회수하되 기존 콘텐츠 폭은 바꾸지 않았다. +1100px 콘텐츠 영역 오른쪽에 48px 퀵 링크와 17px 배치 영역만 추가해 전체 셸을 +1165px로 확장했다. 1680×825 화면에서 실제 토론 목록 폭은 기존과 같은 758px이고, +목록 오른쪽에서 16px 떨어진 지점에 퀵 링크가 바로 이어진다. 목록 높이는 +615px이며 화면 하단까지 사용할 수 있다. | 검증 항목 | WSL | 운영 | |---|---:|---:| -| 컨테이너 폭 | 1320px | 1320px | -| 토론 목록 폭 | 913px | 913px | +| 전체 셸 폭 | 1165px | 1165px | +| 기존 콘텐츠 폭 | 1100px | 1100px | +| 토론 목록 폭 | 758px | 758px | | 퀵 링크 크기 | 48×148px | 48×148px | | 퀵 링크 개수 | 3 | 3 | -| 목록-퀵 링크 간격 | 200px | 200px | +| 목록-퀵 링크 간격 | 16px | 16px | | 목록 스크롤 후 퀵 링크 Y | 339px → 339px | 339px → 339px | | 하단 고정 Footer | 없음 | 없음 | 모바일 390×844에서는 44×136px 퀵 링크를 화면 오른쪽 아래에 배치해 본문 폭을 침범하지 않도록 했다. WSL과 운영의 데스크톱·모바일 브라우저 검증, 테마 계약 테스트 12건, 서비스 상태와 HTTP 200 검사를 모두 통과했다. 운영 적용 전 백업은 -`/var/backups/techflow-flarum/theme-vertical-quick-links-20260819T124728Z`에 +`/var/backups/techflow-flarum/theme-adjacent-quick-links-20260819T125523Z`에 보존했고 최종 LESS SHA-256은 -`cacc3fbbda3893ad481f6c4f78a406315ce45cfd6a86269ffd7d1a662072ab07`이다. +`f586c66c079e71d75b26a9f29a54d15bacd2f63d87eb76cfd69c223300df86cf`이다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 0e3897c..4fdbed0 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -81,8 +81,10 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - 기존 Footer의 `ABLECLOUD Home`, `Blog`, `Online Docs` 링크는 콘텐츠 오른쪽의 세로형 원형 퀵 링크로 보여야 한다. 데스크톱에서는 48px 폭, 모바일에서는 44px 폭이며 토론 목록을 스크롤해도 화면상 Y 좌표가 변하지 않아야 한다. -- 데스크톱 첫 화면의 콘텐츠 컨테이너는 최대 1320px이며, 오른쪽에 80px의 - 안전 여백을 두어 목록과 퀵 링크가 겹치지 않아야 한다. +- 데스크톱 첫 화면은 기존 1100px 콘텐츠 폭을 유지하고, 오른쪽에 퀵 링크 + 배치용 65px만 추가한 최대 1165px 셸을 사용한다. 토론 목록 오른쪽과 48px + 퀵 링크 사이에는 약 16px 간격이 있어야 하며 화면 오른쪽 끝으로 분리되면 + 안 된다. - 좌측 메뉴는 사용자 설정 화면과 같이 고정한다. 높이가 짧아 태그가 모두 보이지 않을 때 내부 스크롤은 허용하지만 `scrollbar-width: none`과 WebKit 규칙으로 스크롤바는 표시하지 않는다. @@ -123,4 +125,4 @@ sudo systemctl restart php8.3-fpm nginx - 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드·게시물 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크가 WSL 및 운영에 반영되었으며, 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-vertical-quick-links-20260819T124728Z`에 보존한다. + `/var/backups/techflow-flarum/theme-adjacent-quick-links-20260819T125523Z`에 보존한다. From 8211df38f7440f416c3b8af86de3641b4044c51e Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 00:44:40 +0900 Subject: [PATCH 09/18] =?UTF-8?q?Community=20=EC=83=81=EB=8B=A8=EA=B3=BC?= =?UTF-8?q?=20=EB=B3=B8=EB=AC=B8=20=ED=8F=AD=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/less/forum.less | 9 ++++++++ deploy/flarum/tests/test_community_theme.py | 3 +++ ...community-index-fixed-feed-validation.json | 23 +++++++++++++------ .../issue-73-community-ui-validation.md | 21 +++++++++++++++++ .../community-theme-rollout-rollback.md | 5 +++- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 275d5dd..ae2d2ec 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -2011,6 +2011,15 @@ body { } body { + // Align the top navigation and welcome area with the 1165px index shell. + // The inner content remains 1100px; the added width represents the quick-link rail. + .App--index .App-header > .container, + .App--index .WelcomeHero > .container { + width: 100%; + max-width: 1165px; + box-sizing: border-box; + } + // Keep the desktop index shell anchored below the fixed header. The full // right column scrolls while a narrow reserved rail sits immediately next // to the unchanged 1100px content area. diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 2508bda..d429565 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -315,6 +315,9 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("#floatright .feedback-menu-right:last-of-type", self.less) self.assertIn("mask-image: var(--ablecloud-footer-icon)", self.less) self.assertIn("legacy footer bar is replaced with a fixed vertical quick-link rail", self.less) + self.assertIn(".App--index .App-header > .container", self.less) + self.assertIn(".App--index .WelcomeHero > .container", self.less) + self.assertIn("Align the top navigation and welcome area", self.less) self.assertIn('right: ~"max(12px, calc((100vw - 1165px) / 2 + 16px))"', self.less) self.assertIn("flex-direction: column", self.less) self.assertIn("max-width: 1165px", self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index eaee0a4..8b38479 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,6 +1,6 @@ { - "schema_version": "1.4", - "executed_at": "2026-08-19T12:55:23Z", + "schema_version": "1.5", + "executed_at": "2026-08-19T15:43:10Z", "scope": "Flarum desktop index fixed navigation, Reddit-style discussion item structure, and right-side quick links", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", @@ -33,7 +33,8 @@ "quick_link_area_expansion_px": 65, "desktop_feed_width_px": 758, "desktop_feed_to_quick_link_gap_px": 16, - "reclaimed_footer_height_px": 56 + "reclaimed_footer_height_px": 56, + "top_shell_alignment": "header and welcome hero use the same 1165px desktop shell as index content" }, "wsl": { "environment": "WSL Ubuntu 24.04", @@ -66,7 +67,11 @@ "feed_scroll_top_px": 360, "feed_to_quick_link_gap_px": 16, "mobile_viewport": { "width": 390, "height": 844 }, - "mobile_quick_link_rail_size_px": { "width": 44, "height": 136 } + "mobile_quick_link_rail_size_px": { "width": 44, "height": 136 }, + "header_shell": { "x": 258, "right": 1423, "width": 1165 }, + "welcome_hero_shell": { "x": 258, "right": 1423, "width": 1165 }, + "index_content_shell": { "x": 258, "right": 1423, "width": 1165 }, + "top_and_content_shell_aligned": true }, "compiled_css_grid_contract": "grid-column-start:1;grid-column-end:-1", "result": "PASS" @@ -74,8 +79,8 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-adjacent-quick-links-20260819T125523Z", - "theme_less_sha256": "f586c66c079e71d75b26a9f29a54d15bacd2f63d87eb76cfd69c223300df86cf", + "backup_root": "/var/backups/techflow-flarum/theme-header-shell-align-20260819T154310Z", + "theme_less_sha256": "2f2557d903df02aedc7a3a252a97048f9cbcf9b91e36fbaebf2aa84bae15cccc", "services": { "nginx": "active", "php8.3-fpm": "active", @@ -119,7 +124,11 @@ "quick_link_rail_y_before_scroll_px": 339, "quick_link_rail_y_after_feed_scroll_px": 339, "feed_scroll_top_px": 350, - "bottom_footer_visible": false + "bottom_footer_visible": false, + "header_shell": { "x": 258, "right": 1423, "width": 1165 }, + "welcome_hero_shell": { "x": 258, "right": 1423, "width": 1165 }, + "index_content_shell": { "x": 258, "right": 1423, "width": 1165 }, + "top_and_content_shell_aligned": true }, "result": "PASS" }, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index a47d27b..c282797 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -270,3 +270,24 @@ Footer가 차지하던 하단 공간은 회수하되 기존 콘텐츠 폭은 바 `/var/backups/techflow-flarum/theme-adjacent-quick-links-20260819T125523Z`에 보존했고 최종 LESS SHA-256은 `f586c66c079e71d75b26a9f29a54d15bacd2f63d87eb76cfd69c223300df86cf`이다. + +### 상단 헤더와 본문 셸 폭 정렬 + +퀵 링크 영역을 추가한 뒤 본문 셸은 1165px이었지만 상단 헤더와 환영 영역은 +Flarum 기본 1100px에 남아 좌우 외곽선이 맞지 않았다. 데스크톱 첫 화면에만 +헤더와 환영 영역의 최대 폭을 1165px로 지정해 본문 셸과 같은 기준선을 사용했다. +모바일과 상세·설정 화면의 폭 규칙은 변경하지 않았다. + +1680×825 화면의 WSL과 운영 측정값은 동일하다. + +| 영역 | X | 오른쪽 | 폭 | +|---|---:|---:|---:| +| 상단 헤더 | 258px | 1423px | 1165px | +| 환영 영역 | 258px | 1423px | 1165px | +| 본문 셸 | 258px | 1423px | 1165px | + +운영 적용 전 백업은 +`/var/backups/techflow-flarum/theme-header-shell-align-20260819T154310Z`에 보존했다. +최종 LESS SHA-256은 +`2f2557d903df02aedc7a3a252a97048f9cbcf9b91e36fbaebf2aa84bae15cccc`이며, +HTTP 200과 Nginx·PHP-FPM·MariaDB·Monitor Timer `active`를 확인했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 4fdbed0..24cf43d 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -85,6 +85,9 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 배치용 65px만 추가한 최대 1165px 셸을 사용한다. 토론 목록 오른쪽과 48px 퀵 링크 사이에는 약 16px 간격이 있어야 하며 화면 오른쪽 끝으로 분리되면 안 된다. +- 데스크톱 첫 화면의 상단 헤더, 환영 영역과 본문 셸은 모두 최대 1165px로 + 동일해야 한다. 1680px Viewport에서는 각 영역의 X가 258px, 오른쪽 끝이 + 1423px인지 확인한다. 모바일과 다른 페이지에는 이 폭을 강제하지 않는다. - 좌측 메뉴는 사용자 설정 화면과 같이 고정한다. 높이가 짧아 태그가 모두 보이지 않을 때 내부 스크롤은 허용하지만 `scrollbar-width: none`과 WebKit 규칙으로 스크롤바는 표시하지 않는다. @@ -125,4 +128,4 @@ sudo systemctl restart php8.3-fpm nginx - 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드·게시물 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크가 WSL 및 운영에 반영되었으며, 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-adjacent-quick-links-20260819T125523Z`에 보존한다. + `/var/backups/techflow-flarum/theme-header-shell-align-20260819T154310Z`에 보존한다. From 7ccc7b6c5e07ec8b2905409fb31596d40a2ed9a1 Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 09:38:10 +0900 Subject: [PATCH 10/18] =?UTF-8?q?Community=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=86=A0=EB=A1=A0=20=EC=9D=BD=EA=B8=B0=20=ED=9D=90=EB=A6=84=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/dist/forum.js | 140 ++++++ .../resources/less/forum.less | 444 +++++++++++++++++- deploy/flarum/tests/test_community_theme.py | 38 ++ ...community-index-fixed-feed-validation.json | 65 ++- .../issue-73-community-ui-validation.md | 51 ++ .../community-theme-rollout-rollback.md | 26 +- 6 files changed, 745 insertions(+), 19 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js index 7eb5804..e02a105 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js +++ b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js @@ -6,6 +6,7 @@ var relativeDatePattern = /(?:방금|전$|후$)/; var absoluteDatePattern = /^(\d{4})-(\d{2})-(\d{2})/; var discussionItemEnhancedAttribute = 'data-ablecloud-post-structure'; + var discussionDetailEnhancedAttribute = 'data-ablecloud-reading-flow'; function formatDiscussionCreatedAt(createdAt) { if (!(createdAt instanceof Date) || Number.isNaN(createdAt.getTime())) { @@ -166,6 +167,144 @@ }); } + function createDetailMetaPart(className, text, href) { + var part = href ? document.createElement('a') : document.createElement('span'); + part.className = className; + part.textContent = text; + + if (href) { + part.href = href; + } + + return part; + } + + function scrollToSolution(event) { + var solution = document.getElementById('ablecloud-solution-post'); + + if (!solution) { + return; + } + + event.preventDefault(); + solution.scrollIntoView({ + behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', + block: 'start', + }); + } + + function enhanceLongTechnicalBlocks(root) { + root.querySelectorAll('.App--discussion .Post-body pre').forEach(function (block) { + if (block.getAttribute('data-ablecloud-log-block') === 'true' || block.scrollHeight <= 480) { + return; + } + + block.setAttribute('data-ablecloud-log-block', 'true'); + block.classList.add('ablecloud-TechnicalBlock--collapsed'); + + var toggle = document.createElement('button'); + toggle.type = 'button'; + toggle.className = 'Button ablecloud-TechnicalBlock-toggle'; + toggle.setAttribute('aria-expanded', 'false'); + toggle.textContent = '전체 로그 보기'; + toggle.addEventListener('click', function () { + var expanded = toggle.getAttribute('aria-expanded') === 'true'; + toggle.setAttribute('aria-expanded', String(!expanded)); + toggle.textContent = expanded ? '전체 로그 보기' : '로그 접기'; + block.classList.toggle('ablecloud-TechnicalBlock--collapsed', expanded); + }); + block.insertAdjacentElement('afterend', toggle); + }); + } + + function enhanceDiscussionDetail(root) { + var page = root.querySelector('.App--discussion .DiscussionPage'); + var hero = root.querySelector('.App--discussion .DiscussionHero'); + var stream = page && page.querySelector('.PostStream'); + var firstItem = stream && stream.querySelector('.PostStream-item[data-number="1"]'); + var firstPost = firstItem && firstItem.querySelector('article.Post'); + + if (!page || !hero || !stream || !firstPost) { + return; + } + + var solutionPost = stream.querySelector('article.Post.Post--bestAnswer'); + var solutionItem = solutionPost && solutionPost.closest('.PostStream-item'); + + if (solutionItem) { + solutionItem.id = 'ablecloud-solution-post'; + } + + if (hero.getAttribute(discussionDetailEnhancedAttribute) !== 'true') { + var heroItems = hero.querySelector('.DiscussionHero-items'); + var authorLink = firstPost.querySelector('.PostUser-name a'); + var authorName = firstPost.querySelector('.PostUser-name .username'); + var publishedAt = firstPost.querySelector('.PostMeta time'); + var meta = document.createElement('li'); + meta.className = 'item-ablecloudMeta ablecloud-DiscussionDetailMeta'; + meta.setAttribute('aria-label', '토론 작성 정보'); + + if (authorName) { + meta.appendChild( + createDetailMetaPart( + 'ablecloud-DiscussionDetailMeta-author', + authorName.textContent.trim(), + authorLink && authorLink.getAttribute('href') + ) + ); + } + + if (publishedAt) { + meta.appendChild(createDetailMetaPart('ablecloud-DiscussionDetailMeta-separator', '·')); + meta.appendChild(createDetailMetaPart('ablecloud-DiscussionDetailMeta-time', publishedAt.textContent.trim())); + } + + if (solutionItem) { + var solutionLink = createDetailMetaPart('ablecloud-SolutionJump', '해결 답변 보기', '#ablecloud-solution-post'); + solutionLink.addEventListener('click', scrollToSolution); + meta.appendChild(solutionLink); + } + + if (heroItems) { + heroItems.appendChild(meta); + } + + var solvedBadge = hero.querySelector('.Badge--bestAnswer'); + + if (solvedBadge) { + solvedBadge.setAttribute('aria-label', '해결됨'); + } + + hero.setAttribute(discussionDetailEnhancedAttribute, 'true'); + } + + var duplicateSolution = firstPost.querySelector('.item-bestAnswerPost'); + + if (duplicateSolution) { + duplicateSolution.setAttribute('aria-hidden', 'true'); + } + + if (!firstItem.querySelector('.ablecloud-InlineReplyPrompt')) { + var nativeReply = page.querySelector('.DiscussionPage-nav .item-controls .SplitDropdown-button'); + + if (nativeReply) { + var replyPrompt = document.createElement('div'); + var replyButton = document.createElement('button'); + replyPrompt.className = 'ablecloud-InlineReplyPrompt'; + replyButton.type = 'button'; + replyButton.className = 'Button ablecloud-InlineReplyPrompt-button'; + replyButton.textContent = '답변을 작성해 주세요'; + replyButton.addEventListener('click', function () { + nativeReply.click(); + }); + replyPrompt.appendChild(replyButton); + firstPost.insertAdjacentElement('afterend', replyPrompt); + } + } + + enhanceLongTechnicalBlocks(root); + } + function formatTagDiscussionDates(root) { root.querySelectorAll('.TagTile-lastPostedDiscussion time[datetime]').forEach(function (time) { var current = time.textContent.trim(); @@ -225,6 +364,7 @@ formatTagDiscussionDates(document); normalizeDiscussionListTargets(document); enhanceDiscussionListItems(document); + enhanceDiscussionDetail(document); }); }; diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index ae2d2ec..3b1ebd6 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -847,7 +847,7 @@ body { .TagTile:focus-within { border-color: #9db9e4; border-top-color: var(--tag-bg); - box-shadow: var(--ablecloud-shadow); + box-shadow: var(--ablecloud-shadow-soft); transform: translateY(-2px); } @@ -3012,6 +3012,448 @@ body { } } +/* + * Discussion reading flow + * + * Keep the established 1100px content width and the adjacent quick-link rail. + * The index no longer uses a welcome banner. Discussion pages use a compact, + * content-aligned title block and a single reading column without side panels. + */ +.App--index .WelcomeHero { + display: none !important; +} + +.App--discussion .DiscussionHero { + border-bottom: 1px solid var(--ablecloud-brand-border); + background: #ffffff !important; + color: var(--ablecloud-brand-ink) !important; + text-align: left; +} + +.App--discussion .DiscussionHero .container { + width: 100%; + max-width: 1100px; + box-sizing: border-box; + padding: 17px 0 15px; +} + +.App--discussion .DiscussionHero-items { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 10px; + margin: 0; +} + +.App--discussion .DiscussionHero-items > li { + display: block; + float: none; + margin: 0; +} + +.App--discussion .DiscussionHero-items > .item-badges { + order: 1; +} + +.App--discussion .DiscussionHero-items > .item-tags { + min-width: 0; + order: 2; +} + +.App--discussion .DiscussionHero-items > .item-title { + flex: 0 0 100%; + order: 3; +} + +.App--discussion .DiscussionHero-items > .item-ablecloudMeta { + flex: 0 0 100%; + order: 4; +} + +.App--discussion .DiscussionHero-title { + max-width: 900px; + margin: 2px 0 0; + color: var(--ablecloud-brand-ink) !important; + font-size: 26px; + font-weight: 780; + letter-spacing: -0.025em; + line-height: 1.38; +} + +.App--discussion .DiscussionHero .Badge--bestAnswer { + display: inline-flex; + align-items: center; + gap: 5px; + width: auto; + height: 28px; + padding: 0 10px; + border: 1px solid #86d2aa; + border-radius: 999px; + background: #d9f5e6; + color: #056038; + box-shadow: none; +} + +.App--discussion .DiscussionHero .Badge--bestAnswer::after { + content: "해결됨"; + font-size: 12px; + font-weight: 750; +} + +.App--discussion .DiscussionHero .Badge--bestAnswer .Badge-icon { + font-size: 12px; +} + +.ablecloud-DiscussionDetailMeta { + display: flex !important; + align-items: center; + min-height: 30px; + gap: 7px; + color: var(--ablecloud-brand-muted); + font-size: 14px; + line-height: 1.45; +} + +.ablecloud-DiscussionDetailMeta-author { + color: var(--ablecloud-brand-primary-deep); + font-weight: 720; +} + +.App--discussion .DiscussionHero .ablecloud-DiscussionDetailMeta-author { + color: var(--ablecloud-brand-primary-deep) !important; +} + +.ablecloud-DiscussionDetailMeta-separator { + color: #9aa9bc; +} + +.ablecloud-SolutionJump { + display: inline-flex; + align-items: center; + min-height: 30px; + margin-left: auto; + padding: 4px 10px; + border: 1px solid #86d2aa; + border-radius: 999px; + background: #f2fbf6; + color: #056038 !important; + font-size: 13px; + font-weight: 750; +} + +.ablecloud-SolutionJump::before { + margin-right: 6px; + content: "↓"; + font-weight: 900; +} + +.App--discussion .DiscussionPage-discussion > .container, +.App--discussion .DiscussionPage-stream, +.App--discussion .PostStream { + width: 100%; + max-width: 1100px; + box-sizing: border-box; +} + +.App--discussion .DiscussionPage-discussion > .container { + display: block; +} + +.App--discussion .PostStream { + margin: 0; +} + +.App--discussion .PostStream-item, +.App--discussion .PostStream-item:has(.Post-actions) { + margin: 0 !important; + padding: 0; + border-bottom: 1px solid var(--ablecloud-brand-border); +} + +.App--discussion .PostStream-item:last-child { + border-bottom: 0; +} + +.App--discussion .PostStream-item article.Post { + margin: 0; + padding-top: 24px; + padding-bottom: 24px; +} + +.App--discussion article.Post > div { + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + padding: 0; +} + +.App--discussion article.Post > div:hover { + border-color: transparent; + box-shadow: none; +} + +.App--discussion .Post-header { + margin-bottom: 12px; +} + +.App--discussion .Post-body { + max-width: none; + color: #26364d; + font-size: 16px; + line-height: 1.82; +} + +.App--discussion .Post-body p, +.App--discussion .Post-body li { + max-width: 82ch; +} + +.App--discussion .Post-body blockquote { + max-width: 82ch; + margin: 18px 0; + padding: 12px 16px; + border-left: 3px solid #8cb0ec; + border-radius: 0 10px 10px 0; + background: #f4f8ff; + color: #425875; +} + +.App--discussion .Post-body pre { + max-width: 100%; + border: 1px solid #d5dfec; + border-radius: 11px; + background: #f6f8fb; + color: #203047; + overflow: auto; +} + +.App--discussion .Post-body pre.ablecloud-TechnicalBlock--collapsed { + max-height: 480px; +} + +.ablecloud-TechnicalBlock-toggle { + min-height: 36px; + margin-top: 8px; + padding: 0 11px; + border: 1px solid var(--ablecloud-brand-border); + background: #f4f8ff; + color: var(--ablecloud-brand-primary-deep); + font-size: 13px; +} + +.App--discussion .Post-actions { + position: static; + right: auto; + bottom: auto; + float: none; + max-width: none; + margin: 18px 0 0; + opacity: 1 !important; +} + +.App--discussion .Post-actions > ul { + justify-content: flex-start; +} + +.App--discussion article.TechFlowPost--assistant > div, +.App--discussion article.Post:has(> div > .Post-header .PostUser-name a[href="/u/TechFlow-Assistant"]) > div, +.App--discussion article.TechFlowPost--knowledge-base > div, +.App--discussion article.Post.Post--bestAnswer > div, +.App--discussion article.TechFlowPost--needs-information > div { + padding: 20px 22px; + border-radius: 14px; + box-shadow: none; +} + +.App--discussion article.TechFlowPost--assistant > div, +.App--discussion article.Post:has(> div > .Post-header .PostUser-name a[href="/u/TechFlow-Assistant"]) > div { + border: 1px solid #a8c5f2; +} + +.App--discussion article.TechFlowPost--knowledge-base > div, +.App--discussion article.Post.Post--bestAnswer > div { + border: 1px solid #86d2aa; + border-left: 5px solid #078248; +} + +.App--discussion article.TechFlowPost--needs-information > div { + border: 1px solid #e4b955; + border-left: 5px solid #d39a18; +} + +.App--discussion .item-bestAnswerPost { + display: none !important; +} + +.App--discussion #ablecloud-solution-post { + scroll-margin-top: 76px; +} + +.ablecloud-InlineReplyPrompt { + margin: 0 20px 24px 105px; +} + +.ablecloud-InlineReplyPrompt-button { + display: flex; + align-items: center; + width: 100%; + min-height: 48px; + justify-content: flex-start; + padding: 0 16px; + border: 1px dashed #9bb8e4; + border-radius: 12px; + background: #f7faff; + color: #4c6382; + font-weight: 650; + box-shadow: none; +} + +.ablecloud-InlineReplyPrompt-button::before { + width: 18px; + height: 18px; + margin-right: 9px; + background: currentColor; + content: ""; + -webkit-mask-image: var(--ablecloud-icon-reply); + mask-image: var(--ablecloud-icon-reply); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: contain; + mask-size: contain; + --ablecloud-icon-reply: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m9 7-5 5 5 5M5 12h8a6 6 0 0 1 6 6v1' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); +} + +.ablecloud-InlineReplyPrompt-button:hover, +.ablecloud-InlineReplyPrompt-button:focus-visible { + border-color: var(--ablecloud-brand-primary); + background: #eef5ff; + color: var(--ablecloud-brand-primary-deep); +} + +@media (min-width: 768px) { + .App--discussion .DiscussionPage-nav { + position: relative !important; + top: auto !important; + width: 100% !important; + margin: 0 0 8px !important; + padding: 14px 0 10px; + z-index: 20; + } + + .App--discussion .DiscussionPage-nav > ul { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + } + + .App--discussion .DiscussionPage-nav > ul > li { + margin-bottom: 0; + } + + .App--discussion .DiscussionPage-nav .item-controls .ButtonGroup { + display: inline-flex; + width: auto; + } + + .App--discussion .DiscussionPage-nav .item-controls .ButtonGroup > .Button { + float: none; + flex: 0 0 auto; + } + + .App--discussion .DiscussionPage-nav .item-scrubber { + margin-left: auto; + } + + .App--discussion .DiscussionPage-nav .PostStreamScrubber { + margin-top: 0; + } + + .App--discussion .DiscussionPage-nav .PostStreamScrubber .Dropdown-menu { + display: none !important; + position: absolute; + top: calc(100% + 8px); + right: 0; + left: auto; + min-width: 240px; + padding: 12px; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 12px; + background: #ffffff; + box-shadow: var(--ablecloud-shadow); + } + + .App--discussion .DiscussionPage-nav .PostStreamScrubber > .Dropdown-toggle { + display: inline-flex !important; + align-items: center; + } + + .App--discussion .DiscussionPage-nav .PostStreamScrubber.open > .Dropdown-menu { + display: block !important; + } + + .App--discussion .DiscussionPage-stream { + width: 100% !important; + margin: 0 !important; + } +} + +@media (max-width: 767px) { + .App--discussion .DiscussionHero .container { + padding: 17px 15px 15px; + } + + .App--discussion .DiscussionHero-title { + font-size: 21px; + line-height: 1.4; + } + + .ablecloud-DiscussionDetailMeta { + flex-wrap: wrap; + font-size: 13px; + } + + .ablecloud-SolutionJump { + margin-left: 0; + } + + .App--discussion .DiscussionPage-stream, + .App--discussion .PostStream { + width: 100% !important; + padding-right: 8px; + padding-left: 8px; + } + + .App--discussion .PostStream-item article.Post { + padding-top: 18px; + padding-bottom: 18px; + } + + .App--discussion article.TechFlowPost--assistant > div, + .App--discussion article.Post:has(> div > .Post-header .PostUser-name a[href="/u/TechFlow-Assistant"]) > div, + .App--discussion article.TechFlowPost--knowledge-base > div, + .App--discussion article.Post.Post--bestAnswer > div, + .App--discussion article.TechFlowPost--needs-information > div { + padding: 16px; + } + + .App--discussion .Post-actions { + position: static; + margin-top: 16px; + } + + .App--discussion .PostStream-item:has(.Post-actions) { + padding-bottom: 0; + } + + .ablecloud-InlineReplyPrompt { + margin: 0 12px 18px 60px; + } +} + @media (prefers-reduced-motion: reduce) { body *, body *::before, diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index d429565..23aed5f 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -176,6 +176,44 @@ def test_reddit_style_discussion_item_structure_contract(self) -> None: with self.subTest(style_contract=style_contract): self.assertIn(style_contract, self.less) + def test_discussion_detail_reading_flow_contract(self) -> None: + for script_contract in ( + "enhanceDiscussionDetail", + "data-ablecloud-reading-flow", + "ablecloud-DiscussionDetailMeta", + "ablecloud-solution-post", + "해결 답변 보기", + "scrollToSolution", + "ablecloud-InlineReplyPrompt", + "답변을 작성해 주세요", + "enhanceLongTechnicalBlocks", + "전체 로그 보기", + "duplicateSolution.setAttribute('aria-hidden', 'true')", + ): + with self.subTest(script_contract=script_contract): + self.assertIn(script_contract, self.forum_js) + + for style_contract in ( + ".App--index .WelcomeHero", + ".App--discussion .DiscussionHero", + ".App--discussion .DiscussionHero-title", + ".ablecloud-DiscussionDetailMeta", + ".ablecloud-SolutionJump", + ".App--discussion .DiscussionPage-stream", + ".App--discussion .PostStream-item", + ".App--discussion .item-bestAnswerPost", + ".ablecloud-InlineReplyPrompt", + ".ablecloud-TechnicalBlock--collapsed", + "single reading column without side panels", + ): + with self.subTest(style_contract=style_contract): + self.assertIn(style_contract, self.less) + + self.assertIn("display: none !important", self.less) + self.assertIn("max-width: 1100px", self.less) + self.assertIn("position: relative !important", self.less) + self.assertIn("justify-content: flex-start", self.less) + def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn(".WelcomeHero .container", self.less) self.assertIn("font-size: 28px", self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 8b38479..030b49f 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,12 +1,12 @@ { - "schema_version": "1.5", - "executed_at": "2026-08-19T15:43:10Z", - "scope": "Flarum desktop index fixed navigation, Reddit-style discussion item structure, and right-side quick links", + "schema_version": "1.6", + "executed_at": "2026-08-20T00:33:55Z", + "scope": "Flarum index without welcome banner, Reddit-style discussion list and detail reading flow, and right-side quick links", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", "change": { "document_scroll": "disabled on desktop index", - "fixed_regions": ["welcome hero", "left navigation", "right quick-link rail"], + "fixed_regions": ["left navigation", "right quick-link rail"], "scroll_region": ".IndexPage-results including .IndexPage-toolbar and .DiscussionList, scrollbar hidden", "discussion_list_owns_scroll": false, "toolbar_scrolls_with_feed": true, @@ -34,7 +34,8 @@ "desktop_feed_width_px": 758, "desktop_feed_to_quick_link_gap_px": 16, "reclaimed_footer_height_px": 56, - "top_shell_alignment": "header and welcome hero use the same 1165px desktop shell as index content" + "welcome_hero": "hidden on the index", + "top_shell_alignment": "header uses the same 1165px desktop shell as index content" }, "wsl": { "environment": "WSL Ubuntu 24.04", @@ -69,7 +70,7 @@ "mobile_viewport": { "width": 390, "height": 844 }, "mobile_quick_link_rail_size_px": { "width": 44, "height": 136 }, "header_shell": { "x": 258, "right": 1423, "width": 1165 }, - "welcome_hero_shell": { "x": 258, "right": 1423, "width": 1165 }, + "welcome_hero_display": "none", "index_content_shell": { "x": 258, "right": 1423, "width": 1165 }, "top_and_content_shell_aligned": true }, @@ -79,8 +80,9 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-header-shell-align-20260819T154310Z", - "theme_less_sha256": "2f2557d903df02aedc7a3a252a97048f9cbcf9b91e36fbaebf2aa84bae15cccc", + "backup_root": "/var/backups/techflow-flarum/theme-detail-toolbar-20260820T003355Z", + "theme_less_sha256": "e56463d030b90df6c9626ab2d20595ae51c4352923f4a0f9c67a8bbc644fd0fd", + "theme_js_sha256": "05115ef3395d87d0f7327a50bac518376075b3cbd9bbeed356f48ee64521d42e", "services": { "nginx": "active", "php8.3-fpm": "active", @@ -126,14 +128,57 @@ "feed_scroll_top_px": 350, "bottom_footer_visible": false, "header_shell": { "x": 258, "right": 1423, "width": 1165 }, - "welcome_hero_shell": { "x": 258, "right": 1423, "width": 1165 }, + "welcome_hero_display": "none", "index_content_shell": { "x": 258, "right": 1423, "width": 1165 }, "top_and_content_shell_aligned": true }, "result": "PASS" }, + "detail_reading_flow": { + "layout": { + "side_information_columns": false, + "content_width_px": 1100, + "inner_stream_width_px": 1070, + "title_alignment": "left", + "navigation": "64px horizontal toolbar above the single reading column", + "normal_posts": "flat with 1px separators", + "semantic_cards": ["AI technical support", "needs more information", "final solution"], + "duplicate_selected_answer_preview": "hidden", + "inline_reply_prompt": "답변을 작성해 주세요", + "long_log_behavior": "collapsed above 480px with Korean expand and collapse controls" + }, + "wsl": { + "url": "http://localhost:18080/d/144-gasangmeosin-webkonsol-yeongyeol-oryu", + "title": "가상머신 웹콘솔 연결 오류", + "author_meta": "sijo · 8일 전", + "post_count": 4, + "first_post_text": "mold에서 가상머신 콘솔보기가 연결중에서 더이상 진행 되지가 않습니다.", + "navigation_height_px": 64, + "stream_width_px": 1070, + "duplicate_solution_visible": false, + "result": "PASS" + }, + "production": { + "url": "https://community.ablecloud.io/d/173-v2keseo-vcenter-eobsi-esxi-hoseuteueseo-gasangmeosin-igwan-gineungeul-jiweonhanayo", + "title": "v2k에서 vCenter 없이 ESXi 호스트에서 가상머신 이관 기능을 지원하나요?", + "author_meta": "dhslove · 2일 전", + "solved_badge": "해결됨", + "post_count": 5, + "post_numbers": [1, 2, 3, 4, 5], + "solution_post_number": 5, + "solution_headings": ["증상", "원인", "해결 방법", "추가 고려사항", "적용 버전"], + "solution_applied_version": "ABLESTACK Diplo", + "solution_jump_final_top_px": 76, + "duplicate_solution_visible": false, + "inline_reply_opened_native_composer": true, + "navigation_height_px": 64, + "stream_width_px": 1070, + "quick_links": ["ABLECLOUD Home", "Blog", "Online Docs"], + "result": "PASS" + } + }, "validation": { - "theme_contract_tests": { "passed": 12, "failed": 0 }, + "theme_contract_tests": { "passed": 13, "failed": 0 }, "discussion_item_enhancement_is_idempotent": true, "summary_uses_existing_store_data": true, "thumbnail_uses_existing_first_post_html": true, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index c282797..a9a9b71 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -291,3 +291,54 @@ Flarum 기본 1100px에 남아 좌우 외곽선이 맞지 않았다. 데스크 최종 LESS SHA-256은 `2f2557d903df02aedc7a3a252a97048f9cbcf9b91e36fbaebf2aa84bae15cccc`이며, HTTP 200과 Nginx·PHP-FPM·MariaDB·Monitor Timer `active`를 확인했다. + +### 메인 환영 배너 제거와 토론 상세 읽기 흐름 적용 + +2026-08-20 기준으로 첫 화면의 Welcome Hero를 표시하지 않도록 변경했다. 상단 +Header와 Index 본문은 기존 1165px 셸을 그대로 사용하며, Reddit형 목록 구조, +280px 좌측 메뉴와 콘텐츠 바로 옆 퀵 링크 위치도 변경하지 않았다. WSL과 운영의 +1680px Viewport에서 Header와 Index 본문은 모두 X 258px, 폭 1165px, 오른쪽 +1423px로 일치했고 현재 목록 20건이 정상 표시됐다. + +토론 상세는 Reddit의 게시물 읽기 순서만 참고했다. 좌우 부가 정보 패널은 만들지 +않고 기존 1100px 콘텐츠 안에서 제목과 게시물 Stream을 단일 열로 구성했다. 기존 +150px 우측 탐색 열의 답장·팔로우·게시물 이동 기능은 본문 위 64px 가로 도구 +모음으로 옮겼다. 제목 영역은 환영 배너 외형 대신 흰 배경에서 해결 상태·태그· +제목·작성자·작성 시각을 왼쪽 정렬한다. + +일반 질문과 답변은 Card Border와 Shadow 없이 1px 구분선으로 나누고 AI 기술지원, +추가 확인 필요, 최종 해결 가이드만 의미 색상 카드로 유지했다. 질문 바로 아래에는 +기존 Flarum 답장 작성기를 여는 `답변을 작성해 주세요` 버튼을 배치했다. 480px보다 +긴 `pre` 로그는 처음에 접고 `전체 로그 보기`와 `로그 접기`로 전환한다. + +해결된 토론은 첫 질문 안에 최종 답변 전체를 다시 보여주지 않는다. 상단의 +`해결 답변 보기`를 누르면 Stream 안의 실제 선택 답변으로 이동한다. 운영 토론 +#173에서 다음 데이터를 브라우저로 직접 확인했다. + +| 검증 항목 | 결과 | +|---|---| +| 토론 제목 | v2k에서 vCenter 없이 ESXi 호스트에서 가상머신 이관 기능을 지원하나요? | +| 작성 정보 | dhslove · 2일 전 | +| 해결 상태 | 해결됨 | +| 실제 게시물 번호 | 1, 2, 3, 4, 5 | +| 선택 답변 | 5번 게시물 | +| 선택 답변 구성 | 증상·원인·해결 방법·추가 고려사항·적용 버전 | +| 적용 버전 표시 | ABLESTACK Diplo | +| 중복 선택 답변 미리보기 | 표시하지 않음 | +| 해결 답변 이동 후 상단 위치 | 76px | +| 상세 본문 폭 | 1070px (1100px Container 내부 여백 포함) | +| 가로 도구 모음 높이 | 64px | + +WSL 토론 #144에서는 제목, 작성자 `sijo`, 게시물 4건과 첫 질문 원문이 실제 +데이터와 일치했다. WSL Cycle은 사용자 39명, 토론 117건, 게시물 305건과 DB·첨부 +해시를 적용·비활성화 롤백·최종 재적용 전후에 동일하게 유지했고 13개 테마 계약 +검사를 통과했다. + +최신 운영 백업은 +`/var/backups/techflow-flarum/theme-detail-toolbar-20260820T003355Z`이며, LESS와 +JS의 운영 Vendor 해시는 각각 +`e56463d030b90df6c9626ab2d20595ae51c4352923f4a0f9c67a8bbc644fd0fd`, +`05115ef3395d87d0f7327a50bac518376075b3cbd9bbeed356f48ee64521d42e`로 로컬 +원본과 일치한다. 외부 HTTPS와 서버 로컬 HTTP는 200이며 Nginx·PHP-FPM·MariaDB· +Community Monitor Timer는 모두 `active`다. DB Schema, 게시물, 첨부, TechFlow AI, +GitHub→Chat Webhook은 변경하지 않았다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 24cf43d..fb8cf0d 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -73,7 +73,7 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - `public/assets/forum-ko.js`가 비어 있지 않고 `core.forum.header.search_placeholder`를 포함해야 한다. - 데스크톱과 모바일에서 홈, 목록, 상세, 검색, 로그인, 글쓰기, 첨부를 확인한다. - 데스크톱 첫 화면에서 문서 전체의 `scrollY`가 `0`으로 유지되는지 확인한다. - Welcome Hero와 좌측 메뉴는 고정하고 `.IndexPage-results` 전체가 스크롤되어야 + Welcome Hero는 표시하지 않고 좌측 메뉴는 고정한다. `.IndexPage-results` 전체가 스크롤되어야 한다. `최신` 선택 콤보를 포함한 `.IndexPage-toolbar`와 토론 행이 함께 이동해야 하며 `.DiscussionList` 자체의 `scrollTop`은 `0`을 유지해야 한다. 우측 영역도 `scrollbar-width: none`과 WebKit 규칙으로 스크롤바를 표시하지 @@ -85,9 +85,9 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 배치용 65px만 추가한 최대 1165px 셸을 사용한다. 토론 목록 오른쪽과 48px 퀵 링크 사이에는 약 16px 간격이 있어야 하며 화면 오른쪽 끝으로 분리되면 안 된다. -- 데스크톱 첫 화면의 상단 헤더, 환영 영역과 본문 셸은 모두 최대 1165px로 - 동일해야 한다. 1680px Viewport에서는 각 영역의 X가 258px, 오른쪽 끝이 - 1423px인지 확인한다. 모바일과 다른 페이지에는 이 폭을 강제하지 않는다. +- 데스크톱 첫 화면의 상단 헤더와 본문 셸은 모두 최대 1165px로 동일해야 한다. + 1680px Viewport에서는 두 영역의 X가 258px, 오른쪽 끝이 1423px인지 확인한다. + 모바일과 다른 페이지에는 이 폭을 강제하지 않는다. - 좌측 메뉴는 사용자 설정 화면과 같이 고정한다. 높이가 짧아 태그가 모두 보이지 않을 때 내부 스크롤은 허용하지만 `scrollbar-width: none`과 WebKit 규칙으로 스크롤바는 표시하지 않는다. @@ -101,6 +101,16 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - 첫 게시물에 이미지가 있으면 첫 이미지를 데스크톱 124×92px, 모바일 88×72px 우측 썸네일로 표시한다. 이미지가 없는 글에는 빈 썸네일 영역을 만들지 않으며, 썸네일은 기존 Store 데이터에서 생성하고 지연 로딩해야 한다. +- 토론 상세는 1100px 콘텐츠 안의 단일 열이어야 한다. 우측 150px 탐색 열은 + 만들지 않고 답장·팔로우·게시물 탐색을 본문 위 64px 가로 도구 모음에 배치한다. +- 상세 제목은 Welcome Hero와 같은 배너 외형을 사용하지 않는다. 흰 배경의 콘텐츠 + 제목 영역에서 해결 상태·태그·제목·작성자·작성 시각을 왼쪽 정렬로 표시한다. +- 일반 답변은 카드 중첩 없이 1px 구분선으로 나누고 AI 기술지원·추가 확인 필요· + 최종 해결 가이드만 의미 색상 카드로 구분한다. +- 첫 게시물 안의 선택 답변 미리보기는 숨기고 `해결 답변 보기`가 실제 선택 답변으로 + 이동해야 한다. 이동 후 선택 답변 상단은 고정 Header 아래에서 보여야 한다. +- 질문 다음에는 `답변을 작성해 주세요` 버튼이 표시되어 기존 Flarum 답장 작성기를 + 열어야 한다. 480px보다 긴 `pre` 로그에는 `전체 로그 보기`와 `로그 접기`가 제공되어야 한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. @@ -125,7 +135,7 @@ sudo systemctl restart php8.3-fpm nginx - GO: HTTP 200, 한글 원문 키 0건, 핵심 기능 정상, 콘텐츠·첨부 불변. - ROLLBACK: 화면·번역·로그인·작성·첨부 중 하나라도 실패. -- 2026-08-19 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드·게시물 - 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크가 WSL 및 - 운영에 반영되었으며, 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-header-shell-align-20260819T154310Z`에 보존한다. +- 2026-08-20 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드·게시물 + 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크, 메인 환영 배너 + 제거와 상세 단일 열 읽기 흐름이 WSL 및 운영에 반영되었다. 최신 운영 백업은 + `/var/backups/techflow-flarum/theme-detail-toolbar-20260820T003355Z`에 보존한다. From f09a70b3fd7a1a48104f7e630210247d9d91f795 Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 09:58:19 +0900 Subject: [PATCH 11/18] =?UTF-8?q?Community=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=84=B8=EB=A1=9C=20=ED=83=90=EC=83=89=EA=B3=BC=20=ED=8F=AD=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/less/forum.less | 97 +++++-------------- deploy/flarum/tests/test_community_theme.py | 7 +- ...community-index-fixed-feed-validation.json | 43 +++++--- .../issue-73-community-ui-validation.md | 36 +++++++ .../community-theme-rollout-rollback.md | 18 +++- 5 files changed, 105 insertions(+), 96 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 3b1ebd6..d7e9f52 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -2010,6 +2010,12 @@ body { overflow: hidden; } + html:has(body > .App--index) { + // Reserve the same vertical-scrollbar gutter used by discussion pages so + // the 1165px index and discussion shells share identical outer edges. + scrollbar-gutter: stable; + } + body { // Align the top navigation and welcome area with the 1165px index shell. // The inner content remains 1100px; the added width represents the quick-link rail. @@ -3017,7 +3023,7 @@ body { * * Keep the established 1100px content width and the adjacent quick-link rail. * The index no longer uses a welcome banner. Discussion pages use a compact, - * content-aligned title block and a single reading column without side panels. + * content-aligned title block and the native Flarum vertical navigator. */ .App--index .WelcomeHero { display: none !important; @@ -3032,9 +3038,9 @@ body { .App--discussion .DiscussionHero .container { width: 100%; - max-width: 1100px; + max-width: 1165px; box-sizing: border-box; - padding: 17px 0 15px; + padding: 17px 80px 15px 15px; } .App--discussion .DiscussionHero-items { @@ -3147,18 +3153,19 @@ body { font-weight: 900; } -.App--discussion .DiscussionPage-discussion > .container, +.App--discussion .DiscussionPage-discussion > .container { + width: 100%; + max-width: 1165px; + box-sizing: border-box; + padding-right: 80px; +} + .App--discussion .DiscussionPage-stream, .App--discussion .PostStream { width: 100%; max-width: 1100px; box-sizing: border-box; } - -.App--discussion .DiscussionPage-discussion > .container { - display: block; -} - .App--discussion .PostStream { margin: 0; } @@ -3334,70 +3341,8 @@ body { } @media (min-width: 768px) { - .App--discussion .DiscussionPage-nav { - position: relative !important; - top: auto !important; - width: 100% !important; - margin: 0 0 8px !important; - padding: 14px 0 10px; - z-index: 20; - } - - .App--discussion .DiscussionPage-nav > ul { - display: flex; - align-items: center; - gap: 8px; - margin: 0; - } - - .App--discussion .DiscussionPage-nav > ul > li { - margin-bottom: 0; - } - - .App--discussion .DiscussionPage-nav .item-controls .ButtonGroup { - display: inline-flex; - width: auto; - } - - .App--discussion .DiscussionPage-nav .item-controls .ButtonGroup > .Button { - float: none; - flex: 0 0 auto; - } - - .App--discussion .DiscussionPage-nav .item-scrubber { - margin-left: auto; - } - - .App--discussion .DiscussionPage-nav .PostStreamScrubber { - margin-top: 0; - } - - .App--discussion .DiscussionPage-nav .PostStreamScrubber .Dropdown-menu { - display: none !important; - position: absolute; - top: calc(100% + 8px); - right: 0; - left: auto; - min-width: 240px; - padding: 12px; - border: 1px solid var(--ablecloud-brand-border); - border-radius: 12px; - background: #ffffff; - box-shadow: var(--ablecloud-shadow); - } - - .App--discussion .DiscussionPage-nav .PostStreamScrubber > .Dropdown-toggle { - display: inline-flex !important; - align-items: center; - } - - .App--discussion .DiscussionPage-nav .PostStreamScrubber.open > .Dropdown-menu { - display: block !important; - } - - .App--discussion .DiscussionPage-stream { - width: 100% !important; - margin: 0 !important; + .App--discussion .DiscussionHero { + margin-top: 16px; } } @@ -3427,6 +3372,12 @@ body { padding-left: 8px; } + .App--discussion .DiscussionPage-discussion > .container { + max-width: none; + padding-right: 15px; + padding-left: 15px; + } + .App--discussion .PostStream-item article.Post { padding-top: 18px; padding-bottom: 18px; diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 23aed5f..39e37c8 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -204,14 +204,17 @@ def test_discussion_detail_reading_flow_contract(self) -> None: ".App--discussion .item-bestAnswerPost", ".ablecloud-InlineReplyPrompt", ".ablecloud-TechnicalBlock--collapsed", - "single reading column without side panels", + "native Flarum vertical navigator", ): with self.subTest(style_contract=style_contract): self.assertIn(style_contract, self.less) self.assertIn("display: none !important", self.less) self.assertIn("max-width: 1100px", self.less) - self.assertIn("position: relative !important", self.less) + self.assertIn("max-width: 1165px", self.less) + self.assertIn("padding-right: 80px", self.less) + self.assertIn("scrollbar-gutter: stable", self.less) + self.assertIn("margin-top: 16px", self.less) self.assertIn("justify-content: flex-start", self.less) def test_compact_hero_and_navigation_width_contract(self) -> None: diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 030b49f..43ba769 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,6 +1,6 @@ { - "schema_version": "1.6", - "executed_at": "2026-08-20T00:33:55Z", + "schema_version": "1.7", + "executed_at": "2026-08-20T00:55:55Z", "scope": "Flarum index without welcome banner, Reddit-style discussion list and detail reading flow, and right-side quick links", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", @@ -69,9 +69,9 @@ "feed_to_quick_link_gap_px": 16, "mobile_viewport": { "width": 390, "height": 844 }, "mobile_quick_link_rail_size_px": { "width": 44, "height": 136 }, - "header_shell": { "x": 258, "right": 1423, "width": 1165 }, + "header_shell": { "x": 250, "right": 1415, "width": 1165 }, "welcome_hero_display": "none", - "index_content_shell": { "x": 258, "right": 1423, "width": 1165 }, + "index_content_shell": { "x": 250, "right": 1415, "width": 1165 }, "top_and_content_shell_aligned": true }, "compiled_css_grid_contract": "grid-column-start:1;grid-column-end:-1", @@ -80,8 +80,8 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-detail-toolbar-20260820T003355Z", - "theme_less_sha256": "e56463d030b90df6c9626ab2d20595ae51c4352923f4a0f9c67a8bbc644fd0fd", + "backup_root": "/var/backups/techflow-flarum/theme-native-nav-shell-20260820T005555Z", + "theme_less_sha256": "7a68b73b596cddef4308bdfaa7c4f7b2206ddab818c2bb32c097331a93aa3298", "theme_js_sha256": "05115ef3395d87d0f7327a50bac518376075b3cbd9bbeed356f48ee64521d42e", "services": { "nginx": "active", @@ -127,20 +127,23 @@ "quick_link_rail_y_after_feed_scroll_px": 339, "feed_scroll_top_px": 350, "bottom_footer_visible": false, - "header_shell": { "x": 258, "right": 1423, "width": 1165 }, + "header_shell": { "x": 250, "right": 1415, "width": 1165 }, "welcome_hero_display": "none", - "index_content_shell": { "x": 258, "right": 1423, "width": 1165 }, + "index_content_shell": { "x": 250, "right": 1415, "width": 1165 }, "top_and_content_shell_aligned": true }, "result": "PASS" }, "detail_reading_flow": { "layout": { - "side_information_columns": false, - "content_width_px": 1100, - "inner_stream_width_px": 1070, + "reddit_side_information_columns": false, + "content_width_px": 1165, + "inner_stream_width_px": 845, "title_alignment": "left", - "navigation": "64px horizontal toolbar above the single reading column", + "navigation": "native 150px sticky Flarum vertical navigator with reply, follow, first/latest post and unread position", + "navigation_quick_link_overlap_px": 0, + "header_to_first_tag_gap_px": 18, + "index_and_discussion_shell_edges_match": true, "normal_posts": "flat with 1px separators", "semantic_cards": ["AI technical support", "needs more information", "final solution"], "duplicate_selected_answer_preview": "hidden", @@ -153,8 +156,12 @@ "author_meta": "sijo · 8일 전", "post_count": 4, "first_post_text": "mold에서 가상머신 콘솔보기가 연결중에서 더이상 진행 되지가 않습니다.", - "navigation_height_px": 64, - "stream_width_px": 1070, + "shell": { "x": 250, "right": 1415, "width": 1165 }, + "navigation_width_px": 150, + "navigation_height_px": 467, + "stream_width_px": 845, + "header_to_first_tag_gap_px": 18, + "navigation_quick_link_overlap_px": 0, "duplicate_solution_visible": false, "result": "PASS" }, @@ -171,8 +178,12 @@ "solution_jump_final_top_px": 76, "duplicate_solution_visible": false, "inline_reply_opened_native_composer": true, - "navigation_height_px": 64, - "stream_width_px": 1070, + "shell": { "x": 250, "right": 1415, "width": 1165 }, + "navigation_width_px": 150, + "navigation_height_px": 517, + "stream_width_px": 845, + "header_to_first_tag_gap_px": 18, + "navigation_quick_link_overlap_px": 0, "quick_links": ["ABLECLOUD Home", "Blog", "Online Docs"], "result": "PASS" } diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index a9a9b71..597ae2e 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -342,3 +342,39 @@ JS의 운영 Vendor 해시는 각각 원본과 일치한다. 외부 HTTPS와 서버 로컬 HTTP는 200이며 Nginx·PHP-FPM·MariaDB· Community Monitor Timer는 모두 `active`다. DB Schema, 게시물, 첨부, TechFlow AI, GitHub→Chat Webhook은 변경하지 않았다. + +### Flarum 세로 탐색 복원과 시작·상세 셸 정렬 + +상세 읽기 흐름을 단일 열로 변경하면서 제거됐던 영역은 Reddit의 부가 정보가 아니라 +Flarum의 고유 토론 탐색 기능이었다. 사용자 검토에 따라 `답장`, `팔로우`, `원본 +게시물`, `최신 게시물`, 현재 게시물 위치와 읽지 않은 게시물 수를 표시하는 150px +세로 탐색 영역을 기존 Sticky 배치로 복원했다. Reddit의 좌우 커뮤니티 정보 패널은 +추가하지 않았다. + +시작 페이지는 퀵 링크 예약 영역을 포함한 1165px 셸이지만 상세 페이지는 1100px에 +남아 있었다. 상세 Hero와 본문 Container도 1165px로 통일하고 오른쪽 80px을 퀵 +링크용으로 예약했다. 이에 따라 본문 845px, 본문과 탐색 사이 75px, 세로 탐색 +150px의 기존 비율을 보존하면서 퀵 링크와 탐색 영역이 겹치지 않는다. 시작 페이지는 +`scrollbar-gutter: stable`을 사용해 상세 페이지의 세로 Scrollbar 공간과 동일한 +기준선을 사용한다. + +또한 고정 Header 실제 하단보다 상세 Hero가 먼저 시작해 태그가 Header에 붙어 보이던 +문제를 수정했다. 데스크톱 상세 Hero를 Header 아래로 16px 이동하고 기존 내부 여백을 +유지했다. WSL과 운영 브라우저에서 Header 하단과 첫 태그 상단 간격은 18px이었다. + +| 검증 항목 | WSL | 운영 | +|---|---:|---:| +| 시작 Header 셸 | X 250 / 폭 1165 / 우측 1415 | X 250 / 폭 1165 / 우측 1415 | +| 시작 본문 셸 | X 250 / 폭 1165 / 우측 1415 | X 250 / 폭 1165 / 우측 1415 | +| 상세 Hero 셸 | X 250 / 폭 1165 / 우측 1415 | X 250 / 폭 1165 / 우측 1415 | +| 상세 본문 셸 | X 250 / 폭 1165 / 우측 1415 | X 250 / 폭 1165 / 우측 1415 | +| 게시물 Stream | 845px | 845px | +| 세로 탐색 | 150×467px | 150×517px | +| Header와 첫 태그 간격 | 18px | 18px | +| 탐색과 퀵 링크 겹침 | 0px | 0px | + +운영 토론 #173의 게시물 1~5, 선택 답변 #5, 중복 미리보기 숨김과 인라인 답변 +작성 기능도 그대로 유지됐다. 최신 운영 백업은 +`/var/backups/techflow-flarum/theme-native-nav-shell-20260820T005555Z`이며 최종 +LESS SHA-256은 +`7a68b73b596cddef4308bdfaa7c4f7b2206ddab818c2bb32c097331a93aa3298`이다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index fb8cf0d..c18d485 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -86,7 +86,8 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 퀵 링크 사이에는 약 16px 간격이 있어야 하며 화면 오른쪽 끝으로 분리되면 안 된다. - 데스크톱 첫 화면의 상단 헤더와 본문 셸은 모두 최대 1165px로 동일해야 한다. - 1680px Viewport에서는 두 영역의 X가 258px, 오른쪽 끝이 1423px인지 확인한다. + `scrollbar-gutter: stable`을 적용한 1680px Viewport에서는 두 영역의 X가 250px, + 오른쪽 끝이 1415px인지 확인한다. 모바일과 다른 페이지에는 이 폭을 강제하지 않는다. - 좌측 메뉴는 사용자 설정 화면과 같이 고정한다. 높이가 짧아 태그가 모두 보이지 않을 때 내부 스크롤은 허용하지만 `scrollbar-width: none`과 WebKit 규칙으로 @@ -101,10 +102,16 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - 첫 게시물에 이미지가 있으면 첫 이미지를 데스크톱 124×92px, 모바일 88×72px 우측 썸네일로 표시한다. 이미지가 없는 글에는 빈 썸네일 영역을 만들지 않으며, 썸네일은 기존 Store 데이터에서 생성하고 지연 로딩해야 한다. -- 토론 상세는 1100px 콘텐츠 안의 단일 열이어야 한다. 우측 150px 탐색 열은 - 만들지 않고 답장·팔로우·게시물 탐색을 본문 위 64px 가로 도구 모음에 배치한다. +- 토론 상세 Hero와 본문 셸은 시작 페이지와 같은 1165px 외곽선을 사용해야 한다. + 게시물 Stream은 845px, 기존 Flarum 세로 탐색은 150px을 유지하고 그 사이에는 + 75px 간격을 둔다. 세로 탐색에는 답장·팔로우·원본/최신 게시물·현재 위치·읽지 + 않음 정보가 표시되어야 한다. +- 상세 셸 오른쪽 80px은 퀵 링크 영역으로 예약하며 세로 탐색과 퀵 링크의 겹침은 + 0px이어야 한다. - 상세 제목은 Welcome Hero와 같은 배너 외형을 사용하지 않는다. 흰 배경의 콘텐츠 제목 영역에서 해결 상태·태그·제목·작성자·작성 시각을 왼쪽 정렬로 표시한다. +- 데스크톱 상세 Hero는 고정 Header 아래에서 16px 내려 배치하고 Header 하단과 첫 + 태그 사이에 약 18px의 시각적 간격이 있어야 한다. - 일반 답변은 카드 중첩 없이 1px 구분선으로 나누고 AI 기술지원·추가 확인 필요· 최종 해결 가이드만 의미 색상 카드로 구분한다. - 첫 게시물 안의 선택 답변 미리보기는 숨기고 `해결 답변 보기`가 실제 선택 답변으로 @@ -137,5 +144,6 @@ sudo systemctl restart php8.3-fpm nginx - ROLLBACK: 화면·번역·로그인·작성·첨부 중 하나라도 실패. - 2026-08-20 운영 적용 완료: 사용자 메뉴 겹침 보완과 Reddit형 평면 피드·게시물 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크, 메인 환영 배너 - 제거와 상세 단일 열 읽기 흐름이 WSL 및 운영에 반영되었다. 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-detail-toolbar-20260820T003355Z`에 보존한다. + 제거, Flarum 세로 탐색 복원과 1165px 상세 셸 정렬이 WSL 및 운영에 반영되었다. + 최신 운영 백업은 + `/var/backups/techflow-flarum/theme-native-nav-shell-20260820T005555Z`에 보존한다. From ed20eae86ee095b448115591952b64984534cad1 Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 10:29:57 +0900 Subject: [PATCH 12/18] =?UTF-8?q?Community=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=EC=99=80=20=EB=B3=B8=EB=AC=B8=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=EC=84=A0=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/less/forum.less | 3 ++- deploy/flarum/tests/test_community_theme.py | 1 + ...community-index-fixed-feed-validation.json | 13 +++++++--- .../issue-73-community-ui-validation.md | 26 +++++++++++++++++++ .../community-theme-rollout-rollback.md | 4 ++- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index d7e9f52..5d1868b 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -2017,9 +2017,10 @@ body { } body { - // Align the top navigation and welcome area with the 1165px index shell. + // Align the top navigation and welcome area with the 1165px page shell. // The inner content remains 1100px; the added width represents the quick-link rail. .App--index .App-header > .container, + .App--discussion .App-header > .container, .App--index .WelcomeHero > .container { width: 100%; max-width: 1165px; diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 39e37c8..32a86b8 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -214,6 +214,7 @@ def test_discussion_detail_reading_flow_contract(self) -> None: self.assertIn("max-width: 1165px", self.less) self.assertIn("padding-right: 80px", self.less) self.assertIn("scrollbar-gutter: stable", self.less) + self.assertIn(".App--discussion .App-header > .container", self.less) self.assertIn("margin-top: 16px", self.less) self.assertIn("justify-content: flex-start", self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 43ba769..3381ad8 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,6 +1,6 @@ { - "schema_version": "1.7", - "executed_at": "2026-08-20T00:55:55Z", + "schema_version": "1.8", + "executed_at": "2026-08-20T01:29:03Z", "scope": "Flarum index without welcome banner, Reddit-style discussion list and detail reading flow, and right-side quick links", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", @@ -80,8 +80,8 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-native-nav-shell-20260820T005555Z", - "theme_less_sha256": "7a68b73b596cddef4308bdfaa7c4f7b2206ddab818c2bb32c097331a93aa3298", + "backup_root": "/var/backups/techflow-flarum/theme-header-shell-align-20260820T0120KST", + "theme_less_sha256": "5408e0c2fe0e52f43e8302adb1fc2f69b9d950de71171963ff172c88159602da", "theme_js_sha256": "05115ef3395d87d0f7327a50bac518376075b3cbd9bbeed356f48ee64521d42e", "services": { "nginx": "active", @@ -143,6 +143,7 @@ "navigation": "native 150px sticky Flarum vertical navigator with reply, follow, first/latest post and unread position", "navigation_quick_link_overlap_px": 0, "header_to_first_tag_gap_px": 18, + "header_hero_body_outer_edges_match": true, "index_and_discussion_shell_edges_match": true, "normal_posts": "flat with 1px separators", "semantic_cards": ["AI technical support", "needs more information", "final solution"], @@ -156,6 +157,7 @@ "author_meta": "sijo · 8일 전", "post_count": 4, "first_post_text": "mold에서 가상머신 콘솔보기가 연결중에서 더이상 진행 되지가 않습니다.", + "header_shell": { "x": 250, "right": 1415, "width": 1165 }, "shell": { "x": 250, "right": 1415, "width": 1165 }, "navigation_width_px": 150, "navigation_height_px": 467, @@ -178,6 +180,7 @@ "solution_jump_final_top_px": 76, "duplicate_solution_visible": false, "inline_reply_opened_native_composer": true, + "header_shell": { "x": 250, "right": 1415, "width": 1165 }, "shell": { "x": 250, "right": 1415, "width": 1165 }, "navigation_width_px": 150, "navigation_height_px": 517, @@ -190,6 +193,8 @@ }, "validation": { "theme_contract_tests": { "passed": 13, "failed": 0 }, + "wsl_rehearsal": "issue-73-header-shell-align-v2 PASS", + "header_hero_body_alignment": "WSL and production x=250, right=1415, width=1165", "discussion_item_enhancement_is_idempotent": true, "summary_uses_existing_store_data": true, "thumbnail_uses_existing_first_post_html": true, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index 597ae2e..a5b5b52 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -378,3 +378,29 @@ Flarum의 고유 토론 탐색 기능이었다. 사용자 검토에 따라 `답 `/var/backups/techflow-flarum/theme-native-nav-shell-20260820T005555Z`이며 최종 LESS SHA-256은 `7a68b73b596cddef4308bdfaa7c4f7b2206ddab818c2bb32c097331a93aa3298`이다. + +### 상세 Header·제목·본문 기준선 최종 통일 + +후속 운영 화면에서 상세 제목과 본문은 1165px 셸을 사용하지만 Header만 Flarum +기본 1100px 컨테이너에 남아 양쪽 기준선이 32.5px씩 어긋나는 것을 확인했다. +데스크톱 상세 화면의 Header에도 페이지 공통 1165px 셸을 적용해 Header, 상세 +Hero, 본문 Container가 같은 외곽선을 사용하도록 교정했다. 내부 로고·제목·게시물은 +각 셸의 15px 안쪽 여백에서 동일하게 시작한다. + +| 브라우저 실측 항목 | WSL | 운영 | +|---|---:|---:| +| Header 셸 | X 250 / 폭 1165 / 우측 1415 | X 250 / 폭 1165 / 우측 1415 | +| 상세 Hero 셸 | X 250 / 폭 1165 / 우측 1415 | X 250 / 폭 1165 / 우측 1415 | +| 상세 본문 셸 | X 250 / 폭 1165 / 우측 1415 | X 250 / 폭 1165 / 우측 1415 | +| 로고·제목·본문 시작선 | X 265 | X 265 | + +WSL 토론 #144와 운영 토론 #173의 실제 데이터를 브라우저로 다시 확인했고, 게시물 +Stream 845px와 Flarum 세로 탐색 150px은 변경하지 않았다. WSL 계약 테스트 13건과 +`issue-73-header-shell-align-v2` 적용·비활성화·재적용 주기가 통과했으며 콘텐츠와 +첨부 해시도 보존됐다. 운영 테마 Source와 Vendor LESS SHA-256은 모두 +`5408e0c2fe0e52f43e8302adb1fc2f69b9d950de71171963ff172c88159602da`로 +일치한다. 최신 운영 백업은 +`/var/backups/techflow-flarum/theme-header-shell-align-20260820T0120KST`이며, +서버 로컬 HTTP 200과 Nginx·PHP-FPM·MariaDB·Community Monitor Timer `active`를 +확인했다. DB Schema, 게시물, 첨부, TechFlow AI와 GitHub→Chat Webhook은 변경하지 +않았다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index c18d485..fae6807 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -103,6 +103,8 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 우측 썸네일로 표시한다. 이미지가 없는 글에는 빈 썸네일 영역을 만들지 않으며, 썸네일은 기존 Store 데이터에서 생성하고 지연 로딩해야 한다. - 토론 상세 Hero와 본문 셸은 시작 페이지와 같은 1165px 외곽선을 사용해야 한다. + 상세 Header도 같은 1165px 외곽선을 사용해 Header·Hero·본문의 좌우 끝이 정확히 + 일치해야 한다. 게시물 Stream은 845px, 기존 Flarum 세로 탐색은 150px을 유지하고 그 사이에는 75px 간격을 둔다. 세로 탐색에는 답장·팔로우·원본/최신 게시물·현재 위치·읽지 않음 정보가 표시되어야 한다. @@ -146,4 +148,4 @@ sudo systemctl restart php8.3-fpm nginx 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크, 메인 환영 배너 제거, Flarum 세로 탐색 복원과 1165px 상세 셸 정렬이 WSL 및 운영에 반영되었다. 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-native-nav-shell-20260820T005555Z`에 보존한다. + `/var/backups/techflow-flarum/theme-header-shell-align-20260820T0120KST`에 보존한다. From dca3809c73882ee77450cd98d8b1b5b16dda09d6 Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 10:48:53 +0900 Subject: [PATCH 13/18] =?UTF-8?q?Community=20=EC=A2=81=EC=9D=80=20?= =?UTF-8?q?=ED=86=A0=EB=A1=A0=20=EB=AA=A9=EB=A1=9D=EA=B3=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EA=B8=B0=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/less/forum.less | 102 +++++++++++++++++- deploy/flarum/tests/test_community_theme.py | 21 ++++ ...community-index-fixed-feed-validation.json | 38 ++++++- .../issue-73-community-ui-validation.md | 34 ++++++ .../community-theme-rollout-rollback.md | 7 +- 5 files changed, 193 insertions(+), 9 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 5d1868b..c7b44fe 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -91,6 +91,22 @@ .fa-shield-alt { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m12 3 8 3v6c0 5-3.5 8-8 10-4.5-2-8-5-8-10V6l8-3Z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E"); } .fa-plus-circle { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M12 8v8M8 12h8' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } .fa-circle { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='7' fill='black'/%3E%3C/svg%3E"); } +.fa-eye { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'/%3E%3Ccircle cx='12' cy='12' r='3' fill='none' stroke='black' stroke-width='2'/%3E%3C/svg%3E"); } +.fa-eye-slash { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 3l18 18M10.7 5.2A10 10 0 0 1 12 5c6 0 10 7 10 7a18 18 0 0 1-3 3.7M6.2 6.2A18 18 0 0 0 2 12s4 7 10 7a9 9 0 0 0 4-.9M9.9 9.9a3 3 0 0 0 4.2 4.2' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-bold { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 4h6a4 4 0 0 1 0 8H7V4Zm0 8h7a4 4 0 0 1 0 8H7v-8Z' fill='none' stroke='black' stroke-width='2.4' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-italic { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 4h8M6 20h8M14 4 10 20' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'/%3E%3C/svg%3E"); } +.fa-code { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m8 5-6 7 6 7M16 5l6 7-6 7M14 3 10 21' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-quote-left { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M10 5H5a3 3 0 0 0-3 3v5h6v6M22 5h-5a3 3 0 0 0-3 3v5h6v6' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-link { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m10 14 4-4M8.5 17.5 6 20a4 4 0 0 1-6-6l4-4a4 4 0 0 1 6 0M15.5 6.5 18 4a4 4 0 0 1 6 6l-4 4a4 4 0 0 1-6 0' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } +.fa-image { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='3' y='4' width='18' height='16' rx='2' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='9' cy='9' r='2' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m4 18 5-5 3 3 2-2 6 5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-list-ul { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='4' cy='6' r='1.5' fill='black'/%3E%3Ccircle cx='4' cy='12' r='1.5' fill='black'/%3E%3Ccircle cx='4' cy='18' r='1.5' fill='black'/%3E%3Cpath d='M9 6h12M9 12h12M9 18h12' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } +.fa-list-ol { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6h12M9 12h12M9 18h12M3 4h2v4M3 11h2l-2 3h2M3 17h2l-2 3h2' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-plus { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 4v16M4 12h16' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E"); } +.fa-strikethrough { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17 6.5C16 5 14.3 4 12 4 8.7 4 7 5.5 7 7.5c0 1.8 1.5 2.8 4 3.5M5 12h14M9 15c.6 3.3 6.9 3.4 7.8.4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } +.fa-subscript { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m5 5 8 12M13 5 5 17M15 17h4l-4 4h4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-superscript { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m5 7 8 12M13 7 5 19M15 3h4l-4 4h4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-terminal { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='3' y='4' width='18' height='16' rx='2' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m7 9 3 3-3 3M12 16h5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } +.fa-caret-square-right { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='3' y='3' width='18' height='18' rx='2' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m10 8 6 4-6 4V8Z' fill='black'/%3E%3C/svg%3E"); } .Search-input::before { font-family: "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2", sans-serif !important; @@ -2527,25 +2543,105 @@ body { } .DiscussionPage-list .DiscussionListItem-content { + display: grid; width: auto; - min-height: 60px; - padding: 10px 104px 10px 50px !important; + min-height: 104px; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto auto; + align-content: start; + padding: 10px 12px 12px 50px !important; + } + + .DiscussionPage-list .DiscussionListItem-author { + position: absolute !important; + top: 11px; + left: 10px; + margin: 0 !important; + } + + .DiscussionPage-list .ablecloud-DiscussionMeta { + min-width: 0; + grid-row: 1; + grid-column: 1; + gap: 4px; + padding-right: 0; + font-size: 11px; + } + + .DiscussionPage-list .ablecloud-DiscussionMeta-category { + max-width: 118px; + flex: 0 1 auto; } .DiscussionPage-list .DiscussionListItem-main { + display: grid !important; width: 100% !important; - min-height: 38px; + min-width: 0; + min-height: 0; + grid-row: 2; + grid-column: 1; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto auto; gap: 0; + margin-top: 4px; + padding-bottom: 22px !important; } .DiscussionPage-list .DiscussionListItem-title { + display: -webkit-box; + width: auto !important; min-width: 0; + grid-row: 1; + grid-column: 1; + overflow: hidden; font-size: 14px; + line-height: 1.4; + text-overflow: initial; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + white-space: normal; + } + + .DiscussionPage-list .ablecloud-DiscussionSummary { + grid-row: 2; + grid-column: 1; + margin: 4px 0 0; + font-size: 12px; + line-height: 1.45; + -webkit-line-clamp: 2; + } + + .DiscussionPage-list .ablecloud-DiscussionMain--withThumbnail { + grid-template-columns: minmax(0, 1fr) 72px; + grid-template-rows: auto auto; + column-gap: 10px; + } + + .DiscussionPage-list .ablecloud-DiscussionMain--withThumbnail .DiscussionListItem-title, + .DiscussionPage-list .ablecloud-DiscussionMain--withThumbnail .ablecloud-DiscussionSummary { + grid-column: 1; + } + + .DiscussionPage-list .ablecloud-DiscussionThumbnail { + width: 72px; + height: 54px; + grid-row-start: 1; + grid-row-end: 3; + grid-column: 2; + align-self: start; } .DiscussionPage-list .DiscussionListItem-info { display: none; } + + .DiscussionPage-list .DiscussionListItem-count, + .DiscussionPage-list .DiscussionListItem:has(.DiscussionListItem-controls) .DiscussionListItem-count { + top: auto; + right: 12px; + bottom: 12px; + transform: none; + } } } diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 32a86b8..7bb5bb0 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -98,6 +98,22 @@ def test_embedded_svg_icon_and_navigation_contract(self) -> None: ".fa-paper-plane", ".fa-photo-video", ".fa-smile", + ".fa-eye", + ".fa-eye-slash", + ".fa-bold", + ".fa-italic", + ".fa-code", + ".fa-quote-left", + ".fa-link", + ".fa-image", + ".fa-list-ul", + ".fa-list-ol", + ".fa-plus", + ".fa-strikethrough", + ".fa-subscript", + ".fa-superscript", + ".fa-terminal", + ".fa-caret-square-right", ): with self.subTest(icon=icon): self.assertIn(icon, self.less) @@ -260,6 +276,11 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("visibility: hidden", self.less) self.assertIn(".fa-bolt", self.less) self.assertIn(".DiscussionPage-list", self.less) + self.assertIn(".DiscussionPage-list .ablecloud-DiscussionMeta", self.less) + self.assertIn(".DiscussionPage-list .ablecloud-DiscussionMain--withThumbnail", self.less) + self.assertIn("grid-template-columns: minmax(0, 1fr) 72px", self.less) + self.assertIn(".DiscussionPage-list .ablecloud-DiscussionSummary", self.less) + self.assertIn(".DiscussionPage-list .DiscussionListItem-count", self.less) self.assertIn("height: calc(100vh - 68px)", self.less) self.assertIn("scrollbar-width: thin", self.less) self.assertIn(".Search-input input:focus-visible", self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 3381ad8..829544b 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,6 +1,6 @@ { - "schema_version": "1.8", - "executed_at": "2026-08-20T01:29:03Z", + "schema_version": "1.9", + "executed_at": "2026-08-20T01:47:43Z", "scope": "Flarum index without welcome banner, Reddit-style discussion list and detail reading flow, and right-side quick links", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", @@ -80,8 +80,8 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-header-shell-align-20260820T0120KST", - "theme_less_sha256": "5408e0c2fe0e52f43e8302adb1fc2f69b9d950de71171963ff172c88159602da", + "backup_root": "/var/backups/techflow-flarum/theme-drawer-readability-icons-20260820T1045KST", + "theme_less_sha256": "6bcbbebba32f0ad2a49f30c66820c8891a93491d25a4e389242aaa10f2025551", "theme_js_sha256": "05115ef3395d87d0f7327a50bac518376075b3cbd9bbeed356f48ee64521d42e", "services": { "nginx": "active", @@ -191,9 +191,37 @@ "result": "PASS" } }, + "narrow_discussion_drawer": { + "width_px": 400, + "width_changed": false, + "layout": [ + "author avatar fixed at the left edge", + "category, author and date metadata on the first row", + "title on an independent two-line row", + "summary on an independent two-line row", + "optional 72x54 thumbnail in a dedicated right column", + "reply count fixed to the bottom-right without overlapping text" + ], + "content_right_padding_px": 12, + "title_and_summary_share_horizontal_flex_row": false, + "thumbnail_size_px": { "width": 72, "height": 54 }, + "wsl_rehearsal": "issue-73-drawer-icons-v3 PASS", + "result": "PASS" + }, + "composer_icon_fallback": { + "implementation": "embedded SVG masks without Font Awesome webfont dependency", + "mapped_icons": [ + "preview", "bold", "italic", "inline code", "quote", "link", "image", + "unordered list", "ordered list", "additional controls", "strikethrough", + "subscript", "superscript", "spoiler", "code block", "block spoiler" + ], + "wsl_browser": { "visible_icon_width_px": "15 to 16", "broken_glyph_count": 0 }, + "production_browser": { "visible_icon_width_px": "15 to 16", "broken_glyph_count": 0 }, + "result": "PASS" + }, "validation": { "theme_contract_tests": { "passed": 13, "failed": 0 }, - "wsl_rehearsal": "issue-73-header-shell-align-v2 PASS", + "wsl_rehearsal": "issue-73-drawer-icons-v3 PASS", "header_hero_body_alignment": "WSL and production x=250, right=1415, width=1165", "discussion_item_enhancement_is_idempotent": true, "summary_uses_existing_store_data": true, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index a5b5b52..acd6dd0 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -404,3 +404,37 @@ Stream 845px와 Flarum 세로 탐색 150px은 변경하지 않았다. WSL 계약 서버 로컬 HTTP 200과 Nginx·PHP-FPM·MariaDB·Community Monitor Timer `active`를 확인했다. DB Schema, 게시물, 첨부, TechFlow AI와 GitHub→Chat Webhook은 변경하지 않았다. + +### 좁은 토론 목록 재배열과 작성기 아이콘 복구 + +상세 화면 왼쪽에 마우스를 가져갔을 때 표시되는 토론 목록의 400px 폭은 유지했다. +기존에는 데스크톱 목록의 가로 Flex 규칙이 좁은 목록에도 상속되어 제목, 요약과 +썸네일이 한 행에서 서로 폭을 빼앗고, 오른쪽 여백도 104px이어서 제목이 몇 글자만 +보였다. + +좁은 목록만 별도 Grid로 분리해 다음 순서로 재배열했다. + +1. 왼쪽에는 작성자 아바타를 고정한다. +2. 첫 행에는 카테고리·작성자·날짜를 표시한다. +3. 제목은 독립 행에서 최대 두 줄을 사용한다. +4. 요약은 제목 아래 독립 행에서 최대 두 줄을 사용한다. +5. 이미지가 있으면 72×54px 썸네일을 오른쪽 전용 열에 표시한다. +6. 댓글 수는 텍스트와 겹치지 않는 카드 오른쪽 아래에 둔다. + +이에 따라 목록 폭은 400px 그대로이고 본문 오른쪽 여백은 104px에서 12px로 +줄었다. 제목·요약·썸네일이 하나의 가로 Flex 행을 공유하지 않으므로 긴 한글 제목과 +요약을 연속해서 읽을 수 있다. + +토론 작성기에서는 Font Awesome 웹폰트 부재 시 미리보기·굵게·기울임·코드·인용· +링크·이미지·글머리/번호 목록 등의 아이콘이 `P` 또는 검은 사각형으로 보였다. +미리보기와 기본 서식 10종, 추가 서식 6종을 기존 내장 SVG Mask 체계에 추가했다. +WSL과 운영 작성기에서 각 아이콘은 15~16px 크기와 실제 Mask URL을 가지며 깨진 +대체 문자는 0건이었다. + +테마 계약 테스트 13건과 WSL의 `issue-73-drawer-icons-v3` 적용·비활성화·재적용 +주기를 통과했다. 운영 Source와 Vendor LESS SHA-256은 모두 +`6bcbbebba32f0ad2a49f30c66820c8891a93491d25a4e389242aaa10f2025551`로 +일치한다. 최신 운영 백업은 +`/var/backups/techflow-flarum/theme-drawer-readability-icons-20260820T1045KST`이며, +서버 로컬 HTTP 200과 Nginx·PHP-FPM·MariaDB·Community Monitor Timer `active`를 +확인했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index fae6807..16e7217 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -120,6 +120,11 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 이동해야 한다. 이동 후 선택 답변 상단은 고정 Header 아래에서 보여야 한다. - 질문 다음에는 `답변을 작성해 주세요` 버튼이 표시되어 기존 Flarum 답장 작성기를 열어야 한다. 480px보다 긴 `pre` 로그에는 `전체 로그 보기`와 `로그 접기`가 제공되어야 한다. +- 왼쪽 토론 목록은 400px 폭을 유지하되 메타 정보, 두 줄 제목, 두 줄 요약을 세로로 + 배치해야 한다. 썸네일은 72×54px 오른쪽 전용 열, 댓글 수는 오른쪽 아래에 두고 + 제목·요약과 겹치지 않아야 한다. +- 작성기의 미리보기·굵게·기울임·코드·인용·링크·이미지·목록·추가 서식 아이콘은 + SVG Mask로 표시되어야 하며 `P`나 검은 사각형 대체 문자가 없어야 한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. @@ -148,4 +153,4 @@ sudo systemctl restart php8.3-fpm nginx 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크, 메인 환영 배너 제거, Flarum 세로 탐색 복원과 1165px 상세 셸 정렬이 WSL 및 운영에 반영되었다. 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-header-shell-align-20260820T0120KST`에 보존한다. + `/var/backups/techflow-flarum/theme-drawer-readability-icons-20260820T1045KST`에 보존한다. From 5ce094094dde6a9626d8269f4940ffa4f4740d80 Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 11:30:45 +0900 Subject: [PATCH 14/18] =?UTF-8?q?Community=20=ED=86=A0=EB=A1=A0=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=A0=88=EC=9D=B4=EC=96=B4=EC=99=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EA=B8=B0=20=EC=B6=A9=EB=8F=8C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/dist/forum.js | 124 +++++++++++++++++- .../resources/less/forum.less | 25 ++++ deploy/flarum/tests/test_community_theme.py | 19 ++- ...community-index-fixed-feed-validation.json | 34 ++++- .../issue-73-community-ui-validation.md | 44 +++++++ .../community-theme-rollout-rollback.md | 9 +- 6 files changed, 240 insertions(+), 15 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js index e02a105..9d91357 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js +++ b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js @@ -7,6 +7,9 @@ var absoluteDatePattern = /^(\d{4})-(\d{2})-(\d{2})/; var discussionItemEnhancedAttribute = 'data-ablecloud-post-structure'; var discussionDetailEnhancedAttribute = 'data-ablecloud-reading-flow'; + var discussionListSnapshotKey = 'ablecloud-community-discussion-list'; + var fallbackPaneAttribute = 'data-ablecloud-fallback-pane'; + var fallbackPaneActive = false; function formatDiscussionCreatedAt(createdAt) { if (!(createdAt instanceof Date) || Number.isNaN(createdAt.getTime())) { @@ -332,11 +335,10 @@ var url = new URL(link.getAttribute('href'), window.location.origin); var canonicalPath = url.pathname.replace(/(\/d\/[^/]+)\/\d+\/?$/, '$1'); - if (canonicalPath === url.pathname) { - return; + if (canonicalPath !== url.pathname) { + link.setAttribute('href', canonicalPath + url.search); } - link.setAttribute('href', canonicalPath + url.search); link.setAttribute('data-ablecloud-start-from-top', 'true'); }); } @@ -348,12 +350,117 @@ return; } - var href = link.getAttribute('href'); + window.requestAnimationFrame(function () { + window.scrollTo(0, 0); + }); - event.preventDefault(); - event.stopImmediatePropagation(); + window.setTimeout(function () { + window.scrollTo(0, 0); + }, 120); + } + + function captureDiscussionListSnapshot(root) { + var list = root.querySelector('.App--index .DiscussionList'); + + if (!list) { + return; + } + + try { + window.sessionStorage.setItem(discussionListSnapshotKey, list.outerHTML); + } catch (error) { + // A browser with disabled session storage can still use the normal back link. + } + } + + function markCurrentDiscussionInPane(pane) { + var currentPath = window.location.pathname.replace(/\/$/, '').replace(/(\/d\/[^/]+)\/\d+$/, '$1'); + + pane.querySelectorAll('.DiscussionListItem').forEach(function (item) { + var link = item.querySelector('.DiscussionListItem-main[href]'); + var active = false; + + if (link) { + try { + active = new URL(link.getAttribute('href'), window.location.origin).pathname.replace(/\/$/, '') === currentPath; + } catch (error) { + active = false; + } + } + + item.classList.toggle('active', active); + }); + } + + function ensureFallbackDiscussionPane(root) { + var page = root.querySelector('.App--discussion .DiscussionPage'); + + if (!page) { + if (fallbackPaneActive && forumApp.pane) { + forumApp.pane.disable(); + } + + fallbackPaneActive = false; + return; + } + + var nativePane = page.querySelector('.DiscussionPage-list:not([' + fallbackPaneAttribute + '="true"])'); + + if (nativePane) { + return; + } + + if (page.querySelector('.DiscussionPage-list[' + fallbackPaneAttribute + '="true"]')) { + return; + } + + var snapshot = ''; + + try { + snapshot = window.sessionStorage.getItem(discussionListSnapshotKey) || ''; + } catch (error) { + snapshot = ''; + } - window.location.assign(href); + if (!snapshot) { + return; + } + + var template = document.createElement('template'); + template.innerHTML = snapshot; + var list = template.content.querySelector('.DiscussionList'); + + if (!list) { + return; + } + + var pane = document.createElement('aside'); + pane.className = 'DiscussionPage-list ablecloud-DiscussionPage-list--fallback'; + pane.setAttribute(fallbackPaneAttribute, 'true'); + pane.setAttribute('aria-label', '최근 토론 목록'); + pane.appendChild(list); + markCurrentDiscussionInPane(pane); + page.insertBefore(pane, page.firstChild); + + if (forumApp.pane) { + forumApp.pane.enable(); + forumApp.pane.hide(); + pane.addEventListener('mouseenter', forumApp.pane.show.bind(forumApp.pane)); + pane.addEventListener('mouseleave', forumApp.pane.onmouseleave.bind(forumApp.pane)); + } + + fallbackPaneActive = true; + } + + function showFallbackPaneFromEdge(event) { + if ( + fallbackPaneActive && + forumApp.pane && + event.clientX < 10 && + !document.querySelector('.Composer.visible:not(.minimized)') + ) { + forumApp.pane.show(); + } } forumApp.initializers.add('ablecloud-community-theme-tag-date', function () { @@ -364,6 +471,8 @@ formatTagDiscussionDates(document); normalizeDiscussionListTargets(document); enhanceDiscussionListItems(document); + captureDiscussionListSnapshot(document); + ensureFallbackDiscussionPane(document); enhanceDiscussionDetail(document); }); }; @@ -377,6 +486,7 @@ }); document.addEventListener('click', openDiscussionFromTop, true); + document.addEventListener('mousemove', showFallbackPaneFromEdge, { passive: true }); }); module.exports = {}; diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index c7b44fe..baa2495 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1819,6 +1819,21 @@ body { box-shadow: 0 20px 52px rgba(24, 71, 142, 0.18), 0 0 0 1px rgba(21, 94, 239, 0.1) !important; } + @media (min-width: 768px) { + .Composer.active:not(.fullScreen) { + min-height: 336px; + } + + .Composer.active:not(.fullScreen) .TextEditor-editor { + padding-bottom: 32px; + } + + .Composer.active:not(.fullScreen) .TextEditor-controls { + position: relative; + bottom: 20px; + } + } + .ComposerBody-header .DiscussionComposer-changeTags { display: inline-flex; align-items: center; @@ -2503,6 +2518,16 @@ body { scrollbar-width: thin; } + // Keep the discussion context available, but never let the hover pane sit + // on top of the composer while the user is writing a new topic or reply. + .App:has(.Composer.visible:not(.minimized)) .DiscussionPage-list { + display: none !important; + } + + .App.hasPane.panePinned:has(.Composer.visible:not(.minimized)) .App-composer { + left: 0; + } + .DiscussionPage-list::-webkit-scrollbar { width: 6px; } diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 7bb5bb0..69df574 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -150,8 +150,18 @@ def test_tag_date_format_contract(self) -> None: self.assertIn("data-ablecloud-start-from-top", self.forum_js) self.assertIn("/(\\/d\\/[^/]+)\\/\\d+\\/?$/", self.forum_js) self.assertIn("openDiscussionFromTop", self.forum_js) - self.assertIn("window.location.assign(href)", self.forum_js) + self.assertNotIn("window.location.assign(href)", self.forum_js) + self.assertNotIn("event.preventDefault()", self.forum_js.split("function openDiscussionFromTop", 1)[1].split("function captureDiscussionListSnapshot", 1)[0]) + self.assertIn("window.scrollTo(0, 0)", self.forum_js) self.assertIn("document.addEventListener('click', openDiscussionFromTop, true)", self.forum_js) + self.assertIn("captureDiscussionListSnapshot", self.forum_js) + self.assertIn("ensureFallbackDiscussionPane", self.forum_js) + self.assertIn("ablecloud-community-discussion-list", self.forum_js) + self.assertIn("data-ablecloud-fallback-pane", self.forum_js) + self.assertIn("forumApp.pane.enable()", self.forum_js) + self.assertNotIn("addFallbackPanePinButton", self.forum_js) + self.assertIn("document.addEventListener('mousemove', showFallbackPaneFromEdge", self.forum_js) + self.assertIn("!document.querySelector('.Composer.visible:not(.minimized)')", self.forum_js) def test_reddit_style_discussion_item_structure_contract(self) -> None: for script_contract in ( @@ -283,6 +293,13 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn(".DiscussionPage-list .DiscussionListItem-count", self.less) self.assertIn("height: calc(100vh - 68px)", self.less) self.assertIn("scrollbar-width: thin", self.less) + self.assertIn(".App:has(.Composer.visible:not(.minimized)) .DiscussionPage-list", self.less) + self.assertIn(".App.hasPane.panePinned:has(.Composer.visible:not(.minimized)) .App-composer", self.less) + self.assertIn(".Composer.active:not(.fullScreen)", self.less) + self.assertIn("min-height: 336px", self.less) + self.assertIn(".Composer.active:not(.fullScreen) .TextEditor-controls", self.less) + self.assertIn("bottom: 20px", self.less) + self.assertIn("padding-bottom: 32px", self.less) self.assertIn(".Search-input input:focus-visible", self.less) self.assertIn("outline: none !important", self.less) self.assertIn('.Header-secondary .Dropdown-menu .Button-icon:not([class*="fa-"])', self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 829544b..97d8d1d 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,6 +1,6 @@ { - "schema_version": "1.9", - "executed_at": "2026-08-20T01:47:43Z", + "schema_version": "2.0", + "executed_at": "2026-08-20T02:23:35Z", "scope": "Flarum index without welcome banner, Reddit-style discussion list and detail reading flow, and right-side quick links", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", @@ -80,9 +80,9 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-drawer-readability-icons-20260820T1045KST", - "theme_less_sha256": "6bcbbebba32f0ad2a49f30c66820c8891a93491d25a4e389242aaa10f2025551", - "theme_js_sha256": "05115ef3395d87d0f7327a50bac518376075b3cbd9bbeed356f48ee64521d42e", + "backup_root": "/var/backups/techflow-flarum/theme-composer-pane-final-20260820T112831KST", + "theme_less_sha256": "3fc9dda37cc2e7e78024d470e122a1b883d023de658c973be9467e40359d0ca9", + "theme_js_sha256": "a52c0bc148280a01f8cbce9a1ce79a28be6d7529bba7591ea1a6a9ed042dfe62", "services": { "nginx": "active", "php8.3-fpm": "active", @@ -219,9 +219,31 @@ "production_browser": { "visible_icon_width_px": "15 to 16", "broken_glyph_count": 0 }, "result": "PASS" }, + "composer_and_discussion_pane": { + "root_cause": "the start-from-top handler forced window.location.assign and discarded Flarum's in-memory discussion-list state", + "navigation_fix": "retain Flarum SPA navigation, normalize the link target, and reset scroll without preventDefault", + "reload_fallback": "store the already-rendered DiscussionList in session storage and restore a 400px pane after a direct detail reload", + "composer_layout": { + "desktop_active_min_height_px": 336, + "submit_bottom_inset_px": { "wsl": 22, "production": 22 }, + "footer_bottom_inset_px": { "wsl": 8, "production": 8 }, + "horizontal_overflow": false + }, + "discussion_pane": { + "width_px": 400, + "item_count": { "wsl": 20, "production": 20 }, + "active_item_after_reload": { "wsl": 1, "production": 1 }, + "list_click_scroll_y": { "wsl": 0, "production": 0 }, + "hidden_while_composer_visible": true, + "restored_after_composer_close": true, + "composer_overlap": { "wsl": false, "production": false } + }, + "wsl_rehearsal": "issue-73-composer-pane-v8 PASS", + "result": "PASS" + }, "validation": { "theme_contract_tests": { "passed": 13, "failed": 0 }, - "wsl_rehearsal": "issue-73-drawer-icons-v3 PASS", + "wsl_rehearsal": "issue-73-composer-pane-v8 PASS", "header_hero_body_alignment": "WSL and production x=250, right=1415, width=1165", "discussion_item_enhancement_is_idempotent": true, "summary_uses_existing_store_data": true, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index acd6dd0..42dd831 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -438,3 +438,47 @@ WSL과 운영 작성기에서 각 아이콘은 15~16px 크기와 실제 Mask URL `/var/backups/techflow-flarum/theme-drawer-readability-icons-20260820T1045KST`이며, 서버 로컬 HTTP 200과 Nginx·PHP-FPM·MariaDB·Community Monitor Timer `active`를 확인했다. + +### 토론 목록 레이어 복원과 작성기 충돌 방지 + +목록에서 상세 화면을 열 때 항상 첫 게시물부터 보이도록 만든 기존 처리가 +`window.location.assign()`으로 전체 페이지 이동을 강제하고 있었다. 이 과정에서 +Flarum이 메모리에 보관하던 토론 목록 상태가 사라져 화면 왼쪽 가장자리에 마우스를 +가져가도 400px 토론 목록 레이어가 만들어지지 않았다. + +전체 페이지 이동을 제거하고 Flarum의 SPA 화면 전환을 유지하도록 변경했다. 링크는 +첫 게시물 경로로 정규화하되 기본 클릭 동작을 막지 않고, 화면 전환 직후 스크롤만 +0으로 맞춘다. 목록에서 상세로 이동한 WSL·운영 브라우저에서 각각 20개 항목의 +`DiscussionPage-list`가 다시 생성되고 `scrollY=0`을 확인했다. 상세 화면을 직접 +새로고침해도 직전에 표시한 목록 Snapshot으로 레이어를 복원하며 현재 토론 1건을 +활성 상태로 표시한다. + +작성기 하단은 Flarum의 300px Inline 높이보다 내부 편집기와 Toolbar가 더 길어 +게시 버튼이 화면 하단에 걸렸다. 데스크톱 활성 작성기의 최소 높이를 336px로 +확보하고 Toolbar를 안쪽으로 배치했다. 편집기 하단 여백도 늘려 마지막 문장이 +Toolbar 아래에 가려지지 않도록 했다. + +토론 목록 레이어가 열린 상태에서 새 토론이나 답변 작성기를 열면 두 화면이 +겹쳤다. 작성기가 `visible`인 동안 목록 레이어만 일시적으로 숨기고, 고정된 Pane이 +작성기 폭을 밀지 않도록 Composer Container의 왼쪽 값을 0으로 복원했다. 작성기를 +닫으면 목록 레이어와 고정 상태가 다시 표시된다. + +| 브라우저 검증 항목 | WSL | 운영 | +|---|---:|---:| +| 복원된 목록 폭·항목 수 | 400px · 20건 | 400px · 20건 | +| 목록 클릭 후 상세 `scrollY` | 0 | 0 | +| 새로고침 후 현재 토론 활성 항목 | 1건 | 1건 | +| 활성 작성기 높이 | 336px | 336px | +| 게시 버튼 하단 여백 | 22px | 22px | +| 작성기 Footer 하단 여백 | 8px | 8px | +| 작성기와 목록 겹침 | 없음 | 없음 | + +테마 계약 테스트 13건과 WSL `issue-73-composer-pane-v8` 적용·비활성화·재적용 +주기를 통과했다. 운영 배포 전 백업은 +`/var/backups/techflow-flarum/theme-composer-pane-final-20260820T112831KST`에 +보존했다. 운영 Source와 Vendor LESS SHA-256은 +`3fc9dda37cc2e7e78024d470e122a1b883d023de658c973be9467e40359d0ca9`, JS SHA-256은 +`a52c0bc148280a01f8cbce9a1ce79a28be6d7529bba7591ea1a6a9ed042dfe62`로 일치한다. +외부 HTTPS 200과 Nginx·PHP-FPM·MariaDB·Community Monitor Timer `active`를 +확인했으며 DB Schema, 게시물, 첨부, TechFlow AI, GitHub→Chat Webhook은 변경하지 +않았다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 16e7217..5435e10 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -125,6 +125,13 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 제목·요약과 겹치지 않아야 한다. - 작성기의 미리보기·굵게·기울임·코드·인용·링크·이미지·목록·추가 서식 아이콘은 SVG Mask로 표시되어야 하며 `P`나 검은 사각형 대체 문자가 없어야 한다. +- 목록에서 상세 화면으로 이동했을 때 전체 페이지를 다시 열지 않고 Flarum SPA + 전환을 유지해야 한다. 상세 화면의 `scrollY`는 0이고 왼쪽 가장자리 Hover 시 + 400px 토론 목록이 표시되어야 한다. 상세 새로고침 뒤에도 직전 목록 Snapshot을 + 사용해 목록을 복원해야 한다. +- 데스크톱 활성 작성기는 최소 336px 높이를 가져야 하며 게시 버튼은 화면 하단에서 + 20px 이상 떨어져야 한다. 작성기가 표시되는 동안 왼쪽 토론 목록은 숨겨져 서로 + 겹치지 않아야 하고, 작성기를 닫으면 목록이 다시 표시되어야 한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. @@ -153,4 +160,4 @@ sudo systemctl restart php8.3-fpm nginx 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크, 메인 환영 배너 제거, Flarum 세로 탐색 복원과 1165px 상세 셸 정렬이 WSL 및 운영에 반영되었다. 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-drawer-readability-icons-20260820T1045KST`에 보존한다. + `/var/backups/techflow-flarum/theme-composer-pane-final-20260820T112831KST`에 보존한다. From 40ef0e108be8a9c44e25ec14ef53591064e1da7f Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 12:32:58 +0900 Subject: [PATCH 15/18] =?UTF-8?q?Community=20=EC=9E=91=EC=84=B1=EA=B8=B0?= =?UTF-8?q?=EC=99=80=20=ED=86=A0=EB=A1=A0=20=EB=AA=A9=EB=A1=9D=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=20=EC=A0=95=EB=A0=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/dist/forum.js | 28 +++++++- .../resources/less/forum.less | 70 ++++++++++++++++++- deploy/flarum/tests/test_community_theme.py | 29 ++++++-- ...community-index-fixed-feed-validation.json | 35 +++++++--- .../issue-73-community-ui-validation.md | 42 +++++++++++ .../community-theme-rollout-rollback.md | 10 ++- 6 files changed, 194 insertions(+), 20 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js index 9d91357..e971235 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js +++ b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js @@ -452,12 +452,35 @@ fallbackPaneActive = true; } + function syncComposerPaneState(root) { + var app = root.querySelector('.App') || document.querySelector('.App'); + var composerOpen = Boolean(document.querySelector('.Composer.visible:not(.minimized)')); + + if (!app) { + return composerOpen; + } + + app.classList.toggle('ablecloud-composer-open', composerOpen); + document.documentElement.classList.toggle('ablecloud-composer-open', composerOpen); + + if (composerOpen && forumApp.pane) { + forumApp.pane.hide(); + } + + return composerOpen; + } + function showFallbackPaneFromEdge(event) { + var composerOpen = syncComposerPaneState(document); + + if (composerOpen) { + return; + } + if ( fallbackPaneActive && forumApp.pane && - event.clientX < 10 && - !document.querySelector('.Composer.visible:not(.minimized)') + event.clientX < 10 ) { forumApp.pane.show(); } @@ -473,6 +496,7 @@ enhanceDiscussionListItems(document); captureDiscussionListSnapshot(document); ensureFallbackDiscussionPane(document); + syncComposerPaneState(document); enhanceDiscussionDetail(document); }); }; diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index baa2495..767838c 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1824,13 +1824,50 @@ body { min-height: 336px; } + .Composer.active:not(.fullScreen) .Composer-content { + height: 100% !important; + box-sizing: border-box; + } + + .Composer.active:not(.fullScreen) .ComposerBody, + .Composer.active:not(.fullScreen) .ComposerBody-content { + height: 100% !important; + min-height: 0; + } + + .Composer.active:not(.fullScreen) .ComposerBody-content, + .Composer.active:not(.fullScreen) .TextEditor { + display: flex; + flex-direction: column; + } + + .Composer.active:not(.fullScreen) .ComposerBody-header, + .Composer.active:not(.fullScreen) .TextEditor-controls { + flex: 0 0 auto; + } + + .Composer.active:not(.fullScreen) .ComposerBody-editor, + .Composer.active:not(.fullScreen) .TextEditor, + .Composer.active:not(.fullScreen) .TextEditor-editorContainer { + height: auto !important; + min-height: 0; + flex: 1 1 auto; + } + + .Composer.active:not(.fullScreen) .ComposerBody-mentionsWrapper, + .Composer.active:not(.fullScreen) .ComposerBody-emojiWrapper { + height: 100%; + min-height: 0; + } + .Composer.active:not(.fullScreen) .TextEditor-editor { - padding-bottom: 32px; + height: 100% !important; + padding-bottom: 12px; } .Composer.active:not(.fullScreen) .TextEditor-controls { - position: relative; - bottom: 20px; + position: static; + bottom: auto; } } @@ -2520,10 +2557,22 @@ body { // Keep the discussion context available, but never let the hover pane sit // on top of the composer while the user is writing a new topic or reply. + // The explicit class is synchronized by forum.js and avoids depending on + // :has() support or on the composer being mounted below .App. + html.ablecloud-composer-open .DiscussionPage-list, + .App.ablecloud-composer-open .DiscussionPage-list, .App:has(.Composer.visible:not(.minimized)) .DiscussionPage-list { display: none !important; } + html.ablecloud-composer-open .DiscussionListItem, + html.ablecloud-composer-open .DiscussionListItem:hover, + html.ablecloud-composer-open .DiscussionListItem:focus-within, + html.ablecloud-composer-open .DiscussionListItem:has(.DiscussionListItem-controls.open) { + z-index: 0 !important; + } + + .App.hasPane.panePinned.ablecloud-composer-open .App-composer, .App.hasPane.panePinned:has(.Composer.visible:not(.minimized)) .App-composer { left: 0; } @@ -2667,6 +2716,21 @@ body { bottom: 12px; transform: none; } + + .DiscussionPage-list .DiscussionListItem-controls { + top: 10px; + right: 10px; + z-index: 1060; + transform: none; + } + + .DiscussionPage-list .DiscussionListItem-controls > .Dropdown-toggle { + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + padding: 0; + } } } diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 69df574..51cd60a 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -161,7 +161,11 @@ def test_tag_date_format_contract(self) -> None: self.assertIn("forumApp.pane.enable()", self.forum_js) self.assertNotIn("addFallbackPanePinButton", self.forum_js) self.assertIn("document.addEventListener('mousemove', showFallbackPaneFromEdge", self.forum_js) - self.assertIn("!document.querySelector('.Composer.visible:not(.minimized)')", self.forum_js) + self.assertIn("syncComposerPaneState", self.forum_js) + self.assertIn("app.classList.toggle('ablecloud-composer-open', composerOpen)", self.forum_js) + self.assertIn("document.documentElement.classList.toggle('ablecloud-composer-open', composerOpen)", self.forum_js) + self.assertIn("composerOpen && forumApp.pane", self.forum_js) + self.assertIn("forumApp.pane.hide()", self.forum_js) def test_reddit_style_discussion_item_structure_contract(self) -> None: for script_contract in ( @@ -293,13 +297,28 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn(".DiscussionPage-list .DiscussionListItem-count", self.less) self.assertIn("height: calc(100vh - 68px)", self.less) self.assertIn("scrollbar-width: thin", self.less) - self.assertIn(".App:has(.Composer.visible:not(.minimized)) .DiscussionPage-list", self.less) - self.assertIn(".App.hasPane.panePinned:has(.Composer.visible:not(.minimized)) .App-composer", self.less) + self.assertIn("html.ablecloud-composer-open .DiscussionPage-list", self.less) + self.assertIn(".App.ablecloud-composer-open .DiscussionPage-list", self.less) + self.assertIn(".App.hasPane.panePinned.ablecloud-composer-open .App-composer", self.less) + self.assertIn(".DiscussionPage-list .DiscussionListItem-controls", self.less) + self.assertIn("top: 10px", self.less) + self.assertIn("width: 32px", self.less) self.assertIn(".Composer.active:not(.fullScreen)", self.less) self.assertIn("min-height: 336px", self.less) + self.assertIn(".Composer.active:not(.fullScreen) .Composer-content", self.less) + self.assertIn("height: 100% !important", self.less) + self.assertIn(".Composer.active:not(.fullScreen) .ComposerBody-content", self.less) + self.assertIn(".Composer.active:not(.fullScreen) .TextEditor", self.less) + self.assertIn("display: flex", self.less) + self.assertIn("flex-direction: column", self.less) + self.assertIn("flex: 1 1 auto", self.less) + self.assertIn(".Composer.active:not(.fullScreen) .TextEditor-editorContainer", self.less) self.assertIn(".Composer.active:not(.fullScreen) .TextEditor-controls", self.less) - self.assertIn("bottom: 20px", self.less) - self.assertIn("padding-bottom: 32px", self.less) + self.assertIn("position: static", self.less) + self.assertIn("bottom: auto", self.less) + self.assertIn("padding-bottom: 12px", self.less) + self.assertIn("html.ablecloud-composer-open .DiscussionListItem:hover", self.less) + self.assertIn("z-index: 0 !important", self.less) self.assertIn(".Search-input input:focus-visible", self.less) self.assertIn("outline: none !important", self.less) self.assertIn('.Header-secondary .Dropdown-menu .Button-icon:not([class*="fa-"])', self.less) diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 97d8d1d..4068117 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -80,9 +80,9 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-composer-pane-final-20260820T112831KST", - "theme_less_sha256": "3fc9dda37cc2e7e78024d470e122a1b883d023de658c973be9467e40359d0ca9", - "theme_js_sha256": "a52c0bc148280a01f8cbce9a1ce79a28be6d7529bba7591ea1a6a9ed042dfe62", + "backup_root": "/var/backups/techflow-flarum/theme-composer-pane-layer-final-20260820T122747KST", + "theme_less_sha256": "b13615bd81ac3f794771969e2f97e7cf0bed1cdd02098266b120ac7019f62da1", + "theme_js_sha256": "7636c2790502f03bf148422baec98b6f76c4d77519d5dcdf783469eff914c643", "services": { "nginx": "active", "php8.3-fpm": "active", @@ -220,30 +220,47 @@ "result": "PASS" }, "composer_and_discussion_pane": { - "root_cause": "the start-from-top handler forced window.location.assign and discarded Flarum's in-memory discussion-list state", + "root_cause": [ + "the start-from-top handler forced window.location.assign and discarded Flarum's in-memory discussion-list state", + "the narrow pane inherited the desktop control rule top 50 percent plus translateY minus 50 percent, which placed the ellipsis above the card", + "the active composer moved its footer upward by 20px, overlapping the editor, while hovered discussion rows could rise above the composer at z-index 1050" + ], "navigation_fix": "retain Flarum SPA navigation, normalize the link target, and reset scroll without preventDefault", "reload_fallback": "store the already-rendered DiscussionList in session storage and restore a 400px pane after a direct detail reload", "composer_layout": { "desktop_active_min_height_px": 336, - "submit_bottom_inset_px": { "wsl": 22, "production": 22 }, - "footer_bottom_inset_px": { "wsl": 8, "production": 8 }, + "editor_height_px": { "wsl": 180, "production": 180 }, + "footer_height_px": { "wsl": 65.5, "production": 65.5 }, + "editor_footer_gap_px": { "wsl": 0, "production": 0 }, + "editor_footer_overlap": { "wsl": false, "production": false }, + "submit_bottom_inset_px": { "wsl": 29.5, "production": 29.5 }, + "footer_bottom_inset_px": { "wsl": 15.5, "production": 15.5 }, + "footer_inside_composer": { "wsl": true, "production": true }, + "background_discussion_z_index_while_composing": { "wsl": 0, "production": 0 }, "horizontal_overflow": false }, "discussion_pane": { "width_px": 400, + "item_control_size_px": { "width": 32, "height": 32 }, + "item_control_top_offset_px": { "wsl": 11, "production": 11 }, + "item_control_inside_card": { "wsl": true, "production": true }, + "item_control_transform": "none", "item_count": { "wsl": 20, "production": 20 }, "active_item_after_reload": { "wsl": 1, "production": 1 }, "list_click_scroll_y": { "wsl": 0, "production": 0 }, "hidden_while_composer_visible": true, "restored_after_composer_close": true, - "composer_overlap": { "wsl": false, "production": false } + "composer_overlap": { "wsl": false, "production": false }, + "explicit_composer_state_class": "ablecloud-composer-open", + "pane_display_while_composing": { "wsl": "none", "production": "none" } }, - "wsl_rehearsal": "issue-73-composer-pane-v8 PASS", + "wsl_rehearsal": "issue-73-composer-layout-flex-20260820 PASS", + "production_browser_https": true, "result": "PASS" }, "validation": { "theme_contract_tests": { "passed": 13, "failed": 0 }, - "wsl_rehearsal": "issue-73-composer-pane-v8 PASS", + "wsl_rehearsal": "issue-73-composer-layout-flex-20260820 PASS", "header_hero_body_alignment": "WSL and production x=250, right=1415, width=1165", "discussion_item_enhancement_is_idempotent": true, "summary_uses_existing_store_data": true, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index 42dd831..1f49a2f 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -482,3 +482,45 @@ Toolbar 아래에 가려지지 않도록 했다. 외부 HTTPS 200과 Nginx·PHP-FPM·MariaDB·Community Monitor Timer `active`를 확인했으며 DB Schema, 게시물, 첨부, TechFlow AI, GitHub→Chat Webhook은 변경하지 않았다. + +### 좌측 목록 메뉴와 토론 작성기 레이어 최종 교정 + +좁은 400px 토론 목록의 더보기 메뉴가 카드 위로 튀어나온 원인은 데스크톱 목록의 +`top: 50%`와 `translateY(-50%)`가 그대로 상속된 것이었다. 좁은 목록에서는 메뉴를 +카드 상단 10px, 오른쪽 10px에 별도로 고정하고 버튼 영역을 32×32px로 통일했다. +WSL과 운영 브라우저 모두 카드 실제 경계에서 메뉴 상단까지 11px이며 버튼 전체가 +카드 안에 포함되고 Transform은 `none`이다. + +토론 생성 작성기에서는 기존 보완 CSS가 하단 도구막대를 20px 위로 이동시켜 본문 +편집 영역과 실제로 겹쳤다. 작성기 내부를 세로 Flex 구조로 바꾸고 편집 영역과 +도구막대를 각각 독립 행으로 배치했다. 두 영역의 경계 간격은 0px이지만 교차 영역은 +없으며, 65.5px 도구막대 전체가 작성기 하단에서 15.5px 안쪽에 남는다. 게시 버튼은 +작성기 하단에서 29.5px 떨어져 있다. + +작성기와 목록이 겹치던 문제는 두 경로를 함께 차단했다. JavaScript가 새 토론 또는 +답변 작성기의 `visible` 상태를 `ablecloud-composer-open` 클래스로 동기화하고 기존 +Pane Controller를 즉시 닫는다. 동시에 작성 중인 화면의 토론 행은 Hover 상태여도 +Z-index를 0으로 유지해 배경 목록이 작성기 앞으로 올라오지 못한다. 목록을 먼저 +고정해 화면에 표시한 뒤 답변 작성기를 연 운영 브라우저 검증에서도 Pane은 +`display:none`, 작성기와의 교차는 `false`였다. + +| 브라우저 검증 항목 | WSL | 운영 | +|---|---:|---:| +| 목록 메뉴 크기 | 32×32px | 32×32px | +| 카드 상단 기준 메뉴 위치 | 11px | 11px | +| 메뉴가 카드 내부에 포함 | 예 | 예 | +| 작성기 높이 | 336px | 336px | +| 편집 영역 / 도구막대 높이 | 180 / 65.5px | 180 / 65.5px | +| 편집 영역과 도구막대 겹침 | 없음 | 없음 | +| 도구막대 하단 여백 | 15.5px | 15.5px | +| 목록을 먼저 연 뒤 작성기 겹침 | 없음 | 없음 | + +계약 테스트 13건과 WSL +`issue-73-composer-layout-flex-20260820` 적용·비활성화·재적용 주기를 통과했다. +운영 배포 전 백업은 +`/var/backups/techflow-flarum/theme-composer-pane-layer-final-20260820T122747KST`에 +보존했다. 운영 Source와 Vendor LESS SHA-256은 +`b13615bd81ac3f794771969e2f97e7cf0bed1cdd02098266b120ac7019f62da1`, JS SHA-256은 +`7636c2790502f03bf148422baec98b6f76c4d77519d5dcdf783469eff914c643`로 일치한다. +외부 HTTPS 브라우저 접속, Nginx·PHP-FPM·MariaDB·Community Monitor Timer +`active`를 확인했으며 원격 임시 배포 자산은 제거했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 5435e10..f669b66 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -132,6 +132,13 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - 데스크톱 활성 작성기는 최소 336px 높이를 가져야 하며 게시 버튼은 화면 하단에서 20px 이상 떨어져야 한다. 작성기가 표시되는 동안 왼쪽 토론 목록은 숨겨져 서로 겹치지 않아야 하고, 작성기를 닫으면 목록이 다시 표시되어야 한다. +- 작성기 본문 편집 영역과 하단 도구막대는 독립된 세로 Flex 행이어야 한다. 두 영역의 + 경계는 맞닿을 수 있지만 교차하면 안 되며 도구막대 하단 전체가 작성기 경계 안에 + 있어야 한다. 작성 중인 배경 토론 행은 Hover 상태에서도 작성기보다 앞으로 나오면 + 안 된다. +- 400px 토론 목록의 더보기 메뉴는 32×32px이고 카드 상단과 오른쪽에서 약 10px + 안쪽에 있어야 한다. `translateY`를 사용하지 않으며 버튼 전체가 카드 경계 안에 + 포함되어야 한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. @@ -160,4 +167,5 @@ sudo systemctl restart php8.3-fpm nginx 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크, 메인 환영 배너 제거, Flarum 세로 탐색 복원과 1165px 상세 셸 정렬이 WSL 및 운영에 반영되었다. 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-composer-pane-final-20260820T112831KST`에 보존한다. + `/var/backups/techflow-flarum/theme-composer-pane-layer-final-20260820T122747KST`에 + 보존한다. From aea374a1154e180c635601dc60c12dad88f53127 Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 13:10:27 +0900 Subject: [PATCH 16/18] =?UTF-8?q?Community=20=EB=AC=B4=ED=95=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=EA=B3=BC=20=EC=83=88=20=ED=86=A0?= =?UTF-8?q?=EB=A1=A0=20=EB=8C=80=ED=99=94=EC=83=81=EC=9E=90=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/dist/forum.js | 296 +++++++++++++++++- .../resources/less/forum.less | 280 +++++++++++++++++ deploy/flarum/tests/test_community_theme.py | 32 ++ ...community-index-fixed-feed-validation.json | 57 +++- .../issue-73-community-ui-validation.md | 35 +++ .../community-theme-rollout-rollback.md | 18 +- 6 files changed, 708 insertions(+), 10 deletions(-) diff --git a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js index e971235..814ed4f 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js +++ b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js @@ -10,6 +10,278 @@ var discussionListSnapshotKey = 'ablecloud-community-discussion-list'; var fallbackPaneAttribute = 'data-ablecloud-fallback-pane'; var fallbackPaneActive = false; + var infiniteLoadObserver = null; + var infiniteLoadTarget = null; + var emojiPalette = null; + var emojiPaletteTrigger = null; + var emojiSelectionRange = null; + var commonEmojis = [ + ['😀', '웃는 얼굴'], + ['😃', '활짝 웃는 얼굴'], + ['😊', '미소'], + ['😂', '기쁨의 눈물'], + ['🙂', '살짝 미소'], + ['😉', '윙크'], + ['😍', '반함'], + ['🤔', '생각 중'], + ['😮', '놀람'], + ['😢', '슬픔'], + ['😭', '울음'], + ['😅', '안도'], + ['😎', '멋짐'], + ['👍', '좋아요'], + ['👎', '싫어요'], + ['👏', '박수'], + ['🙏', '부탁과 감사'], + ['✅', '확인'], + ['❗', '중요'], + ['⚠️', '주의'], + ['💡', '아이디어'], + ['🎉', '축하'], + ['❤️', '하트'], + ['🔥', '불꽃'], + ]; + + function disconnectInfiniteDiscussionLoading() { + if (infiniteLoadObserver) { + infiniteLoadObserver.disconnect(); + } + + infiniteLoadObserver = null; + infiniteLoadTarget = null; + } + + function enhanceInfiniteDiscussionLoading(root) { + var scrollRoot = root.querySelector('.App--index .IndexPage-results'); + var loadMore = root.querySelector('.App--index .DiscussionList-loadMore'); + var button = loadMore && loadMore.querySelector('button'); + + if (!scrollRoot || !loadMore || !button || typeof window.IntersectionObserver !== 'function') { + disconnectInfiniteDiscussionLoading(); + return; + } + + if (loadMore.getAttribute('data-ablecloud-auto-load') !== 'true') { + loadMore.setAttribute('data-ablecloud-auto-load', 'true'); + } + + if (infiniteLoadTarget === loadMore) { + return; + } + + disconnectInfiniteDiscussionLoading(); + infiniteLoadTarget = loadMore; + infiniteLoadObserver = new window.IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + var currentButton = entry.target.querySelector('button'); + + if ( + !entry.isIntersecting || + !currentButton || + currentButton.disabled || + entry.target.getAttribute('data-ablecloud-loading') === 'true' + ) { + return; + } + + entry.target.setAttribute('data-ablecloud-loading', 'true'); + currentButton.click(); + + window.setTimeout(function () { + if (entry.target.isConnected) { + entry.target.removeAttribute('data-ablecloud-loading'); + } + }, 750); + }); + }, + { + root: scrollRoot, + rootMargin: '0px 0px 120px 0px', + threshold: 0.01, + } + ); + infiniteLoadObserver.observe(loadMore); + } + + function closeEmojiPalette() { + if (emojiPalette && emojiPalette.parentNode) { + emojiPalette.parentNode.removeChild(emojiPalette); + } + + if (emojiPaletteTrigger) { + emojiPaletteTrigger.setAttribute('aria-expanded', 'false'); + } + + emojiPalette = null; + emojiPaletteTrigger = null; + emojiSelectionRange = null; + } + + function rememberEmojiCaret(editor) { + var selection = window.getSelection(); + + if (selection && selection.rangeCount) { + var range = selection.getRangeAt(0); + + if (editor.contains(range.commonAncestorContainer)) { + return range.cloneRange(); + } + } + + var fallbackRange = document.createRange(); + fallbackRange.selectNodeContents(editor); + fallbackRange.collapse(false); + return fallbackRange; + } + + function insertEmojiAtCaret(editor, emoji) { + if (!editor || !emoji) { + return; + } + + var selection = window.getSelection(); + var range = emojiSelectionRange && emojiSelectionRange.startContainer.isConnected + ? emojiSelectionRange + : rememberEmojiCaret(editor); + + editor.focus(); + + if (selection) { + selection.removeAllRanges(); + selection.addRange(range); + } + + var value = emoji + ' '; + var inserted = false; + + try { + inserted = document.execCommand('insertText', false, value); + } catch (error) { + inserted = false; + } + + if (!inserted) { + range.deleteContents(); + var textNode = document.createTextNode(value); + range.insertNode(textNode); + range.setStartAfter(textNode); + range.collapse(true); + if (selection) { + selection.removeAllRanges(); + selection.addRange(range); + } + editor.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + function openEmojiPalette(trigger) { + var composer = trigger.closest('.Composer'); + var editor = composer && composer.querySelector('.TextEditor-editor[contenteditable="true"]'); + + if (!composer || !editor) { + return; + } + + closeEmojiPalette(); + document.querySelectorAll('.EmojiDropdown').forEach(function (dropdown) { + dropdown.remove(); + }); + + emojiSelectionRange = rememberEmojiCaret(editor); + emojiPaletteTrigger = trigger; + trigger.setAttribute('aria-expanded', 'true'); + + var palette = document.createElement('div'); + palette.className = 'ablecloud-EmojiPicker'; + palette.setAttribute('role', 'dialog'); + palette.setAttribute('aria-label', '이모지 선택'); + + var header = document.createElement('div'); + header.className = 'ablecloud-EmojiPicker-header'; + + var title = document.createElement('strong'); + title.textContent = '이모지 선택'; + header.appendChild(title); + + var closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.className = 'ablecloud-EmojiPicker-close'; + closeButton.setAttribute('data-ablecloud-emoji-close', 'true'); + closeButton.setAttribute('aria-label', '이모지 선택창 닫기'); + closeButton.textContent = '×'; + header.appendChild(closeButton); + palette.appendChild(header); + + var help = document.createElement('p'); + help.className = 'ablecloud-EmojiPicker-help'; + help.textContent = '삽입할 이모지를 선택하세요.'; + palette.appendChild(help); + + var grid = document.createElement('div'); + grid.className = 'ablecloud-EmojiPicker-grid'; + + commonEmojis.forEach(function (item) { + var button = document.createElement('button'); + button.type = 'button'; + button.className = 'ablecloud-EmojiPicker-option'; + button.setAttribute('data-ablecloud-emoji', item[0]); + button.setAttribute('aria-label', item[1]); + button.title = item[1]; + button.textContent = item[0]; + grid.appendChild(button); + }); + + palette.appendChild(grid); + composer.appendChild(palette); + emojiPalette = palette; + } + + function handleEmojiPaletteClick(event) { + var trigger = event.target.closest && event.target.closest('.Composer button[aria-label="이모지 삽입"]'); + + if (trigger) { + event.preventDefault(); + event.stopImmediatePropagation(); + + if (emojiPalette) { + closeEmojiPalette(); + } else { + openEmojiPalette(trigger); + } + + return; + } + + var choice = event.target.closest && event.target.closest('[data-ablecloud-emoji]'); + + if (choice && emojiPalette && emojiPalette.contains(choice)) { + event.preventDefault(); + event.stopImmediatePropagation(); + var editor = emojiPalette.closest('.Composer').querySelector('.TextEditor-editor[contenteditable="true"]'); + insertEmojiAtCaret(editor, choice.getAttribute('data-ablecloud-emoji')); + closeEmojiPalette(); + return; + } + + if (event.target.closest && event.target.closest('[data-ablecloud-emoji-close="true"]')) { + event.preventDefault(); + event.stopImmediatePropagation(); + closeEmojiPalette(); + return; + } + + if (emojiPalette && !emojiPalette.contains(event.target)) { + closeEmojiPalette(); + } + } + + function handleEmojiPaletteKeydown(event) { + if (event.key === 'Escape' && emojiPalette) { + event.preventDefault(); + closeEmojiPalette(); + } + } function formatDiscussionCreatedAt(createdAt) { if (!(createdAt instanceof Date) || Number.isNaN(createdAt.getTime())) { @@ -455,13 +727,25 @@ function syncComposerPaneState(root) { var app = root.querySelector('.App') || document.querySelector('.App'); var composerOpen = Boolean(document.querySelector('.Composer.visible:not(.minimized)')); + var newDiscussionOpen = Boolean( + document.querySelector('.Composer.visible:not(.minimized) .ComposerBody--discussion') + ); + + document.documentElement.classList.toggle('ablecloud-composer-open', composerOpen); + document.documentElement.classList.toggle('ablecloud-new-discussion-open', newDiscussionOpen); + document.body.classList.toggle('ablecloud-composer-open', composerOpen); + document.body.classList.toggle('ablecloud-new-discussion-open', newDiscussionOpen); if (!app) { return composerOpen; } app.classList.toggle('ablecloud-composer-open', composerOpen); - document.documentElement.classList.toggle('ablecloud-composer-open', composerOpen); + app.classList.toggle('ablecloud-new-discussion-open', newDiscussionOpen); + + if (!composerOpen) { + closeEmojiPalette(); + } if (composerOpen && forumApp.pane) { forumApp.pane.hide(); @@ -494,6 +778,7 @@ formatTagDiscussionDates(document); normalizeDiscussionListTargets(document); enhanceDiscussionListItems(document); + enhanceInfiniteDiscussionLoading(document); captureDiscussionListSnapshot(document); ensureFallbackDiscussionPane(document); syncComposerPaneState(document); @@ -504,12 +789,21 @@ schedule(); new MutationObserver(schedule).observe(document.body, { + attributes: true, + attributeFilter: ['class'], childList: true, characterData: true, subtree: true, }); + document.addEventListener('click', function () { + window.setTimeout(schedule, 0); + window.setTimeout(schedule, 150); + window.setTimeout(schedule, 600); + }, true); document.addEventListener('click', openDiscussionFromTop, true); + document.addEventListener('click', handleEmojiPaletteClick, true); + document.addEventListener('keydown', handleEmojiPaletteKeydown, true); document.addEventListener('mousemove', showFallbackPaneFromEdge, { passive: true }); }); diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 767838c..5286176 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -1869,6 +1869,192 @@ body { position: static; bottom: auto; } + + &.ablecloud-new-discussion-open { + overflow: hidden; + } + + &.ablecloud-new-discussion-open .App-composer { + position: fixed; + z-index: 1040; + inset: 0; + display: flex; + width: 100vw; + height: 100vh; + align-items: center; + justify-content: center; + padding: 76px 24px 24px; + background: rgba(20, 42, 75, 0.46); + pointer-events: auto; + } + + &.ablecloud-new-discussion-open .App-composer > .container { + display: flex; + width: 100%; + max-width: 960px; + height: 100%; + align-items: center; + justify-content: center; + margin: 0; + padding: 0; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) { + position: relative; + bottom: auto !important; + width: 100%; + max-width: 960px; + height: calc(100vh - 120px) !important; + max-height: 650px; + min-height: 520px; + margin: 0 !important; + overflow: visible !important; + border-radius: 18px; + background: #fff; + box-shadow: 0 28px 80px rgba(10, 35, 72, 0.34) !important; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-handle, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls .item-minimize, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls .item-fullScreen { + display: none; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-content, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-content { + height: 100% !important; + min-height: 0; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-content, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor { + display: flex; + flex-direction: column; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-header, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor-controls { + flex: 0 0 auto; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-editor { + display: flex; + height: auto !important; + min-height: 0; + flex: 1 1 auto; + flex-direction: column; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor { + height: 100% !important; + min-height: 0; + flex: 1 1 auto; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor-editorContainer { + height: auto !important; + min-height: 0; + flex: 1 1 auto; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-mentionsWrapper, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-emojiWrapper, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor-editor { + height: 100% !important; + min-height: 0; + } + } + + .ablecloud-EmojiPicker { + position: absolute; + right: 20px; + bottom: 76px; + z-index: 1090; + width: 344px; + padding: 14px; + border: 1px solid #b9cdec; + border-radius: 14px; + background: #fff; + box-shadow: 0 18px 48px rgba(24, 71, 142, 0.24); + } + + .ablecloud-EmojiPicker-header { + display: flex; + align-items: center; + justify-content: space-between; + color: var(--ablecloud-brand-ink); + font-size: 15px; + } + + .ablecloud-EmojiPicker-close { + display: inline-flex; + width: 30px; + height: 30px; + align-items: center; + justify-content: center; + padding: 0; + border: 0; + border-radius: 50%; + background: transparent; + color: var(--ablecloud-brand-muted); + font-size: 22px; + line-height: 1; + } + + .ablecloud-EmojiPicker-close:hover, + .ablecloud-EmojiPicker-close:focus-visible { + outline: none; + background: #eaf2ff; + color: var(--ablecloud-brand-primary-deep); + } + + .ablecloud-EmojiPicker-help { + margin: 2px 0 12px; + color: var(--ablecloud-brand-muted); + font-size: 13px; + } + + .ablecloud-EmojiPicker-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 7px; + } + + .ablecloud-EmojiPicker-option { + display: inline-flex; + min-width: 0; + height: 43px; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid transparent; + border-radius: 10px; + background: #f5f8fd; + font-family: "Segoe UI Emoji", "Apple Color Emoji", sans-serif; + font-size: 23px; + line-height: 1; + } + + .ablecloud-EmojiPicker-option:hover, + .ablecloud-EmojiPicker-option:focus-visible { + border-color: #8cb0ec; + outline: none; + background: #eaf2ff; + box-shadow: 0 0 0 2px rgba(21, 94, 239, 0.12); + } + + .EmojiDropdown > li:not(.Dropdown-header) > button { + display: flex; + align-items: center; + gap: 0; + font-size: 0; + } + + .EmojiDropdown > li:not(.Dropdown-header) > button img.emoji { + width: 24px; + height: 24px; + margin: 0 auto; } .ComposerBody-header .DiscussionComposer-changeTags { @@ -1915,6 +2101,10 @@ body { background: #fbfdff; } + .DiscussionList-loadMore[data-ablecloud-auto-load="true"][data-ablecloud-loading="true"] .Button { + pointer-events: none; + } + .Composer-controls .Button { border: 1px solid transparent; border-radius: 999px; @@ -3154,6 +3344,96 @@ body { border-radius: 14px 14px 0 0; } + &.ablecloud-new-discussion-open { + overflow: hidden; + } + + &.ablecloud-new-discussion-open .App-composer { + position: fixed; + z-index: 1040; + inset: 54px 0 0; + display: flex; + width: 100vw; + height: calc(100vh - 54px); + align-items: center; + justify-content: center; + padding: 8px; + background: rgba(20, 42, 75, 0.46); + pointer-events: auto; + } + + &.ablecloud-new-discussion-open .App-composer > .container { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) { + position: relative; + bottom: auto !important; + width: 100%; + height: 100% !important; + min-height: 0; + margin: 0 !important; + overflow: visible !important; + border-radius: 14px; + background: #fff; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-handle, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls .item-minimize, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls .item-fullScreen { + display: none; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-content, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-content { + height: 100% !important; + min-height: 0; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-content, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor { + display: flex; + flex-direction: column; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-editor { + display: flex; + height: auto !important; + min-height: 0; + flex: 1 1 auto; + flex-direction: column; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor { + height: 100% !important; + min-height: 0; + flex: 1 1 auto; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor-editorContainer { + height: auto !important; + min-height: 0; + flex: 1 1 auto; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-mentionsWrapper, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-emojiWrapper, + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .TextEditor-editor { + height: 100% !important; + min-height: 0; + } + + .ablecloud-EmojiPicker { + right: 12px; + bottom: 72px; + width: calc(100% - 24px); + max-width: 344px; + } + .Modal-content { border-radius: 14px; } diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 51cd60a..9878508 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -167,6 +167,38 @@ def test_tag_date_format_contract(self) -> None: self.assertIn("composerOpen && forumApp.pane", self.forum_js) self.assertIn("forumApp.pane.hide()", self.forum_js) + def test_infinite_scroll_emoji_and_new_discussion_dialog_contract(self) -> None: + for script_contract in ( + "enhanceInfiniteDiscussionLoading", + "new window.IntersectionObserver", + "rootMargin: '0px 0px 120px 0px'", + "currentButton.click()", + "data-ablecloud-auto-load", + "handleEmojiPaletteClick", + "ablecloud-EmojiPicker", + "data-ablecloud-emoji", + "document.execCommand('insertText'", + "삽입할 이모지를 선택하세요.", + "ablecloud-new-discussion-open", + "attributeFilter: ['class']", + "window.setTimeout(schedule, 150)", + "window.setTimeout(schedule, 600)", + ): + with self.subTest(script_contract=script_contract): + self.assertIn(script_contract, self.forum_js) + + for style_contract in ( + "&.ablecloud-new-discussion-open .App-composer", + "background: rgba(20, 42, 75, 0.46)", + "max-width: 960px", + "height: calc(100vh - 120px) !important", + ".ablecloud-EmojiPicker-grid", + "grid-template-columns: repeat(6, minmax(0, 1fr))", + ".EmojiDropdown > li:not(.Dropdown-header) > button", + ): + with self.subTest(style_contract=style_contract): + self.assertIn(style_contract, self.less) + def test_reddit_style_discussion_item_structure_contract(self) -> None: for script_contract in ( "enhanceDiscussionListItems", diff --git a/docs/evidence/issue-73/community-index-fixed-feed-validation.json b/docs/evidence/issue-73/community-index-fixed-feed-validation.json index 4068117..c93b8fb 100644 --- a/docs/evidence/issue-73/community-index-fixed-feed-validation.json +++ b/docs/evidence/issue-73/community-index-fixed-feed-validation.json @@ -1,7 +1,7 @@ { "schema_version": "2.0", - "executed_at": "2026-08-20T02:23:35Z", - "scope": "Flarum index without welcome banner, Reddit-style discussion list and detail reading flow, and right-side quick links", + "executed_at": "2026-08-20T04:09:15Z", + "scope": "Flarum index infinite loading, Korean emoji insertion, central new-discussion dialog, Reddit-style discussion list and detail reading flow", "design_reference": "https://www.reddit.com/new/?feed=home", "navigation_reference": "https://community.ablecloud.io/settings", "change": { @@ -10,6 +10,10 @@ "scroll_region": ".IndexPage-results including .IndexPage-toolbar and .DiscussionList, scrollbar hidden", "discussion_list_owns_scroll": false, "toolbar_scrolls_with_feed": true, + "discussion_loading": "IntersectionObserver automatically activates the native load-more action near the feed end", + "load_more_button": "retained only as a no-IntersectionObserver fallback", + "emoji_picker": "24 common Unicode emoji with Korean labels; native internal English names hidden", + "new_discussion_composer": "central modal dialog with a full-viewport backdrop; reply composer unchanged", "feed": "flat transparent feed with row separators and no outer container card", "individual_row_card": false, "discussion_item_structure": [ @@ -43,6 +47,7 @@ "browser": { "viewport": { "width": 1680, "height": 825 }, "enhanced_discussion_count": 20, + "automatic_discussion_counts": [20, 40, 60], "sample_discussion_ids": [144, 140, 139, 138, 137], "sample_row_heights_px": [137, 137, 137, 156, 156], "title_line_height_px": 25, @@ -80,9 +85,9 @@ "production": { "host": "172.16.0.234", "url": "https://community.ablecloud.io/", - "backup_root": "/var/backups/techflow-flarum/theme-composer-pane-layer-final-20260820T122747KST", - "theme_less_sha256": "b13615bd81ac3f794771969e2f97e7cf0bed1cdd02098266b120ac7019f62da1", - "theme_js_sha256": "7636c2790502f03bf148422baec98b6f76c4d77519d5dcdf783469eff914c643", + "backup_root": "/var/backups/techflow-flarum/theme-infinite-emoji-dialog-final-20260820T1430KST", + "theme_less_sha256": "1aff8b1ec4afb8225f960e3cb71666f2707c527b8222239d064df36b9046672b", + "theme_js_sha256": "977a9bf06026ab4ff9ec8d5348fcf07edcaa58d38d7abaf94a11114d0a586779", "services": { "nginx": "active", "php8.3-fpm": "active", @@ -94,6 +99,7 @@ "browser": { "viewport": { "width": 1680, "height": 825 }, "enhanced_discussion_count": 20, + "automatic_discussion_counts": [20, 40, 60], "solved_sample_discussion_id": 173, "solved_sample_row_height_px": 187, "plain_sample_discussion_id": 144, @@ -219,6 +225,43 @@ "production_browser": { "visible_icon_width_px": "15 to 16", "broken_glyph_count": 0 }, "result": "PASS" }, + "infinite_loading_emoji_and_new_discussion_dialog": { + "infinite_loading": { + "trigger": "IntersectionObserver rooted at .IndexPage-results with a 120px bottom margin", + "wsl_browser_counts": [20, 40, 60], + "production_browser_counts": [20, 40, 60], + "manual_button_click_required": false, + "native_button_fallback_retained": true, + "repeated_dom_mutation_removed": true + }, + "emoji_picker": { + "language": "Korean", + "option_count": 24, + "native_internal_name_visible": false, + "native_dropdown_visible": false, + "inserted_value": "😊", + "production_editor_text": "이모지 삽입 확인 😊 ", + "insertion_result": "PASS" + }, + "new_discussion_dialog": { + "scope": "new discussion only; reply composer unchanged", + "first_click_state_sync": true, + "viewport_px": { "width": 1680, "height": 881 }, + "composer_rect_px": { "x": 410.875, "y": 206.5, "width": 843.25, "height": 520, "bottom": 726.5 }, + "backdrop_rect_px": { "x": 0, "y": 0, "width": 1665, "height": 881 }, + "backdrop_color": "rgba(20, 42, 75, 0.46)", + "editor_height_px": 376.5, + "toolbar_height_px": 65.5, + "editor_toolbar_overlap": false, + "minimize_control": "hidden", + "fullscreen_control": "hidden", + "close_control": "visible", + "result": "PASS" + }, + "wsl_rehearsal": "issue-73-browser-final-20260820 PASS", + "production_deployment": "PASS", + "result": "PASS" + }, "composer_and_discussion_pane": { "root_cause": [ "the start-from-top handler forced window.location.assign and discarded Flarum's in-memory discussion-list state", @@ -259,8 +302,8 @@ "result": "PASS" }, "validation": { - "theme_contract_tests": { "passed": 13, "failed": 0 }, - "wsl_rehearsal": "issue-73-composer-layout-flex-20260820 PASS", + "theme_contract_tests": { "passed": 14, "failed": 0 }, + "wsl_rehearsal": "issue-73-browser-final-20260820 PASS", "header_hero_body_alignment": "WSL and production x=250, right=1415, width=1165", "discussion_item_enhancement_is_idempotent": true, "summary_uses_existing_store_data": true, diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index 1f49a2f..33d61e4 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -524,3 +524,38 @@ Z-index를 0으로 유지해 배경 목록이 작성기 앞으로 올라오지 `7636c2790502f03bf148422baec98b6f76c4d77519d5dcdf783469eff914c643`로 일치한다. 외부 HTTPS 브라우저 접속, Nginx·PHP-FPM·MariaDB·Community Monitor Timer `active`를 확인했으며 원격 임시 배포 자산은 제거했다. + +### 무한 스크롤·한글 이모지·새 토론 중앙 대화상자 + +목록 끝의 `더 보기` 버튼을 사용자가 직접 누르던 흐름을 변경했다. 목록 스크롤 +영역을 기준으로 하단 120px 안에 기존 Flarum 로딩 버튼이 들어오면 +`IntersectionObserver`가 기존 버튼 동작을 한 번만 실행한다. Flarum의 페이지네이션과 +오류 처리는 그대로 사용하며, `IntersectionObserver`를 사용할 수 없는 브라우저에는 +기존 버튼을 폴백으로 남겼다. WSL과 운영 브라우저에서 수동 버튼 클릭 없이 토론 +수가 각각 `20 → 40 → 60`으로 증가했다. 같은 DOM 속성을 반복 기록해 화면이 +지연되던 초기 구현도 실제 운영 브라우저 검증에서 발견해, 값이 바뀔 때만 표식을 +기록하도록 교정했다. + +기존 이모지 확장은 `regional_indicator_a` 같은 내부 영문 이름을 먼저 노출하고 +버튼을 누르면 콜론만 입력해 사용법을 알기 어려웠다. 새 선택기는 자주 쓰는 Unicode +이모지 24개를 한글 접근성 이름과 함께 제공한다. 운영에서 `미소` 항목을 선택해 +편집기에 `이모지 삽입 확인 😊 `가 들어가고 선택기가 닫히는 것까지 확인했다. 기존 +영문 내부 이름과 원래 Emoji Dropdown은 표시되지 않았다. + +새 토론 작성기는 기존 화면 하단 레이어를 사용하지 않는다. 새 토론에만 전체 화면 +배경 차단 레이어와 중앙 대화상자를 적용하고, 상세 화면의 답장 작성기는 기존 Flarum +흐름을 유지한다. 운영 1680×881 화면에서 대화상자는 X 410.875, Y 206.5, +843.25×520px이며 편집 영역은 376.5px, 하단 도구막대는 65.5px로 서로 겹치지 +않았다. 최소화와 전체 화면 버튼은 숨기고 닫기 버튼만 유지했다. Flarum이 기존 +작성기 DOM의 Class를 늦게 바꾸는 경우도 최초 클릭 후 자동으로 상태를 다시 +동기화하도록 보완했다. + +테마 계약 테스트 14건과 WSL `issue-73-browser-final-20260820` 적용·비활성화· +재적용 주기가 통과했다. 운영 최종 백업은 +`/var/backups/techflow-flarum/theme-infinite-emoji-dialog-final-20260820T1430KST`에 +보존했다. 운영 Vendor JS SHA-256은 +`977a9bf06026ab4ff9ec8d5348fcf07edcaa58d38d7abaf94a11114d0a586779`, LESS는 +`1aff8b1ec4afb8225f960e3cb71666f2707c527b8222239d064df36b9046672b`이며 WSL +Source와 일치한다. Nginx·PHP-FPM·MariaDB는 모두 `active`, 서버 로컬 HTTP는 +200이다. DB Schema, 게시물, 첨부, TechFlow AI와 GitHub→Chat Webhook은 +변경하지 않았으며 임시 배포 파일은 제거했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index f669b66..131f92a 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -139,6 +139,19 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - 400px 토론 목록의 더보기 메뉴는 32×32px이고 카드 상단과 오른쪽에서 약 10px 안쪽에 있어야 한다. `translateY`를 사용하지 않으며 버튼 전체가 카드 경계 안에 포함되어야 한다. +- 시작 페이지의 목록 끝까지 스크롤하면 기존 `더 보기` 버튼을 직접 누르지 않아도 + 다음 20건이 자동으로 로드되어야 한다. 연속 두 번 스크롤해 20→40→60건을 + 확인하고, 같은 로딩 표식 때문에 DOM 변경이 반복되지 않는지 화면 조작 지연도 + 함께 확인한다. 브라우저가 `IntersectionObserver`를 지원하지 않는 경우에는 기존 + 버튼을 사용할 수 있어야 한다. +- 새 토론의 `이모지 삽입` 버튼은 한글 안내가 있는 공통 이모지 24개 선택기를 + 표시해야 한다. `regional_indicator_a` 같은 내부 영문 이름이나 원래 Dropdown이 + 함께 표시되면 안 되며, 항목 선택 후 실제 편집기 본문에 Unicode 이모지가 + 들어가는지 확인한다. +- 새 토론 작성기는 화면 하단 레이어가 아니라 전체 화면 배경 차단 레이어 위의 중앙 + 대화상자로 표시되어야 한다. 최소화·전체 화면은 숨기고 닫기는 유지한다. 본문 + 편집 영역과 도구막대가 독립 행으로 대화상자 안에 모두 들어가며, 첫 클릭만으로 + 대화상자 상태가 적용되어야 한다. 답장 작성기는 기존 흐름을 유지한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. @@ -167,5 +180,6 @@ sudo systemctl restart php8.3-fpm nginx 정보 계층·이미지 썸네일, 하단 Footer 제거와 우측 고정 퀵 링크, 메인 환영 배너 제거, Flarum 세로 탐색 복원과 1165px 상세 셸 정렬이 WSL 및 운영에 반영되었다. 최신 운영 백업은 - `/var/backups/techflow-flarum/theme-composer-pane-layer-final-20260820T122747KST`에 - 보존한다. + `/var/backups/techflow-flarum/theme-infinite-emoji-dialog-final-20260820T1430KST`에 + 보존한다. 목록 자동 로딩, 한글 이모지 삽입, 새 토론 중앙 대화상자까지 운영 + 브라우저에서 확인했다. From 2b64a44c2d60d03f9b5c537706cef2f2136a551f Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 13:35:57 +0900 Subject: [PATCH 17/18] =?UTF-8?q?Community=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EC=99=80=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/less/forum.less | 181 +++++++++++++++++- deploy/flarum/tests/test_community_theme.py | 10 + .../community-search-ui-validation.json | 63 ++++++ .../issue-73-community-ui-validation.md | 32 ++++ .../community-theme-rollout-rollback.md | 4 + 5 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 docs/evidence/issue-73/community-search-ui-validation.json diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 5286176..1028a94 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -37,7 +37,8 @@ .fa-sort { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m7 10 5 5 5-5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } .fa-search { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='6' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m16 16 4 4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } .fa-check { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m5 12 4 4L19 6' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } -.fa-times { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 6l12 12M18 6 6 18' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } +.fa-times, +.fa-times-circle { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M8 8l8 8M16 8l-8 8' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); } .fa-flag { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 3v18M6 5h12l-2.5 4L18 13H6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } .fa-bell { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } .fa-envelope { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='3' y='5' width='18' height='14' rx='2' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m4 7 8 6 8-6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } @@ -108,9 +109,25 @@ .fa-terminal { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='3' y='4' width='18' height='16' rx='2' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m7 9 3 3-3 3M12 16h5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } .fa-caret-square-right { --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='3' y='3' width='18' height='18' rx='2' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m10 8 6 4-6 4V8Z' fill='black'/%3E%3C/svg%3E"); } -.Search-input::before { - font-family: "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2", sans-serif !important; - content: "⌕" !important; +.Search .Search-input::before { + position: absolute; + top: 50%; + left: 14px; + z-index: 1; + display: block; + width: 18px; + height: 18px; + background: #5275a7 !important; + content: "" !important; + transform: translateY(-50%); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='6' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m16 16 4 4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") !important; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='6' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='m16 16 4 4' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") !important; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: contain; + mask-size: contain; } /* Chromium의 기본 검색 초기화 글리프가 웹폰트 부재 시 사각형이 되지 않게 한다. */ @@ -133,6 +150,162 @@ mask-size: contain; } +/* + * 검색 확장 기능은 해결된 토론 한 건을 태그·제목·질문·최종 답변의 + * 가로 4열로 출력한다. 헤더 검색 폭에서는 각 열이 한두 글자로 쪼개지므로 + * 결과 한 건을 위에서 아래로 읽는 단일 열 카드로 재구성한다. + */ +.Search-clear { + display: inline-flex; + align-items: center; + justify-content: center; + color: #5275a7; +} + +.Search-clear .fa-times-circle { + width: 18px; + height: 18px; +} + +.Search .Search-clear .fa-times-circle::before { + display: block !important; + width: 18px !important; + height: 18px !important; + background: currentColor !important; + content: "" !important; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M8 8l8 8M16 8l-8 8' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") !important; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='black' stroke-width='2'/%3E%3Cpath d='M8 8l8 8M16 8l-8 8' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") !important; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: contain; + mask-size: contain; +} + +.Header-secondary .Search .Search-results { + right: 0; + left: auto; + width: ~"min(560px, calc(100vw - 24px))" !important; + max-height: ~"min(72vh, 680px)" !important; + padding: 8px 0; + overflow-x: hidden; + overflow-y: auto; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 14px; + background: #fff; + box-shadow: 0 18px 44px rgba(20, 47, 84, 0.18); + overflow-wrap: anywhere; + word-break: keep-all; + scrollbar-color: #8da4c5 transparent; + scrollbar-width: thin; +} + +.Search-results > li { + width: 100%; +} + +.Search-results .Dropdown-header { + padding: 10px 16px 8px; + color: var(--ablecloud-brand-ink); + font-size: 14px; + font-weight: 750; +} + +.Search-results > li > a { + min-width: 0; + padding: 11px 16px; + color: var(--ablecloud-brand-ink); +} + +.Search-results > li > a:hover, +.Search-results > li.active > a { + background: var(--ablecloud-brand-primary-faint); +} + +.Search-results .DiscussionSearchResult > a, +.Search-results .SolutionSearchResult > a { + display: block !important; + min-height: 0; + padding: 13px 16px; + border-top: 1px solid #edf2f8; +} + +.Search-results .SolutionSearchResult-tags { + display: block; + width: auto; + margin: 0 0 8px; +} + +.Search-results .SolutionSearchResult-tags .TagsLabel { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.Search-results .SolutionSearchResult-tags .TagLabel { + max-width: 100%; + margin: 0; + border-radius: 5px; +} + +.Search-results .DiscussionSearchResult-title { + width: auto; + margin: 0; + color: var(--ablecloud-brand-ink); + font-size: 15px; + font-weight: 750; + line-height: 1.45; + overflow-wrap: anywhere; + word-break: keep-all; +} + +.Search-results .DiscussionSearchResult-excerpt { + display: -webkit-box; + width: auto; + margin: 6px 0 0; + overflow: hidden; + color: var(--ablecloud-brand-muted); + font-size: 13.5px; + line-height: 1.55; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.Search-results .SolutionSearchResult-bestAnswer { + display: -webkit-box; + width: auto; + margin-top: 9px; + padding: 9px 11px; + border-left: 3px solid #34a66f; + border-radius: 8px; + background: var(--ablecloud-brand-solution); + color: #315f4a; + -webkit-line-clamp: 2; +} + +.Search-results mark { + padding: 0 2px; + border-radius: 3px; + background: #fff1a8; + color: inherit; + font-weight: 750; +} + +@media (max-width: 767px) { + .Header-secondary .Search .Search-results { + right: 12px; + left: 12px; + width: auto !important; + max-height: calc(100vh - 94px) !important; + } + + .Search-results .DiscussionSearchResult > a, + .Search-results .SolutionSearchResult > a { + padding: 12px 14px; + } +} + :root { --ablecloud-brand-primary: #155eef; --ablecloud-brand-primary-hover: #004eeb; diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 9878508..3c4bfbc 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -364,6 +364,16 @@ def test_compact_hero_and_navigation_width_contract(self) -> None: self.assertIn("transform: none", self.less) self.assertIn('.Search-input input[type="search"]::-webkit-search-cancel-button', self.less) self.assertIn("-webkit-appearance: none", self.less) + self.assertIn(".fa-times-circle", self.less) + self.assertIn(".Search .Search-input::before", self.less) + self.assertIn(".Search .Search-clear .fa-times-circle::before", self.less) + self.assertIn(".Header-secondary .Search .Search-results", self.less) + self.assertIn(".Search-results .SolutionSearchResult > a", self.less) + self.assertIn("display: block !important", self.less) + self.assertIn('width: ~"min(560px, calc(100vw - 24px))"', self.less) + self.assertIn(".Search-results .SolutionSearchResult-tags .TagsLabel", self.less) + self.assertIn(".Search-results .SolutionSearchResult-bestAnswer", self.less) + self.assertIn("-webkit-line-clamp: 2", self.less) self.assertIn(".DiscussionListItem-count::before", self.less) self.assertIn(".DiscussionListItem.unread .DiscussionListItem-count:hover::before", self.less) self.assertIn(".Post-actions", self.less) diff --git a/docs/evidence/issue-73/community-search-ui-validation.json b/docs/evidence/issue-73/community-search-ui-validation.json new file mode 100644 index 0000000..88b928a --- /dev/null +++ b/docs/evidence/issue-73/community-search-ui-validation.json @@ -0,0 +1,63 @@ +{ + "schema_version": "1.0", + "executed_at": "2026-08-20T13:34:58+09:00", + "scope": "Community header search input icons and discussion search result readability", + "query": "v2k", + "root_cause": [ + "the solved-search extension rendered tags, title, question excerpt and selected-answer excerpt as a horizontal flex row inside the 400px native search menu", + "the theme icon fallback did not map fa-times-circle and the search prefix used an operating-system symbol glyph" + ], + "implementation": { + "result_panel_width_px": 560, + "result_layout": "single vertical reading flow: tags, title, question excerpt, selected-answer excerpt", + "question_excerpt_lines": 2, + "selected_answer_excerpt_lines": 2, + "mobile_panel": "12px viewport side insets and viewport-height scrolling", + "search_icon": "embedded SVG mask", + "clear_icon": "embedded SVG mask for fa-times-circle", + "external_icon_font_dependency": false + }, + "wsl": { + "url": "http://localhost:18080/", + "rehearsal": "search-ui-20260820-r3 PASS", + "panel_rect_px": { "x": 403, "y": 67, "width": 560, "height": 456 }, + "solution_result_count": 1, + "result_section_overlap": false, + "search_icon_mask_present": true, + "clear_icon_mask_present": true, + "result": "PASS" + }, + "production": { + "url": "https://community.ablecloud.io/", + "backup_root": "/var/backups/techflow-flarum/theme-search-ui-20260820", + "theme_less_sha256": "61598a1d4be2c48b21cf839546546f5c036c545c580b8d54f763f17127b662b5", + "panel_rect_px": { "x": 403, "y": 67, "width": 560, "height": 518 }, + "solution_result_count": 2, + "discussion_result_count_including_solutions": 4, + "result_section_overlap": false, + "search_icon_mask_present": true, + "clear_icon_mask_present": true, + "services": { + "nginx": "active", + "php8.3-fpm": "active", + "mariadb": "active" + }, + "local_http_status": 200, + "external_browser_https": true, + "temporary_remote_assets_removed": true, + "result": "PASS" + }, + "validation": { + "theme_contract_tests": { "passed": 14, "failed": 0 }, + "git_diff_check": "PASS", + "source_and_vendor_hash_match": true + }, + "protected_scope": { + "database_schema_changed": false, + "community_content_changed": false, + "attachments_changed": false, + "techflow_ai_services_changed": false, + "github_chat_webhook_changed": false + }, + "decision": "PASS" +} diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index 33d61e4..e3d220a 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -559,3 +559,35 @@ Z-index를 0으로 유지해 배경 목록이 작성기 앞으로 올라오지 Source와 일치한다. Nginx·PHP-FPM·MariaDB는 모두 `active`, 서버 로컬 HTTP는 200이다. DB Schema, 게시물, 첨부, TechFlow AI와 GitHub→Chat Webhook은 변경하지 않았으며 임시 배포 파일은 제거했다. + +### 검색 입력 아이콘과 검색 결과 가독성 교정 + +`v2k` 검색을 운영 브라우저에서 재현한 결과 데이터 손상은 없었다. 해결된 토론 +검색 확장 기능이 태그·제목·질문 요약·선택 답변 요약을 기존 400px 검색 메뉴에서 +가로 Flex 행으로 배치해 각 영역이 51~115px로 줄어든 것이 글자가 한두 자씩 +끊기던 원인이었다. 검색 입력의 접두 아이콘은 운영체제 기호 글꼴에 의존했고, +검색 비우기 버튼의 `fa-times-circle`은 테마 SVG 목록에 없어 사각형으로 표시됐다. + +검색 결과 한 건을 `태그 → 제목 → 질문 요약 → 선택 답변 요약`의 단일 세로 흐름으로 +재구성했다. 패널은 데스크톱 최대 560px, 모바일은 화면 양쪽 12px 여백을 사용하며, +질문과 선택 답변 요약은 각각 두 줄까지만 표시한다. 긴 한글은 단어 단위로 줄바꿈하고 +결과 내부 요소는 서로 겹치지 않는다. 검색과 비우기 아이콘은 모두 내장 SVG Mask로 +바꿔 외부 Font Awesome 또는 운영체제 기호 글꼴에 의존하지 않는다. + +| 브라우저 검증 항목 | WSL | 운영 | +|---|---:|---:| +| 검색어 | `v2k` | `v2k` | +| 결과 패널 폭 | 560px | 560px | +| 해결된 토론 결과 | 1건 | 2건 | +| 태그·제목·요약·답변 겹침 | 없음 | 없음 | +| 검색 SVG Mask | 정상 | 정상 | +| 비우기 SVG Mask | 정상 | 정상 | + +계약 테스트 14건과 WSL `search-ui-20260820-r3` 적용·비활성화·재적용 주기를 +통과했다. 운영 배포 전 백업은 +`/var/backups/techflow-flarum/theme-search-ui-20260820`에 보존했다. 운영 Source와 +Vendor LESS SHA-256은 모두 +`61598a1d4be2c48b21cf839546546f5c036c545c580b8d54f763f17127b662b5`로 일치하며, +Nginx·PHP-FPM·MariaDB는 `active`, 서버 로컬 HTTP는 200이다. DB Schema, 게시물, +첨부, TechFlow AI와 GitHub→Chat Webhook은 변경하지 않았고 원격 임시 파일은 +제거했다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 131f92a..0e63511 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -152,6 +152,10 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 대화상자로 표시되어야 한다. 최소화·전체 화면은 숨기고 닫기는 유지한다. 본문 편집 영역과 도구막대가 독립 행으로 대화상자 안에 모두 들어가며, 첫 클릭만으로 대화상자 상태가 적용되어야 한다. 답장 작성기는 기존 흐름을 유지한다. +- 헤더 검색에 `v2k`처럼 해결 답변과 일반 토론이 함께 조회되는 검색어를 입력한다. + 검색 결과 한 건은 태그·제목·질문 요약·선택 답변 요약이 위에서 아래로 표시되어야 + 하며, 네 영역이 가로 열로 압축되거나 글자가 한두 자씩 끊기면 안 된다. 검색 접두 + 아이콘과 비우기 아이콘은 사각형 대체 문자가 아닌 SVG Mask로 보여야 한다. - 사용자 프로필·설정 화면을 1,150px 폭에서 스크롤했을 때 고정 메뉴 폭이 280px로 유지되고 메뉴 오른쪽 끝이 본문 왼쪽보다 작거나 같은지 확인한다. - `Answered`, `Best Answer`, `Select Best Answer` 원문 노출이 없어야 한다. From a7b7f88fb435b589fcef4a6da6a5b98745fef9a1 Mon Sep 17 00:00:00 2001 From: DH Park Date: Thu, 20 Aug 2026 16:14:17 +0900 Subject: [PATCH 18/18] =?UTF-8?q?Community=20=EB=AA=A8=EB=B0=94=EC=9D=BC?= =?UTF-8?q?=20=EB=A9=94=EB=89=B4=EC=99=80=20=EC=9E=91=EC=84=B1=EA=B8=B0=20?= =?UTF-8?q?UX=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/dist/forum.js | 92 ++++ .../resources/less/forum.less | 416 ++++++++++++++++++ deploy/flarum/tests/test_community_theme.py | 56 +++ ...y-mobile-navigation-dialog-validation.json | 87 ++++ ...mmunity-shared-shell-width-validation.json | 71 +++ .../issue-73-community-ui-validation.md | 107 +++++ .../community-theme-rollout-rollback.md | 25 +- 7 files changed, 852 insertions(+), 2 deletions(-) create mode 100644 docs/evidence/issue-73/community-mobile-navigation-dialog-validation.json create mode 100644 docs/evidence/issue-73/community-shared-shell-width-validation.json diff --git a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js index 814ed4f..bd52c32 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js +++ b/deploy/flarum/extensions/ablecloud-community-theme/js/dist/forum.js @@ -754,6 +754,94 @@ return composerOpen; } + function syncMobileNavigationState(root) { + var app = root.querySelector('.App') || document.querySelector('.App'); + var toggle = root.querySelector('.Navigation-drawer') || document.querySelector('.Navigation-drawer'); + + if (!app || !toggle) { + return; + } + + var open = app.classList.contains('drawerOpen'); + toggle.setAttribute('aria-label', open ? '탐색 서랍 닫기' : '탐색 서랍 열기'); + toggle.setAttribute('title', open ? '메뉴 닫기' : '메뉴 열기'); + + if (!open) { + root.querySelectorAll('.drawer-backdrop').forEach(function (backdrop) { + backdrop.remove(); + }); + } + } + + function handleMobileOverlayKeydown(event) { + if (event.key !== 'Escape') { + return; + } + + var app = document.querySelector('.App.drawerOpen'); + var drawerToggle = app && document.querySelector('.Navigation-drawer'); + + if (drawerToggle) { + event.preventDefault(); + drawerToggle.click(); + } + } + + function closeMobileNavigation(event) { + var app = document.querySelector('.App.drawerOpen'); + + if (!app) { + return false; + } + + if (event) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + + app.classList.remove('drawerOpen'); + document.querySelectorAll('.drawer-backdrop').forEach(function (backdrop) { + backdrop.remove(); + }); + syncMobileNavigationState(document); + return true; + } + + function handleMobileNavigationClick(event) { + var toggle = event.target.closest && event.target.closest('.Navigation-drawer'); + var backdrop = event.target.closest && event.target.closest('.drawer-backdrop'); + + if ((toggle || backdrop) && document.querySelector('.App.drawerOpen')) { + closeMobileNavigation(event); + } + } + + function handleMobileComposerCloseClick(event) { + var closeButton = event.target.closest && event.target.closest('.Composer-controls .item-close button'); + + if (!closeButton || !document.body.classList.contains('ablecloud-composer-open')) { + return; + } + + event.preventDefault(); + event.stopImmediatePropagation(); + var title = document.querySelector('.Composer input[placeholder="토의 제목"]'); + var editor = document.querySelector('.Composer .TextEditor-editor'); + var hasContent = Boolean( + (title && title.value.trim()) || + (editor && editor.textContent.trim()) + ); + + if (hasContent && !window.confirm('작성 중인 내용을 취소하고 대화상자를 닫으시겠습니까?')) { + return; + } + + forumApp.composer.hide(); + window.setTimeout(function () { + syncComposerPaneState(document); + }, 0); + } + function showFallbackPaneFromEdge(event) { var composerOpen = syncComposerPaneState(document); @@ -782,6 +870,7 @@ captureDiscussionListSnapshot(document); ensureFallbackDiscussionPane(document); syncComposerPaneState(document); + syncMobileNavigationState(document); enhanceDiscussionDetail(document); }); }; @@ -802,8 +891,11 @@ window.setTimeout(schedule, 600); }, true); document.addEventListener('click', openDiscussionFromTop, true); + document.addEventListener('click', handleMobileNavigationClick, true); + document.addEventListener('click', handleMobileComposerCloseClick, true); document.addEventListener('click', handleEmojiPaletteClick, true); document.addEventListener('keydown', handleEmojiPaletteKeydown, true); + document.addEventListener('keydown', handleMobileOverlayKeydown, true); document.addEventListener('mousemove', showFallbackPaneFromEdge, { passive: true }); }); diff --git a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less index 1028a94..87282bb 100644 --- a/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less +++ b/deploy/flarum/extensions/ablecloud-community-theme/resources/less/forum.less @@ -307,6 +307,8 @@ } :root { + --ablecloud-page-shell-width: 1165px; + --ablecloud-page-rail-space: 80px; --ablecloud-brand-primary: #155eef; --ablecloud-brand-primary-hover: #004eeb; --ablecloud-brand-primary-deep: #0b3b91; @@ -2435,6 +2437,34 @@ body { } @media (min-width: 768px) { + /* + * Shared desktop shell + * + * Flarum's default desktop container is 1100px while the discussion feed + * uses a 1165px shell (1100px content + the adjacent quick-link rail). + * Apply that same outer edge to tags, profile, settings and security pages. + * The extra 65px is reserved on the right, so the established 1070px inner + * content width does not stretch and never sits underneath the fixed rail. + */ + .App-header > .container, + .WelcomeHero > .container, + .TagHero > .container, + .UserHero .container, + .TagsPage > .container, + .UserPage > .container { + width: 100%; + max-width: var(--ablecloud-page-shell-width); + box-sizing: border-box; + } + + .WelcomeHero > .container, + .TagHero > .container, + .UserHero .container, + .TagsPage > .container, + .UserPage > .container { + padding-right: var(--ablecloud-page-rail-space); + } + html:has(body > .App--index), body:has(> .App--index) { height: 100%; @@ -3107,6 +3137,215 @@ body { min-height: 54px; } + // The mobile title control already carries the current discussion filter. + // Do not leave the desktop side-navigation card behind as an empty pill. + .App--index .IndexPage-nav { + display: block !important; + width: 0; + height: 0; + margin: 0; + overflow: visible; + pointer-events: none; + } + + .App--index .IndexPage-nav > ul { + width: 0; + height: 0; + margin: 0; + padding: 0; + overflow: visible; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + } + + .App--index .IndexPage-nav .item-nav, + .App--index .IndexPage-nav .item-newDiscussion { + pointer-events: auto; + } + + /* + * Mobile navigation drawer + * + * Desktop header controls are a horizontal flex row. Reset them to a + * vertical, scrollable menu while the Flarum drawer is open so every + * action remains inside the 270px drawer. + */ + .App.drawerOpen .App-drawer { + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + background: #fff; + box-shadow: 16px 0 36px rgba(20, 42, 75, 0.22); + scrollbar-width: none; + } + + .App.drawerOpen .App-drawer::-webkit-scrollbar { + display: none; + } + + .App.drawerOpen .App-header, + .App.drawerOpen .App-header > .container { + width: 270px; + min-height: 100%; + height: auto; + box-sizing: border-box; + overflow: visible; + background: #fff; + } + + &.ablecloud-composer-open .Composer:not(.fullScreen)::before { + z-index: 0; + pointer-events: none; + } + + .App.drawerOpen .App-header > .container { + padding: 0 14px 24px; + } + + .App.drawerOpen .Header-title { + display: flex; + width: 100%; + min-height: 66px; + height: 66px; + align-items: center; + box-sizing: border-box; + padding: 0 48px 0 2px; + border-bottom: 1px solid var(--ablecloud-brand-border); + } + + .App.drawerOpen .Header-title img { + width: auto; + max-width: 190px; + max-height: 30px; + } + + .App.drawerOpen .Header-primary, + .App.drawerOpen .Header-secondary, + .App.drawerOpen .Header-secondary > ul { + width: 100%; + height: auto; + min-height: 0; + } + + .App.drawerOpen .Header-secondary > ul { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 4px; + margin: 0; + padding: 12px 0 0; + } + + .App.drawerOpen .Header-secondary > ul > li { + display: block !important; + width: 100%; + height: auto; + min-height: 46px; + margin: 0; + } + + .App.drawerOpen .Header-secondary > ul > li > .Button, + .App.drawerOpen .Header-secondary .Dropdown-toggle { + display: flex; + width: 100%; + min-height: 46px; + align-items: center; + justify-content: flex-start; + box-sizing: border-box; + padding: 10px 12px; + border: 0; + border-radius: 10px; + background: transparent; + color: #405878; + box-shadow: none; + text-align: left; + } + + .App.drawerOpen .Header-secondary > ul > li > .Button:hover, + .App.drawerOpen .Header-secondary > ul > li > .Button:focus-visible, + .App.drawerOpen .Header-secondary .Dropdown-toggle:hover, + .App.drawerOpen .Header-secondary .Dropdown-toggle:focus-visible { + background: var(--ablecloud-brand-primary-soft); + color: var(--ablecloud-brand-primary-deep); + transform: none; + } + + .App.drawerOpen .Header-secondary .item-search, + .App.drawerOpen .Header-secondary .Search, + .App.drawerOpen .Header-secondary .Search-input, + .App.drawerOpen .Header-secondary .Search-input input { + width: 100% !important; + max-width: none; + box-sizing: border-box; + } + + .App.drawerOpen .Header-secondary .Search-input input { + padding-left: 42px; + } + + .App.drawerOpen .Header-secondary .item-locale .dropdown:not(.open) > .Dropdown-menu, + .App.drawerOpen .Header-secondary .item-session .dropdown:not(.open) > .Dropdown-menu { + display: none !important; + } + + .App.drawerOpen .Header-secondary .item-locale .dropdown.open > .Dropdown-menu, + .App.drawerOpen .Header-secondary .item-session .dropdown.open > .Dropdown-menu { + position: static; + display: block !important; + visibility: visible !important; + float: none; + width: 100%; + min-width: 0; + max-height: none; + box-sizing: border-box; + margin: 4px 0 0; + padding: 6px; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 10px; + background: var(--ablecloud-brand-primary-faint); + box-shadow: none; + transform: none; + } + + // Flarum inserts a full-viewport dropdown backdrop on touch devices. + // The submenu is already contained by our drawer, so that backdrop must + // not cover or tint the drawer contents. + .App.drawerOpen .Header-secondary .Dropdown-menu > .dropdown-backdrop { + display: none !important; + } + + .App.drawerOpen .App-navigation { + z-index: 1060; + width: 270px; + pointer-events: none; + } + + .App.drawerOpen .Navigation-drawer { + position: fixed; + top: 11px; + left: 214px; + display: inline-flex; + width: 44px; + min-width: 44px; + height: 44px; + min-height: 44px; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 50%; + background: #f4f8ff; + color: var(--ablecloud-brand-primary-deep); + box-shadow: 0 4px 12px rgba(24, 71, 142, 0.12); + pointer-events: auto; + } + + .App.drawerOpen .Navigation-drawer .fa-bars { + --ablecloud-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 6l12 12M18 6 6 18' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); + } + .WelcomeHero { padding: 0; } @@ -3517,6 +3756,74 @@ body { border-radius: 14px 14px 0 0; } + // All mobile dialogs share a fixed header, visible close action and an + // independently scrollable body inside the viewport. + .Modal-content { + display: flex; + max-height: calc(100vh - 24px); + flex-direction: column; + overflow: hidden; + border-radius: 14px; + } + + .Modal-header { + position: relative; + min-height: 58px; + flex: 0 0 auto; + box-sizing: border-box; + padding: 17px 62px 14px; + border-bottom: 1px solid var(--ablecloud-brand-border); + background: var(--ablecloud-brand-primary-faint); + } + + .Modal-body { + min-height: 0; + flex: 1 1 auto; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + } + + .Modal-footer { + flex: 0 0 auto; + } + + .Modal-close { + top: 7px !important; + right: auto !important; + left: 9px !important; + width: 44px; + height: 44px; + } + + .Modal-close .Button { + display: inline-flex; + width: 44px; + min-width: 44px; + height: 44px; + min-height: 44px; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 50%; + background: #fff; + color: var(--ablecloud-brand-primary-deep); + box-shadow: 0 4px 12px rgba(24, 71, 142, 0.1); + } + + .Modal-close .Button-icon { + display: none !important; + } + + .Modal-close .Button::after { + color: currentColor; + content: "\00d7"; + font-size: 28px; + font-weight: 400; + line-height: 1; + } + &.ablecloud-new-discussion-open { overflow: hidden; } @@ -3560,6 +3867,57 @@ body { display: none; } + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls { + position: absolute; + top: 8px; + right: auto; + left: 8px; + z-index: 12; + display: block; + margin: 0; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls .item-close { + position: static; + display: block !important; + width: 44px; + height: 44px; + margin: 0; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls .item-close .Button { + display: inline-flex; + width: 44px; + min-width: 44px; + height: 44px; + min-height: 44px; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 50%; + background: #fff; + color: var(--ablecloud-brand-primary-deep); + box-shadow: 0 4px 12px rgba(24, 71, 142, 0.1); + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls .item-close .Button-icon { + display: none; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-controls .item-close .Button::after { + color: currentColor; + content: "\00d7"; + font-size: 28px; + font-weight: 400; + line-height: 1; + } + + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-header > .item-title { + padding-right: 48px; + padding-left: 48px; + } + &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .Composer-content, &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody, &.ablecloud-new-discussion-open .Composer:not(.fullScreen) .ComposerBody-content { @@ -3600,6 +3958,64 @@ body { min-height: 0; } + // Use the same explicit close action for new discussions and replies. + // Mobile users should never have to minimize a composer to dismiss it. + &.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-handle, + &.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls .item-minimize, + &.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls .item-fullScreen { + display: none !important; + } + + &.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls { + position: absolute; + top: 8px; + right: auto; + left: 8px; + z-index: 1002; + display: block; + margin: 0; + } + + &.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls .item-close { + position: static; + display: block !important; + width: 44px; + height: 44px; + margin: 0; + } + + &.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls .item-close .Button { + display: inline-flex; + width: 44px; + min-width: 44px; + height: 44px; + min-height: 44px; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid var(--ablecloud-brand-border); + border-radius: 50%; + background: #fff; + color: var(--ablecloud-brand-primary-deep); + box-shadow: 0 4px 12px rgba(24, 71, 142, 0.1); + } + + &.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls .item-close .Button-icon { + display: none !important; + } + + &.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls .item-close .Button::after { + color: currentColor; + content: "\00d7"; + font-size: 28px; + font-weight: 400; + line-height: 1; + } + + &.ablecloud-composer-open .Composer:not(.fullScreen) .ComposerBody-header > .item-title { + padding-left: 48px; + } + .ablecloud-EmojiPicker { right: 12px; bottom: 72px; diff --git a/deploy/flarum/tests/test_community_theme.py b/deploy/flarum/tests/test_community_theme.py index 3c4bfbc..a0e1706 100644 --- a/deploy/flarum/tests/test_community_theme.py +++ b/deploy/flarum/tests/test_community_theme.py @@ -199,6 +199,47 @@ def test_infinite_scroll_emoji_and_new_discussion_dialog_contract(self) -> None: with self.subTest(style_contract=style_contract): self.assertIn(style_contract, self.less) + def test_mobile_navigation_and_dialog_contract(self) -> None: + for script_contract in ( + "syncMobileNavigationState", + "탐색 서랍 닫기", + "탐색 서랍 열기", + "handleMobileOverlayKeydown", + "handleMobileNavigationClick", + "handleMobileComposerCloseClick", + "document.body.classList.contains('ablecloud-composer-open')", + "forumApp.composer.hide()", + "작성 중인 내용을 취소하고 대화상자를 닫으시겠습니까?", + "drawerToggle.click()", + ): + with self.subTest(script_contract=script_contract): + self.assertIn(script_contract, self.forum_js) + + for style_contract in ( + ".App--index .IndexPage-nav", + ".App--index .IndexPage-nav > ul", + "pointer-events: none", + ".App.drawerOpen .App-drawer", + ".App.drawerOpen .Header-secondary > ul", + ".dropdown:not(.open) > .Dropdown-menu", + ".App.drawerOpen .Header-secondary .Dropdown-menu > .dropdown-backdrop", + "flex-direction: column", + ".App.drawerOpen .Navigation-drawer", + ".App.drawerOpen .Navigation-drawer .fa-bars", + ".Modal-body", + "max-height: calc(100vh - 24px)", + ".Modal-close .Button", + ".Modal-close .Button::after", + ".Composer-controls .item-close", + ".Composer-controls .item-close .Button::after", + "&.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls .item-minimize", + "&.ablecloud-composer-open .Composer:not(.fullScreen) .Composer-controls .item-close", + "z-index: 1002", + "display: block !important", + ): + with self.subTest(style_contract=style_contract): + self.assertIn(style_contract, self.less) + def test_reddit_style_discussion_item_structure_contract(self) -> None: for script_contract in ( "enhanceDiscussionListItems", @@ -281,6 +322,21 @@ def test_discussion_detail_reading_flow_contract(self) -> None: self.assertIn("justify-content: flex-start", self.less) def test_compact_hero_and_navigation_width_contract(self) -> None: + self.assertIn("--ablecloud-page-shell-width: 1165px", self.less) + self.assertIn("--ablecloud-page-rail-space: 80px", self.less) + for shell_selector in ( + ".App-header > .container", + ".WelcomeHero > .container", + ".TagHero > .container", + ".UserHero .container", + ".TagsPage > .container", + ".UserPage > .container", + ): + with self.subTest(shell_selector=shell_selector): + self.assertIn(shell_selector, self.less) + self.assertIn("max-width: var(--ablecloud-page-shell-width)", self.less) + self.assertIn("padding-right: var(--ablecloud-page-rail-space)", self.less) + self.assertIn("Shared desktop shell", self.less) self.assertIn(".WelcomeHero .container", self.less) self.assertIn("font-size: 28px", self.less) self.assertIn("width: 280px", self.less) diff --git a/docs/evidence/issue-73/community-mobile-navigation-dialog-validation.json b/docs/evidence/issue-73/community-mobile-navigation-dialog-validation.json new file mode 100644 index 0000000..91ec630 --- /dev/null +++ b/docs/evidence/issue-73/community-mobile-navigation-dialog-validation.json @@ -0,0 +1,87 @@ +{ + "schema_version": "1.0", + "executed_at": "2026-08-20T15:17:00+09:00", + "scope": "Mobile drawer submenu backdrop and new-discussion/reply close-interface correction", + "viewport": {"width": 390, "height": 844}, + "wsl": { + "url": "http://localhost:18080", + "environment": "Ubuntu 24.04 WSL", + "rehearsal_result": "/srv/techflow-flarum-staging/rehearsals/issue-73/issue73-mobile-submenu-reply-final-20260820/result.json", + "rehearsal_cycle": ["enable", "validate", "disable", "rollback-validate", "re-enable"], + "content_integrity": { + "users": 39, + "discussions": 117, + "posts": 305, + "content_sha256": "efc9dfbd1f9d6a235378d240494dd94a192d52ea6e956f9167d4a2d44eb11cb7", + "uploads_sha256": "19cdf52600535380969a176d81bf66678b200a4247694641d00b470b8637a97c" + }, + "browser": { + "empty_navigation_pill": false, + "index_navigation_rect": {"width": 0, "height": 0, "background": "transparent", "border": "none"}, + "drawer_rect": {"width": 270, "height": 844}, + "drawer_hidden_dropdowns_take_space": false, + "locale_submenu_rect": {"x": 24, "y": 202, "width": 222, "height": 131.1875}, + "locale_transition": ["ko", "en", "ko"], + "touch_dropdown_backdrop_hidden": true, + "drawer_close_button": {"width": 44, "height": 44, "label": "탐색 서랍 닫기"}, + "drawer_backdrop_after_close": 0, + "composer_close_button": {"width": 44, "height": 44, "label": "닫기", "pointer_target": true}, + "composer_close_result": "PASS", + "reply_close_button": {"x": 9, "y": 9, "width": 44, "height": 44, "pointer_target": true}, + "reply_minimize_display": "none", + "reply_fullscreen_display": "none", + "reply_close_result": "PASS", + "tag_modal_close_button": {"width": 44, "height": 44, "marker": "×"}, + "tag_modal_close_result": "PASS" + }, + "result": "PASS" + }, + "production": { + "url": "https://community.ablecloud.io", + "host": "172.16.0.234", + "backup_root": "/var/backups/techflow-flarum/theme-mobile-submenu-reply-20260820T151526KST", + "source_less_sha256": "1424f73c1a8b4416e569d7bb32d6fa29a9694fcb9013c2f672030e8138915ca6", + "vendor_less_sha256": "1424f73c1a8b4416e569d7bb32d6fa29a9694fcb9013c2f672030e8138915ca6", + "source_js_sha256": "5ad7bb5e00dda6c36c5e97c194d09aefc137c9834cdf9059916f5f0bf956e8cb", + "vendor_js_sha256": "5ad7bb5e00dda6c36c5e97c194d09aefc137c9834cdf9059916f5f0bf956e8cb", + "local_http_status": 200, + "external_https_status": 200, + "discussion_https_status": 200, + "services": {"nginx": "active", "php8.3-fpm": "active", "mariadb": "active"}, + "browser": { + "empty_navigation_pill": false, + "index_navigation_rect": {"width": 0, "height": 0, "background": "transparent", "border": "none"}, + "drawer_rect": {"width": 270, "height": 844}, + "drawer_hidden_dropdowns_take_space": false, + "locale_submenu_rect": {"x": 24, "y": 202, "width": 222, "height": 131.1875}, + "locale_transition": ["ko", "en", "ko"], + "touch_dropdown_backdrop_hidden": true, + "drawer_backdrop_after_close": 0, + "composer_rect": {"x": 8, "y": 62, "width": 374, "height": 335.1875}, + "composer_close_button": {"x": 17, "y": 71, "width": 44, "height": 44, "pointer_target": true}, + "composer_close_result": "PASS", + "reply_close_button": {"x": 9, "y": 9, "width": 44, "height": 44, "pointer_target": true}, + "reply_minimize_display": "none", + "reply_fullscreen_display": "none", + "reply_close_result": "PASS", + "tag_modal_close_button": {"x": 10, "y": 8, "width": 44, "height": 44, "marker": "×"}, + "tag_modal_body_scroll": "auto", + "tag_modal_close_result": "PASS", + "visual_check": "PASS" + }, + "result": "PASS" + }, + "tests": { + "command": "python3 -m unittest -v deploy.flarum.tests.test_community_theme", + "passed": 15, + "failed": 0, + "git_diff_check": "PASS" + }, + "protected_scope": { + "database_schema_changed": false, + "discussion_or_attachment_changed": false, + "techflow_ai_changed": false, + "github_chat_webhook_changed": false + }, + "result": "PASS" +} diff --git a/docs/evidence/issue-73/community-shared-shell-width-validation.json b/docs/evidence/issue-73/community-shared-shell-width-validation.json new file mode 100644 index 0000000..b3470e0 --- /dev/null +++ b/docs/evidence/issue-73/community-shared-shell-width-validation.json @@ -0,0 +1,71 @@ +{ + "schema_version": "1.0", + "executed_at": "2026-08-20T14:21:54+09:00", + "scope": "Align tags, tag hero, user profile, settings and security pages to the discussion-list desktop shell", + "desktop_contract": { + "shell_width_px": 1165, + "shell_left_px_at_1280": 50, + "shell_right_px_at_1280": 1215, + "inner_content_right_px": 1135, + "quick_link_rail_left_px": 1143.5, + "quick_link_clearance_px": 8.5, + "reserved_right_space_px": 80, + "mobile_override": false + }, + "before": { + "tags_shell_width_px": 1100, + "user_shell_width_px": 1100, + "settings_shell_width_px": 1100, + "security_shell_width_px": 1100, + "left_px_at_1280": 82.5, + "right_px_at_1280": 1182.5 + }, + "wsl": { + "url": "http://localhost:18080", + "environment": "Ubuntu 24.04 WSL", + "rehearsal_result": "/srv/techflow-flarum-staging/rehearsals/issue-73/issue73-shell-width-20260820/result.json", + "rehearsal_cycle": ["enable", "validate", "disable", "rollback-validate", "re-enable"], + "result": "PASS", + "browser": { + "viewport": {"width": 1280, "height": 720}, + "tags": {"header_width": 1165, "hero_width": 1165, "page_width": 1165, "content_width": 1070}, + "profile": {"header_width": 1165, "hero_width": 1165, "page_width": 1165, "content_right": 1135}, + "settings": {"header_width": 1165, "hero_width": 1165, "page_width": 1165, "content_right": 1135}, + "security": {"header_width": 1165, "hero_width": 1165, "page_width": 1165, "content_right": 1135}, + "quick_link_overlap": false + } + }, + "production": { + "url": "https://community.ablecloud.io", + "host": "172.16.0.234", + "backup_root": "/var/backups/techflow-flarum/theme-shell-width-20260820T142154KST", + "source_less_sha256": "6bff0d72cdee7420aa38cd195229787a39cc64a351b8b18eee61955235a48746", + "vendor_less_sha256": "6bff0d72cdee7420aa38cd195229787a39cc64a351b8b18eee61955235a48746", + "local_http_status": 200, + "external_https_status": 200, + "services": {"nginx": "active", "php8.3-fpm": "active", "mariadb": "active"}, + "browser": { + "viewport": {"width": 1280, "height": 720}, + "discussion_list_reference": {"header_width": 1165, "page_width": 1165, "content_right": 1135}, + "tags": {"header_width": 1165, "hero_width": 1165, "page_width": 1165, "content_width": 1070}, + "profile": {"header_width": 1165, "hero_width": 1165, "page_width": 1165, "content_right": 1135}, + "settings": {"header_width": 1165, "hero_width": 1165, "page_width": 1165, "content_right": 1135}, + "security": {"header_width": 1165, "hero_width": 1165, "page_width": 1165, "content_right": 1135}, + "quick_link_overlap": false, + "visual_check": "PASS" + } + }, + "tests": { + "command": "python -m unittest discover -s deploy/flarum/tests -p test_*.py -v", + "passed": 14, + "failed": 0, + "git_diff_check": "PASS" + }, + "protected_scope": { + "database_schema_changed": false, + "discussion_or_attachment_changed": false, + "techflow_ai_changed": false, + "github_chat_webhook_changed": false + }, + "result": "PASS" +} diff --git a/docs/reports/issue-73-community-ui-validation.md b/docs/reports/issue-73-community-ui-validation.md index e3d220a..ba7f978 100644 --- a/docs/reports/issue-73-community-ui-validation.md +++ b/docs/reports/issue-73-community-ui-validation.md @@ -591,3 +591,110 @@ Vendor LESS SHA-256은 모두 Nginx·PHP-FPM·MariaDB는 `active`, 서버 로컬 HTTP는 200이다. DB Schema, 게시물, 첨부, TechFlow AI와 GitHub→Chat Webhook은 변경하지 않았고 원격 임시 파일은 제거했다. + +### 태그·프로필·설정·보안 화면 폭 통일 + +운영 브라우저의 1,280×720 화면에서 토론 목록은 Header와 Page가 1,165px인데, +태그·프로필·설정·보안 화면은 Flarum 기본 1,100px Container를 사용하고 있었다. +이 때문에 동일한 Header를 사용해도 좌우 끝이 각각 32.5px 안쪽으로 들어가 화면을 +이동할 때 폭이 달라 보였다. + +공통 데스크톱 셸을 1,165px로 정의하고 Header, Welcome/Tag/User Hero, TagsPage, +UserPage에 동일하게 적용했다. 단순히 내부 콘텐츠를 넓히지 않고 오른쪽 80px을 +고정 퀵 링크 영역으로 예약해 기존 1,070px 내부 폭을 유지했다. 그 결과 태그와 +사용자 화면의 콘텐츠 오른쪽 끝은 목록과 같은 X 1,135px이고, 퀵 링크 시작점 +X 1,143.5px보다 왼쪽에 있어 겹치지 않는다. 768px 미만 화면에는 이 규칙을 +적용하지 않는다. + +| 운영 브라우저 검증 화면 | Header | Hero | Page | 콘텐츠 오른쪽 | +|---|---:|---:|---:|---:| +| 토론 목록 기준 | 1,165px | 해당 없음 | 1,165px | 1,135px | +| 태그 | 1,165px | 1,165px | 1,165px | 1,135px | +| 사용자 프로필 | 1,165px | 1,165px | 1,165px | 1,135px | +| 설정 | 1,165px | 1,165px | 1,165px | 1,135px | +| 보안 | 1,165px | 1,165px | 1,165px | 1,135px | + +테마 계약 테스트 14건과 WSL +`issue73-shell-width-20260820` 적용·비활성화·재적용 주기를 통과했다. 운영 배포 +전 백업은 `/var/backups/techflow-flarum/theme-shell-width-20260820T142154KST`에 +보존했다. 운영 Source와 Vendor LESS SHA-256은 모두 +`6bff0d72cdee7420aa38cd195229787a39cc64a351b8b18eee61955235a48746`로 일치한다. +Nginx·PHP-FPM·MariaDB는 `active`, 서버 로컬 HTTP와 외부 HTTPS는 200이다. +DB Schema, 게시물, 첨부, TechFlow AI와 GitHub→Chat Webhook은 변경하지 않았다. + +### 모바일 하위 메뉴와 새 토론·댓글 닫기 통일 + +모바일 Drawer 안에서 `한국어` 같은 하위 메뉴를 펼칠 때 Flarum Core가 터치 +기기용 전체 화면 `.dropdown-backdrop`을 메뉴 내부에 추가하고 있었다. 테마가 +하위 메뉴를 Drawer 안의 정적 목록으로 바꾼 뒤에도 이 Backdrop이 남아 Drawer와 +본문을 함께 어둡게 덮는 것이 화면 깨짐의 원인이었다. Drawer의 Locale·Session +메뉴 안에서만 이 터치 Backdrop을 제거해 다른 Modal이나 전체 Drawer Backdrop의 +닫기 동작은 유지했다. + +새 토론의 닫기 버튼은 44×44px로 존재했지만 Composer의 `::before` 레이어보다 +낮은 층에 있어 화면에 가려질 수 있었다. Composer 배경 레이어는 입력을 받지 않는 +최하위 층으로 내리고, 닫기 제어는 `z-index: 1002`에 배치했다. 같은 규칙을 새 토론뿐 +아니라 댓글 작성기에도 적용해 모바일 댓글에서 최소화·전체 화면 버튼을 숨기고 +명시적인 `×` 닫기 버튼을 제공한다. 닫기 처리는 빈 작성기와 작성 중인 작성기 모두 +공통 경로를 사용하며, 작성 중이면 기존 취소 확인을 거친다. + +| 390×844 브라우저 검증 | WSL | 운영 | +|---|---:|---:| +| Locale 하위 메뉴 | 222×131.1875px | 222×131.1875px | +| 언어 전환 | `ko → en → ko` | `ko → en → ko` | +| Drawer 닫기 | 정상 | 정상 | +| 새 토론 닫기 | 44×44px·Hit 정상 | 44×44px·Hit 정상 | +| 댓글 닫기 | 44×44px·Hit 정상 | 44×44px·Hit 정상 | +| 댓글 최소화·전체 화면 | 숨김 | 숨김 | +| 닫기 후 Composer | 없음 | 없음 | + +테마 계약 테스트 15건과 WSL +`issue73-mobile-submenu-reply-final-20260820` 활성화·비활성화 롤백·재활성화 +주기를 통과했다. 사용자 39명, 토론 117건, 게시물 305건과 콘텐츠·업로드 해시는 +전 과정에서 동일했다. 운영 백업은 +`/var/backups/techflow-flarum/theme-mobile-submenu-reply-20260820T151526KST`에 +보존했다. Source와 Vendor LESS SHA-256은 +`1424f73c1a8b4416e569d7bb32d6fa29a9694fcb9013c2f672030e8138915ca6`, JS는 +`5ad7bb5e00dda6c36c5e97c194d09aefc137c9834cdf9059916f5f0bf956e8cb`로 각각 +일치한다. Nginx·PHP-FPM·MariaDB는 `active`, 외부 HTTPS는 200이며 DB Schema, +게시물, 첨부, TechFlow AI와 GitHub→Chat Webhook은 변경하지 않았다. + +### 모바일 메인·좌측 메뉴·대화상자 인터페이스 교정 + +390×844 모바일 화면에서 메인 Header 아래에 비어 있는 둥근 사각형이 남던 원인은 +데스크톱 좌측 메뉴의 `.IndexPage-nav > ul` 배경·테두리·높이가 모바일에도 +적용됐기 때문이다. 모바일에서는 현재 필터와 새 토론 버튼만 Header에서 사용할 수 +있도록 메뉴 컨테이너 자체는 0×0 투명 영역으로 만들고 두 버튼의 포인터 입력만 +유지했다. WSL과 운영에서 Header 아래 빈 사각형이 없어졌고 `모든 토의`, 새 토론, +정렬·새로고침 기능은 그대로 동작한다. + +좌측 메뉴가 가로로 깨진 원인은 데스크톱 `.Header-secondary > ul` Flex 행과 숨겨진 +Locale·Session Dropdown이 모바일 Drawer에서도 높이를 차지한 데 있었다. Drawer를 +270px 단일 열로 재구성하고 검색·언어·신고·알림·계정을 46px 행으로 정렬했다. +닫힌 Dropdown은 `display:none`, 펼친 Dropdown만 정적 하위 목록으로 표시한다. +44×44px 닫기 버튼과 배경 클릭·ESC 닫기를 제공하고, Flarum 전환 후 남던 Backdrop도 +제거한다. 운영에서 열림 Backdrop 1개, 닫힘 뒤 0개를 확인했다. + +토론 작성기의 닫기 버튼은 DOM에 있었지만 Flarum 모바일 작성기의 `::before` +레이어가 터치 입력을 가로채고 있었다. 이 레이어의 Pointer Event를 제거하고 +44×44px `×` 버튼을 고정했다. 작성 내용이 있으면 취소 확인 후 닫고, 내용이 없으면 +즉시 닫는다. 태그 선택을 포함한 Flarum Modal도 같은 44×44px `×`, 고정 Header, +스크롤 가능한 Body 규칙을 사용한다. + +| 모바일 브라우저 검증 항목 | WSL | 운영 | +|---|---:|---:| +| 빈 Navigation 영역 | 0×0px·투명 | 0×0px·투명 | +| Drawer | 270×844px | 270×844px | +| Drawer 닫힘 뒤 Backdrop | 0건 | 0건 | +| 토론 작성 닫기 버튼 | 44×44px·동작 | 44×44px·동작 | +| 태그 Modal 닫기 버튼 | 44×44px·동작 | 44×44px·동작 | +| Modal Body 스크롤 | `auto` | `auto` | + +테마 계약 테스트 15건과 WSL +`issue73-mobile-final-20260820` 적용·비활성화·재적용 주기를 통과했다. 사용자 39명, +토론 117건, 게시물 305건과 콘텐츠·업로드 해시는 전 과정에서 동일했다. 운영 배포 +전 백업은 `/var/backups/techflow-flarum/theme-mobile-ui-20260820T145819KST`에 +보존했다. Source와 Vendor LESS SHA-256은 모두 +`35dc0b6a285f2242b66a2be3253f71de432ec92412c53206173d99c2482547ee`로 일치하고, +Nginx·PHP-FPM·MariaDB는 `active`, 서버 로컬 HTTP와 외부 목록·상세 HTTPS는 200이다. +DB Schema, 게시물, 첨부, TechFlow AI와 GitHub→Chat Webhook은 변경하지 않았다. diff --git a/docs/runbooks/community-theme-rollout-rollback.md b/docs/runbooks/community-theme-rollout-rollback.md index 0e63511..faed702 100644 --- a/docs/runbooks/community-theme-rollout-rollback.md +++ b/docs/runbooks/community-theme-rollout-rollback.md @@ -88,7 +88,10 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ - 데스크톱 첫 화면의 상단 헤더와 본문 셸은 모두 최대 1165px로 동일해야 한다. `scrollbar-gutter: stable`을 적용한 1680px Viewport에서는 두 영역의 X가 250px, 오른쪽 끝이 1415px인지 확인한다. - 모바일과 다른 페이지에는 이 폭을 강제하지 않는다. + 태그·태그별 목록·사용자 프로필·설정·보안 화면의 Header·Hero·본문도 같은 + 1165px 외곽선을 사용한다. 본문과 Hero의 오른쪽 80px은 고정 퀵 링크 영역으로 + 예약해 기존 1070px 내부 콘텐츠 폭을 유지하고 겹침을 방지한다. 모바일에는 이 + 데스크톱 폭을 강제하지 않는다. - 좌측 메뉴는 사용자 설정 화면과 같이 고정한다. 높이가 짧아 태그가 모두 보이지 않을 때 내부 스크롤은 허용하지만 `scrollbar-width: none`과 WebKit 규칙으로 스크롤바는 표시하지 않는다. @@ -152,6 +155,21 @@ sudo bash deploy/flarum/apply-community-theme-update.sh \ 대화상자로 표시되어야 한다. 최소화·전체 화면은 숨기고 닫기는 유지한다. 본문 편집 영역과 도구막대가 독립 행으로 대화상자 안에 모두 들어가며, 첫 클릭만으로 대화상자 상태가 적용되어야 한다. 답장 작성기는 기존 흐름을 유지한다. +- 390×844 모바일 시작 화면에서 Header 아래 `.IndexPage-nav > ul`은 빈 둥근 + 사각형을 만들지 않아야 한다. 현재 필터와 새 토론 버튼은 Header에서 계속 + 동작해야 한다. +- 모바일 좌측 Drawer는 270px 단일 열로 표시하고 검색·언어·신고·알림·계정을 + 세로 행으로 배치한다. 닫힌 Locale·Session Dropdown은 공간을 차지하지 않아야 + 하며 44×44px 닫기 버튼, 배경 클릭, ESC로 닫은 뒤 `.drawer-backdrop`은 0건이어야 한다. +- 모바일 토론 작성기와 모든 Flarum Modal에는 항상 보이는 44×44px `×` 닫기 + 버튼을 제공한다. 닫기 버튼 중심의 실제 Hit Target이 버튼 자신인지 확인하고, + 태그 Modal은 고정 Header와 독립적으로 스크롤되는 Body를 사용해야 한다. +- 모바일 Drawer에서 언어·계정 하위 메뉴를 펼칠 때 Flarum의 터치 전용 + `.dropdown-backdrop`이 Drawer를 덮거나 색을 입히면 안 된다. `한국어 → English → + 한국어` 전환 후 Drawer가 정상 닫히고 최종 언어가 `ko`인지 확인한다. +- 모바일 새 토론과 댓글 작성기는 모두 44×44px `×` 닫기를 제공한다. 댓글 작성기에는 + 최소화·전체 화면 버튼을 표시하지 않으며, 닫기 후 `.ablecloud-composer-open` 상태와 + 보이는 Composer가 모두 제거되어야 한다. - 헤더 검색에 `v2k`처럼 해결 답변과 일반 토론이 함께 조회되는 검색어를 입력한다. 검색 결과 한 건은 태그·제목·질문 요약·선택 답변 요약이 위에서 아래로 표시되어야 하며, 네 영역이 가로 열로 압축되거나 글자가 한두 자씩 끊기면 안 된다. 검색 접두 @@ -186,4 +204,7 @@ sudo systemctl restart php8.3-fpm nginx 최신 운영 백업은 `/var/backups/techflow-flarum/theme-infinite-emoji-dialog-final-20260820T1430KST`에 보존한다. 목록 자동 로딩, 한글 이모지 삽입, 새 토론 중앙 대화상자까지 운영 - 브라우저에서 확인했다. + 브라우저에서 확인했다. 모바일 메인·Drawer·대화상자 보완의 최신 백업은 + `/var/backups/techflow-flarum/theme-mobile-submenu-reply-20260820T151526KST`이며, + 390×844 운영 브라우저에서 Drawer 하위 메뉴 전환과 새 토론·댓글 작성기 닫기를 + 확인했다.