From 5496735e2fe5ef8b05a399116b6488e2dc87ef42 Mon Sep 17 00:00:00 2001 From: Lectom Date: Wed, 20 May 2026 13:34:19 +0900 Subject: [PATCH] =?UTF-8?q?make=20dev/dev-debug=20=EA=B5=AC=EB=B6=84.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 8 +++- .../src/components/layout/AppLayout.tsx | 7 +-- .../src/features/auth/AuthCallbackPage.tsx | 3 +- adminfront/src/features/auth/LoginPage.tsx | 3 +- adminfront/src/lib/debugLog.ts | 8 ++++ backend/internal/logger/client_log_policy.go | 15 ++++-- .../internal/logger/client_log_policy_test.go | 12 ++++- docker-compose.yaml | 7 +++ test/frontend_dev_bind_mount_policy_test.sh | 9 +++- test/make_dev_targets_test.sh | 48 +++++++++++++++++++ userfront/lib/core/services/log_policy.dart | 22 +++++++-- .../lib/core/services/logger_service.dart | 19 ++------ userfront/lib/core/services/runtime_env.dart | 22 ++++++--- userfront/scripts/dev-server.sh | 8 +++- userfront/test/log_policy_test.dart | 46 ++++++++++++++++-- 15 files changed, 192 insertions(+), 45 deletions(-) create mode 100644 adminfront/src/lib/debugLog.ts diff --git a/Makefile b/Makefile index 34535890..d42ada66 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ ifneq (,$(wildcard ./.env)) COMPOSE_DROP_ENV_ARGS += --env-file .env endif -.PHONY: build-auth-config validate-auth-config verify-auth-config render-ory-config up up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory up-dev up-front-dev dev down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app +.PHONY: build-auth-config validate-auth-config verify-auth-config render-ory-config up up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory up-dev up-front-dev dev dev-debug down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app # --- 인증 설정 빌드/검증 --- build-auth-config: @@ -128,7 +128,11 @@ up-front-dev: up-infra up-ory up-backend dev: up-dev @echo "Starting development app containers in foreground attach mode..." - docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build $(DEV_SERVICES) + BACKEND_LOG_LEVEL=info CLIENT_LOG_DEBUG=false VITE_CLIENT_LOG_DEBUG=false docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build $(DEV_SERVICES) + +dev-debug: up-dev + @echo "Starting development app containers in foreground attach debug mode..." + BACKEND_LOG_LEVEL=debug CLIENT_LOG_DEBUG=true VITE_CLIENT_LOG_DEBUG=true USERFRONT_FLUTTER_RUN_FLAGS=--debug docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build $(DEV_SERVICES) # --- 종료 (Down) --- down: diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index d8cdeca4..2c18e121 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -34,6 +34,7 @@ import { } from "../../../../common/shell"; import { buildAuthenticatedOrgChartUrl } from "../../features/users/orgChartPicker"; import { fetchMe } from "../../lib/adminApi"; +import { debugLog } from "../../lib/debugLog"; import { t } from "../../lib/i18n"; import { isSuperAdminRole } from "../../lib/roles"; import { @@ -154,10 +155,10 @@ function AppLayout() { } = useQuery({ queryKey: ["me"], queryFn: async () => { - console.debug("[AppLayout] Fetching profile..."); + debugLog("[AppLayout] Fetching profile..."); try { const data = await fetchMe(); - console.debug("[AppLayout] Profile fetched successfully:", data.email); + debugLog("[AppLayout] Profile fetched successfully:", data.email); return data; } catch (err) { console.error("[AppLayout] Failed to fetch profile:", err); @@ -273,7 +274,7 @@ function AppLayout() { (window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }) ._IS_TEST_MODE === true; - console.debug("[AppLayout] Auth state check:", { + debugLog("[AppLayout] Auth state check:", { isLoading: auth.isLoading, isAuthenticated: auth.isAuthenticated, isTest, diff --git a/adminfront/src/features/auth/AuthCallbackPage.tsx b/adminfront/src/features/auth/AuthCallbackPage.tsx index b7754b0a..ab4f10b6 100644 --- a/adminfront/src/features/auth/AuthCallbackPage.tsx +++ b/adminfront/src/features/auth/AuthCallbackPage.tsx @@ -2,13 +2,14 @@ import { ShieldHalf } from "lucide-react"; import { useEffect } from "react"; import { useAuth } from "react-oidc-context"; import { useNavigate } from "react-router-dom"; +import { debugLog } from "../../lib/debugLog"; function AuthCallbackPage() { const auth = useAuth(); const navigate = useNavigate(); useEffect(() => { - console.debug("[AuthCallbackPage] State:", { + debugLog("[AuthCallbackPage] State:", { isAuthenticated: auth.isAuthenticated, isLoading: auth.isLoading, error: auth.error, diff --git a/adminfront/src/features/auth/LoginPage.tsx b/adminfront/src/features/auth/LoginPage.tsx index b0fcabf1..d0ad94e1 100644 --- a/adminfront/src/features/auth/LoginPage.tsx +++ b/adminfront/src/features/auth/LoginPage.tsx @@ -10,6 +10,7 @@ import { CardHeader, CardTitle, } from "../../components/ui/card"; +import { debugLog } from "../../lib/debugLog"; function LoginPage() { const auth = useAuth(); @@ -20,7 +21,7 @@ function LoginPage() { const shouldAutoLogin = searchParams.get("auto") === "1"; useEffect(() => { - console.debug("[LoginPage] Auth state check:", { + debugLog("[LoginPage] Auth state check:", { isAuthenticated: auth.isAuthenticated, isLoading: auth.isLoading, returnTo, diff --git a/adminfront/src/lib/debugLog.ts b/adminfront/src/lib/debugLog.ts new file mode 100644 index 00000000..6825d132 --- /dev/null +++ b/adminfront/src/lib/debugLog.ts @@ -0,0 +1,8 @@ +const CLIENT_DEBUG_LOG_ENABLED = new Set(["1", "true", "yes", "y", "on"]).has( + String(import.meta.env.VITE_CLIENT_LOG_DEBUG ?? "").trim().toLowerCase(), +); + +export function debugLog(...args: Parameters) { + if (!CLIENT_DEBUG_LOG_ENABLED) return; + console.debug(...args); +} diff --git a/backend/internal/logger/client_log_policy.go b/backend/internal/logger/client_log_policy.go index 2b9613f1..880b9514 100644 --- a/backend/internal/logger/client_log_policy.go +++ b/backend/internal/logger/client_log_policy.go @@ -37,20 +37,25 @@ func IsProductionEnv(appEnv string) bool { return IsProductionLikeEnv(appEnv) } -func parseBoolFlag(raw string) bool { +func parseOptionalBoolFlag(raw string) (bool, bool) { switch strings.ToLower(strings.TrimSpace(raw)) { case "1", "true", "yes", "y", "on": - return true + return true, true + case "0", "false", "no", "n", "off": + return false, true default: - return false + return false, false } } -func ClientDebugEnabled(appEnv, productionDebugFlag string) bool { +func ClientDebugEnabled(appEnv, debugFlag string) bool { + if enabled, ok := parseOptionalBoolFlag(debugFlag); ok { + return enabled + } if !IsProductionEnv(appEnv) { return true } - return parseBoolFlag(productionDebugFlag) + return false } func NormalizeClientLogLevel(level string) slog.Level { diff --git a/backend/internal/logger/client_log_policy_test.go b/backend/internal/logger/client_log_policy_test.go index a4bc998d..efd66585 100644 --- a/backend/internal/logger/client_log_policy_test.go +++ b/backend/internal/logger/client_log_policy_test.go @@ -9,7 +9,14 @@ import ( func TestClientDebugEnabled(t *testing.T) { t.Run("non production enables debug by default", func(t *testing.T) { assert.True(t, ClientDebugEnabled("dev", "")) - assert.True(t, ClientDebugEnabled("local", "false")) + assert.True(t, ClientDebugEnabled("local", "")) + }) + + t.Run("explicit debug flag applies in non production", func(t *testing.T) { + assert.True(t, ClientDebugEnabled("dev", "true")) + assert.True(t, ClientDebugEnabled("local", "1")) + assert.False(t, ClientDebugEnabled("dev", "false")) + assert.False(t, ClientDebugEnabled("local", "0")) }) t.Run("production disables debug by default", func(t *testing.T) { @@ -37,6 +44,8 @@ func TestShouldAcceptClientLog(t *testing.T) { assert.True(t, ShouldAcceptClientLog("stage", "", "ERROR")) assert.True(t, ShouldAcceptClientLog("production", "true", "INFO")) assert.True(t, ShouldAcceptClientLog("dev", "", "INFO")) + assert.False(t, ShouldAcceptClientLog("dev", "false", "INFO")) + assert.True(t, ShouldAcceptClientLog("dev", "false", "WARN")) } func TestShouldFilterNoisyClientInfo(t *testing.T) { @@ -44,6 +53,7 @@ func TestShouldFilterNoisyClientInfo(t *testing.T) { assert.True(t, ShouldFilterNoisyClientInfo("stage", "", "Navigating to /ko/signin")) assert.False(t, ShouldFilterNoisyClientInfo("production", "true", "Navigating to /ko/signin")) assert.False(t, ShouldFilterNoisyClientInfo("dev", "", "Navigating to /ko/signin")) + assert.True(t, ShouldFilterNoisyClientInfo("dev", "false", "Navigating to /ko/signin")) } func TestSanitizeClientLogData(t *testing.T) { diff --git a/docker-compose.yaml b/docker-compose.yaml index 6e6ba575..a36785ea 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,6 +9,8 @@ services: environment: - APP_ENV=${APP_ENV:-development} - GO_ENV=${APP_ENV:-development} + - BACKEND_LOG_LEVEL=${BACKEND_LOG_LEVEL:-info} + - CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false} - COOKIE_SECRET=${COOKIE_SECRET} - JWT_SECRET=${JWT_SECRET} - NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY} @@ -58,6 +60,7 @@ services: - APP_ENV=${APP_ENV:-development} - API_PROXY_TARGET=http://baron_backend:3000 - USERFRONT_URL=${USERFRONT_URL} + - VITE_CLIENT_LOG_DEBUG=${VITE_CLIENT_LOG_DEBUG:-false} ports: - "${ADMINFRONT_PORT:-5173}:5173" volumes: @@ -82,6 +85,7 @@ services: - APP_ENV=${APP_ENV:-development} - API_PROXY_TARGET=http://baron_backend:3000 - USERFRONT_URL=${USERFRONT_URL} + - VITE_CLIENT_LOG_DEBUG=${VITE_CLIENT_LOG_DEBUG:-false} ports: - "${DEVFRONT_PORT:-5174}:5173" volumes: @@ -106,6 +110,7 @@ services: - APP_ENV=${APP_ENV:-development} - API_PROXY_TARGET=http://baron_backend:3000 - USERFRONT_URL=${USERFRONT_URL} + - VITE_CLIENT_LOG_DEBUG=${VITE_CLIENT_LOG_DEBUG:-false} ports: - "${ORGFRONT_PORT:-5175}:5175" volumes: @@ -132,7 +137,9 @@ services: - BACKEND_URL=${BACKEND_URL:-} - USERFRONT_URL=${USERFRONT_URL} - APP_ENV=${APP_ENV} + - CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false} - USERFRONT_INTERNAL_PORT=5000 + - USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-} volumes: - ./userfront/lib:/workspace/userfront/lib - ./userfront/assets:/workspace/userfront/assets diff --git a/test/frontend_dev_bind_mount_policy_test.sh b/test/frontend_dev_bind_mount_policy_test.sh index eb6ef3fd..139e1617 100644 --- a/test/frontend_dev_bind_mount_policy_test.sh +++ b/test/frontend_dev_bind_mount_policy_test.sh @@ -40,7 +40,14 @@ grep -Fq -- "AS dev" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must grep -Fq -- "AS production" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must keep an explicit production target" grep -Fq -- "flutter run" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must use flutter run" grep -Fq -- "--wasm" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must keep WebAssembly enabled" -grep -Fq -- "--debug" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must run in debug mode" +grep -Fq -- "--dart-define=CLIENT_LOG_DEBUG=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass client log debug mode through dart-define" +grep -Fq -- "--dart-define=APP_ENV=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass app env through dart-define" +grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must accept optional Flutter run flags" +assert_contains 'CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}' +assert_contains 'USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-}' +if grep -Fq -- "--debug" "$USERFRONT_DEV_SERVER"; then + fail "make dev must not hard-code Flutter debug mode in the userfront dev server" +fi if grep -Fq -- "--release" "$USERFRONT_DEV_SERVER"; then fail "userfront dev server must not run Flutter in release mode" fi diff --git a/test/make_dev_targets_test.sh b/test/make_dev_targets_test.sh index 4d9453f8..ae91abb9 100644 --- a/test/make_dev_targets_test.sh +++ b/test/make_dev_targets_test.sh @@ -54,6 +54,54 @@ if ! grep -q -- " --build" <<<"$app_up_line"; then exit 1 fi +if ! grep -q -- "BACKEND_LOG_LEVEL=info" <<<"$app_up_line"; then + echo "make dev must run backend at info log level." >&2 + exit 1 +fi + +if ! grep -q -- "CLIENT_LOG_DEBUG=false" <<<"$app_up_line"; then + echo "make dev must disable verbose client debug log ingestion." >&2 + exit 1 +fi + +if ! grep -q -- "VITE_CLIENT_LOG_DEBUG=false" <<<"$app_up_line"; then + echo "make dev must disable React client debug console logs." >&2 + exit 1 +fi + +if grep -q -- "BACKEND_LOG_LEVEL=debug" <<<"$app_up_line"; then + echo "make dev must not run backend at debug log level." >&2 + exit 1 +fi + +if grep -q -- "USERFRONT_FLUTTER_RUN_FLAGS=--debug" <<<"$app_up_line"; then + echo "make dev must not run userfront with explicit Flutter debug flags." >&2 + exit 1 +fi + +dry_run_dev_debug="$( + make --dry-run --always-make -C "$repo_root" dev-debug DEV_SERVICES="backend userfront" 2>&1 +)" + +if ! grep -q "Ensuring Infra stack" <<<"$dry_run_dev_debug"; then + echo "make dev-debug must ensure the infra stack first." >&2 + exit 1 +fi + +if ! grep -q "Ensuring Ory stack" <<<"$dry_run_dev_debug"; then + echo "make dev-debug must ensure the Ory stack first." >&2 + exit 1 +fi + +dev_debug_app_up_line="$( + grep -E "BACKEND_LOG_LEVEL=debug CLIENT_LOG_DEBUG=true VITE_CLIENT_LOG_DEBUG=true USERFRONT_FLUTTER_RUN_FLAGS=--debug docker compose .* -f docker-compose.yaml up .*backend.*userfront" <<<"$dry_run_dev_debug" | tail -1 +)" + +if [[ -z "$dev_debug_app_up_line" ]]; then + echo "make dev-debug must run app services with explicit backend, client log, and userfront debug flags." >&2 + exit 1 +fi + dry_run_up_dev="$( make --dry-run --always-make -C "$repo_root" up-dev 2>&1 )" diff --git a/userfront/lib/core/services/log_policy.dart b/userfront/lib/core/services/log_policy.dart index b9bf15e3..df8e3c24 100644 --- a/userfront/lib/core/services/log_policy.dart +++ b/userfront/lib/core/services/log_policy.dart @@ -29,23 +29,37 @@ class LogPolicy { env == 'staging'; } - static bool parseBoolFlag(String? raw) { + static ({bool enabled, bool specified}) parseOptionalBoolFlag(String? raw) { final value = (raw ?? '').trim().toLowerCase(); - return value == '1' || + if (value == '1' || value == 'true' || value == 'yes' || value == 'y' || - value == 'on'; + value == 'on') { + return (enabled: true, specified: true); + } + if (value == '0' || + value == 'false' || + value == 'no' || + value == 'n' || + value == 'off') { + return (enabled: false, specified: true); + } + return (enabled: false, specified: false); } static bool debugEnabled({ required String? appEnv, required String? productionDebugFlag, }) { + final flag = parseOptionalBoolFlag(productionDebugFlag); + if (flag.specified) { + return flag.enabled; + } if (!isProductionEnv(appEnv)) { return true; } - return parseBoolFlag(productionDebugFlag); + return false; } static bool shouldRelayClientLog({ diff --git a/userfront/lib/core/services/logger_service.dart b/userfront/lib/core/services/logger_service.dart index 99802d10..d96e8fed 100644 --- a/userfront/lib/core/services/logger_service.dart +++ b/userfront/lib/core/services/logger_service.dart @@ -1,10 +1,10 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:logging/logging.dart' as std_log; import 'package:logger/logger.dart' as pretty_log; import 'auth_proxy_service.dart'; import 'log_policy.dart'; +import 'runtime_env.dart'; /// Global Logger Service for Baron SSO Frontend class LoggerService { @@ -16,10 +16,10 @@ class LoggerService { late final String _productionDebugFlag; LoggerService._internal() { - _appEnv = _envOrDefault('APP_ENV', 'dev'); - _productionDebugFlag = _envOrDefault( + _appEnv = envOrDefault('APP_ENV', 'dev'); + _productionDebugFlag = envOrDefault( 'CLIENT_LOG_DEBUG', - _envOrDefault('USERFRONT_DEBUG_LOG', ''), + envOrDefault('USERFRONT_DEBUG_LOG', ''), ); final debugEnabled = LogPolicy.debugEnabled( appEnv: _appEnv, @@ -54,17 +54,6 @@ class LoggerService { }); } - static String _envOrDefault(String key, String fallback) { - if (!dotenv.isInitialized) { - return fallback; - } - final value = dotenv.env[key]; - if (value == null || value.trim().isEmpty) { - return fallback; - } - return value; - } - /// Initialize the logger. Call this in main.dart static void init() { // Accessing the instance triggers the constructor diff --git a/userfront/lib/core/services/runtime_env.dart b/userfront/lib/core/services/runtime_env.dart index 89ea64ac..8e817a68 100644 --- a/userfront/lib/core/services/runtime_env.dart +++ b/userfront/lib/core/services/runtime_env.dart @@ -1,5 +1,11 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; +const _compileTimeEnv = { + 'APP_ENV': String.fromEnvironment('APP_ENV'), + 'CLIENT_LOG_DEBUG': String.fromEnvironment('CLIENT_LOG_DEBUG'), + 'USERFRONT_DEBUG_LOG': String.fromEnvironment('USERFRONT_DEBUG_LOG'), +}; + String runtimeOriginFallback() { try { final origin = Uri.base.origin; @@ -11,14 +17,18 @@ String runtimeOriginFallback() { } String envOrDefault(String key, String fallback) { - if (!dotenv.isInitialized) { - return fallback; + if (dotenv.isInitialized) { + final value = dotenv.env[key]; + if (value != null && value.trim().isNotEmpty) { + return value; + } } - final value = dotenv.env[key]; - if (value == null || value.trim().isEmpty) { - return fallback; + + final compileTimeValue = _compileTimeEnv[key]; + if (compileTimeValue != null && compileTimeValue.trim().isNotEmpty) { + return compileTimeValue; } - return value; + return fallback; } String sanitizedUrl(String value) { diff --git a/userfront/scripts/dev-server.sh b/userfront/scripts/dev-server.sh index b5f63594..e3767470 100644 --- a/userfront/scripts/dev-server.sh +++ b/userfront/scripts/dev-server.sh @@ -5,10 +5,14 @@ cd /workspace /bin/sh ./scripts/sync_userfront_locales.sh cd /workspace/userfront -exec flutter run \ +set -- flutter run \ -d web-server \ --web-hostname 0.0.0.0 \ --web-port "${USERFRONT_INTERNAL_PORT:-5000}" \ --wasm \ - --debug \ + --dart-define=CLIENT_LOG_DEBUG="${CLIENT_LOG_DEBUG:-false}" \ + --dart-define=APP_ENV="${APP_ENV:-dev}" \ + ${USERFRONT_FLUTTER_RUN_FLAGS:-} \ --no-web-resources-cdn + +exec "$@" diff --git a/userfront/test/log_policy_test.dart b/userfront/test/log_policy_test.dart index 1b472a69..f53e69a8 100644 --- a/userfront/test/log_policy_test.dart +++ b/userfront/test/log_policy_test.dart @@ -9,14 +9,30 @@ void main() { isTrue, ); expect( - LogPolicy.debugEnabled( - appEnv: 'development', - productionDebugFlag: 'false', - ), + LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: ''), isTrue, ); }); + test('explicit debug flag applies in development-like environment', () { + expect( + LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'true'), + isTrue, + ); + expect( + LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '1'), + isTrue, + ); + expect( + LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'false'), + isFalse, + ); + expect( + LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '0'), + isFalse, + ); + }); + test('production disables debug unless explicitly enabled', () { expect( LogPolicy.debugEnabled(appEnv: 'production', productionDebugFlag: ''), @@ -94,6 +110,28 @@ void main() { isTrue, ); }); + + test( + 'explicit development debug false forwards only warning or higher', + () { + expect( + LogPolicy.shouldRelayClientLog( + level: 'INFO', + appEnv: 'dev', + productionDebugFlag: 'false', + ), + isFalse, + ); + expect( + LogPolicy.shouldRelayClientLog( + level: 'WARN', + appEnv: 'dev', + productionDebugFlag: 'false', + ), + isTrue, + ); + }, + ); }); group('LogPolicy.sanitize', () {