From 2a9ab0ddc50b2c4e89781451a392209b90a55490 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 16:49:14 +0900 Subject: [PATCH 01/20] =?UTF-8?q?userfront:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EA=B8=B0=EB=B3=B8=20=EA=B0=9C=EC=9D=B8?= =?UTF-8?q?(Personal)=20=ED=85=8C=EB=84=8C=ED=8A=B8=20=EA=B0=80=EC=9E=85,?= =?UTF-8?q?=20=EA=B8=B0=EC=97=85=20=EC=86=8C=EC=86=8D=EC=9D=80=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=20=EB=AC=B8=EC=9D=98=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=95=88=EB=82=B4=20=EC=B9=B4=EB=93=9C=EB=A1=9C=20=EA=B0=9C?= =?UTF-8?q?=ED=8E=B8=20(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/signup_screen.dart | 139 +++++++++++++----- 1 file changed, 99 insertions(+), 40 deletions(-) diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index 20afee5f..e0b9cd37 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:userfront/i18n.dart'; import '../../../core/i18n/locale_utils.dart'; import '../../../core/services/auth_proxy_service.dart'; @@ -1788,24 +1789,88 @@ Matters not expressly provided in this Policy are governed by the Company's inte crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (_affiliationType == 'AFFILIATE') ...[ - const SizedBox(height: 14), - DropdownButtonFormField( - key: ValueKey(_companyCode ?? 'none'), - initialValue: _companyCode, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.signup.profile.company', + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primaryContainer + .withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.25), ), - border: const OutlineInputBorder(), ), - items: _tenants.map((t) { - return DropdownMenuItem( - value: t['slug'], - child: Text(t['name'] ?? t['slug']), - ); - }).toList(), - onChanged: (val) => - setState(() => _companyCode = val), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon( + Icons.business, + color: Theme.of(context) + .colorScheme + .primary, + size: 24, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + '기업 소속 가입 안내', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + '기업/가족사 소속 회원은 바로 가입하는 대신 별도 문의를 통해 가입 및 워크스페이스 연동이 진행됩니다.\n\n아래 버튼을 눌러 담당자에게 가입 문의 이메일을 보내거나 baroncs@baroncs.co.kr로 직접 문의해 주시기 바랍니다.', + style: TextStyle( + fontSize: 13, + height: 1.5, + color: _signupInk + .withValues(alpha: 0.8), + ), + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: () async { + final Uri emailUri = Uri( + scheme: 'mailto', + path: 'baroncs@baroncs.co.kr', + query: Uri.encodeFull( + 'subject=[Baron SSO] 기업 소속 가입 및 연동 문의', + ), + ); + if (await canLaunchUrl(emailUri)) { + await launchUrl(emailUri); + } + }, + icon: const Icon(Icons.mail_outline, + size: 18), + label: const Text('기업 소속 문의하기'), + style: FilledButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 12), + ), + ), + ], + ), ), ], ], @@ -1814,30 +1879,26 @@ Matters not expressly provided in this Policy are governed by the Company's inte ], ), ), - const SizedBox(height: 18), - _buildProfileFieldGroup( - title: _affiliationType == 'AFFILIATE' - ? tr('ui.userfront.signup.profile.department') - : tr( + if (_affiliationType == 'GENERAL') ...[ + const SizedBox(height: 18), + _buildProfileFieldGroup( + title: tr( + 'ui.userfront.signup.profile.department_optional', + ), + description: '선택 입력 항목입니다.', + isDesktop: isDesktop, + child: TextFormField( + controller: _deptController, + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + labelText: tr( 'ui.userfront.signup.profile.department_optional', ), - description: _affiliationType == 'AFFILIATE' - ? '가족사 사용자는 부서명을 입력해주세요.' - : '선택 입력 항목입니다.', - isDesktop: isDesktop, - child: TextFormField( - controller: _deptController, - onChanged: (_) => setState(() {}), - decoration: InputDecoration( - labelText: _affiliationType == 'AFFILIATE' - ? tr('ui.userfront.signup.profile.department') - : tr( - 'ui.userfront.signup.profile.department_optional', - ), - border: const OutlineInputBorder(), + border: const OutlineInputBorder(), + ), ), ), - ), + ], ], ), ), @@ -2317,10 +2378,8 @@ Matters not expressly provided in this Policy are governed by the Company's inte if (_affiliationType == 'GENERAL') { canGoNext = nameOk; } else { - // AFFILIATE 필수: 이름 + 가족사 선택 + 부서명 - final companyOk = _companyCode != null; - final deptOk = _deptController.text.trim().isNotEmpty; - canGoNext = nameOk && companyOk && deptOk; + // 기업 소속(AFFILIATE)인 경우 직접 가입 대신 문의로 안내하므로 다음 단계 진행을 차단합니다. + canGoNext = false; } } From 95a2730e7126342df17f08772d70cea5b76baf6e Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 16:56:03 +0900 Subject: [PATCH 02/20] =?UTF-8?q?backend:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=8C=80?= =?UTF-8?q?=EC=A1=B0=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EA=B0=80=EC=A1=B1?= =?UTF-8?q?=EC=82=AC(AFFILIATE)=20=EA=B0=95=EC=A0=9C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20=EC=A0=84=EB=A9=B4=20=EC=A0=9C=EA=B1=B0=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EA=B8=B0=EB=B3=B8=20=EA=B0=9C=EC=9D=B8(Personal)?= =?UTF-8?q?=20=EA=B0=80=EC=9E=85=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 70 +++---------------- .../handler/auth_handler_signup_test.go | 10 +-- 2 files changed, 12 insertions(+), 68 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 541db7b3..a9379f0a 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -712,69 +712,17 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error { } // 소속이 비어 있는 일반 가입자는 PERSONAL tenant를 자동 생성해 대표소속을 보장합니다. - tenantSlug := strings.TrimSpace(req.TenantSlug) + // 모든 온라인 가입자는 기본적으로 개인(Personal) 테넌트 소속으로 가입합니다. + // 기업/가족사 소속 연동은 별도 문의를 통해 처리되므로 온라인 가입 흐름에서는 제외합니다. + req.AffiliationType = "GENERAL" + slog.Info("[Signup] Forcing AffiliationType to GENERAL (Default personal tenant signup policy)", "email", req.Email) + var tenantID *string - - parts := strings.Split(req.Email, "@") - if len(parts) != 2 { - return errorJSON(c, fiber.StatusBadRequest, "Invalid email format") - } - domainName := parts[1] - - // Check if this domain belongs to a predefined family affiliate - isInternal, _ := h.isAffiliateTenant(c.Context(), domainName) - - // [Strict Policy] Force AffiliationType based on predefined family slugs (User cannot choose) - if isInternal { - req.AffiliationType = "AFFILIATE" - slog.Info("[Signup] Forcing AffiliationType to AFFILIATE", "email", req.Email) - } else { - req.AffiliationType = "GENERAL" - slog.Info("[Signup] Forcing AffiliationType to GENERAL", "email", req.Email) - } - - if tenantSlug != "" { - // [Security] Cross-check: If domain is NOT internal, they cannot provide a tenantSlug - if !isInternal { - slog.Warn("[Signup] Security violation: non-internal email providing tenantSlug", "email", req.Email) - return errorJSON(c, fiber.StatusForbidden, "Only affiliate members can join an organization.") - } - - if !affiliateSlugs[strings.ToLower(tenantSlug)] { - return errorJSON(c, fiber.StatusForbidden, "The selected organization is not a valid family affiliate.") - } - - tenant, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug) - if err == nil && tenant != nil { - if tenant.Status == domain.TenantStatusActive { - slog.Info("[Signup] Assigning tenant by manual slug", "email", req.Email, "tenant", tenant.Slug) - tenantSlug = tenant.Slug - tenantID = &tenant.ID - } else { - return errorJSON(c, fiber.StatusForbidden, "The specified organization is not active.") - } - } else { - slog.Warn("[Signup] Attempted to join non-existent organization", "slug", tenantSlug, "email", req.Email) - return errorJSON(c, fiber.StatusNotFound, "The specified organization code was not found.") - } - } else { - // If it's a family affiliate domain, they MUST select one of the family companies - if isInternal { - return errorJSON(c, fiber.StatusBadRequest, "Please select your organization.") - } - } - - if tenantID == nil && req.AffiliationType == "AFFILIATE" { - return errorJSON(c, fiber.StatusBadRequest, "We couldn't verify your organization affiliation. Please check your choice.") - } - if tenantID == nil && req.AffiliationType == "GENERAL" { - tenant, err := createPersonalTenantForUser(c.Context(), h.TenantService, req.Email) - if err != nil { - return errorJSON(c, fiber.StatusServiceUnavailable, "failed to create personal tenant") - } - tenantSlug = tenant.Slug - tenantID = &tenant.ID + tenant, err := createPersonalTenantForUser(c.Context(), h.TenantService, req.Email) + if err != nil { + return errorJSON(c, fiber.StatusServiceUnavailable, "failed to create personal tenant") } + tenantID = &tenant.ID // Normalize Phone (E.164 형태로 보관) normalizedPhone := domain.NormalizePhoneNumber(req.Phone) diff --git a/backend/internal/handler/auth_handler_signup_test.go b/backend/internal/handler/auth_handler_signup_test.go index 0ee23240..2fdd4ea9 100644 --- a/backend/internal/handler/auth_handler_signup_test.go +++ b/backend/internal/handler/auth_handler_signup_test.go @@ -116,22 +116,18 @@ func TestSignup_TenantSlugValidation(t *testing.T) { assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) - t.Run("Active Tenant Slug", func(t *testing.T) { + t.Run("Success creates Personal Tenant", func(t *testing.T) { reqBody := domain.SignupRequest{ Email: "user@hanmaceng.co.kr", Password: "StrongPass123!", Name: "Test User", Phone: "010-1234-5678", TermsAccepted: true, - TenantSlug: "hanmac", } body, _ := json.Marshal(reqBody) - validTenant := &domain.Tenant{ID: "t1", Slug: "hanmac", Status: domain.TenantStatusActive} - mockTenantSvc.On("GetTenantByDomain", mock.Anything, "hanmaceng.co.kr").Return(&domain.Tenant{Slug: "hanmac"}, nil).Once() - mockTenantSvc.On("ProvisionTenantByDomain", mock.Anything, "hanmaceng.co.kr").Return(validTenant, nil).Maybe() - mockTenantSvc.On("GetTenantBySlug", mock.Anything, "hanmac").Return(validTenant, nil).Once() - mockTenantSvc.On("GetTenant", mock.Anything, "t1").Return(validTenant, nil).Once() + validTenant := &domain.Tenant{ID: "personal-t1", Slug: "personal-slug", Status: domain.TenantStatusActive} + mockTenantSvc.On("RegisterTenant", mock.Anything, "Personal - user@hanmaceng.co.kr", mock.Anything, domain.TenantTypePersonal, "Automatically provisioned personal tenant", []string(nil), (*string)(nil), "").Return(validTenant, nil).Once() mockIdp.On("CreateUser", mock.Anything, mock.Anything).Return("user-id", nil).Once() mockRedis.On("Delete", mock.Anything).Return(nil) From b1c853b3c304a53cc902d6ba6e5c9e0d0a04aab3 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 16:59:32 +0900 Subject: [PATCH 03/20] =?UTF-8?q?userfront:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=99=94=EB=A9=B4=20=EB=82=B4=20=EC=86=8C=EC=86=8D?= =?UTF-8?q?=20=EA=B5=AC=EB=B6=84=20=EB=AA=85=EC=B9=AD=20=EA=B0=9C=ED=8E=B8?= =?UTF-8?q?=20(=EC=9D=BC=EB=B0=98/=EA=B0=80=EC=A1=B1=EC=82=AC=20->=20?= =?UTF-8?q?=EA=B0=9C=EC=9D=B8/=EA=B8=B0=EC=97=85=EC=86=8C=EC=86=8D)=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=A4=EB=AA=85=20=EB=AC=B8=EA=B5=AC=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- userfront/assets/translations/en.toml | 4 ++-- userfront/assets/translations/ko.toml | 4 ++-- userfront/assets/translations/template.toml | 4 ++-- userfront/lib/features/auth/presentation/signup_screen.dart | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 1b608e0b..ce551efb 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -1,8 +1,8 @@ [domain] [domain.affiliation] -affiliate = "Affiliate" -general = "General" +affiliate = "Corporate/Affiliate" +general = "Personal" [domain.company] baron = "Baron" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 76b6952a..8723e746 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -1,8 +1,8 @@ [domain] [domain.affiliation] -affiliate = "가족사 임직원" -general = "일반 사용자" +affiliate = "기업 소속" +general = "개인 (Personal)" [domain.company] baron = "바론" diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index f6776236..4aebc2bc 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -1,8 +1,8 @@ [domain] [domain.affiliation] -affiliate = "" -general = "" +affiliate = "Corporate/Affiliate" +general = "Personal" [domain.company] baron = "" diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index e0b9cd37..e265a268 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -1743,7 +1743,7 @@ Matters not expressly provided in this Policy are governed by the Company's inte const SizedBox(height: 18), _buildProfileFieldGroup( title: tr('ui.userfront.signup.profile.affiliation_type'), - description: '소속 유형과 회사 정보를 입력합니다.', + description: '개인 가입 혹은 기업 연동을 선택합니다.', isDesktop: isDesktop, trailing: null, child: Column( From 40eaadd88d229fb0a9e7ce43fb18860424ae4317 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:03:20 +0900 Subject: [PATCH 04/20] =?UTF-8?q?userfront:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=8B=A8=EA=B3=84=EC=97=90=EC=84=9C=20=EC=86=8C?= =?UTF-8?q?=EC=86=8D=20=EC=9C=A0=ED=98=95=20=EC=84=A0=ED=83=9D/=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=A0=88=EA=B1=B0=EC=8B=9C=20UI=EB=A5=BC=20?= =?UTF-8?q?=EC=99=84=EC=A0=84=ED=9E=88=20=EC=A0=9C=EA=B1=B0=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20100%=20=EA=B0=9C=EC=9D=B8=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=ED=99=94=20=EB=B0=8F=20=EA=B8=B0=EC=97=85=20?= =?UTF-8?q?=EB=AC=B8=EC=9D=98=20=EB=B0=B0=EB=84=88=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/signup_screen.dart | 211 +++++------------- 1 file changed, 57 insertions(+), 154 deletions(-) diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index e265a268..846087b8 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -1740,165 +1740,74 @@ Matters not expressly provided in this Policy are governed by the Company's inte ), ), ), - const SizedBox(height: 18), - _buildProfileFieldGroup( - title: tr('ui.userfront.signup.profile.affiliation_type'), - description: '개인 가입 혹은 기업 연동을 선택합니다.', - isDesktop: isDesktop, - trailing: null, + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _signupSurface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _signupBorder, + ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - DropdownButtonFormField( - key: ValueKey(_affiliationType), - initialValue: _affiliationType, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.signup.profile.affiliation_type', + Row( + children: [ + Icon( + Icons.business, + color: Theme.of(context).colorScheme.primary, + size: 20, ), - border: const OutlineInputBorder(), - ), - items: [ - DropdownMenuItem( - value: 'GENERAL', - child: Text(tr('domain.affiliation.general')), - ), - DropdownMenuItem( - value: 'AFFILIATE', - child: Text(tr('domain.affiliation.affiliate')), + const SizedBox(width: 8), + Expanded( + child: Text( + '기업/가족사 소속이신가요?', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: _signupInk, + ), + ), ), ], - onChanged: _isAffiliateLocked - ? null - : (val) { - if (val == null) { - return; - } - setState(() { - _affiliationType = val; - if (_affiliationType == 'GENERAL') { - _companyCode = null; - } - }); - }, ), - AnimatedSize( - duration: const Duration(milliseconds: 180), - curve: Curves.easeOut, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (_affiliationType == 'AFFILIATE') ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primaryContainer - .withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.25), - ), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Icon( - Icons.business, - color: Theme.of(context) - .colorScheme - .primary, - size: 24, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - '기업 소속 가입 안내', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - color: Theme.of(context) - .colorScheme - .primary, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - Text( - '기업/가족사 소속 회원은 바로 가입하는 대신 별도 문의를 통해 가입 및 워크스페이스 연동이 진행됩니다.\n\n아래 버튼을 눌러 담당자에게 가입 문의 이메일을 보내거나 baroncs@baroncs.co.kr로 직접 문의해 주시기 바랍니다.', - style: TextStyle( - fontSize: 13, - height: 1.5, - color: _signupInk - .withValues(alpha: 0.8), - ), - ), - const SizedBox(height: 16), - FilledButton.icon( - onPressed: () async { - final Uri emailUri = Uri( - scheme: 'mailto', - path: 'baroncs@baroncs.co.kr', - query: Uri.encodeFull( - 'subject=[Baron SSO] 기업 소속 가입 및 연동 문의', - ), - ); - if (await canLaunchUrl(emailUri)) { - await launchUrl(emailUri); - } - }, - icon: const Icon(Icons.mail_outline, - size: 18), - label: const Text('기업 소속 문의하기'), - style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric( - vertical: 12), - ), - ), - ], - ), - ), - ], - ], + const SizedBox(height: 8), + Text( + '기업 및 가족사 임직원은 온라인 즉시 가입 대신 별도 연동 문의를 통해 워크스페이스 계정이 발급됩니다.\n\n아래 버튼을 눌러 담당자에게 문의 메일을 발송해 주시기 바랍니다.', + style: TextStyle( + fontSize: 12, + height: 1.45, + color: _signupInk.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: () async { + final Uri emailUri = Uri( + scheme: 'mailto', + path: 'baroncs@baroncs.co.kr', + query: Uri.encodeFull( + 'subject=[Baron SSO] 기업 소속 가입 및 연동 문의', + ), + ); + if (await canLaunchUrl(emailUri)) { + await launchUrl(emailUri); + } + }, + icon: const Icon(Icons.mail_outline, size: 16), + label: const Text('기업 소속 문의하기'), + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 10), ), ), ], ), ), - if (_affiliationType == 'GENERAL') ...[ - const SizedBox(height: 18), - _buildProfileFieldGroup( - title: tr( - 'ui.userfront.signup.profile.department_optional', - ), - description: '선택 입력 항목입니다.', - isDesktop: isDesktop, - child: TextFormField( - controller: _deptController, - onChanged: (_) => setState(() {}), - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.signup.profile.department_optional', - ), - border: const OutlineInputBorder(), - ), - ), - ), - ], ], ), ), @@ -2374,13 +2283,7 @@ Matters not expressly provided in this Policy are governed by the Company's inte canGoNext = true; } if (_currentStep == 3) { - final nameOk = _nameController.text.trim().isNotEmpty; - if (_affiliationType == 'GENERAL') { - canGoNext = nameOk; - } else { - // 기업 소속(AFFILIATE)인 경우 직접 가입 대신 문의로 안내하므로 다음 단계 진행을 차단합니다. - canGoNext = false; - } + canGoNext = _nameController.text.trim().isNotEmpty; } return Scaffold( From 2cd2ce4c02eb4a974688487db806b41735927111 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:13:45 +0900 Subject: [PATCH 05/20] =?UTF-8?q?userfront:=20=EC=86=8C=EC=86=8D=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EB=8B=A8=EA=B3=84=20=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B2=80=EC=A6=9D=EB=90=9C=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9D=84=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=ED=8C=90=EC=A0=95=ED=95=98=EC=97=AC=20=EA=B0=80?= =?UTF-8?q?=EC=A1=B1=EC=82=AC=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=AC=B8=EC=9D=98=20=EC=B9=B4=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=95=EC=A0=9C=20=EB=85=B8=EC=B6=9C=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=B0=A8=EB=8B=A8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/signup_screen.dart | 186 ++++++++++-------- 1 file changed, 104 insertions(+), 82 deletions(-) diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index 846087b8..70c926be 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -1712,104 +1712,126 @@ Matters not expressly provided in this Policy are governed by the Company's inte horizontal: isDesktop ? 32 : 20, vertical: isDesktop ? 32 : 24, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - tr('msg.userfront.signup.profile.title'), - style: TextStyle( - fontSize: isDesktop ? 28 : 20, - fontWeight: FontWeight.w700, - height: 1.25, - color: _signupInk, - ), - ), - const SizedBox(height: 12), - _buildProfileInfoNoticeCard(isDesktop: isDesktop), - SizedBox(height: isDesktop ? 28 : 24), - _buildProfileFieldGroup( - title: tr('ui.userfront.signup.profile.name'), - description: '기본 정보', - isDesktop: isDesktop, - child: TextFormField( - controller: _nameController, - onChanged: (_) => setState(() {}), - decoration: InputDecoration( - labelText: tr('ui.userfront.signup.profile.name'), - border: const OutlineInputBorder(), - ), - ), - ), - const SizedBox(height: 24), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: _signupSurface, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: _signupBorder, - ), - ), - child: Column( + child: _affiliationType == 'AFFILIATE' + ? Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - children: [ - Icon( - Icons.business, - color: Theme.of(context).colorScheme.primary, - size: 20, + Text( + '가족사 임직원 가입 안내', + style: TextStyle( + fontSize: isDesktop ? 28 : 20, + fontWeight: FontWeight.w700, + height: 1.25, + color: _signupInk, + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primaryContainer + .withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.25), ), - const SizedBox(width: 8), - Expanded( - child: Text( - '기업/가족사 소속이신가요?', + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon( + Icons.business, + color: Theme.of(context).colorScheme.primary, + size: 24, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + '기업 및 가족사 임직원 대상자입니다.', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + const SizedBox(height: 14), + Text( + '귀하의 이메일 도메인은 기업/가족사 임직원 대상자입니다. 온라인 직접 가입 대신 별도의 조인 문의를 통해 가입 및 워크스페이스 연동이 진행됩니다.\n\n아래 버튼을 눌러 담당자에게 가입 연동 문의 메일을 발송하시거나 사내 시스템 관리자에게 문의해 주시기 바랍니다.', style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: _signupInk, + fontSize: 13, + height: 1.55, + color: _signupInk.withValues(alpha: 0.8), ), ), - ), - ], + const SizedBox(height: 20), + FilledButton.icon( + onPressed: () async { + final Uri emailUri = Uri( + scheme: 'mailto', + path: 'baroncs@baroncs.co.kr', + query: Uri.encodeFull( + 'subject=[Baron SSO] 기업 소속 가입 및 연동 문의', + ), + ); + if (await canLaunchUrl(emailUri)) { + await launchUrl(emailUri); + } + }, + icon: const Icon(Icons.mail_outline, size: 18), + label: const Text('기업 소속 문의하기'), + style: FilledButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 14), + ), + ), + ], + ), ), - const SizedBox(height: 8), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ Text( - '기업 및 가족사 임직원은 온라인 즉시 가입 대신 별도 연동 문의를 통해 워크스페이스 계정이 발급됩니다.\n\n아래 버튼을 눌러 담당자에게 문의 메일을 발송해 주시기 바랍니다.', + tr('msg.userfront.signup.profile.title'), style: TextStyle( - fontSize: 12, - height: 1.45, - color: _signupInk.withValues(alpha: 0.7), + fontSize: isDesktop ? 28 : 20, + fontWeight: FontWeight.w700, + height: 1.25, + color: _signupInk, ), ), const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: () async { - final Uri emailUri = Uri( - scheme: 'mailto', - path: 'baroncs@baroncs.co.kr', - query: Uri.encodeFull( - 'subject=[Baron SSO] 기업 소속 가입 및 연동 문의', - ), - ); - if (await canLaunchUrl(emailUri)) { - await launchUrl(emailUri); - } - }, - icon: const Icon(Icons.mail_outline, size: 16), - label: const Text('기업 소속 문의하기'), - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + _buildProfileInfoNoticeCard(isDesktop: isDesktop), + SizedBox(height: isDesktop ? 28 : 24), + _buildProfileFieldGroup( + title: tr('ui.userfront.signup.profile.name'), + description: '기본 정보', + isDesktop: isDesktop, + child: TextFormField( + controller: _nameController, + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + labelText: tr('ui.userfront.signup.profile.name'), + border: const OutlineInputBorder(), ), - padding: const EdgeInsets.symmetric(vertical: 10), ), ), ], ), - ), - ], - ), ), ), ), @@ -2283,7 +2305,7 @@ Matters not expressly provided in this Policy are governed by the Company's inte canGoNext = true; } if (_currentStep == 3) { - canGoNext = _nameController.text.trim().isNotEmpty; + canGoNext = _affiliationType == 'GENERAL' && _nameController.text.trim().isNotEmpty; } return Scaffold( From d3ae4c7e3868bc039c813477bcfd570b4cf50743 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:13:53 +0900 Subject: [PATCH 06/20] =?UTF-8?q?userfront:=20=EA=B0=9C=EC=9D=B8/=EA=B8=B0?= =?UTF-8?q?=EC=97=85=20=EC=86=8C=EC=86=8D=20=EC=9D=B4=EC=A0=95=ED=91=9C=20?= =?UTF-8?q?=EB=B2=88=EC=97=AD=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- userfront/assets/translations/en.toml | 4 ++-- userfront/assets/translations/ko.toml | 4 ++-- userfront/assets/translations/template.toml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index ce551efb..1b608e0b 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -1,8 +1,8 @@ [domain] [domain.affiliation] -affiliate = "Corporate/Affiliate" -general = "Personal" +affiliate = "Affiliate" +general = "General" [domain.company] baron = "Baron" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 8723e746..76b6952a 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -1,8 +1,8 @@ [domain] [domain.affiliation] -affiliate = "기업 소속" -general = "개인 (Personal)" +affiliate = "가족사 임직원" +general = "일반 사용자" [domain.company] baron = "바론" diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index 4aebc2bc..f6776236 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -1,8 +1,8 @@ [domain] [domain.affiliation] -affiliate = "Corporate/Affiliate" -general = "Personal" +affiliate = "" +general = "" [domain.company] baron = "" From 721f8475b312f349efba3e95a4708c08a86e0d7e Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:17:48 +0900 Subject: [PATCH 07/20] =?UTF-8?q?userfront:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=8B=A8=EA=B3=84=EC=97=90=EC=84=9C=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=8C=90=EC=A0=95=20=EC=97=86=EC=9D=B4=20=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC=20=EB=B7=B0=EB=A5=BC=20=EB=85=B8=EC=B6=9C=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EA=B8=B0=EC=97=85=20=EC=9E=84=EC=A7=81=EC=9B=90=20?= =?UTF-8?q?=EA=B0=80=EC=9D=B4=EB=93=9C=20=EB=B0=B0=EB=84=88=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/signup_screen.dart | 186 ++++++++---------- 1 file changed, 82 insertions(+), 104 deletions(-) diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index 70c926be..b043e6df 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -1712,126 +1712,104 @@ Matters not expressly provided in this Policy are governed by the Company's inte horizontal: isDesktop ? 32 : 20, vertical: isDesktop ? 32 : 24, ), - child: _affiliationType == 'AFFILIATE' - ? Column( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + tr('msg.userfront.signup.profile.title'), + style: TextStyle( + fontSize: isDesktop ? 28 : 20, + fontWeight: FontWeight.w700, + height: 1.25, + color: _signupInk, + ), + ), + const SizedBox(height: 12), + _buildProfileInfoNoticeCard(isDesktop: isDesktop), + SizedBox(height: isDesktop ? 28 : 24), + _buildProfileFieldGroup( + title: tr('ui.userfront.signup.profile.name'), + description: '기본 정보', + isDesktop: isDesktop, + child: TextFormField( + controller: _nameController, + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + labelText: tr('ui.userfront.signup.profile.name'), + border: const OutlineInputBorder(), + ), + ), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _signupSurface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _signupBorder, + ), + ), + child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - '가족사 임직원 가입 안내', - style: TextStyle( - fontSize: isDesktop ? 28 : 20, - fontWeight: FontWeight.w700, - height: 1.25, - color: _signupInk, - ), - ), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primaryContainer - .withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.25), + Row( + children: [ + Icon( + Icons.business, + color: Theme.of(context).colorScheme.primary, + size: 20, ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Icon( - Icons.business, - color: Theme.of(context).colorScheme.primary, - size: 24, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - '기업 및 가족사 임직원 대상자입니다.', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], - ), - const SizedBox(height: 14), - Text( - '귀하의 이메일 도메인은 기업/가족사 임직원 대상자입니다. 온라인 직접 가입 대신 별도의 조인 문의를 통해 가입 및 워크스페이스 연동이 진행됩니다.\n\n아래 버튼을 눌러 담당자에게 가입 연동 문의 메일을 발송하시거나 사내 시스템 관리자에게 문의해 주시기 바랍니다.', + const SizedBox(width: 8), + Expanded( + child: Text( + '기업/가족사 소속이신가요?', style: TextStyle( - fontSize: 13, - height: 1.55, - color: _signupInk.withValues(alpha: 0.8), + fontWeight: FontWeight.bold, + fontSize: 14, + color: _signupInk, ), ), - const SizedBox(height: 20), - FilledButton.icon( - onPressed: () async { - final Uri emailUri = Uri( - scheme: 'mailto', - path: 'baroncs@baroncs.co.kr', - query: Uri.encodeFull( - 'subject=[Baron SSO] 기업 소속 가입 및 연동 문의', - ), - ); - if (await canLaunchUrl(emailUri)) { - await launchUrl(emailUri); - } - }, - icon: const Icon(Icons.mail_outline, size: 18), - label: const Text('기업 소속 문의하기'), - style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric( - vertical: 14), - ), - ), - ], - ), + ), + ], ), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + const SizedBox(height: 8), Text( - tr('msg.userfront.signup.profile.title'), + '기업 및 가족사 임직원은 온라인 즉시 가입 대신 별도 연동 문의를 통해 워크스페이스 계정이 발급됩니다.\n\n해당하시는 경우, 아래 버튼을 눌러 담당자에게 문의 메일을 발송해 주시기 바랍니다.', style: TextStyle( - fontSize: isDesktop ? 28 : 20, - fontWeight: FontWeight.w700, - height: 1.25, - color: _signupInk, + fontSize: 12, + height: 1.45, + color: _signupInk.withValues(alpha: 0.7), ), ), const SizedBox(height: 12), - _buildProfileInfoNoticeCard(isDesktop: isDesktop), - SizedBox(height: isDesktop ? 28 : 24), - _buildProfileFieldGroup( - title: tr('ui.userfront.signup.profile.name'), - description: '기본 정보', - isDesktop: isDesktop, - child: TextFormField( - controller: _nameController, - onChanged: (_) => setState(() {}), - decoration: InputDecoration( - labelText: tr('ui.userfront.signup.profile.name'), - border: const OutlineInputBorder(), + OutlinedButton.icon( + onPressed: () async { + final Uri emailUri = Uri( + scheme: 'mailto', + path: 'baroncs@baroncs.co.kr', + query: Uri.encodeFull( + 'subject=[Baron SSO] 기업 소속 가입 및 연동 문의', + ), + ); + if (await canLaunchUrl(emailUri)) { + await launchUrl(emailUri); + } + }, + icon: const Icon(Icons.mail_outline, size: 16), + label: const Text('기업 소속 문의하기'), + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), + padding: const EdgeInsets.symmetric(vertical: 10), ), ), ], ), + ), + ], + ), ), ), ), @@ -2305,7 +2283,7 @@ Matters not expressly provided in this Policy are governed by the Company's inte canGoNext = true; } if (_currentStep == 3) { - canGoNext = _affiliationType == 'GENERAL' && _nameController.text.trim().isNotEmpty; + canGoNext = _nameController.text.trim().isNotEmpty; } return Scaffold( From 544aa4472ab7fd361daf9bf52c9b72bc1f699f64 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:21:53 +0900 Subject: [PATCH 08/20] =?UTF-8?q?userfront:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=8B=A8=EA=B3=84=EC=97=90=EC=84=9C=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=8C=90=EC=A0=95=20=EB=B0=8F=20=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9E=91=EC=84=B1=20=EB=B2=84=ED=8A=BC=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=98=EA=B3=A0=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=EC=8B=9D=20=EA=B8=B0=EC=97=85=20=EA=B0=80=EC=9E=85=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EB=B0=B0=EB=84=88=EB=A1=9C=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/signup_screen.dart | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index b043e6df..f4713391 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -1775,36 +1775,13 @@ Matters not expressly provided in this Policy are governed by the Company's inte ), const SizedBox(height: 8), Text( - '기업 및 가족사 임직원은 온라인 즉시 가입 대신 별도 연동 문의를 통해 워크스페이스 계정이 발급됩니다.\n\n해당하시는 경우, 아래 버튼을 눌러 담당자에게 문의 메일을 발송해 주시기 바랍니다.', + '기업 및 가족사 임직원은 온라인 즉시 가입 대신 별도 연동 문의를 통해 워크스페이스 계정이 발급됩니다.\n\n해당하시는 경우, 가입을 중단하시고 사내 관리자 또는 담당자(baroncs@baroncs.co.kr)에게 문의해 주시기 바랍니다.', style: TextStyle( fontSize: 12, height: 1.45, color: _signupInk.withValues(alpha: 0.7), ), ), - const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: () async { - final Uri emailUri = Uri( - scheme: 'mailto', - path: 'baroncs@baroncs.co.kr', - query: Uri.encodeFull( - 'subject=[Baron SSO] 기업 소속 가입 및 연동 문의', - ), - ); - if (await canLaunchUrl(emailUri)) { - await launchUrl(emailUri); - } - }, - icon: const Icon(Icons.mail_outline, size: 16), - label: const Text('기업 소속 문의하기'), - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(vertical: 10), - ), - ), ], ), ), From ac3226e9399eec2166eafce6b9a7b52128b5d768 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:29:03 +0900 Subject: [PATCH 09/20] =?UTF-8?q?backend:=20=EA=B0=9C=EC=9D=B8=20=ED=85=8C?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=9E=90=EB=8F=99=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20=EC=8A=AC=EB=9F=AC=EA=B7=B8=20=EA=B8=B8=EC=9D=B4=20?= =?UTF-8?q?=EC=B4=88=EA=B3=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20503=20=EC=98=A4=EB=A5=98=20=EC=9B=90=EC=B2=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20=EC=BD=94=EB=93=9C=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/tenant_assignment_policy.go | 3 ++ .../handler/tenant_assignment_policy_test.go | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 backend/internal/handler/tenant_assignment_policy_test.go diff --git a/backend/internal/handler/tenant_assignment_policy.go b/backend/internal/handler/tenant_assignment_policy.go index fa381207..63678875 100644 --- a/backend/internal/handler/tenant_assignment_policy.go +++ b/backend/internal/handler/tenant_assignment_policy.go @@ -135,6 +135,9 @@ func createPersonalTenantForUser(ctx context.Context, tenantService service.Tena normalizedEmail = "user" } slug := "personal-" + strings.ReplaceAll(uuid.NewString(), "-", "") + if len(slug) > 32 { + slug = slug[:32] + } tenant, err := tenantService.RegisterTenant( ctx, fmt.Sprintf("Personal - %s", normalizedEmail), diff --git a/backend/internal/handler/tenant_assignment_policy_test.go b/backend/internal/handler/tenant_assignment_policy_test.go new file mode 100644 index 00000000..eb53acb7 --- /dev/null +++ b/backend/internal/handler/tenant_assignment_policy_test.go @@ -0,0 +1,49 @@ +package handler + +import ( + "context" + "strings" + "testing" + + "baron-sso-backend/internal/domain" + "baron-sso-backend/internal/utils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestCreatePersonalTenantForUser_SlugLength(t *testing.T) { + mockTenantService := &MockTenantService{} + ctx := context.Background() + + var capturedSlug string + mockTenantService.On( + "RegisterTenant", + ctx, + "Personal - user@example.com", + mock.AnythingOfType("string"), + domain.TenantTypePersonal, + "Automatically provisioned personal tenant", + []string(nil), + (*string)(nil), + "", + ).Run(func(args mock.Arguments) { + capturedSlug = args.String(2) + }).Return(&domain.Tenant{ + ID: "personal-tenant-id", + Slug: "personal-slug", + Name: "Personal - user@example.com", + }, nil) + + tenant, err := createPersonalTenantForUser(ctx, mockTenantService, "user@example.com") + assert.NoError(t, err) + assert.NotNil(t, tenant) + + // Ensure the generated slug is strictly 32 characters or less + assert.LessOrEqual(t, len(capturedSlug), 32) + assert.True(t, strings.HasPrefix(capturedSlug, "personal-")) + + // Ensure that the captured slug actually passes ValidateSlug! + valid, msg := utils.ValidateSlug(capturedSlug) + assert.True(t, valid, "Slug must be valid: "+msg) +} From b1a8df34438e92192a5a8eacd80cf48fe9463914 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:30:39 +0900 Subject: [PATCH 10/20] =?UTF-8?q?userfront:=20=EA=B8=B0=EC=97=85=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=95=88=EB=82=B4=20=EB=B0=B0=EB=84=88=20?= =?UTF-8?q?=EB=82=B4=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EB=AC=B8=EA=B5=AC?= =?UTF-8?q?=EB=A5=BC=20=EA=B8=B0=ED=9A=8D=20=EA=B0=9C=EC=A0=95=EC=95=88?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=A0=95=EB=B0=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- userfront/lib/features/auth/presentation/signup_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index f4713391..7b98c01c 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -1775,7 +1775,7 @@ Matters not expressly provided in this Policy are governed by the Company's inte ), const SizedBox(height: 8), Text( - '기업 및 가족사 임직원은 온라인 즉시 가입 대신 별도 연동 문의를 통해 워크스페이스 계정이 발급됩니다.\n\n해당하시는 경우, 가입을 중단하시고 사내 관리자 또는 담당자(baroncs@baroncs.co.kr)에게 문의해 주시기 바랍니다.', + '기업 및 가족사 임직원은 연동 문의가 필요합니다.\n\n해당하시는 경우, 사내 관리자 또는 담당자(baroncs@baroncs.co.kr)에게 문의해 주시기 바랍니다.', style: TextStyle( fontSize: 12, height: 1.45, From 26c4666a891baa9f4cff7ed19c87b173d60370c0 Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:39:25 +0900 Subject: [PATCH 11/20] =?UTF-8?q?backend:=20=EC=9D=BC=EB=B0=98=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EC=9E=84=EC=9D=98=EC=9D=98=20=EA=B0=9C?= =?UTF-8?q?=EB=B3=84=20=ED=85=8C=EB=84=8C=ED=8A=B8=20=EC=8B=A0=EC=84=A4=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20=EC=9D=B4=EB=AF=B8=20=EC=8B=9C=EB=93=9C?= =?UTF-8?q?=EB=90=9C=20shared=20'personal'=20=ED=85=8C=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=ED=95=A0=EB=8B=B9=20=EA=B5=AC=ED=98=84=20(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 10 +++++++--- backend/internal/handler/auth_handler_signup_test.go | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index a9379f0a..8bee99d9 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -718,9 +718,13 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error { slog.Info("[Signup] Forcing AffiliationType to GENERAL (Default personal tenant signup policy)", "email", req.Email) var tenantID *string - tenant, err := createPersonalTenantForUser(c.Context(), h.TenantService, req.Email) - if err != nil { - return errorJSON(c, fiber.StatusServiceUnavailable, "failed to create personal tenant") + tenant, err := h.TenantService.GetTenantBySlug(c.Context(), "personal") + if err != nil || tenant == nil { + // Fallback: 만약 시드된 personal 테넌트가 없을 경우 개인별 테넌트를 자동 생성합니다. + tenant, err = createPersonalTenantForUser(c.Context(), h.TenantService, req.Email) + if err != nil { + return errorJSON(c, fiber.StatusServiceUnavailable, "failed to resolve personal tenant") + } } tenantID = &tenant.ID diff --git a/backend/internal/handler/auth_handler_signup_test.go b/backend/internal/handler/auth_handler_signup_test.go index 2fdd4ea9..7d1e479e 100644 --- a/backend/internal/handler/auth_handler_signup_test.go +++ b/backend/internal/handler/auth_handler_signup_test.go @@ -127,6 +127,7 @@ func TestSignup_TenantSlugValidation(t *testing.T) { body, _ := json.Marshal(reqBody) validTenant := &domain.Tenant{ID: "personal-t1", Slug: "personal-slug", Status: domain.TenantStatusActive} + mockTenantSvc.On("GetTenantBySlug", mock.Anything, "personal").Return((*domain.Tenant)(nil), assert.AnError).Once() mockTenantSvc.On("RegisterTenant", mock.Anything, "Personal - user@hanmaceng.co.kr", mock.Anything, domain.TenantTypePersonal, "Automatically provisioned personal tenant", []string(nil), (*string)(nil), "").Return(validTenant, nil).Once() mockIdp.On("CreateUser", mock.Anything, mock.Anything).Return("user-id", nil).Once() mockRedis.On("Delete", mock.Anything).Return(nil) From c990bd591b681e6085f76ee548f363e488505b7d Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 17:53:24 +0900 Subject: [PATCH 12/20] =?UTF-8?q?adminfront:=20=EA=B6=8C=ED=95=9C=EB=B6=80?= =?UTF-8?q?=EC=97=AC=20=EC=84=B8=EB=B6=80=20=ED=83=AD=EC=97=90=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B2=84=EC=9B=8D=EC=8A=A4=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C(worksmobile=5Fviewers/managers)=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0,=20?= =?UTF-8?q?=EC=84=B8=EB=B6=80=20=EA=B6=8C=ED=95=9C=20=EB=B6=80=EC=97=AC=20?= =?UTF-8?q?=EC=9E=90=EA=B2=A9=EC=9D=84=20Super=20Admin=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EC=8A=B9=EA=B2=A9=20(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coverage/adminTenantTabs.test.tsx | 2 +- .../TenantFineGrainedPermissionsTab.tsx | 73 +++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/adminfront/src/features/coverage/adminTenantTabs.test.tsx b/adminfront/src/features/coverage/adminTenantTabs.test.tsx index 2ec4b09f..efa5ea33 100644 --- a/adminfront/src/features/coverage/adminTenantTabs.test.tsx +++ b/adminfront/src/features/coverage/adminTenantTabs.test.tsx @@ -61,7 +61,7 @@ const users = [ id: "user-owner", name: "Owner User", email: "owner@example.com", - role: "tenant_admin", + role: "super_admin", status: "active", }, { diff --git a/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsTab.tsx b/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsTab.tsx index 779237b1..c7dc5c80 100644 --- a/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsTab.tsx +++ b/adminfront/src/features/tenants/routes/TenantFineGrainedPermissionsTab.tsx @@ -31,13 +31,13 @@ import { import { toast } from "../../../components/ui/use-toast"; import { addTenantRelation, + fetchMe, fetchTenantRelations, fetchUsers, removeTenantRelation, type TenantRelation, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; -import { useTenantPermission } from "../hooks/useTenantPermission"; interface TenantFineGrainedPermissionsTabProps { tenantIdProp?: string; @@ -48,8 +48,11 @@ export function TenantFineGrainedPermissionsTab({ }: TenantFineGrainedPermissionsTabProps = {}) { const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>(); const tenantId = tenantIdProp || tenantIdParam || ""; - const { hasPermission } = useTenantPermission(tenantId); - const isWritable = hasPermission("manage_admins"); + const { data: profile } = useQuery({ + queryKey: ["me"], + queryFn: fetchMe, + }); + const isWritable = profile?.role === "super_admin"; const queryClient = useQueryClient(); const [searchTerm, setSearchTerm] = useState(""); const [isDialogOpen, setIsDialogOpen] = useState(false); @@ -75,7 +78,13 @@ export function TenantFineGrainedPermissionsTab({ > = {}; for (const user of relationsQuery.data) { initialMap[user.userId] = {}; - const tabs = ["profile", "permissions", "organization", "schema"]; + const tabs = [ + "profile", + "permissions", + "organization", + "schema", + "worksmobile", + ]; for (const tab of tabs) { const isWrite = user.relations.includes(`${tab}_managers`); const isRead = user.relations.includes(`${tab}_viewers`); @@ -337,6 +346,12 @@ export function TenantFineGrainedPermissionsTab({ {t("ui.admin.tenants.detail.tab_schema", "사용자 스키마")} + + {t( + "ui.admin.tenants.detail.tab_worksmobile", + "네이버웍스 연동", + )} + {t("ui.common.action", "작업")} @@ -346,7 +361,7 @@ export function TenantFineGrainedPermissionsTab({ {relations.length === 0 ? ( {t( @@ -387,6 +402,14 @@ export function TenantFineGrainedPermissionsTab({ ? "read" : "none"; + const worksmobileVal = user.relations.includes( + "worksmobile_managers", + ) + ? "write" + : user.relations.includes("worksmobile_viewers") + ? "read" + : "none"; + const curProfileVal = localTenantPermissions[user.userId]?.profile ?? profileVal; @@ -398,6 +421,9 @@ export function TenantFineGrainedPermissionsTab({ organizationVal; const curSchemaVal = localTenantPermissions[user.userId]?.schema ?? schemaVal; + const curWorksmobileVal = + localTenantPermissions[user.userId]?.worksmobile ?? + worksmobileVal; return ( + + +