From a2f2b2dd7135a1c29af3d41aa06790417688a3d9 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 2 Apr 2026 11:01:23 +0900 Subject: [PATCH 01/39] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=99=9C?= =?UTF-8?q?=EC=84=B1=20=EC=84=B8=EC=85=98=20=EC=A1=B0=ED=9A=8C=C2=B7?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/main.go | 2 + backend/internal/handler/auth_handler.go | 337 ++++++++++++++ .../handler/auth_handler_login_test.go | 21 + .../handler/auth_handler_sessions_test.go | 157 +++++++ backend/internal/handler/user_handler_test.go | 20 + .../internal/service/kratos_admin_service.go | 97 ++++ backend/internal/service/mock_common_test.go | 20 + locales/en.toml | 27 ++ locales/ko.toml | 422 ++++++++++++++++- userfront/assets/translations/en.toml | 27 ++ userfront/assets/translations/ko.toml | 220 +++++++++ .../lib/core/services/auth_proxy_service.dart | 23 + .../lib/features/dashboard/domain/models.dart | 56 +++ .../providers/user_sessions_provider.dart | 68 +++ .../presentation/dashboard_screen.dart | 426 ++++++++++++++++++ 15 files changed, 1922 insertions(+), 1 deletion(-) create mode 100644 backend/internal/handler/auth_handler_sessions_test.go create mode 100644 userfront/lib/features/dashboard/domain/providers/user_sessions_provider.dart diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 0392a1d8..b8f3502d 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -581,6 +581,8 @@ func main() { user.Post("/me/password", authHandler.ChangeMyPassword) user.Post("/me/send-code", authHandler.SendUpdateCode) user.Post("/me/verify-code", authHandler.VerifyUpdateCode) + user.Get("/sessions", authHandler.ListMySessions) + user.Delete("/sessions/:id", authHandler.DeleteMySession) user.Get("/rp/linked", authHandler.ListLinkedRps) user.Get("/rp/history", authHandler.ListRpHistory) user.Delete("/rp/linked/:id", authHandler.RevokeLinkedRp) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 53d105a1..e73f112a 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -6664,6 +6664,167 @@ type rpHistoryItem struct { Status string `json:"status"` } +type userSessionItem struct { + SessionID string `json:"session_id"` + AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + IssuedAt *time.Time `json:"issued_at,omitempty"` + LastSeenAt *time.Time `json:"last_seen_at,omitempty"` + IPAddress string `json:"ip_address,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + ClientID string `json:"client_id,omitempty"` + AppName string `json:"app_name,omitempty"` + IsCurrent bool `json:"is_current"` + IsActive bool `json:"is_active"` +} + +type userSessionListResponse struct { + Items []userSessionItem `json:"items"` +} + +func (h *AuthHandler) ListMySessions(c *fiber.Ctx) error { + if h.KratosAdmin == nil { + return errorJSON(c, fiber.StatusServiceUnavailable, "kratos admin unavailable") + } + + profile, err := h.resolveCurrentProfile(c) + if err != nil { + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") + } + if strings.TrimSpace(profile.ID) == "" { + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") + } + + sessions, err := h.KratosAdmin.ListIdentitySessions(c.Context(), profile.ID) + if err != nil { + return errorJSON(c, fiber.StatusInternalServerError, "Failed to fetch sessions") + } + + currentSessionID := h.resolveCurrentSessionID(c) + auditHints := h.loadSessionAuditHints(c.Context(), profile.ID) + + items := make([]userSessionItem, 0, len(sessions)) + for _, session := range sessions { + if !session.Active { + continue + } + item := userSessionItem{ + SessionID: session.ID, + IsCurrent: session.ID != "" && session.ID == currentSessionID, + IsActive: session.Active, + } + if !session.AuthenticatedAt.IsZero() { + ts := session.AuthenticatedAt + item.AuthenticatedAt = &ts + item.LastSeenAt = &ts + } + if !session.ExpiresAt.IsZero() { + ts := session.ExpiresAt + item.ExpiresAt = &ts + } + if !session.IssuedAt.IsZero() { + ts := session.IssuedAt + item.IssuedAt = &ts + if item.AuthenticatedAt == nil { + item.AuthenticatedAt = &ts + } + if item.LastSeenAt == nil { + item.LastSeenAt = &ts + } + } + if hint, ok := auditHints[session.ID]; ok { + if item.IPAddress == "" { + item.IPAddress = hint.IPAddress + } + if item.UserAgent == "" { + item.UserAgent = hint.UserAgent + } + if item.ClientID == "" { + item.ClientID = hint.ClientID + } + if item.AppName == "" { + item.AppName = hint.AppName + } + if hint.Timestamp != nil { + item.LastSeenAt = hint.Timestamp + } + } + if item.UserAgent == "" && len(session.Devices) > 0 { + deviceUserAgent := strings.TrimSpace(session.Devices[0].UserAgent) + if !looksLikeInternalUserAgent(deviceUserAgent) { + item.UserAgent = deviceUserAgent + } + } + if item.IPAddress == "" && len(session.Devices) > 0 { + item.IPAddress = strings.TrimSpace(session.Devices[0].IPAddress) + } + items = append(items, item) + } + + sort.Slice(items, func(i, j int) bool { + if items[i].IsCurrent != items[j].IsCurrent { + return items[i].IsCurrent + } + iTime := latestSessionTimestamp(items[i]) + jTime := latestSessionTimestamp(items[j]) + if iTime.Equal(jTime) { + return items[i].SessionID < items[j].SessionID + } + return iTime.After(jTime) + }) + + return c.JSON(userSessionListResponse{Items: items}) +} + +func (h *AuthHandler) DeleteMySession(c *fiber.Ctx) error { + if h.KratosAdmin == nil { + return errorJSON(c, fiber.StatusServiceUnavailable, "kratos admin unavailable") + } + + profile, err := h.resolveCurrentProfile(c) + if err != nil { + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") + } + targetSessionID := strings.TrimSpace(c.Params("id")) + if targetSessionID == "" { + return errorJSON(c, fiber.StatusBadRequest, "session id is required") + } + + mySessions, err := h.KratosAdmin.ListIdentitySessions(c.Context(), profile.ID) + if err != nil { + return errorJSON(c, fiber.StatusInternalServerError, "Failed to fetch sessions") + } + ownedSession := false + for _, candidate := range mySessions { + if strings.TrimSpace(candidate.ID) == targetSessionID { + ownedSession = true + break + } + } + if !ownedSession { + return errorJSON(c, fiber.StatusForbidden, "forbidden") + } + + session, err := h.KratosAdmin.GetSession(c.Context(), targetSessionID) + if err != nil { + return errorJSON(c, fiber.StatusInternalServerError, "Failed to fetch session") + } + if session == nil { + h.writeSessionRevokedAuditLog(c, profile.ID, h.resolveCurrentSessionID(c), targetSessionID, "already_missing") + return c.JSON(fiber.Map{"status": "ok"}) + } + + result := "revoked" + if !session.Active { + result = "already_inactive" + } else if err := h.KratosAdmin.DeleteSession(c.Context(), targetSessionID); err != nil { + return errorJSON(c, fiber.StatusInternalServerError, "Failed to delete session") + } + + h.writeSessionRevokedAuditLog(c, profile.ID, h.resolveCurrentSessionID(c), targetSessionID, result) + return c.JSON(fiber.Map{"status": "ok"}) +} + func (h *AuthHandler) ListRpHistory(c *fiber.Ctx) error { subject, err := h.resolveConsentSubject(c) if err != nil || subject == "" { @@ -6751,3 +6912,179 @@ func (h *AuthHandler) ListRpHistory(c *fiber.Ctx) error { return c.JSON(fiber.Map{"items": items}) } + +type sessionAuditHint struct { + Timestamp *time.Time + IPAddress string + UserAgent string + ClientID string + AppName string +} + +func latestSessionTimestamp(item userSessionItem) time.Time { + for _, candidate := range []*time.Time{item.LastSeenAt, item.AuthenticatedAt, item.IssuedAt} { + if candidate != nil { + return *candidate + } + } + return time.Time{} +} + +func (h *AuthHandler) resolveCurrentSessionID(c *fiber.Ctx) string { + if c == nil { + return "" + } + if token := h.getBearerToken(c); token != "" { + if sessionID := extractSessionIDFromJWT(token); sessionID != "" { + return sessionID + } + if sessionID, err := h.getKratosSessionID(token); err == nil { + return sessionID + } + } + if cookie := c.Get("Cookie"); cookie != "" { + if sessionID, err := h.getKratosSessionIDWithCookie(cookie); err == nil { + return sessionID + } + } + return "" +} + +func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string) map[string]sessionAuditHint { + hints := make(map[string]sessionAuditHint) + if h.AuditRepo == nil || strings.TrimSpace(userID) == "" { + return hints + } + + logs, err := h.AuditRepo.FindByUserAndEvents(ctx, userID, []string{ + "login_success", + "qr_login_success", + "link_login_success", + "code_login_success", + "password_login_success", + "POST /api/v1/auth/oidc/login/accept", + "POST /api/v1/auth/password/login", + "POST /api/v1/auth/magic-link/verify", + "POST /api/v1/auth/login/code/verify", + "POST /api/v1/auth/qr/approve", + "session.revoked", + }, 200) + if err != nil { + return hints + } + + for _, log := range logs { + sessionID := strings.TrimSpace(log.SessionID) + if sessionID == "" { + sessionID = strings.TrimSpace(extractApprovedSessionIDFromAuditDetails(log.Details)) + } + if sessionID == "" { + sessionID = strings.TrimSpace(extractSessionIDFromAuditDetails(log.Details)) + } + if sessionID == "" { + continue + } + + existing, ok := hints[sessionID] + if ok && existing.Timestamp != nil && existing.Timestamp.After(log.Timestamp) { + continue + } + + ts := log.Timestamp + ipAddress := strings.TrimSpace(log.IPAddress) + userAgent := strings.TrimSpace(log.UserAgent) + clientID, appName := deriveSessionClientInfo(log) + if details, err := parseAuditDetails(log.Details); err == nil { + if approvedIP, ok := details["approved_ip"].(string); ok && strings.TrimSpace(approvedIP) != "" { + ipAddress = strings.TrimSpace(approvedIP) + } + if approvedUserAgent, ok := details["approved_user_agent"].(string); ok && strings.TrimSpace(approvedUserAgent) != "" { + userAgent = strings.TrimSpace(approvedUserAgent) + } + } + if looksLikeInternalUserAgent(userAgent) { + userAgent = "" + } + hints[sessionID] = sessionAuditHint{ + Timestamp: &ts, + IPAddress: ipAddress, + UserAgent: userAgent, + ClientID: clientID, + AppName: appName, + } + } + return hints +} + +func deriveSessionClientInfo(log domain.AuditLog) (string, string) { + details, _ := parseAuditDetails(log.Details) + clientID := "" + appName := "" + if details != nil { + if value, ok := details["client_id"].(string); ok { + clientID = strings.TrimSpace(value) + } + if value, ok := details["client_name"].(string); ok { + appName = strings.TrimSpace(value) + } + } + path := strings.ToLower(extractAuditPath(log)) + if appName == "" { + switch { + case strings.Contains(path, "/api/v1/auth/oidc/login/accept"): + appName = "OIDC 로그인" + case strings.Contains(path, "/api/v1/auth/qr/approve"): + appName = "QR 로그인" + case strings.Contains(path, "/api/v1/auth/login/code/verify"): + appName = "코드 로그인" + case strings.Contains(path, "/api/v1/auth/magic-link/verify"): + appName = "링크 로그인" + case strings.Contains(path, "/api/v1/auth/password/login"): + appName = "비밀번호 로그인" + } + } + if appName == "" && clientID != "" { + appName = clientID + } + return clientID, appName +} + +func looksLikeInternalUserAgent(userAgent string) bool { + normalized := strings.ToLower(strings.TrimSpace(userAgent)) + if normalized == "" { + return false + } + return strings.HasPrefix(normalized, "go-http-client/") || + strings.HasPrefix(normalized, "fasthttp") || + strings.HasPrefix(normalized, "fiber") +} + +func (h *AuthHandler) writeSessionRevokedAuditLog(c *fiber.Ctx, actorIdentityID string, actorSessionID string, targetSessionID string, result string) { + if h.AuditRepo == nil { + return + } + + details := map[string]any{ + "target_session_id": strings.TrimSpace(targetSessionID), + "revoke_result": strings.TrimSpace(result), + } + if strings.TrimSpace(actorSessionID) != "" { + details["actor_session_id"] = strings.TrimSpace(actorSessionID) + } + raw, err := json.Marshal(details) + if err != nil { + return + } + + _ = h.AuditRepo.Create(&domain.AuditLog{ + EventID: fmt.Sprintf("session-revoked-%d", time.Now().UnixNano()), + Timestamp: time.Now().UTC(), + UserID: strings.TrimSpace(actorIdentityID), + SessionID: strings.TrimSpace(actorSessionID), + EventType: "session.revoked", + Status: "success", + IPAddress: extractClientIPFromHeaders(c), + UserAgent: strings.TrimSpace(c.Get("User-Agent")), + Details: string(raw), + }) +} diff --git a/backend/internal/handler/auth_handler_login_test.go b/backend/internal/handler/auth_handler_login_test.go index 5386bdc5..3d098bbe 100644 --- a/backend/internal/handler/auth_handler_login_test.go +++ b/backend/internal/handler/auth_handler_login_test.go @@ -122,6 +122,27 @@ func (m *MockKratosAdminService) DeleteIdentity(ctx context.Context, identityID return nil } +func (m *MockKratosAdminService) ListIdentitySessions(ctx context.Context, identityID string) ([]service.KratosSession, error) { + args := m.Called(ctx, identityID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]service.KratosSession), args.Error(1) +} + +func (m *MockKratosAdminService) GetSession(ctx context.Context, sessionID string) (*service.KratosSession, error) { + args := m.Called(ctx, sessionID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*service.KratosSession), args.Error(1) +} + +func (m *MockKratosAdminService) DeleteSession(ctx context.Context, sessionID string) error { + args := m.Called(ctx, sessionID) + return args.Error(0) +} + // --- Helper --- func newAuthLoginTestApp(h *AuthHandler) *fiber.App { diff --git a/backend/internal/handler/auth_handler_sessions_test.go b/backend/internal/handler/auth_handler_sessions_test.go new file mode 100644 index 00000000..ab6c2e2e --- /dev/null +++ b/backend/internal/handler/auth_handler_sessions_test.go @@ -0,0 +1,157 @@ +package handler + +import ( + "baron-sso-backend/internal/domain" + "baron-sso-backend/internal/service" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestListMySessions_Success(t *testing.T) { + now := time.Date(2026, 4, 2, 1, 2, 3, 0, time.UTC) + setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) { + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": now.Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, + }, + }), nil + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{ + { + ID: "current-sid", + Active: true, + AuthenticatedAt: now, + ExpiresAt: now.Add(24 * time.Hour), + }, + { + ID: "other-sid", + Active: true, + AuthenticatedAt: now.Add(-2 * time.Hour), + ExpiresAt: now.Add(22 * time.Hour), + }, + }, nil).Once() + + auditRepo := &mockAuditRepo{ + logs: []domain.AuditLog{ + { + UserID: "user-123", + EventType: "login_success", + SessionID: "other-sid", + Timestamp: now.Add(-30 * time.Minute), + IPAddress: "203.0.113.10", + UserAgent: "Mozilla/5.0", + }, + }, + } + + h := &AuthHandler{ + KratosAdmin: mockKratos, + AuditRepo: auditRepo, + } + + app := fiber.New() + app.Get("/api/v1/user/sessions", h.ListMySessions) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var body struct { + Items []struct { + SessionID string `json:"session_id"` + IsCurrent bool `json:"is_current"` + IsActive bool `json:"is_active"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + } `json:"items"` + } + err = json.NewDecoder(resp.Body).Decode(&body) + assert.NoError(t, err) + if assert.Len(t, body.Items, 2) { + assert.Equal(t, "current-sid", body.Items[0].SessionID) + assert.True(t, body.Items[0].IsCurrent) + assert.Equal(t, "other-sid", body.Items[1].SessionID) + assert.True(t, body.Items[1].IsActive) + assert.Equal(t, "203.0.113.10", body.Items[1].IPAddress) + assert.Equal(t, "Mozilla/5.0", body.Items[1].UserAgent) + } + + mockKratos.AssertExpectations(t) +} + +func TestDeleteMySession_Success(t *testing.T) { + setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) { + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": time.Now().UTC().Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, + }, + }), nil + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{ + {ID: "target-sid", Active: true}, + }, nil).Once() + mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{ + ID: "target-sid", + Active: true, + }, nil).Once() + mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once() + + auditRepo := &mockAuditRepo{} + h := &AuthHandler{ + KratosAdmin: mockKratos, + AuditRepo: auditRepo, + } + + app := fiber.New() + app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession) + + req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + req.Header.Set("User-Agent", "session-test-agent") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + if assert.Len(t, auditRepo.logs, 1) { + assert.Equal(t, "session.revoked", auditRepo.logs[0].EventType) + assert.Equal(t, "user-123", auditRepo.logs[0].UserID) + assert.Equal(t, "current-sid", auditRepo.logs[0].SessionID) + assert.Contains(t, auditRepo.logs[0].Details, "target-sid") + } + + mockKratos.AssertExpectations(t) +} diff --git a/backend/internal/handler/user_handler_test.go b/backend/internal/handler/user_handler_test.go index 0927a3c6..2adcea28 100644 --- a/backend/internal/handler/user_handler_test.go +++ b/backend/internal/handler/user_handler_test.go @@ -56,6 +56,26 @@ func (m *MockKratosAdmin) DeleteIdentity(ctx context.Context, id string) error { return m.Called(ctx, id).Error(0) } +func (m *MockKratosAdmin) ListIdentitySessions(ctx context.Context, identityID string) ([]service.KratosSession, error) { + args := m.Called(ctx, identityID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]service.KratosSession), args.Error(1) +} + +func (m *MockKratosAdmin) GetSession(ctx context.Context, sessionID string) (*service.KratosSession, error) { + args := m.Called(ctx, sessionID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*service.KratosSession), args.Error(1) +} + +func (m *MockKratosAdmin) DeleteSession(ctx context.Context, sessionID string) error { + return m.Called(ctx, sessionID).Error(0) +} + type MockOryProvider struct { mock.Mock } diff --git a/backend/internal/service/kratos_admin_service.go b/backend/internal/service/kratos_admin_service.go index 35141017..f1e062ac 100644 --- a/backend/internal/service/kratos_admin_service.go +++ b/backend/internal/service/kratos_admin_service.go @@ -27,6 +27,21 @@ type KratosIdentity struct { UpdatedAt time.Time `json:"updated_at,omitempty"` } +type KratosSessionDevice struct { + UserAgent string `json:"user_agent,omitempty"` + IPAddress string `json:"ip_address,omitempty"` +} + +type KratosSession struct { + ID string `json:"id"` + Active bool `json:"active"` + AuthenticatedAt time.Time `json:"authenticated_at,omitempty"` + ExpiresAt time.Time `json:"expires_at,omitempty"` + IssuedAt time.Time `json:"issued_at,omitempty"` + Identity *KratosIdentity `json:"identity,omitempty"` + Devices []KratosSessionDevice `json:"devices,omitempty"` +} + type KratosAdminService interface { ListIdentities(ctx context.Context) ([]KratosIdentity, error) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) @@ -34,6 +49,9 @@ type KratosAdminService interface { UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error DeleteIdentity(ctx context.Context, identityID string) error + ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error) + GetSession(ctx context.Context, sessionID string) (*KratosSession, error) + DeleteSession(ctx context.Context, sessionID string) error } type kratosAdminService struct { @@ -239,6 +257,85 @@ func (s *kratosAdminService) UpdateIdentityPassword(ctx context.Context, identit return nil } +func (s *kratosAdminService) ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error) { + endpoint := fmt.Sprintf("%s/admin/identities/%s/sessions", strings.TrimRight(s.AdminURL, "/"), identityID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + resp, err := s.httpClient().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, nil + } + if resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return nil, fmt.Errorf("kratos admin list identity sessions failed status=%d body=%s", resp.StatusCode, string(body)) + } + + var sessions []KratosSession + if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil { + return nil, err + } + return sessions, nil +} + +func (s *kratosAdminService) GetSession(ctx context.Context, sessionID string) (*KratosSession, error) { + endpoint := fmt.Sprintf("%s/admin/sessions/%s", strings.TrimRight(s.AdminURL, "/"), sessionID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + resp, err := s.httpClient().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, nil + } + if resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return nil, fmt.Errorf("kratos admin get session failed status=%d body=%s", resp.StatusCode, string(body)) + } + + var session KratosSession + if err := json.NewDecoder(resp.Body).Decode(&session); err != nil { + return nil, err + } + return &session, nil +} + +func (s *kratosAdminService) DeleteSession(ctx context.Context, sessionID string) error { + endpoint := fmt.Sprintf("%s/admin/sessions/%s", strings.TrimRight(s.AdminURL, "/"), sessionID) + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + resp, err := s.httpClient().Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil + } + if resp.StatusCode >= 300 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return fmt.Errorf("kratos admin delete session failed status=%d body=%s", resp.StatusCode, string(body)) + } + return nil +} + func hashPasswordForKratosAdmin(password string) (string, error) { hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { diff --git a/backend/internal/service/mock_common_test.go b/backend/internal/service/mock_common_test.go index fdf0e6d0..e5330ba3 100644 --- a/backend/internal/service/mock_common_test.go +++ b/backend/internal/service/mock_common_test.go @@ -110,3 +110,23 @@ func (m *MockKratosAdminServiceShared) UpdateIdentityPassword(ctx context.Contex func (m *MockKratosAdminServiceShared) DeleteIdentity(ctx context.Context, identityID string) error { return m.Called(ctx, identityID).Error(0) } + +func (m *MockKratosAdminServiceShared) ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error) { + args := m.Called(ctx, identityID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]KratosSession), args.Error(1) +} + +func (m *MockKratosAdminServiceShared) GetSession(ctx context.Context, sessionID string) (*KratosSession, error) { + args := m.Called(ctx, sessionID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*KratosSession), args.Error(1) +} + +func (m *MockKratosAdminServiceShared) DeleteSession(ctx context.Context, sessionID string) error { + return m.Called(ctx, sessionID).Error(0) +} diff --git a/locales/en.toml b/locales/en.toml index 28da2b1f..9f07f641 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -559,6 +559,20 @@ empty = "No linked apps yet." empty_detail = "Linked apps and their latest activity will appear here." error = "Could not load linked apps." +[msg.userfront.dashboard.sessions] +browser = "Browser: {{value}}" +empty = "No active sessions." +empty_detail = "Devices signed in with this account will appear here." +error = "Could not load sessions." +os = "OS: {{value}}" +recent_app = "Recent app: {{app}}" +session_id = "Session ID: {{id}}" + +[msg.userfront.dashboard.sessions.revoke] +confirm = "End the session for {{target}}?\nThat device will need to sign in again." +error = "Could not end the session: {{error}}" +success = "The session has been ended." + [msg.userfront.dashboard.approved_session] copy_click = "{{label}}: {{id}}\\\\\\\\\\\\\\\\nClick to copy." copy_tap = "{{label}}: {{id}}\\\\\\\\\\\\\\\\nTap to copy." @@ -735,6 +749,7 @@ uppercase = "At least one uppercase letter" [msg.userfront.sections] apps_subtitle = "Your linked apps and their latest sign-in status." audit_subtitle = "Recent access history for Baron sign-in." +sessions_subtitle = "Your currently signed-in devices and browser sessions." [msg.userfront.settings] disabled = "Account settings are currently unavailable." @@ -2070,6 +2085,17 @@ status_history = "Activity history" [ui.userfront.dashboard.activity] linked = "Linked" +[ui.userfront.dashboard.sessions] +active_badge = "Active" +current_badge = "Current" +current_disabled = "Current session" +unknown_device = "Unknown device" +unknown_session = "Session" + +[ui.userfront.dashboard.sessions.revoke] +action = "End session" +title = "End session" + [ui.userfront.dashboard.approved_session] default = "Default" userfront = "Approved UserFront session ID" @@ -2204,6 +2230,7 @@ title = "Create a new password" [ui.userfront.sections] apps = "Apps" audit = "Audit" +sessions = "Sessions" [ui.userfront.session] active = "Active session" diff --git a/locales/ko.toml b/locales/ko.toml index 3c995a3d..72bf3eed 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -73,7 +73,402 @@ scope_admin = "Scoped to /admin" session_ttl = "Session TTL: 15m admin" tenant_headers = "Tenant-aware headers" -[msg.admin.api_keys] +[msg.admin.common] +forbidden = "이 작업을 수행할 권한이 없습니다." + +[msg.admin.audit] +empty = "아직 수집된 감사 로그가 없습니다." +end = "감사 로그의 마지막입니다." +load_error = "감사 로그를 불러오지 못했습니다: {{error}}" +loading = "감사 로그를 불러오는 중..." +subtitle = "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다." + +[msg.admin.header] +subtitle = "Tenant isolation & least privilege by default" + +[msg.admin.notice] +idp_policy = "IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다." +scope = "관리 기능은 /admin 네임스페이스에서만 노출합니다." + +[msg.admin.org] +hover_member_info = "마우스를 올리면 상세 정보를 확인할 수 있습니다." +import_description = "CSV 파일을 업로드하여 조직도를 일괄 등록합니다." +import_error = "조직도 임포트 중 오류가 발생했습니다." +import_success = "조직도가 성공적으로 임포트되었습니다." + +[msg.admin.overview] +description = "모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다." +idp_fallback = "Fallback: Descope" +idp_primary = "IDP: Ory primary" + +[msg.admin.tenants] +approve_confirm = "이 테넌트를 승인하시겠습니까?" +approve_success = "테넌트가 승인되었습니다." +delete_confirm = "테넌트 \"{{name}}\"를 삭제할까요?" +delete_success = "테넌트가 삭제되었습니다." +empty = "아직 등록된 테넌트가 없습니다." +fetch_error = "테넌트 목록 조회에 실패했습니다." +missing_id = "테넌트 ID가 없습니다." +not_found = "테넌트를 찾을 수 없습니다." +remove_sub_confirm = "테넌트 \"{{name}}\"을(를) 하위 조직에서 제외할까요?" +subtitle = "현재 등록된 테넌트를 확인하고 상태를 관리합니다." + +[msg.dev.auth] +access_denied_description = "DevFront는 관리자 전용 화면입니다. 권한이 필요하면 관리자에게 요청해 주세요." +access_denied_title = "접근 권한이 없습니다." + +[msg.dev.forbidden] +default = "해당 리소스에 접근할 권한이 없습니다. 관리자에게 문의하세요." +rp_admin = "RP 관리자는 담당 앱의 리소스만 조회할 수 있습니다." +tenant_admin = "테넌트 관리자 권한이 올바르게 설정되지 않았거나 만료되었습니다." +user = "일반 사용자는 관리자 화면에 접근할 수 없습니다." +title = "{{resource}} 접근 권한 없음" + +[msg.dev.audit] +empty = "조회된 감사 로그가 없습니다." +forbidden = "감사 로그를 조회할 권한이 없습니다. 관리자에게 권한을 요청해주세요." +load_error = "감사 로그 조회 실패: {{error}}" +loaded_count = "로드된 로그 {{count}}건" +loading = "감사 로그를 불러오는 중..." +subtitle = "현재 테넌트/앱 범위의 DevFront 작업 이력을 조회합니다." + +[msg.dev.clients] +deleted = "앱이 삭제되었습니다." +delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." +delete_error = "삭제 실패: {{error}}" +load_error = "앱 정보를 불러오지 못했습니다: {{error}}" +loading = "앱 정보를 불러오는 중..." +showing = "전체 {{total}}개 중 {{shown}}개를 표시하는 중입니다." + +[msg.dev.sidebar] +notice = "개발자 전용 콘솔입니다." +notice_detail = "연동 앱 등록 및 관리를 수행할 수 있습니다." + +[msg.dev.clients.general.public_key] +auth_method_client_secret_basic_help = "일반적인 서버 사이드 앱 인증 방식입니다." +auth_method_none_help = "PKCE 기반 public client에 사용하는 방식입니다." +auth_method_private_key_jwt_help = "Trusted RP bootstrap과 JAR 검증에 필요한 서명 키 기반 인증 방식입니다." +guide_example = "권장 예시: https://rp.example.com/.well-known/jwks.json" +guide_intro = "JWKS URI는 Baron이 만드는 값이 아니라 RP backend가 공개키를 노출하는 URL입니다." +guide_step_1 = "RP 서버에서 key pair를 생성하고 private key는 RP backend에만 보관합니다." +guide_step_2 = "RP backend가 public key를 JWKS(JSON Web Key Set) 형태로 제공하는 endpoint를 준비합니다." +guide_step_3 = "예: https://rp.example.com/.well-known/jwks.json 같은 URL을 DevFront에 입력합니다." +headless_help = "애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다." +jwks_inline_help = "SSH-RSA 공개키 형식을 우선 권장합니다. 'ssh-rsa AAA...' 형식으로 입력하면 Baron이 OIDC 표준인 JWKS(JSON)로 자동 변환하여 저장합니다." +jwks_uri_help = "RP backend가 제공하는 공개키 endpoint URL을 입력하세요. 예: https://rp.example.com/.well-known/jwks.json" +request_object_alg_help = "Headless Login을 사용할 때 JAR(Request Object) 서명 알고리즘을 명시합니다." +source_help = "애플리케이션의 공개키(SSH-RSA)를 직접 등록하거나, 운영 환경이라면 JWKS URI를 통해 자동으로 검증할 수 있습니다." +subtitle = "Trusted RP 판정에 필요한 공개키와 headless login 관련 설정을 관리합니다." + +[msg.dev.clients.general.public_key.validation] +headless_requires_alg = "Headless Login을 사용하려면 Request Object Signing Algorithm을 입력해야 합니다." +headless_requires_private_key_jwt = "Headless Login을 사용하려면 token endpoint auth method가 private_key_jwt여야 합니다." +headless_requires_public_key = "Headless Login을 사용하려면 JWKS URI가 필요합니다." +invalid_jwks_inline = "입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다." +invalid_jwks_uri = "JWKS URI 형식이 올바르지 않습니다." +missing_jwks_inline = "공개키(SSH-RSA 또는 JWKS)를 입력해야 합니다." +missing_jwks_uri = "JWKS URI를 입력해야 합니다." +private_key_jwt_requires_public_key = "서명 키 기반 인증을 사용하려면 JWKS URI가 필요합니다." + +[msg.userfront.audit] +date = "접속일자: {{value}}" +device = "접속환경: {{value}}" +end = "더 이상 항목이 없습니다." +ip = "접속 IP: {{value}}" +load_more_error = "더 불러오지 못했습니다." +result = "인증결과: {{value}}" +session_id = "Session ID: {{value}}" +status = "현황: (준비중)" + +[msg.userfront.dashboard] +approved_device = "승인 기기: {{device}}" +approved_ip = "승인 IP: {{ip}}" +audit_empty = "최근 접속 이력이 없습니다." +audit_load_error = "접속이력을 불러오지 못했습니다." +auth_method = "인증수단: {{method}}" +client_id = "Client ID: {{id}}" +client_id_missing = "Client ID 없음" +current_status = "현재 상태: {{status}}" +last_auth = "최근 인증: {{value}}" +link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다." +link_open_error = "해당 링크를 열 수 없습니다." +render_error = "대시보드 렌더링 오류: {{error}}" +session_id_copied = "세션 ID가 복사되었습니다." + +[msg.userfront.error] +detail_contact = "관리자에게 문의해 주세요." +detail_generic = "오류가 발생했습니다." +detail_request = "요청을 처리하는 중 문제가 발생했습니다." +id = "오류 ID: {{id}}" +title = "인증 과정에서 오류가 발생했습니다" +title_generic = "오류가 발생했습니다" +title_with_code = "오류: {{code}}" +type = "오류 종류: {{type}}" + +[msg.userfront.forgot] +description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다." +dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다." +error = "전송에 실패했습니다: {{error}}" +input_required = "이메일 또는 휴대폰 번호를 입력해주세요." +sent = "비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요." + +[msg.userfront.login] +cookie_check_failed = "로그인 확인 실패: {{error}}" +dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다." +link_failed = "오류: {{error}}" +link_send_failed = "전송 실패: {{error}}" +link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다." +link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다." +link_timeout = "시간이 경과되었습니다." +no_account = "계정이 없으신가요?" +oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요." +qr_expired = "시간이 경과되었습니다." +qr_init_failed = "QR 초기화에 실패했습니다: {{error}}" +qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다" +token_missing = "로그인 토큰을 확인할 수 없습니다." +verification_failed = "승인 처리에 실패했습니다: {{error}}" + +[msg.userfront.login_success] +subtitle = "성공적으로 로그인되었습니다." + +[msg.userfront.consent] +accept_error = "동의 처리에 실패했습니다: {{error}}" +client_id = "클라이언트 ID: {{id}}" +client_unknown = "알 수 없는 앱" +description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\n계속 진행하려면 동의 여부를 선택해 주세요." +load_error = "동의 정보를 불러오는데 실패했습니다: {{error}}" +missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다." +redirect_notice = "동의 후 자동으로 서비스로 이동합니다." +scope_count = "총 {{count}}개" + +[msg.userfront.profile] +department_missing = "소속 정보 없음" +department_required = "소속을 입력해주세요." +email_missing = "이메일 없음" +greeting = "안녕하세요, {{name}}님" +load_failed = "정보를 불러올 수 없습니다." +name_missing = "이름 없음" +name_required = "이름을 입력해주세요." +phone_required = "휴대폰 번호를 입력해주세요." +phone_verify_required = "휴대폰 번호 인증이 필요합니다." +update_failed = "수정 실패: {{error}}" +update_success = "정보가 수정되었습니다." + +[msg.userfront.qr] +camera_error = "카메라 오류: {{error}}" +permission_error = "카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요." +permission_required = "카메라 권한이 필요합니다." + +[msg.userfront.reset] +invalid_body = "비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요." +invalid_link = "유효하지 않은 재설정 링크입니다. (loginId/token 누락)" +invalid_title = "유효하지 않은 링크입니다." +policy_loading = "비밀번호 정책을 불러오는 중입니다..." +success = "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요." + +[msg.userfront.sections] +apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다." +audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다." +sessions_subtitle = "현재 로그인된 기기와 브라우저 세션입니다." + +[msg.userfront.settings] +disabled = "현재 계정 설정 화면은 준비 중입니다." + +[msg.userfront.signup] +failed = "가입 실패: {{error}}" +privacy_full = "개인정보 수집 및 이용 동의 전문..." +tos_full = "서비스 이용약관 전문..." + +[ui.admin.audit] +export_csv = "Export CSV" +load_more = "Load more" +target = "Target · {{target}}" +title = "감사 로그" + +[ui.admin.groups] +import_csv = "CSV 임포트" + +[ui.admin.header] +plane = "Admin Plane" +subtitle = "관리 및 정책 운영" + +[ui.admin.nav] +api_keys = "API 키" +audit_logs = "감사 로그" +auth_guard = "인증 가드" +logout = "로그아웃" +overview = "개요" +relying_parties = "애플리케이션(RP)" +tenant_dashboard = "테넌트 대시보드" +user_groups = "유저 그룹" +tenants = "테넌트" +users = "사용자" + +[ui.admin.org] +download_template = "템플릿 다운로드" +import_btn = "임포트" +import_title = "조직도 대량 등록" +start_import = "임포트 시작" + +[ui.admin.overview] +kicker = "Global Overview" +title = "통합 대시보드" + +[ui.admin.profile] +manageable_tenants = "관리 가능한 테넌트" + +[ui.admin.role] +rp_admin = "RP ADMIN" +super_admin = "SUPER ADMIN" +tenant_admin = "TENANT ADMIN" +user = "TENANT MEMBER" + +[ui.admin.tenants] +add = "테넌트 추가" +title = "테넌트 목록" + +[ui.common.badge] +admin_only = "Admin only" +command_only = "Command only" +system = "System" + +[ui.common.status] +active = "활성" +blocked = "차단됨" +failure = "실패" +inactive = "비활성" +ok = "정상" +pending = "준비 중" +success = "성공" + +[ui.dev.nav] +clients = "연동 앱" +logout = "로그아웃" + +[ui.dev.tenant] +single_notice = "단일 테넌트에 소속되어 전환할 필요가 없습니다." +switch_success = "테넌트 전환 완료" +workspace = "작업 테넌트 (컨텍스트)" +workspace_desc = "현재 작업 중인 테넌트를 선택하고 저장하여 API 요청 컨텍스트를 변경합니다." + +[ui.dev.audit] +load_more = "더 보기" +title = "감사 로그" + +[ui.dev.profile] +menu_aria = "계정 메뉴 열기" +menu_title = "계정" +unknown_email = "unknown@example.com" +unknown_name = "Unknown User" +title = "내 정보" +subtitle = "사용자 상세 정보 및 할당된 역할(Role)을 확인합니다." +loading = "프로필 정보를 불러오는 중..." +error = "프로필 정보를 불러오지 못했습니다." + +[ui.dev.clients] +new = "연동 앱 추가" +search_placeholder = "연동 앱 이름/ID로 검색..." +tenant_scoped = "Tenant-scoped" +untitled = "Untitled" + +[ui.dev.dashboard] +ready_badge = "devfront ready" + +[ui.dev.header] +plane = "Dev Plane" +subtitle = "Manage your applications" + +[ui.dev.session] +auto_extend = "세션 만료 관리" +active = "세션 활성" +disabled = "자동 연장 비활성화" +unknown = "알 수 없음" +expired = "세션 만료" +expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음" +remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음" +refresh = "세션 만료 시간 갱신" +refreshing = "세션 만료 시간 갱신 중..." + +[ui.userfront.app_label] +admin_console = "Admin Console" +baron = "Baron 로그인" +dev_console = "Dev Console" + +[ui.userfront.auth_method] +ory = "Ory 세션" +session = "세션" + +[ui.userfront.dashboard] +last_auth_label = "최근 인증" +status_history = "상태 이력" + +[ui.userfront.device] +android = "Mobile(Android)" +ios = "Mobile(iOS)" +linux = "Desktop(Linux)" +macos = "Desktop(macOS)" +windows = "Desktop(Windows)" + +[ui.userfront.error] +go_home = "홈으로 이동" +go_login = "로그인으로 이동" + +[ui.userfront.forgot] +heading = "비밀번호를 잊으셨나요?" +input_label = "이메일 또는 휴대폰 번호" +submit = "재설정 링크 전송" +title = "비밀번호 재설정" + +[ui.userfront.login] +forgot_password = "비밀번호를 잊으셨나요?" +signup = "회원가입" + +[ui.userfront.login_success] +later = "나중에 하기 (대시보드로 이동)" +qr = "QR 인증 (카메라 켜기)" +title = "로그인 완료" + +[ui.userfront.consent] +accept = "동의하고 계속하기" +requested_scopes = "요청된 권한" +title = "접근 권한 요청" + +[ui.userfront.nav] +dashboard = "대시보드" +logout = "로그아웃" +profile = "내 정보" +qr_scan = "QR 스캔" + +[ui.userfront.profile] +department_empty = "소속 정보 없음" +manage = "프로필 관리" +user_fallback = "사용자" + +[ui.userfront.qr] +rescan = "다시 스캔" +result_success = "승인 완료" +title = "Scan QR Code" + +[ui.userfront.reset] +confirm_password = "새 비밀번호 확인" +new_password = "새 비밀번호" +submit = "비밀번호 변경" +subtitle = "새로운 비밀번호 설정" +title = "새 비밀번호 설정" + +[ui.userfront.sections] +apps = "나의 App 현황" +audit = "접속이력" +sessions = "활성 세션" + +[ui.userfront.session] +active = "세션 활성" +unknown = "알 수 없음" + +[ui.userfront.signup] +complete = "가입 완료" +next_step = "다음 단계" +title = "회원가입" [msg.admin.api_keys.create] error = "API 키 생성에 실패했습니다." @@ -559,6 +954,20 @@ empty = "연동된 앱이 없습니다." empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." error = "연동 정보를 불러오지 못했습니다." +[msg.userfront.dashboard.sessions] +browser = "브라우저: {{value}}" +empty = "활성 세션이 없습니다." +empty_detail = "같은 계정으로 로그인한 기기가 여기에 표시됩니다." +error = "세션 정보를 불러오지 못했습니다." +os = "OS: {{value}}" +recent_app = "최근 접속 앱: {{app}}" +session_id = "세션 ID: {{id}}" + +[msg.userfront.dashboard.sessions.revoke] +confirm = "{{target}} 세션을 종료하시겠습니까?\n대상 기기에서는 다시 로그인이 필요합니다." +error = "세션 종료 실패: {{error}}" +success = "세션이 종료되었습니다." + [msg.userfront.dashboard.approved_session] copy_click = "{{label}}: {{id}}\\\\n클릭하면 복사됩니다." copy_tap = "{{label}}: {{id}}\\\\n탭하면 복사됩니다." @@ -2070,6 +2479,17 @@ status_history = "상태 이력" [ui.userfront.dashboard.activity] linked = "연동됨" +[ui.userfront.dashboard.sessions] +active_badge = "활성화" +current_badge = "현재 접속중" +current_disabled = "현재 세션" +unknown_device = "알 수 없는 기기" +unknown_session = "세션 정보" + +[ui.userfront.dashboard.sessions.revoke] +action = "세션 종료" +title = "세션 종료" + [ui.userfront.dashboard.approved_session] default = "승인한 세션 ID" userfront = "승인한 Userfront 세션 ID" diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 86f56c94..3a27641a 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -94,6 +94,20 @@ empty = "No linked apps yet." empty_detail = "Linked apps and their latest activity will appear here." error = "Could not load linked apps." +[msg.userfront.dashboard.sessions] +browser = "Browser: {value}" +empty = "No active sessions." +empty_detail = "Devices signed in with this account will appear here." +error = "Could not load sessions." +os = "OS: {value}" +recent_app = "Recent app: {app}" +session_id = "Session ID: {id}" + +[msg.userfront.dashboard.sessions.revoke] +confirm = "End the session for {target}?\nThat device will need to sign in again." +error = "Could not end the session: {error}" +success = "The session has been ended." + [msg.userfront.dashboard.approved_session] copy_click = "{label}: {id}\\\\\\\\\\\\\\\\nClick to copy." copy_tap = "{label}: {id}\\\\\\\\\\\\\\\\nTap to copy." @@ -270,6 +284,7 @@ uppercase = "At least one uppercase letter" [msg.userfront.sections] apps_subtitle = "Your linked apps and their latest sign-in status." audit_subtitle = "Recent access history for Baron sign-in." +sessions_subtitle = "Your currently signed-in devices and browser sessions." [msg.userfront.settings] disabled = "Account settings are currently unavailable." @@ -450,6 +465,17 @@ status_history = "Activity history" [ui.userfront.dashboard.activity] linked = "Linked" +[ui.userfront.dashboard.sessions] +active_badge = "Active" +current_badge = "Current" +current_disabled = "Current session" +unknown_device = "Unknown device" +unknown_session = "Session" + +[ui.userfront.dashboard.sessions.revoke] +action = "End session" +title = "End session" + [ui.userfront.dashboard.approved_session] default = "Default" userfront = "Approved UserFront session ID" @@ -584,6 +610,7 @@ title = "Create a new password" [ui.userfront.sections] apps = "Apps" audit = "Audit" +sessions = "Sessions" [ui.userfront.session] active = "Active session" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 482b2186..3c565d55 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -171,6 +171,215 @@ qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 token_missing = "로그인 토큰을 확인할 수 없습니다." verification_failed = "승인 처리에 실패했습니다: {error}" +[msg.userfront.login_success] +subtitle = "성공적으로 로그인되었습니다." + +[msg.userfront.consent] +accept_error = "동의 처리에 실패했습니다: {error}" +client_id = "클라이언트 ID: {id}" +client_unknown = "알 수 없는 앱" +description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\n계속 진행하려면 동의 여부를 선택해 주세요." +load_error = "동의 정보를 불러오는데 실패했습니다: {error}" +missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다." +redirect_notice = "동의 후 자동으로 서비스로 이동합니다." +scope_count = "총 {count}개" + +[msg.userfront.profile] +department_missing = "소속 정보 없음" +department_required = "소속을 입력해주세요." +email_missing = "이메일 없음" +greeting = "안녕하세요, {name}님" +load_failed = "정보를 불러올 수 없습니다." +name_missing = "이름 없음" +name_required = "이름을 입력해주세요." +phone_required = "휴대폰 번호를 입력해주세요." +phone_verify_required = "휴대폰 번호 인증이 필요합니다." +update_failed = "수정 실패: {error}" +update_success = "정보가 수정되었습니다." + +[msg.userfront.qr] +camera_error = "카메라 오류: {error}" +permission_error = "카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요." +permission_required = "카메라 권한이 필요합니다." + +[msg.userfront.reset] +invalid_body = "비밀번호 재설정 링크가 만료되었거나 잘못되었습니다. 다시 시도해주세요." +invalid_link = "유효하지 않은 재설정 링크입니다. (loginId/token 누락)" +invalid_title = "유효하지 않은 링크입니다." +policy_loading = "비밀번호 정책을 불러오는 중입니다..." +success = "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요." + +[msg.userfront.sections] +apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다." +audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다." +sessions_subtitle = "현재 로그인된 기기와 브라우저 세션입니다." + +[msg.userfront.settings] +disabled = "현재 계정 설정 화면은 준비 중입니다." + +[msg.userfront.signup] +failed = "가입 실패: {error}" +privacy_full = "개인정보 수집 및 이용 동의 전문..." +tos_full = "서비스 이용약관 전문..." + +[ui.common.badge] +admin_only = "Admin only" +command_only = "Command only" +system = "System" + +[ui.common.status] +active = "활성" +blocked = "차단됨" +failure = "실패" +inactive = "비활성" +ok = "정상" +pending = "준비 중" +success = "성공" + +[ui.userfront.app_label] +admin_console = "Admin Console" +baron = "Baron 로그인" +dev_console = "Dev Console" + +[ui.userfront.auth_method] +ory = "Ory 세션" +session = "세션" + +[ui.userfront.dashboard] +last_auth_label = "최근 인증" +status_history = "상태 이력" + +[ui.userfront.device] +android = "Mobile(Android)" +ios = "Mobile(iOS)" +linux = "Desktop(Linux)" +macos = "Desktop(macOS)" +windows = "Desktop(Windows)" + +[ui.userfront.error] +go_home = "홈으로 이동" +go_login = "로그인으로 이동" + +[ui.userfront.forgot] +heading = "비밀번호를 잊으셨나요?" +input_label = "이메일 또는 휴대폰 번호" +submit = "재설정 링크 전송" +title = "비밀번호 재설정" + +[ui.userfront.login] +forgot_password = "비밀번호를 잊으셨나요?" +signup = "회원가입" + +[ui.userfront.login_success] +later = "나중에 하기 (대시보드로 이동)" +qr = "QR 인증 (카메라 켜기)" +title = "로그인 완료" + +[ui.userfront.consent] +accept = "동의하고 계속하기" +requested_scopes = "요청된 권한" +title = "접근 권한 요청" + +[ui.userfront.nav] +dashboard = "대시보드" +logout = "로그아웃" +profile = "내 정보" +qr_scan = "QR 스캔" + +[ui.userfront.profile] +department_empty = "소속 정보 없음" +manage = "프로필 관리" +user_fallback = "사용자" + +[ui.userfront.qr] +rescan = "다시 스캔" +result_success = "승인 완료" +title = "Scan QR Code" + +[ui.userfront.reset] +confirm_password = "새 비밀번호 확인" +new_password = "새 비밀번호" +submit = "비밀번호 변경" +subtitle = "새로운 비밀번호 설정" +title = "새 비밀번호 설정" + +[ui.userfront.sections] +apps = "나의 App 현황" +audit = "접속이력" +sessions = "활성 세션" + +[ui.userfront.session] +active = "세션 활성" +unknown = "알 수 없음" + +[ui.userfront.signup] +complete = "가입 완료" +next_step = "다음 단계" +title = "회원가입" + +[msg.userfront.dashboard.activities] +empty = "연동된 앱이 없습니다." +empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." +error = "연동 정보를 불러오지 못했습니다." + +[msg.userfront.dashboard.sessions] +browser = "브라우저: {value}" +empty = "활성 세션이 없습니다." +empty_detail = "같은 계정으로 로그인한 기기가 여기에 표시됩니다." +error = "세션 정보를 불러오지 못했습니다." +os = "OS: {value}" +recent_app = "최근 접속 앱: {app}" +session_id = "세션 ID: {id}" + +[msg.userfront.dashboard.sessions.revoke] +confirm = "{target} 세션을 종료하시겠습니까?\n대상 기기에서는 다시 로그인이 필요합니다." +error = "세션 종료 실패: {error}" +success = "세션이 종료되었습니다." + +[msg.userfront.dashboard.approved_session] +copy_click = "{label}: {id}\n클릭하면 복사됩니다." +copy_tap = "{label}: {id}\n탭하면 복사됩니다." +none = "{label} 없음" + +[msg.userfront.dashboard.revoke] +confirm = "{app} 앱과의 연동을 해지하시겠습니까?\n해지하면 다음 로그인 시 다시 동의가 필요합니다." +error = "해지 실패: {error}" +success = "{app} 연동이 해지되었습니다." + +[msg.userfront.dashboard.scopes] +empty = "요청된 권한이 없습니다." + +[msg.userfront.dashboard.timeline] +load_error = "접속이력을 불러오지 못했습니다." + +[msg.userfront.error.whitelist] +"$normalizedCode" = "{error}" +bad_request = "입력값을 확인해 주세요." +invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요." +not_found = "요청한 페이지를 찾을 수 없습니다." +password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다." +rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요." +recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요." +recovery_invalid = "재설정 링크가 유효하지 않습니다." +settings_disabled = "현재 계정 설정 화면은 준비 중입니다." +verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요." + +[msg.userfront.error.ory] +"$normalizedCode" = "{error}" +access_denied = "사용자가 동의를 거부했습니다." +consent_required = "앱 접근 동의가 필요합니다." +interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요." +invalid_client = "클라이언트 인증 정보가 유효하지 않습니다." +invalid_grant = "인증 요청이 만료되었거나 유효하지 않습니다." +invalid_request = "잘못된 요청입니다." +invalid_scope = "요청한 권한 범위가 유효하지 않습니다." +login_required = "로그인이 필요합니다." +request_forbidden = "요청이 거부되었습니다." +server_error = "인증 서버 오류가 발생했습니다." +temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습니다." +unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다." +unsupported_response_type = "지원하지 않는 응답 타입입니다." + [msg.userfront.login.link] approved = "msg.userfront.login.link.approved" helper = "입력하신 정보로 로그인 링크를 전송합니다." @@ -450,6 +659,17 @@ status_history = "상태 이력" [ui.userfront.dashboard.activity] linked = "연동됨" +[ui.userfront.dashboard.sessions] +active_badge = "활성화" +current_badge = "현재 접속중" +current_disabled = "현재 세션" +unknown_device = "알 수 없는 기기" +unknown_session = "세션 정보" + +[ui.userfront.dashboard.sessions.revoke] +action = "세션 종료" +title = "세션 종료" + [ui.userfront.dashboard.approved_session] default = "승인한 세션 ID" userfront = "승인한 Userfront 세션 ID" diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart index 5b21ea20..57eec236 100644 --- a/userfront/lib/core/services/auth_proxy_service.dart +++ b/userfront/lib/core/services/auth_proxy_service.dart @@ -241,6 +241,29 @@ class AuthProxyService { } } + static Future revokeSession(String sessionId) async { + final url = Uri.parse('$_baseUrl/api/v1/user/sessions/$sessionId'); + final useCookie = AuthTokenStore.usesCookie(); + final token = AuthTokenStore.getToken(); + final client = createHttpClient(withCredentials: useCookie); + try { + final headers = {'Content-Type': 'application/json'}; + if (!useCookie && token != null && token.isNotEmpty) { + headers['Authorization'] = 'Bearer $token'; + } + final response = await client.delete(url, headers: headers); + if (response.statusCode != 200) { + throw _error( + 'err.userfront.dashboard.sessions.revoke', + '세션 종료에 실패했습니다: {{error}}', + detail: response.body, + ); + } + } finally { + client.close(); + } + } + static Future> verifyLoginShortCode( String shortCode, { bool verifyOnly = false, diff --git a/userfront/lib/features/dashboard/domain/models.dart b/userfront/lib/features/dashboard/domain/models.dart index f45c858f..3f633490 100644 --- a/userfront/lib/features/dashboard/domain/models.dart +++ b/userfront/lib/features/dashboard/domain/models.dart @@ -170,3 +170,59 @@ class RpHistoryItem { ); } } + +class UserSessionSummary { + final String sessionId; + final DateTime? authenticatedAt; + final DateTime? expiresAt; + final DateTime? issuedAt; + final DateTime? lastSeenAt; + final String ipAddress; + final String userAgent; + final String clientId; + final String appName; + final bool isCurrent; + final bool isActive; + + UserSessionSummary({ + required this.sessionId, + this.authenticatedAt, + this.expiresAt, + this.issuedAt, + this.lastSeenAt, + required this.ipAddress, + required this.userAgent, + required this.clientId, + required this.appName, + required this.isCurrent, + required this.isActive, + }); + + factory UserSessionSummary.fromJson(Map json) { + DateTime? parseDate(dynamic raw) { + final value = raw?.toString(); + if (value == null || value.isEmpty) { + return null; + } + try { + return DateTime.parse(value).toLocal(); + } catch (_) { + return null; + } + } + + return UserSessionSummary( + sessionId: json['session_id']?.toString() ?? '', + authenticatedAt: parseDate(json['authenticated_at']), + expiresAt: parseDate(json['expires_at']), + issuedAt: parseDate(json['issued_at']), + lastSeenAt: parseDate(json['last_seen_at']), + ipAddress: json['ip_address']?.toString() ?? '', + userAgent: json['user_agent']?.toString() ?? '', + clientId: json['client_id']?.toString() ?? '', + appName: json['app_name']?.toString() ?? '', + isCurrent: json['is_current'] == true, + isActive: json['is_active'] != false, + ); + } +} diff --git a/userfront/lib/features/dashboard/domain/providers/user_sessions_provider.dart b/userfront/lib/features/dashboard/domain/providers/user_sessions_provider.dart new file mode 100644 index 00000000..881a88f0 --- /dev/null +++ b/userfront/lib/features/dashboard/domain/providers/user_sessions_provider.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/services/auth_proxy_service.dart'; +import '../../../../core/services/auth_token_store.dart'; +import '../../../../core/services/http_client.dart'; +import '../models.dart'; + +class UserSessionsNotifier extends AsyncNotifier> { + @override + Future> build() async { + return _fetchSessions(); + } + + String _envOrDefault(String key, String fallback) { + if (!dotenv.isInitialized) { + return fallback; + } + return dotenv.env[key] ?? fallback; + } + + Future> _fetchSessions() async { + final baseUrl = _envOrDefault('BACKEND_URL', 'https://sso.hmac.kr'); + final url = Uri.parse('$baseUrl/api/v1/user/sessions'); + + final useCookie = AuthTokenStore.usesCookie(); + final token = AuthTokenStore.getToken(); + + final client = createHttpClient(withCredentials: useCookie); + final headers = {'Content-Type': 'application/json'}; + if (!useCookie && token != null) { + headers['Authorization'] = 'Bearer $token'; + } + + try { + final response = await client.get(url, headers: headers); + if (response.statusCode != 200) { + throw Exception('Failed to load sessions: ${response.statusCode}'); + } + + final body = jsonDecode(response.body) as Map; + final items = (body['items'] as List?) ?? const []; + return items + .whereType>() + .map(UserSessionSummary.fromJson) + .toList(); + } finally { + client.close(); + } + } + + Future refresh() async { + state = const AsyncLoading(); + state = await AsyncValue.guard(_fetchSessions); + } + + Future revokeSession(String sessionId) async { + await AuthProxyService.revokeSession(sessionId); + await refresh(); + } +} + +final userSessionsProvider = + AsyncNotifierProvider>(() { + return UserSessionsNotifier(); + }); diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 11f80c7a..bcbbe6ba 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../domain/session_time_resolver.dart'; import '../domain/providers/linked_rps_provider.dart'; +import '../domain/providers/user_sessions_provider.dart'; import '../../../../core/notifiers/auth_notifier.dart'; import '../../../../core/services/auth_proxy_service.dart'; import '../../../../core/services/auth_token_store.dart'; @@ -45,6 +46,7 @@ class _DashboardScreenState extends ConsumerState { bool _auditLoading = false; bool _auditLoadingMore = false; bool _isRevoking = false; + String? _revokingSessionId; bool _redirectingToSignin = false; bool _authBootstrapInProgress = false; @@ -130,6 +132,67 @@ class _DashboardScreenState extends ConsumerState { } } + Future _onRevokeSession(UserSessionSummary session) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(tr('ui.userfront.dashboard.sessions.revoke.title')), + content: Text( + tr( + 'msg.userfront.dashboard.sessions.revoke.confirm', + params: { + 'target': session.isCurrent + ? tr('ui.userfront.dashboard.sessions.current_badge') + : _sessionDisplayLabel(session), + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(tr('ui.common.cancel')), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: Text(tr('ui.userfront.dashboard.sessions.revoke.action')), + ), + ], + ), + ); + + if (confirmed != true) { + return; + } + + setState(() => _revokingSessionId = session.sessionId); + try { + await ref.read(userSessionsProvider.notifier).revokeSession( + session.sessionId, + ); + if (!mounted) { + return; + } + ToastService.success( + tr('msg.userfront.dashboard.sessions.revoke.success'), + ); + } catch (e) { + if (!mounted) { + return; + } + ToastService.error( + tr( + 'msg.userfront.dashboard.sessions.revoke.error', + params: {'error': '$e'}, + ), + ); + } finally { + if (mounted) { + setState(() => _revokingSessionId = null); + } + } + } + void _onScanQR() { context.push('/scan'); } @@ -310,9 +373,11 @@ class _DashboardScreenState extends ConsumerState { _revokedClientIds.clear(); }); ref.invalidate(linkedRpsProvider); + ref.invalidate(userSessionsProvider); await Future.wait([ ref.read(linkedRpsProvider.future), + ref.read(userSessionsProvider.future), ref.read(authTimelineProvider.notifier).refresh(), ]); @@ -758,6 +823,15 @@ class _DashboardScreenState extends ConsumerState { ), const SizedBox(height: 28), ], + _buildSectionTitle( + tr('ui.userfront.sections.sessions'), + tr( + 'msg.userfront.sections.sessions_subtitle', + ), + ), + const SizedBox(height: 12), + _buildSessionSection(isMobile), + const SizedBox(height: 28), _buildSectionTitle( tr('ui.userfront.sections.apps'), tr('msg.userfront.sections.apps_subtitle'), @@ -883,6 +957,358 @@ class _DashboardScreenState extends ConsumerState { ); } + Widget _buildSessionSection(bool isMobile) { + final sessionsState = ref.watch(userSessionsProvider); + return sessionsState.when( + data: (sessions) { + if (sessions.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('msg.userfront.dashboard.sessions.empty'), + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 6), + Text( + tr('msg.userfront.dashboard.sessions.empty_detail'), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ); + } + return _buildSessionGrid(sessions, isMobile); + }, + loading: () => const SizedBox( + height: 100, + child: Center(child: CircularProgressIndicator()), + ), + error: (error, stack) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('msg.userfront.dashboard.sessions.error'), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () => ref.read(userSessionsProvider.notifier).refresh(), + child: Text(tr('ui.common.retry')), + ), + ], + ), + ); + } + + Widget _buildSessionGrid(List sessions, bool isMobile) { + return LayoutBuilder( + builder: (context, constraints) { + int crossAxisCount; + if (constraints.maxWidth > 1200) { + crossAxisCount = 3; + } else if (constraints.maxWidth > 800) { + crossAxisCount = 2; + } else { + crossAxisCount = 1; + } + const spacing = 12.0; + final cardWidth = + (constraints.maxWidth - (spacing * (crossAxisCount - 1))) / + crossAxisCount; + + return Wrap( + spacing: spacing, + runSpacing: spacing, + children: sessions.map((session) { + return SizedBox( + width: cardWidth, + child: _buildSessionCard(session, cardWidth: cardWidth), + ); + }).toList(), + ); + }, + ); + } + + Widget _buildSessionCard(UserSessionSummary session, {double? cardWidth}) { + final isCurrent = session.isCurrent; + final statusColor = session.isActive ? Colors.green : Colors.grey; + final primaryTime = + session.lastSeenAt ?? + session.authenticatedAt ?? + session.issuedAt ?? + session.expiresAt; + final primaryTimeLabel = primaryTime != null + ? _formatDateTime(primaryTime) + : tr('ui.userfront.session.unknown'); + final sessionLabel = _sessionPrimaryLabel(session); + final clientLabel = _sessionClientLabel(session); + final browserLabel = _sessionBrowserLabel(session.userAgent); + final osLabel = _sessionOsLabel(session.userAgent); + final canRevoke = !isCurrent && _revokingSessionId == null; + + return Container( + width: cardWidth ?? 320, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isCurrent ? Colors.blueGrey : _border, + width: isCurrent ? 1.5 : 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 8), + blurRadius: 12, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + sessionLabel, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: _ink, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isCurrent ? Colors.blueGrey : statusColor, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + isCurrent + ? tr('ui.userfront.dashboard.sessions.current_badge') + : session.isActive + ? tr('ui.userfront.dashboard.sessions.active_badge') + : tr('ui.common.status.inactive'), + style: const TextStyle( + fontSize: 11, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + if (clientLabel.isNotEmpty) ...[ + Text( + clientLabel, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: _ink, + ), + ), + const SizedBox(height: 8), + ], + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _buildInfoChip(Icons.access_time, primaryTimeLabel), + if (session.ipAddress.isNotEmpty) + _buildInfoChip(Icons.public, session.ipAddress), + ], + ), + if (browserLabel.isNotEmpty || osLabel.isNotEmpty) ...[ + const SizedBox(height: 12), + if (browserLabel.isNotEmpty) + Text( + tr( + 'msg.userfront.dashboard.sessions.browser', + params: {'value': browserLabel}, + ), + style: TextStyle(fontSize: 13, color: Colors.grey[700]), + ), + if (osLabel.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + tr( + 'msg.userfront.dashboard.sessions.os', + params: {'value': osLabel}, + ), + style: TextStyle(fontSize: 13, color: Colors.grey[700]), + ), + ], + ], + if (session.clientId.trim().isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + tr( + 'msg.userfront.dashboard.client_id', + params: {'id': session.clientId}, + ), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + const SizedBox(height: 8), + Text( + tr( + 'msg.userfront.dashboard.sessions.session_id', + params: {'id': _compactSessionId(session.sessionId)}, + ), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: canRevoke ? () => _onRevokeSession(session) : null, + style: OutlinedButton.styleFrom( + foregroundColor: canRevoke ? Colors.redAccent : Colors.grey, + side: BorderSide( + color: canRevoke ? Colors.redAccent : Colors.grey, + width: 0.6, + ), + padding: const EdgeInsets.symmetric(vertical: 10), + ), + child: _revokingSessionId == session.sessionId + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.redAccent, + ), + ) + : Text( + isCurrent + ? tr('ui.userfront.dashboard.sessions.current_disabled') + : tr('ui.userfront.dashboard.sessions.revoke.action'), + ), + ), + ), + ], + ), + ); + } + + String _sessionDisplayLabel(UserSessionSummary session) { + if (session.userAgent.trim().isNotEmpty) { + return _sessionUserAgentLabel(session.userAgent); + } + return tr('ui.userfront.dashboard.sessions.unknown_device'); + } + + String _sessionPrimaryLabel(UserSessionSummary session) { + if (session.isCurrent) { + return tr('ui.userfront.dashboard.sessions.current_badge'); + } + final appName = session.appName.trim(); + if (appName.isNotEmpty) { + return appName; + } + return tr('ui.userfront.dashboard.sessions.unknown_session'); + } + + String _sessionClientLabel(UserSessionSummary session) { + final appName = session.appName.trim(); + if (appName.isEmpty || session.isCurrent) { + return ''; + } + return tr( + 'msg.userfront.dashboard.sessions.recent_app', + params: {'app': appName}, + ); + } + + String _sessionUserAgentLabel(String userAgent) { + final lower = userAgent.toLowerCase(); + if (lower.isEmpty) { + return tr('ui.userfront.dashboard.sessions.unknown_device'); + } + if (_looksLikeInternalUserAgent(lower)) { + return ''; + } + if (lower.contains('iphone') || lower.contains('ios')) { + return tr('ui.userfront.device.ios'); + } + if (lower.contains('android')) { + return tr('ui.userfront.device.android'); + } + if (lower.contains('windows')) { + return tr('ui.userfront.device.windows', fallback: 'Desktop(Windows)'); + } + if (lower.contains('mac os') || lower.contains('macintosh')) { + return tr('ui.userfront.device.macos', fallback: 'Desktop(macOS)'); + } + if (lower.contains('linux')) { + return tr('ui.userfront.device.linux'); + } + return userAgent; + } + + String _sessionBrowserLabel(String userAgent) { + final lower = userAgent.toLowerCase(); + if (lower.isEmpty || _looksLikeInternalUserAgent(lower)) { + return ''; + } + if (lower.contains('edg/')) { + return 'Edge'; + } + if (lower.contains('chrome/') && !lower.contains('edg/')) { + return 'Chrome'; + } + if (lower.contains('firefox/')) { + return 'Firefox'; + } + if (lower.contains('safari/') && !lower.contains('chrome/')) { + return 'Safari'; + } + if (lower.contains('samsungbrowser/')) { + return 'Samsung Internet'; + } + if (lower.contains('flutter')) { + return 'Flutter'; + } + return ''; + } + + String _sessionOsLabel(String userAgent) { + final lower = userAgent.toLowerCase(); + if (lower.isEmpty || _looksLikeInternalUserAgent(lower)) { + return ''; + } + if (lower.contains('iphone') || lower.contains('ios')) { + return 'iOS'; + } + if (lower.contains('android')) { + return 'Android'; + } + if (lower.contains('windows')) { + return 'Windows'; + } + if (lower.contains('mac os') || lower.contains('macintosh')) { + return 'macOS'; + } + if (lower.contains('linux')) { + return 'Linux'; + } + return ''; + } + + bool _looksLikeInternalUserAgent(String userAgent) { + return userAgent.startsWith('go-http-client/') || + userAgent.startsWith('fasthttp') || + userAgent.startsWith('fiber'); + } + Widget _buildInfoChip(IconData icon, String label) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), From 1524da2d6a7ef459bbda50c4eab187ca60c96e64 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 2 Apr 2026 11:46:41 +0900 Subject: [PATCH 02/39] =?UTF-8?q?=EC=84=B8=EC=85=98=20=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=20=EC=8B=9C=20Hydra=20=ED=86=A0=ED=81=B0=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=EB=8F=84=20=ED=95=A8=EA=BB=98=20=EB=AC=B4=ED=9A=A8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 159 +++++++++++++++++- .../handler/auth_handler_sessions_test.go | 111 +++++++++--- .../internal/service/hydra_admin_service.go | 2 + 3 files changed, 247 insertions(+), 25 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index e73f112a..1810dea7 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -1002,6 +1002,18 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string) map[strin return claims } +func withOidcSessionMetadata(claims map[string]any, sessionID string) map[string]any { + if claims == nil { + claims = map[string]any{} + } + sessionID = strings.TrimSpace(sessionID) + if sessionID != "" { + claims["session_id"] = sessionID + claims["sid"] = sessionID + } + return claims +} + func collectEmailList(traits map[string]any, primaryEmail string) []string { emails := make([]string, 0) seen := make(map[string]struct{}) @@ -4755,7 +4767,10 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error { slog.Error("failed to load identity for skip consent", "error", err, "subject", consentRequest.Subject) // 신원 정보를 가져오지 못하면 자동 승인을 진행할 수 없으므로 일반 흐름(UI 노출)으로 진행 } else { - sessionClaims := buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope) + sessionClaims := withOidcSessionMetadata( + buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope), + h.resolveCurrentSessionID(c), + ) acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), challenge, consentRequest, sessionClaims) if err != nil { slog.Error("failed to auto-accept hydra consent request", "error", err) @@ -4875,7 +4890,11 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error { if loginID := pickLoginIDFromTraits(identity.Traits); loginID != "" { c.Locals("login_id", loginID) } - sessionClaims := buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope) + currentSessionID := h.resolveCurrentSessionID(c) + sessionClaims := withOidcSessionMetadata( + buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope), + currentSessionID, + ) acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest, sessionClaims) if err != nil { @@ -4902,12 +4921,17 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error { "scopes": consentRequest.RequestedScope, "client_name": consentRequest.Client.ClientName, } + if currentSessionID != "" { + detailsMap["session_id"] = currentSessionID + detailsMap["approved_session_id"] = currentSessionID + } detailsBytes, _ := json.Marshal(detailsMap) _ = h.AuditRepo.Create(&domain.AuditLog{ EventID: GenerateSecureToken(16), Timestamp: time.Now(), UserID: consentRequest.Subject, + SessionID: currentSessionID, EventType: "consent.granted", Status: "success", IPAddress: c.IP(), @@ -5050,12 +5074,12 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe // 1. Try to fetch real profile if token/cookie exists if token != "" || cookie != "" { // Try Redis Cache - if h.RedisService != nil && token != "" { - cacheKey = "cache:profile:token:" + token + if h.RedisService != nil && token == "" && cookie != "" { + cacheKey = "cache:profile:cookie:" + cookie cached, _ := h.RedisService.Get(cacheKey) if cached != "" { if json.Unmarshal([]byte(cached), &profile) == nil { - slog.Debug("Profile loaded from cache", "token", token[:10]+"...", "role", profile.Role) + slog.Debug("Profile loaded from cache", "role", profile.Role) } } } @@ -5146,7 +5170,7 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe // IMPORTANT: In dev mode, if role was overridden, we should NOT cache it under the token key // or we should include the mock role in the cache key. // For simplicity, let's skip caching if mockRole is present in dev. - if h.RedisService != nil && cacheKey != "" && err == nil && !(isDev && mockRole != "") { + if h.RedisService != nil && token == "" && cacheKey != "" && err == nil && !(isDev && mockRole != "") { if data, err := json.Marshal(profile); err == nil { ttlStr := os.Getenv("PROFILE_CACHE_TTL") ttl := 30 * time.Minute // Default TTL @@ -6208,6 +6232,10 @@ func (h *AuthHandler) getHydraProfile(ctx context.Context, token string) (*domai slog.Warn("Hydra token is not active") return nil, errors.New("token is not active") } + if err := h.validateHydraTokenSession(ctx, intro); err != nil { + slog.Warn("Hydra token session validation failed", "error", err) + return nil, err + } slog.Info("Hydra token introspected", "subject", intro.Subject, "client_id", intro.ClientID) @@ -6820,6 +6848,9 @@ func (h *AuthHandler) DeleteMySession(c *fiber.Ctx) error { } else if err := h.KratosAdmin.DeleteSession(c.Context(), targetSessionID); err != nil { return errorJSON(c, fiber.StatusInternalServerError, "Failed to delete session") } + if err := h.revokeHydraSessionAccess(c.Context(), profile.ID, targetSessionID); err != nil { + return errorJSON(c, fiber.StatusInternalServerError, "Failed to revoke linked app sessions") + } h.writeSessionRevokedAuditLog(c, profile.ID, h.resolveCurrentSessionID(c), targetSessionID, result) return c.JSON(fiber.Map{"status": "ok"}) @@ -7049,6 +7080,122 @@ func deriveSessionClientInfo(log domain.AuditLog) (string, string) { return clientID, appName } +func extractStringLikeValue(raw any) string { + switch value := raw.(type) { + case string: + return strings.TrimSpace(value) + default: + text := strings.TrimSpace(fmt.Sprint(value)) + if text == "" || text == "" { + return "" + } + return text + } +} + +func extractHydraSessionID(ext map[string]interface{}) string { + if len(ext) == 0 { + return "" + } + for _, key := range []string{"session_id", "sid", "sessionId"} { + if value := extractStringLikeValue(ext[key]); value != "" { + return value + } + } + return "" +} + +func (h *AuthHandler) validateHydraTokenSession(ctx context.Context, intro *service.HydraIntrospectionResponse) error { + if h == nil || h.KratosAdmin == nil || intro == nil { + return nil + } + + sessionID := extractHydraSessionID(intro.Ext) + if sessionID == "" { + return nil + } + + session, err := h.KratosAdmin.GetSession(ctx, sessionID) + if err != nil { + return fmt.Errorf("kratos session lookup failed: %w", err) + } + if session == nil { + return errors.New("linked session not found") + } + if !session.Active { + return errors.New("linked session is inactive") + } + if identityID := strings.TrimSpace(session.Identity.ID); identityID != "" && strings.TrimSpace(intro.Subject) != "" && identityID != strings.TrimSpace(intro.Subject) { + return errors.New("linked session subject mismatch") + } + return nil +} + +func (h *AuthHandler) loadSessionClientBindings(ctx context.Context, userID string) map[string][]string { + bindings := make(map[string][]string) + if h == nil || h.AuditRepo == nil || strings.TrimSpace(userID) == "" { + return bindings + } + + logs, err := h.AuditRepo.FindByUserAndEvents(ctx, userID, []string{ + "consent.granted", + "POST /api/v1/auth/oidc/login/accept", + }, 200) + if err != nil { + return bindings + } + + for _, log := range logs { + sessionID := strings.TrimSpace(log.SessionID) + if sessionID == "" { + sessionID = strings.TrimSpace(extractApprovedSessionIDFromAuditDetails(log.Details)) + } + if sessionID == "" { + sessionID = strings.TrimSpace(extractSessionIDFromAuditDetails(log.Details)) + } + if sessionID == "" { + continue + } + + clientID, _ := deriveSessionClientInfo(log) + clientID = strings.TrimSpace(clientID) + if clientID == "" { + continue + } + + existing := bindings[sessionID] + seen := false + for _, candidate := range existing { + if candidate == clientID { + seen = true + break + } + } + if !seen { + bindings[sessionID] = append(existing, clientID) + } + } + + return bindings +} + +func (h *AuthHandler) revokeHydraSessionAccess(ctx context.Context, userID string, sessionID string) error { + if h == nil || h.Hydra == nil { + return nil + } + + clientIDs := h.loadSessionClientBindings(ctx, userID)[strings.TrimSpace(sessionID)] + if len(clientIDs) == 0 { + return h.Hydra.RevokeConsentSessions(ctx, userID, "") + } + for _, clientID := range clientIDs { + if err := h.Hydra.RevokeConsentSessions(ctx, userID, clientID); err != nil { + return err + } + } + return nil +} + func looksLikeInternalUserAgent(userAgent string) bool { normalized := strings.ToLower(strings.TrimSpace(userAgent)) if normalized == "" { diff --git a/backend/internal/handler/auth_handler_sessions_test.go b/backend/internal/handler/auth_handler_sessions_test.go index ab6c2e2e..e0c95d41 100644 --- a/backend/internal/handler/auth_handler_sessions_test.go +++ b/backend/internal/handler/auth_handler_sessions_test.go @@ -3,7 +3,9 @@ package handler import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/service" + "context" "encoding/json" + "io" "net/http" "net/http/httptest" "testing" @@ -102,23 +104,40 @@ func TestListMySessions_Success(t *testing.T) { } func TestDeleteMySession_Success(t *testing.T) { - setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) { - if r.URL.Path == "/sessions/whoami" { - return httpJSONAny(r, http.StatusOK, map[string]any{ - "id": "current-sid", - "authenticated_at": time.Now().UTC().Format(time.RFC3339), - "identity": map[string]any{ - "id": "user-123", - "traits": map[string]any{ - "email": "user@example.com", - "name": "User", - "role": "user", + t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") + var hydraRevokeCalls int + client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { + switch r.URL.Host { + case "kratos.test": + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": time.Now().UTC().Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, }, - }, - }), nil + }), nil + } + case "hydra.test": + if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" { + if r.URL.Query().Get("subject") != "user-123" { + t.Fatalf("unexpected revoke subject: %s", r.URL.Query().Get("subject")) + } + if r.URL.Query().Get("client") != "devfront" { + t.Fatalf("unexpected revoke client: %s", r.URL.Query().Get("client")) + } + hydraRevokeCalls++ + return httpResponse(r, http.StatusNoContent, ""), nil + } } return httpResponse(r, http.StatusNotFound, "not found"), nil - })) + })} + setDefaultHTTPClientForTest(t, client.Transport) mockKratos := new(MockKratosAdminService) mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{ @@ -134,7 +153,17 @@ func TestDeleteMySession_Success(t *testing.T) { h := &AuthHandler{ KratosAdmin: mockKratos, AuditRepo: auditRepo, + Hydra: &service.HydraAdminService{ + AdminURL: "http://hydra.test", + HTTPClient: client, + }, } + auditRepo.logs = append(auditRepo.logs, domain.AuditLog{ + UserID: "user-123", + EventType: "POST /api/v1/auth/oidc/login/accept", + SessionID: "target-sid", + Details: `{"client_id":"devfront","client_name":"Devfront"}`, + }) app := fiber.New() app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession) @@ -146,12 +175,56 @@ func TestDeleteMySession_Success(t *testing.T) { resp, err := app.Test(req, -1) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) - if assert.Len(t, auditRepo.logs, 1) { - assert.Equal(t, "session.revoked", auditRepo.logs[0].EventType) - assert.Equal(t, "user-123", auditRepo.logs[0].UserID) - assert.Equal(t, "current-sid", auditRepo.logs[0].SessionID) - assert.Contains(t, auditRepo.logs[0].Details, "target-sid") + if assert.Len(t, auditRepo.logs, 2) { + assert.Equal(t, "session.revoked", auditRepo.logs[len(auditRepo.logs)-1].EventType) + assert.Equal(t, "user-123", auditRepo.logs[len(auditRepo.logs)-1].UserID) + assert.Equal(t, "current-sid", auditRepo.logs[len(auditRepo.logs)-1].SessionID) + assert.Contains(t, auditRepo.logs[len(auditRepo.logs)-1].Details, "target-sid") } + assert.Equal(t, 1, hydraRevokeCalls) mockKratos.AssertExpectations(t) } + +func TestGetHydraProfile_RejectsInactiveLinkedSession(t *testing.T) { + client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { + if r.URL.Host == "hydra.test" && r.URL.Path == "/oauth2/introspect" { + body, _ := io.ReadAll(r.Body) + if string(body) != "token=opaque-token" { + t.Fatalf("unexpected introspect body: %s", string(body)) + } + return httpJSONAny(r, http.StatusOK, map[string]any{ + "active": true, + "sub": "user-123", + "client_id": "devfront", + "ext": map[string]any{ + "session_id": "target-sid", + }, + }), nil + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })} + + mockKratos := new(MockKratosAdminService) + mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{ + ID: "target-sid", + Active: false, + Identity: &service.KratosIdentity{ + ID: "user-123", + }, + }, nil).Once() + + h := &AuthHandler{ + KratosAdmin: mockKratos, + Hydra: &service.HydraAdminService{ + AdminURL: "http://hydra.test", + HTTPClient: client, + }, + } + + profile, err := h.getHydraProfile(context.Background(), "opaque-token") + assert.Nil(t, profile) + assert.Error(t, err) + assert.Contains(t, err.Error(), "inactive") + mockKratos.AssertExpectations(t) +} diff --git a/backend/internal/service/hydra_admin_service.go b/backend/internal/service/hydra_admin_service.go index 6bafc76c..8dda5318 100644 --- a/backend/internal/service/hydra_admin_service.go +++ b/backend/internal/service/hydra_admin_service.go @@ -264,6 +264,8 @@ func (s *HydraAdminService) RevokeConsentSessions(ctx context.Context, subject, } if clientID != "" { params["client"] = clientID + } else { + params["all"] = "true" } endpoint, err := s.buildURLWithParams("/oauth2/auth/sessions/consent", params) if err != nil { From 6b115799c32b4195efe5635399d7058f5ea034ac Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 2 Apr 2026 13:33:59 +0900 Subject: [PATCH 03/39] =?UTF-8?q?=ED=99=9C=EC=84=B1=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C=20=EA=B7=9C=EC=B9=99=20=ED=86=B5=EC=9D=BC?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/dashboard_screen.dart | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index bcbbe6ba..777f43ca 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -35,6 +35,7 @@ class _DashboardScreenState extends ConsumerState { static const _surface = Colors.white; static const _border = Color(0xFFE5E7EB); static const _subtle = Color(0xFFF7F8FA); + static const double _dashboardCardSpacing = 12; static const double _historySessionMinWidth = 92; static const double _historyOtherColumnsBaselineWidth = 780; static const int _historySessionMinVisibleChars = 8; @@ -167,9 +168,9 @@ class _DashboardScreenState extends ConsumerState { setState(() => _revokingSessionId = session.sessionId); try { - await ref.read(userSessionsProvider.notifier).revokeSession( - session.sessionId, - ); + await ref + .read(userSessionsProvider.notifier) + .revokeSession(session.sessionId); if (!mounted) { return; } @@ -825,9 +826,7 @@ class _DashboardScreenState extends ConsumerState { ], _buildSectionTitle( tr('ui.userfront.sections.sessions'), - tr( - 'msg.userfront.sections.sessions_subtitle', - ), + tr('msg.userfront.sections.sessions_subtitle'), ), const SizedBox(height: 12), _buildSessionSection(isMobile), @@ -1007,22 +1006,15 @@ class _DashboardScreenState extends ConsumerState { Widget _buildSessionGrid(List sessions, bool isMobile) { return LayoutBuilder( builder: (context, constraints) { - int crossAxisCount; - if (constraints.maxWidth > 1200) { - crossAxisCount = 3; - } else if (constraints.maxWidth > 800) { - crossAxisCount = 2; - } else { - crossAxisCount = 1; - } - const spacing = 12.0; - final cardWidth = - (constraints.maxWidth - (spacing * (crossAxisCount - 1))) / - crossAxisCount; + final crossAxisCount = _dashboardCardColumnCount(constraints.maxWidth); + final cardWidth = _dashboardCardWidth( + constraints.maxWidth, + crossAxisCount, + ); return Wrap( - spacing: spacing, - runSpacing: spacing, + spacing: _dashboardCardSpacing, + runSpacing: _dashboardCardSpacing, children: sessions.map((session) { return SizedBox( width: cardWidth, @@ -1189,7 +1181,9 @@ class _DashboardScreenState extends ConsumerState { ) : Text( isCurrent - ? tr('ui.userfront.dashboard.sessions.current_disabled') + ? tr( + 'ui.userfront.dashboard.sessions.current_disabled', + ) : tr('ui.userfront.dashboard.sessions.revoke.action'), ), ), @@ -1447,15 +1441,7 @@ class _DashboardScreenState extends ConsumerState { builder: (context, constraints) { final maxWidth = constraints.maxWidth; - // 화면 너비에 따른 컬럼 수 및 초기 표시 개수 결정 - int crossAxisCount; - if (maxWidth > 1200) { - crossAxisCount = 4; - } else if (maxWidth > 800) { - crossAxisCount = 3; - } else { - crossAxisCount = 2; - } + final crossAxisCount = _dashboardCardColumnCount(maxWidth); // 초기 표시 개수는 한 줄에 표시되는 개수와 동일하게 설정 (요청에 따라 유동적 조절 가능) final int initialVisibleCount = crossAxisCount; @@ -1468,17 +1454,14 @@ class _DashboardScreenState extends ConsumerState { visibleActivities = activities.take(initialVisibleCount).toList(); } - // 카드의 너비를 화면 너비에 맞춰 계산 (여백 고려) - const spacing = 12.0; - final double cardWidth = - (maxWidth - (spacing * (crossAxisCount - 1))) / crossAxisCount; + final cardWidth = _dashboardCardWidth(maxWidth, crossAxisCount); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( - spacing: spacing, - runSpacing: spacing, + spacing: _dashboardCardSpacing, + runSpacing: _dashboardCardSpacing, children: visibleActivities.map((item) { return SizedBox( width: cardWidth, @@ -1746,6 +1729,21 @@ class _DashboardScreenState extends ConsumerState { ); } + int _dashboardCardColumnCount(double maxWidth) { + if (maxWidth > 1200) { + return 4; + } + if (maxWidth > 800) { + return 3; + } + return 2; + } + + double _dashboardCardWidth(double maxWidth, int crossAxisCount) { + return (maxWidth - (_dashboardCardSpacing * (crossAxisCount - 1))) / + crossAxisCount; + } + Widget _buildHistoryTable(AuthTimelineState state) { return _buildHistoryContainer( child: Column( From fe70fd216baa615efb43b38d29f8f5d7972420a1 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 11:15:48 +0900 Subject: [PATCH 04/39] =?UTF-8?q?=EC=84=B8=EC=85=98=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=20=EB=94=94=EB=B2=84=EA=B7=B8=EC=9A=A9=20=EC=8B=9C=EB=82=98?= =?UTF-8?q?=EB=A6=AC=EC=98=A4=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/auth_handler_login_test.go | 151 +++++++++++ .../handler/auth_handler_sessions_test.go | 244 ++++++++++++++++++ .../tests/session-cross-browser-debug.spec.ts | 200 ++++++++++++++ 3 files changed, 595 insertions(+) create mode 100644 userfront-e2e/tests/session-cross-browser-debug.spec.ts diff --git a/backend/internal/handler/auth_handler_login_test.go b/backend/internal/handler/auth_handler_login_test.go index 3d098bbe..747f476e 100644 --- a/backend/internal/handler/auth_handler_login_test.go +++ b/backend/internal/handler/auth_handler_login_test.go @@ -7,6 +7,7 @@ package handler import ( "baron-sso-backend/internal/domain" + "baron-sso-backend/internal/middleware" "baron-sso-backend/internal/service" "bytes" "context" @@ -637,6 +638,156 @@ func TestPasswordLogin_OIDC_Success(t *testing.T) { } } +func TestPasswordLogin_OIDC_AuditIncludesClientMetadata(t *testing.T) { + mockIdp := new(MockIdentityProvider) + mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{ + SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"}, + Subject: "kratos-identity-id", + }, nil) + + hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet: + json.NewEncoder(w).Encode(domain.HydraLoginRequest{ + Challenge: "challenge-123", + Client: domain.HydraClient{ + ClientID: "devfront", + ClientName: "DevFront", + Metadata: map[string]interface{}{"status": "active"}, + }, + }) + case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut: + json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"}) + default: + http.NotFound(w, r) + } + }) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil) + + auditRepo := &mockAuditRepo{} + h := &AuthHandler{ + IdpProvider: mockIdp, + KratosAdmin: mockKratos, + AuditRepo: auditRepo, + Hydra: &service.HydraAdminService{ + AdminURL: "http://hydra.test", + HTTPClient: &http.Client{Transport: mockHydraTransport(hydraHandler)}, + }, + } + + app := fiber.New() + app.Use(middleware.AuditMiddleware(middleware.AuditConfig{ + Repo: auditRepo, + BodyDump: true, + })) + app.Post("/api/v1/auth/password/login", h.PasswordLogin) + + body, _ := json.Marshal(map[string]string{ + "loginId": "user@example.com", + "password": "password", + "login_challenge": "challenge-123", + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/login", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes)) + } + + if len(auditRepo.logs) != 1 { + t.Fatalf("expected 1 audit log, got %d", len(auditRepo.logs)) + } + + log := auditRepo.logs[0] + if log.EventType != "POST /api/v1/auth/password/login" { + t.Fatalf("expected password login audit event, got %q", log.EventType) + } + if log.UserID != "kratos-identity-id" { + t.Fatalf("expected audit user_id kratos-identity-id, got %q", log.UserID) + } + + details, err := parseAuditDetails(log.Details) + if err != nil { + t.Fatalf("failed to parse audit details: %v", err) + } + if got, _ := details["client_id"].(string); got != "devfront" { + t.Fatalf("expected client_id devfront, got %v", details["client_id"]) + } + if got, _ := details["client_name"].(string); got != "DevFront" { + t.Fatalf("expected client_name DevFront, got %v", details["client_name"]) + } +} + +func TestPasswordLogin_UserFront_AuditIncludesDefaultClientMetadata(t *testing.T) { + mockIdp := new(MockIdentityProvider) + mockIdp.On("SignIn", "user@example.com", "password").Return(&domain.AuthInfo{ + SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"}, + Subject: "kratos-identity-id", + }, nil) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "user@example.com").Return("kratos-identity-id", nil) + + auditRepo := &mockAuditRepo{} + h := &AuthHandler{ + IdpProvider: mockIdp, + KratosAdmin: mockKratos, + AuditRepo: auditRepo, + } + + app := fiber.New() + app.Use(middleware.AuditMiddleware(middleware.AuditConfig{ + Repo: auditRepo, + BodyDump: true, + })) + app.Post("/api/v1/auth/password/login", h.PasswordLogin) + + body, _ := json.Marshal(map[string]string{ + "loginId": "user@example.com", + "password": "password", + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/login", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("expected 200, got %d, body: %s", resp.StatusCode, string(bodyBytes)) + } + + if len(auditRepo.logs) != 1 { + t.Fatalf("expected 1 audit log, got %d", len(auditRepo.logs)) + } + if auditRepo.logs[0].UserID != "kratos-identity-id" { + t.Fatalf("expected audit user_id kratos-identity-id, got %q", auditRepo.logs[0].UserID) + } + + details, err := parseAuditDetails(auditRepo.logs[0].Details) + if err != nil { + t.Fatalf("failed to parse audit details: %v", err) + } + if got, _ := details["client_id"].(string); got != "userfront" { + t.Fatalf("expected client_id userfront, got %v", details["client_id"]) + } + if got, _ := details["client_name"].(string); got != "UserFront" { + t.Fatalf("expected client_name UserFront, got %v", details["client_name"]) + } +} + func TestHeadlessPasswordLogin_HeadlessLoginClientSuccess(t *testing.T) { mockIdp := new(MockIdentityProvider) mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{ diff --git a/backend/internal/handler/auth_handler_sessions_test.go b/backend/internal/handler/auth_handler_sessions_test.go index e0c95d41..39e6f599 100644 --- a/backend/internal/handler/auth_handler_sessions_test.go +++ b/backend/internal/handler/auth_handler_sessions_test.go @@ -103,6 +103,250 @@ func TestListMySessions_Success(t *testing.T) { mockKratos.AssertExpectations(t) } +func TestListMySessions_UsesConsentGrantForAppName(t *testing.T) { + now := time.Date(2026, 4, 2, 4, 40, 0, 0, time.UTC) + setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) { + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": now.Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, + }, + }), nil + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{ + { + ID: "current-sid", + Active: true, + AuthenticatedAt: now, + ExpiresAt: now.Add(24 * time.Hour), + }, + { + ID: "c7c721ea-session", + Active: true, + AuthenticatedAt: now.Add(-5 * time.Minute), + ExpiresAt: now.Add(23*time.Hour + 55*time.Minute), + }, + }, nil).Once() + + auditRepo := &mockAuditRepo{ + logs: []domain.AuditLog{ + { + UserID: "user-123", + EventType: "consent.granted", + SessionID: "c7c721ea-session", + Timestamp: now, + Details: `{"client_id":"devfront","client_name":"DevFront","session_id":"c7c721ea-session","approved_session_id":"c7c721ea-session"}`, + }, + }, + } + + h := &AuthHandler{ + KratosAdmin: mockKratos, + AuditRepo: auditRepo, + } + + app := fiber.New() + app.Get("/api/v1/user/sessions", h.ListMySessions) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var body struct { + Items []struct { + SessionID string `json:"session_id"` + AppName string `json:"app_name"` + ClientID string `json:"client_id"` + } `json:"items"` + } + err = json.NewDecoder(resp.Body).Decode(&body) + assert.NoError(t, err) + if assert.Len(t, body.Items, 2) { + assert.Equal(t, "c7c721ea-session", body.Items[1].SessionID) + assert.Equal(t, "DevFront", body.Items[1].AppName) + assert.Equal(t, "devfront", body.Items[1].ClientID) + } + + mockKratos.AssertExpectations(t) +} + +func TestListMySessions_PreservesAppNameFromOlderConsentGrant(t *testing.T) { + now := time.Date(2026, 4, 2, 4, 40, 0, 0, time.UTC) + setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) { + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": now.Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, + }, + }), nil + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{ + { + ID: "current-sid", + Active: true, + AuthenticatedAt: now, + ExpiresAt: now.Add(24 * time.Hour), + }, + { + ID: "c7c721ea-session", + Active: true, + AuthenticatedAt: now.Add(-5 * time.Minute), + ExpiresAt: now.Add(23*time.Hour + 55*time.Minute), + }, + }, nil).Once() + + auditRepo := &mockAuditRepo{ + logs: []domain.AuditLog{ + { + UserID: "user-123", + EventType: "consent.granted", + SessionID: "c7c721ea-session", + Timestamp: now.Add(-30 * time.Second), + IPAddress: "203.0.113.10", + Details: `{"client_id":"devfront","client_name":"DevFront","session_id":"c7c721ea-session"}`, + }, + { + UserID: "user-123", + EventType: "login_success", + SessionID: "c7c721ea-session", + Timestamp: now, + IPAddress: "10.0.0.12", + UserAgent: "Mozilla/5.0", + }, + }, + } + + h := &AuthHandler{ + KratosAdmin: mockKratos, + AuditRepo: auditRepo, + } + + app := fiber.New() + app.Get("/api/v1/user/sessions", h.ListMySessions) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var body struct { + Items []struct { + SessionID string `json:"session_id"` + AppName string `json:"app_name"` + ClientID string `json:"client_id"` + IPAddress string `json:"ip_address"` + } `json:"items"` + } + err = json.NewDecoder(resp.Body).Decode(&body) + assert.NoError(t, err) + if assert.Len(t, body.Items, 2) { + assert.Equal(t, "c7c721ea-session", body.Items[1].SessionID) + assert.Equal(t, "DevFront", body.Items[1].AppName) + assert.Equal(t, "devfront", body.Items[1].ClientID) + assert.Equal(t, "203.0.113.10", body.Items[1].IPAddress) + } + + mockKratos.AssertExpectations(t) +} + +func TestListMySessions_CurrentSessionFallsBackToRequestMetadata(t *testing.T) { + now := time.Date(2026, 4, 6, 1, 2, 3, 0, time.UTC) + setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) { + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": now.Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, + }, + }), nil + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{ + { + ID: "current-sid", + Active: true, + AuthenticatedAt: now, + ExpiresAt: now.Add(24 * time.Hour), + }, + }, nil).Once() + + h := &AuthHandler{ + KratosAdmin: mockKratos, + AuditRepo: &mockAuditRepo{}, + } + + app := fiber.New() + app.Get("/api/v1/user/sessions", h.ListMySessions) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/146.0.0.0 Safari/537.36") + req.Header.Set("X-Forwarded-For", "203.0.113.25") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var body struct { + Items []struct { + SessionID string `json:"session_id"` + IsCurrent bool `json:"is_current"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` + ClientID string `json:"client_id"` + AppName string `json:"app_name"` + } `json:"items"` + } + err = json.NewDecoder(resp.Body).Decode(&body) + assert.NoError(t, err) + if assert.Len(t, body.Items, 1) { + assert.Equal(t, "current-sid", body.Items[0].SessionID) + assert.True(t, body.Items[0].IsCurrent) + assert.Equal(t, "203.0.113.25", body.Items[0].IPAddress) + assert.Contains(t, body.Items[0].UserAgent, "Mozilla/5.0") + assert.Equal(t, "userfront", body.Items[0].ClientID) + assert.Equal(t, "UserFront", body.Items[0].AppName) + } + + mockKratos.AssertExpectations(t) +} + func TestDeleteMySession_Success(t *testing.T) { t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") var hydraRevokeCalls int diff --git a/userfront-e2e/tests/session-cross-browser-debug.spec.ts b/userfront-e2e/tests/session-cross-browser-debug.spec.ts new file mode 100644 index 00000000..b22a025a --- /dev/null +++ b/userfront-e2e/tests/session-cross-browser-debug.spec.ts @@ -0,0 +1,200 @@ +import { expect, test, type BrowserContext, type Page } from '@playwright/test'; + +const USERFRONT_BASE_URL = process.env.USERFRONT_BASE_URL ?? 'https://sso-test.hmac.kr'; +const ADMINFRONT_URL = process.env.ADMINFRONT_URL ?? 'http://localhost:5173'; +const LOGIN_ID = process.env.E2E_LOGIN_ID ?? ''; +const PASSWORD = process.env.E2E_PASSWORD ?? ''; + +type SessionApiResponse = { + items?: Array<{ + session_id?: string; + client_id?: string; + app_name?: string; + is_current?: boolean; + user_agent?: string; + ip_address?: string; + }>; +}; + +function ensureCredentials(): void { + if (!LOGIN_ID || !PASSWORD) { + test.skip(true, 'E2E credentials are required'); + } +} + +async function enableFlutterAccessibility(page: Page): Promise { + await page.waitForTimeout(300); + const button = page.getByRole('button', { name: 'Enable accessibility' }); + if (await button.count()) { + try { + await button.click({ force: true }); + } catch { + return; + } + const placeholder = page.locator('flt-semantics-placeholder'); + if (await placeholder.count()) { + await placeholder.first().click({ force: true }); + } + await page.waitForTimeout(800); + } +} + +async function clickPasswordTab(page: Page): Promise { + await page.waitForTimeout(900); + const pane = page.locator('flt-glass-pane'); + await pane.click({ + position: { x: 522, y: 158 }, + force: true, + }); + await page.waitForTimeout(120); + await pane.click({ + position: { x: 522, y: 158 }, + force: true, + }); + await page.waitForTimeout(200); +} + +async function fillAt(page: Page, x: number, y: number, value: string): Promise { + const pane = page.locator('flt-glass-pane'); + await pane.click({ position: { x, y }, force: true }); + await page.waitForTimeout(100); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); + await page.keyboard.type(value); +} + +async function loginViaUserFront(page: Page): Promise { + await page.waitForURL(/\/ko\/(signin|login)/, { timeout: 30_000 }); + const loginIdInput = page.getByPlaceholder(/이메일 또는 휴대폰 번호|email|phone/i); + const passwordInput = page.getByPlaceholder(/비밀번호|password/i); + const submitButton = page.getByRole('button', { name: /로그인|Login/i }); + + if ((await loginIdInput.count()) >= 1 && (await passwordInput.count()) >= 1) { + await loginIdInput.first().fill(LOGIN_ID); + await passwordInput.first().fill(PASSWORD); + await submitButton.click(); + return; + } + + await clickPasswordTab(page); + await fillAt(page, 640, 245, LOGIN_ID); + await fillAt(page, 640, 311, PASSWORD); + await page.locator('flt-glass-pane').click({ + position: { x: 640, y: 381 }, + force: true, + }); +} + +async function ensureConsentIfNeeded(page: Page): Promise { + if (!/\/ko\/consent/.test(page.url())) { + return; + } + + const allowButton = page + .getByRole('button') + .filter({ hasText: /허용|동의|Accept|Allow/i }) + .first(); + + if (await allowButton.count()) { + await allowButton.click({ force: true }); + } +} + +async function captureUserSessionsOnReload(page: Page): Promise { + const responsePromise = page.waitForResponse( + (response) => + response.request().method() === 'GET' && + response.url().includes('/api/v1/user/sessions'), + { timeout: 30_000 }, + ); + + await page.reload({ waitUntil: 'domcontentloaded' }); + const response = await responsePromise; + return (await response.json()) as SessionApiResponse; +} + +async function loginUserFront(context: BrowserContext): Promise { + const page = await context.newPage(); + await page.goto(`${USERFRONT_BASE_URL}/ko/signin`, { + waitUntil: 'domcontentloaded', + }); + await loginViaUserFront(page); + await expect(page).toHaveURL(/\/ko\/dashboard/, { timeout: 60_000 }); + return page; +} + +async function loginAdminFront(context: BrowserContext): Promise { + const page = await context.newPage(); + await page.goto(ADMINFRONT_URL, { waitUntil: 'domcontentloaded' }); + const ssoButton = page.getByRole('button', { name: /SSO 계정으로 로그인|SSO/i }); + if (await ssoButton.count()) { + await ssoButton.click({ force: true }); + await page.waitForTimeout(1500); + } + if (/\/login$/.test(page.url())) { + const authorizeUrl = await page.evaluate(() => { + const origin = window.location.origin; + const authority = 'https://sso-test.hmac.kr/oidc'; + const params = new URLSearchParams({ + client_id: 'adminfront', + redirect_uri: `${origin}/auth/callback`, + response_type: 'code', + scope: 'openid offline_access profile email', + state: `pw-${Date.now()}`, + nonce: `pw-${Date.now()}`, + code_challenge: 'test-code-challenge-test-code-challenge-test', + code_challenge_method: 'plain', + }); + return `${authority}/oauth2/auth?${params.toString()}`; + }); + await page.goto(authorizeUrl, { waitUntil: 'domcontentloaded' }); + } + await loginViaUserFront(page); + await ensureConsentIfNeeded(page); + await page.waitForURL(/localhost:5173|\/auth\/callback|\/dashboard|\/tenants/, { + timeout: 60_000, + }); + return page; +} + +test.describe('cross-browser session debug', () => { + test('userfront session card should map adminfront session metadata across contexts', async ({ + browser, + }, testInfo) => { + ensureCredentials(); + + const userfrontContext = await browser.newContext({ locale: 'ko-KR' }); + const adminfrontContext = await browser.newContext({ locale: 'ko-KR' }); + + const userfrontPage = await loginUserFront(userfrontContext); + const adminfrontPage = await loginAdminFront(adminfrontContext); + + const sessionsPayload = await captureUserSessionsOnReload(userfrontPage); + const items = sessionsPayload.items ?? []; + const adminfrontItems = items.filter((item) => + (item.client_id ?? '').toLowerCase().includes('adminfront'), + ); + const unknownCards = await userfrontPage.locator('text=세션 정보').allTextContents(); + const adminFrontCards = await userfrontPage.locator('text=AdminFront').allTextContents(); + + await testInfo.attach('user-sessions.json', { + body: JSON.stringify(sessionsPayload, null, 2), + contentType: 'application/json', + }); + await testInfo.attach('card-summary.json', { + body: JSON.stringify( + { + unknownCards, + adminFrontCards, + currentUrl: userfrontPage.url(), + adminfrontUrl: adminfrontPage.url(), + }, + null, + 2, + ), + contentType: 'application/json', + }); + + expect(adminfrontItems.length).toBeGreaterThan(0); + }); +}); From 8942c78fb427c5b0bf42a863dad137a3eaa04d85 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 11:17:17 +0900 Subject: [PATCH 05/39] =?UTF-8?q?=ED=99=9C=EC=84=9C=20=EC=84=B8=EC=85=98?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C=20audit=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EA=B8=B0=EB=A1=9D=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 169 +++++++++++++++--- .../presentation/dashboard_screen.dart | 39 ++-- 2 files changed, 173 insertions(+), 35 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 1810dea7..c3b84696 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -17,6 +17,7 @@ import ( "io" "log/slog" "math/rand" + "net" "net/http" "net/url" "os" @@ -2426,6 +2427,8 @@ func (h *AuthHandler) HeadlessPasswordLogin(c *fiber.Ctx) error { return errorJSONCode(c, status, code, message) } + c.Locals("user_id", authInfo.Subject) + c.Locals("login_id", loginID) setSessionIDLocal(c, authInfo.SessionToken) acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), loginChallenge, authInfo.Subject) @@ -2732,7 +2735,15 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { ale.Status = fiber.StatusOK ale.LatencyMs = time.Since(startTime) + c.Locals("user_id", authInfo.Subject) + c.Locals("login_id", loginID) setSessionIDLocal(c, authInfo.SessionToken) + if req.LoginChallenge == "" { + attachAuditClientDetails(c, domain.HydraClient{ + ClientID: "userfront", + ClientName: "UserFront", + }) + } ale.Log(slog.LevelInfo, "Login successful", slog.String("provider", h.IdpProvider.Name()), slog.String("subject", authInfo.Subject)) // --- OIDC 로그인 흐름 처리 --- @@ -2741,11 +2752,14 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { // Check if the client is active loginReq, err := h.Hydra.GetLoginRequest(c.Context(), req.LoginChallenge) - if err == nil && loginReq != nil && loginReq.Client.Metadata != nil { - if status, ok := loginReq.Client.Metadata["status"].(string); ok { - if strings.ToLower(status) == "inactive" { - slog.Warn("Login rejected for inactive client in PasswordLogin", "client_id", loginReq.Client.ClientID) - return fiber.NewError(fiber.StatusForbidden, "The client application is disabled.") + if err == nil && loginReq != nil { + attachAuditClientDetails(c, loginReq.Client) + if loginReq.Client.Metadata != nil { + if status, ok := loginReq.Client.Metadata["status"].(string); ok { + if strings.ToLower(status) == "inactive" { + slog.Warn("Login rejected for inactive client in PasswordLogin", "client_id", loginReq.Client.ClientID) + return fiber.NewError(fiber.StatusForbidden, "The client application is disabled.") + } } } } @@ -2778,6 +2792,27 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { return c.JSON(resp) } +func attachAuditClientDetails(c *fiber.Ctx, client domain.HydraClient) { + if c == nil { + return + } + + clientID := strings.TrimSpace(client.ClientID) + if clientID == "" { + return + } + + clientName := strings.TrimSpace(client.ClientName) + if clientName == "" { + clientName = clientID + } + + c.Locals("audit_details_extra", map[string]any{ + "client_id": clientID, + "client_name": clientName, + }) +} + // InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 이메일 또는 SMS를 보냅니다. func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { startTime := time.Now() @@ -4983,18 +5018,7 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { // Check if the client is active loginReq, err := h.Hydra.GetLoginRequest(c.Context(), req.LoginChallenge) if err == nil && loginReq != nil { - // Audit 상세 정보 보강: OIDC 로그인 시점에 client 정보를 저장 - clientID := strings.TrimSpace(loginReq.Client.ClientID) - if clientID != "" { - clientName := strings.TrimSpace(loginReq.Client.ClientName) - if clientName == "" { - clientName = clientID - } - c.Locals("audit_details_extra", map[string]any{ - "client_id": clientID, - "client_name": clientName, - }) - } + attachAuditClientDetails(c, loginReq.Client) if loginReq.Client.Metadata != nil { if status, ok := loginReq.Client.Metadata["status"].(string); ok { @@ -6786,6 +6810,9 @@ func (h *AuthHandler) ListMySessions(c *fiber.Ctx) error { if item.IPAddress == "" && len(session.Devices) > 0 { item.IPAddress = strings.TrimSpace(session.Devices[0].IPAddress) } + if item.IsCurrent { + applyCurrentSessionRequestHints(c, &item) + } items = append(items, item) } @@ -6981,6 +7008,46 @@ func (h *AuthHandler) resolveCurrentSessionID(c *fiber.Ctx) string { return "" } +func applyCurrentSessionRequestHints(c *fiber.Ctx, item *userSessionItem) { + if c == nil || item == nil || !item.IsCurrent { + return + } + + if item.IPAddress == "" { + item.IPAddress = strings.TrimSpace(resolveRequestClientIP(c)) + } + if item.UserAgent == "" { + userAgent := strings.TrimSpace(c.Get("User-Agent")) + if !looksLikeInternalUserAgent(userAgent) { + item.UserAgent = userAgent + } + } + if strings.TrimSpace(item.ClientID) == "" { + item.ClientID = "userfront" + } + if strings.TrimSpace(item.AppName) == "" { + item.AppName = "UserFront" + } +} + +func resolveRequestClientIP(c *fiber.Ctx) string { + if c == nil { + return "" + } + if forwarded := c.Get("X-Forwarded-For"); forwarded != "" { + parts := strings.Split(forwarded, ",") + if len(parts) > 0 { + if ip := strings.TrimSpace(parts[0]); ip != "" { + return ip + } + } + } + if realIP := strings.TrimSpace(c.Get("X-Real-IP")); realIP != "" { + return realIP + } + return c.IP() +} + func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string) map[string]sessionAuditHint { hints := make(map[string]sessionAuditHint) if h.AuditRepo == nil || strings.TrimSpace(userID) == "" { @@ -6993,6 +7060,7 @@ func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string) "link_login_success", "code_login_success", "password_login_success", + "consent.granted", "POST /api/v1/auth/oidc/login/accept", "POST /api/v1/auth/password/login", "POST /api/v1/auth/magic-link/verify", @@ -7016,11 +7084,6 @@ func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string) continue } - existing, ok := hints[sessionID] - if ok && existing.Timestamp != nil && existing.Timestamp.After(log.Timestamp) { - continue - } - ts := log.Timestamp ipAddress := strings.TrimSpace(log.IPAddress) userAgent := strings.TrimSpace(log.UserAgent) @@ -7036,17 +7099,75 @@ func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string) if looksLikeInternalUserAgent(userAgent) { userAgent = "" } - hints[sessionID] = sessionAuditHint{ + hints[sessionID] = mergeSessionAuditHint(hints[sessionID], sessionAuditHint{ Timestamp: &ts, IPAddress: ipAddress, UserAgent: userAgent, ClientID: clientID, AppName: appName, - } + }) } return hints } +func mergeSessionAuditHint(existing sessionAuditHint, candidate sessionAuditHint) sessionAuditHint { + if candidate.Timestamp != nil && + (existing.Timestamp == nil || candidate.Timestamp.After(*existing.Timestamp)) { + existing.Timestamp = candidate.Timestamp + } + if shouldReplaceSessionIP(existing.IPAddress, candidate.IPAddress) { + existing.IPAddress = candidate.IPAddress + } + if existing.UserAgent == "" && candidate.UserAgent != "" { + existing.UserAgent = candidate.UserAgent + } + if existing.ClientID == "" && candidate.ClientID != "" { + existing.ClientID = candidate.ClientID + } + if existing.AppName == "" && candidate.AppName != "" { + existing.AppName = candidate.AppName + } + return existing +} + +func shouldReplaceSessionIP(existing string, candidate string) bool { + existing = strings.TrimSpace(existing) + candidate = strings.TrimSpace(candidate) + if candidate == "" { + return false + } + if existing == "" { + return true + } + if isPrivateIPAddress(existing) && !isPrivateIPAddress(candidate) { + return true + } + return false +} + +func isPrivateIPAddress(raw string) bool { + ip := net.ParseIP(strings.TrimSpace(raw)) + if ip == nil { + return false + } + if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() { + return true + } + for _, cidr := range []string{ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "100.64.0.0/10", + "fc00::/7", + } { + _, network, err := net.ParseCIDR(cidr) + if err == nil && network.Contains(ip) { + return true + } + } + return false +} + func deriveSessionClientInfo(log domain.AuditLog) (string, string) { details, _ := parseAuditDetails(log.Details) clientID := "" diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 777f43ca..40490b52 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -1201,25 +1201,42 @@ class _DashboardScreenState extends ConsumerState { } String _sessionPrimaryLabel(UserSessionSummary session) { - if (session.isCurrent) { - return tr('ui.userfront.dashboard.sessions.current_badge'); + final appLabel = _sessionAppLabel(session); + if (appLabel.isNotEmpty) { + return appLabel; } - final appName = session.appName.trim(); - if (appName.isNotEmpty) { - return appName; + if (session.isCurrent) { + return 'UserFront'; } return tr('ui.userfront.dashboard.sessions.unknown_session'); } String _sessionClientLabel(UserSessionSummary session) { + return ''; + } + + String _sessionAppLabel(UserSessionSummary session) { final appName = session.appName.trim(); - if (appName.isEmpty || session.isCurrent) { - return ''; + if (appName.isNotEmpty) { + return appName; } - return tr( - 'msg.userfront.dashboard.sessions.recent_app', - params: {'app': appName}, - ); + final clientId = session.clientId.trim().toLowerCase(); + if (clientId.isEmpty) { + return session.isCurrent ? 'UserFront' : ''; + } + if (clientId.contains('adminfront')) { + return 'AdminFront'; + } + if (clientId.contains('devfront')) { + return 'DevFront'; + } + if (clientId.contains('userfront')) { + return 'UserFront'; + } + if (clientId.contains('baron')) { + return tr('ui.userfront.app_label.baron'); + } + return session.clientId.trim(); } String _sessionUserAgentLabel(String userAgent) { From 6a3bb19e7dd89454bebe04f08f399913fd1a59a7 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 13:19:05 +0900 Subject: [PATCH 06/39] =?UTF-8?q?=EC=84=B8=EC=85=98=20=EB=A7=8C=EB=A3=8C?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=ED=86=A0=EA=B8=80=20=EB=8F=99=EC=9E=91?= =?UTF-8?q?=EC=9D=84=20=EC=8B=A4=EC=A0=9C=20=EC=A0=95=EC=B1=85=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/AppLayout.tsx | 51 +++++++++++++++++- adminfront/src/lib/auth.ts | 2 +- adminfront/src/lib/sessionSliding.test.ts | 53 +++++++++++++++++++ adminfront/src/lib/sessionSliding.ts | 31 +++++++++++ devfront/src/components/layout/AppLayout.tsx | 51 +++++++++++++++++- devfront/src/lib/auth.ts | 2 +- devfront/src/lib/sessionSliding.ts | 31 +++++++++++ 7 files changed, 217 insertions(+), 4 deletions(-) diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index 62a5e680..f82e6ea7 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -19,7 +19,10 @@ import { useAuth } from "react-oidc-context"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { fetchMe } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; -import { shouldAttemptSlidingSessionRenew } from "../../lib/sessionSliding"; +import { + shouldAttemptSlidingSessionRenew, + shouldAttemptUnlimitedSessionRenew, +} from "../../lib/sessionSliding"; import LanguageSelector from "../common/LanguageSelector"; import RoleSwitcher from "./RoleSwitcher"; @@ -221,6 +224,52 @@ function AppLayout() { isSessionExpiryEnabled, ]); + useEffect(() => { + const maybeKeepSessionAlive = async () => { + const now = Date.now(); + if ( + !shouldAttemptUnlimitedSessionRenew({ + expiresAtSec: auth.user?.expires_at, + nowMs: now, + isEnabled: isSessionExpiryEnabled, + isAuthenticated: auth.isAuthenticated, + isLoading: auth.isLoading, + isRenewInFlight: isRenewInFlightRef.current, + lastAttemptAtMs: lastRenewAttemptAtRef.current, + }) + ) { + return; + } + + isRenewInFlightRef.current = true; + lastRenewAttemptAtRef.current = now; + + try { + await auth.signinSilent(); + } catch (error) { + console.error("세션 무제한 유지 갱신에 실패했습니다.", error); + } finally { + isRenewInFlightRef.current = false; + } + }; + + const timer = window.setInterval(() => { + void maybeKeepSessionAlive(); + }, 30_000); + + void maybeKeepSessionAlive(); + + return () => { + window.clearInterval(timer); + }; + }, [ + auth, + auth.isAuthenticated, + auth.isLoading, + auth.user?.expires_at, + isSessionExpiryEnabled, + ]); + useEffect(() => { const routeKey = `${location.pathname}${location.search}${location.hash}`; if (lastVisitedRouteRef.current === null) { diff --git a/adminfront/src/lib/auth.ts b/adminfront/src/lib/auth.ts index 8f46d964..aab02a2b 100644 --- a/adminfront/src/lib/auth.ts +++ b/adminfront/src/lib/auth.ts @@ -10,7 +10,7 @@ export const oidcConfig: AuthProviderProps = { scope: "openid offline_access profile email", // offline_access for refresh token post_logout_redirect_uri: window.location.origin, userStore: new WebStorageStateStore({ store: window.localStorage }), - automaticSilentRenew: true, + automaticSilentRenew: false, }; export const userManager = new UserManager({ diff --git a/adminfront/src/lib/sessionSliding.test.ts b/adminfront/src/lib/sessionSliding.test.ts index 410ac63e..cce36661 100644 --- a/adminfront/src/lib/sessionSliding.test.ts +++ b/adminfront/src/lib/sessionSliding.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { SESSION_RENEW_THRESHOLD_MS, shouldAttemptSlidingSessionRenew, + shouldAttemptUnlimitedSessionRenew, } from "./sessionSliding"; describe("shouldAttemptSlidingSessionRenew", () => { @@ -71,3 +72,55 @@ describe("shouldAttemptSlidingSessionRenew", () => { ).toBe(false); }); }); + +describe("shouldAttemptUnlimitedSessionRenew", () => { + const nowMs = 1_700_000_000_000; + + it("returns false when unlimited mode is not active", () => { + expect( + shouldAttemptUnlimitedSessionRenew({ + expiresAtSec: Math.floor( + (nowMs + SESSION_RENEW_THRESHOLD_MS - 1_000) / 1000, + ), + nowMs, + isEnabled: true, + isAuthenticated: true, + isLoading: false, + isRenewInFlight: false, + lastAttemptAtMs: 0, + }), + ).toBe(false); + }); + + it("returns true near expiry when session expiry management is disabled", () => { + expect( + shouldAttemptUnlimitedSessionRenew({ + expiresAtSec: Math.floor( + (nowMs + SESSION_RENEW_THRESHOLD_MS - 1_000) / 1000, + ), + nowMs, + isEnabled: false, + isAuthenticated: true, + isLoading: false, + isRenewInFlight: false, + lastAttemptAtMs: 0, + }), + ).toBe(true); + }); + + it("returns false when the token still has enough remaining lifetime", () => { + expect( + shouldAttemptUnlimitedSessionRenew({ + expiresAtSec: Math.floor( + (nowMs + SESSION_RENEW_THRESHOLD_MS + 1_000) / 1000, + ), + nowMs, + isEnabled: false, + isAuthenticated: true, + isLoading: false, + isRenewInFlight: false, + lastAttemptAtMs: 0, + }), + ).toBe(false); + }); +}); diff --git a/adminfront/src/lib/sessionSliding.ts b/adminfront/src/lib/sessionSliding.ts index 7096e7f3..be152778 100644 --- a/adminfront/src/lib/sessionSliding.ts +++ b/adminfront/src/lib/sessionSliding.ts @@ -43,3 +43,34 @@ export function shouldAttemptSlidingSessionRenew({ return true; } + +export function shouldAttemptUnlimitedSessionRenew({ + expiresAtSec, + nowMs, + isEnabled, + isAuthenticated, + isLoading, + isRenewInFlight, + lastAttemptAtMs, + thresholdMs = SESSION_RENEW_THRESHOLD_MS, + throttleMs = SESSION_RENEW_THROTTLE_MS, +}: SlidingSessionRenewDecisionParams) { + if (isEnabled || !isAuthenticated || isLoading || isRenewInFlight) { + return false; + } + + if (typeof expiresAtSec !== "number") { + return false; + } + + const remainingMs = expiresAtSec * 1000 - nowMs; + if (remainingMs <= 0 || remainingMs > thresholdMs) { + return false; + } + + if (nowMs - lastAttemptAtMs < throttleMs) { + return false; + } + + return true; +} diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx index 21c43d00..4e0eb33b 100644 --- a/devfront/src/components/layout/AppLayout.tsx +++ b/devfront/src/components/layout/AppLayout.tsx @@ -15,7 +15,10 @@ import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { fetchMe } from "../../features/auth/authApi"; import { t } from "../../lib/i18n"; import { resolveProfileRole } from "../../lib/role"; -import { shouldAttemptSlidingSessionRenew } from "../../lib/sessionSliding"; +import { + shouldAttemptSlidingSessionRenew, + shouldAttemptUnlimitedSessionRenew, +} from "../../lib/sessionSliding"; import LanguageSelector from "../common/LanguageSelector"; import { Toaster } from "../ui/toaster"; @@ -151,6 +154,52 @@ function AppLayout() { isSessionExpiryEnabled, ]); + useEffect(() => { + const maybeKeepSessionAlive = async () => { + const now = Date.now(); + if ( + !shouldAttemptUnlimitedSessionRenew({ + expiresAtSec: auth.user?.expires_at, + nowMs: now, + isEnabled: isSessionExpiryEnabled, + isAuthenticated: auth.isAuthenticated, + isLoading: auth.isLoading, + isRenewInFlight: isRenewInFlightRef.current, + lastAttemptAtMs: lastRenewAttemptAtRef.current, + }) + ) { + return; + } + + isRenewInFlightRef.current = true; + lastRenewAttemptAtRef.current = now; + + try { + await auth.signinSilent(); + } catch (error) { + console.error("세션 무제한 유지 갱신에 실패했습니다.", error); + } finally { + isRenewInFlightRef.current = false; + } + }; + + const timer = window.setInterval(() => { + void maybeKeepSessionAlive(); + }, 30_000); + + void maybeKeepSessionAlive(); + + return () => { + window.clearInterval(timer); + }; + }, [ + auth, + auth.isAuthenticated, + auth.isLoading, + auth.user?.expires_at, + isSessionExpiryEnabled, + ]); + useEffect(() => { const routeKey = `${location.pathname}${location.search}${location.hash}`; if (lastVisitedRouteRef.current === null) { diff --git a/devfront/src/lib/auth.ts b/devfront/src/lib/auth.ts index f424d9d9..d0f0772e 100644 --- a/devfront/src/lib/auth.ts +++ b/devfront/src/lib/auth.ts @@ -11,7 +11,7 @@ export const oidcConfig: AuthProviderProps = { post_logout_redirect_uri: window.location.origin, popup_redirect_uri: `${window.location.origin}/auth/callback`, userStore: new WebStorageStateStore({ store: window.localStorage }), - automaticSilentRenew: true, + automaticSilentRenew: false, }; export const userManager = new UserManager({ diff --git a/devfront/src/lib/sessionSliding.ts b/devfront/src/lib/sessionSliding.ts index 7096e7f3..be152778 100644 --- a/devfront/src/lib/sessionSliding.ts +++ b/devfront/src/lib/sessionSliding.ts @@ -43,3 +43,34 @@ export function shouldAttemptSlidingSessionRenew({ return true; } + +export function shouldAttemptUnlimitedSessionRenew({ + expiresAtSec, + nowMs, + isEnabled, + isAuthenticated, + isLoading, + isRenewInFlight, + lastAttemptAtMs, + thresholdMs = SESSION_RENEW_THRESHOLD_MS, + throttleMs = SESSION_RENEW_THROTTLE_MS, +}: SlidingSessionRenewDecisionParams) { + if (isEnabled || !isAuthenticated || isLoading || isRenewInFlight) { + return false; + } + + if (typeof expiresAtSec !== "number") { + return false; + } + + const remainingMs = expiresAtSec * 1000 - nowMs; + if (remainingMs <= 0 || remainingMs > thresholdMs) { + return false; + } + + if (nowMs - lastAttemptAtMs < throttleMs) { + return false; + } + + return true; +} From 2ca26cafb29616fa55a857e1cfb96028b9341076 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 13:25:36 +0900 Subject: [PATCH 07/39] =?UTF-8?q?=EC=84=B8=EC=85=98=20IP=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EC=99=80=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 48 +--------- .../handler/auth_handler_sessions_test.go | 2 +- .../internal/middleware/audit_middleware.go | 14 +-- .../middleware/audit_middleware_test.go | 24 +++++ backend/internal/utils/client_ip.go | 87 +++++++++++++++++++ backend/internal/utils/client_ip_test.go | 24 +++++ .../lib/core/services/auth_proxy_service.dart | 35 ++++++++ .../lib/core/services/logout_service.dart | 39 +++++++++ .../presentation/dashboard_screen.dart | 4 +- .../presentation/pages/profile_page.dart | 4 +- userfront/test/logout_service_test.dart | 74 ++++++++++++++++ 11 files changed, 292 insertions(+), 63 deletions(-) create mode 100644 backend/internal/utils/client_ip.go create mode 100644 backend/internal/utils/client_ip_test.go create mode 100644 userfront/lib/core/services/logout_service.dart create mode 100644 userfront/test/logout_service_test.dart diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index c3b84696..2dcac48c 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -17,7 +17,6 @@ import ( "io" "log/slog" "math/rand" - "net" "net/http" "net/url" "os" @@ -4043,18 +4042,7 @@ func extractClientIPFromHeaders(c *fiber.Ctx) string { if c == nil { return "" } - if forwarded := c.Get("X-Forwarded-For"); forwarded != "" { - parts := strings.Split(forwarded, ",") - if len(parts) > 0 { - if ip := strings.TrimSpace(parts[0]); ip != "" { - return ip - } - } - } - if realIP := strings.TrimSpace(c.Get("X-Real-IP")); realIP != "" { - return realIP - } - return c.IP() + return utils.ResolveClientIP(c.Get("X-Forwarded-For"), c.Get("X-Real-IP"), c.IP()) } type authTimelineItem struct { @@ -7034,18 +7022,7 @@ func resolveRequestClientIP(c *fiber.Ctx) string { if c == nil { return "" } - if forwarded := c.Get("X-Forwarded-For"); forwarded != "" { - parts := strings.Split(forwarded, ",") - if len(parts) > 0 { - if ip := strings.TrimSpace(parts[0]); ip != "" { - return ip - } - } - } - if realIP := strings.TrimSpace(c.Get("X-Real-IP")); realIP != "" { - return realIP - } - return c.IP() + return utils.ResolveClientIP(c.Get("X-Forwarded-For"), c.Get("X-Real-IP"), c.IP()) } func (h *AuthHandler) loadSessionAuditHints(ctx context.Context, userID string) map[string]sessionAuditHint { @@ -7146,26 +7123,7 @@ func shouldReplaceSessionIP(existing string, candidate string) bool { } func isPrivateIPAddress(raw string) bool { - ip := net.ParseIP(strings.TrimSpace(raw)) - if ip == nil { - return false - } - if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() { - return true - } - for _, cidr := range []string{ - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "100.64.0.0/10", - "fc00::/7", - } { - _, network, err := net.ParseCIDR(cidr) - if err == nil && network.Contains(ip) { - return true - } - } - return false + return utils.IsPrivateOrReservedIP(raw) } func deriveSessionClientInfo(log domain.AuditLog) (string, string) { diff --git a/backend/internal/handler/auth_handler_sessions_test.go b/backend/internal/handler/auth_handler_sessions_test.go index 39e6f599..928468ee 100644 --- a/backend/internal/handler/auth_handler_sessions_test.go +++ b/backend/internal/handler/auth_handler_sessions_test.go @@ -317,7 +317,7 @@ func TestListMySessions_CurrentSessionFallsBackToRequestMetadata(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil) req.Header.Set("Cookie", "ory_kratos_session=valid") req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/146.0.0.0 Safari/537.36") - req.Header.Set("X-Forwarded-For", "203.0.113.25") + req.Header.Set("X-Forwarded-For", "100.100.100.1, 203.0.113.25") resp, err := app.Test(req, -1) assert.NoError(t, err) diff --git a/backend/internal/middleware/audit_middleware.go b/backend/internal/middleware/audit_middleware.go index 59746e1d..a0c5c6fe 100644 --- a/backend/internal/middleware/audit_middleware.go +++ b/backend/internal/middleware/audit_middleware.go @@ -7,7 +7,6 @@ import ( "fmt" "log/slog" "reflect" - "strings" "sync" "time" @@ -217,16 +216,5 @@ func AuditMiddleware(config AuditConfig) fiber.Handler { } func extractClientIP(c *fiber.Ctx) string { - if forwarded := c.Get("X-Forwarded-For"); forwarded != "" { - parts := strings.Split(forwarded, ",") - if len(parts) > 0 { - if ip := strings.TrimSpace(parts[0]); ip != "" { - return ip - } - } - } - if realIP := strings.TrimSpace(c.Get("X-Real-IP")); realIP != "" { - return realIP - } - return c.IP() + return utils.ResolveClientIP(c.Get("X-Forwarded-For"), c.Get("X-Real-IP"), c.IP()) } diff --git a/backend/internal/middleware/audit_middleware_test.go b/backend/internal/middleware/audit_middleware_test.go index 9998429b..d553ad40 100644 --- a/backend/internal/middleware/audit_middleware_test.go +++ b/backend/internal/middleware/audit_middleware_test.go @@ -117,6 +117,30 @@ func TestAuditMiddleware(t *testing.T) { mockRepo.AssertExpectations(t) }) + t.Run("POST request - Prefer public forwarded IP", func(t *testing.T) { + app := fiber.New() + mockRepo := new(MockAuditRepository) + + app.Use(AuditMiddleware(AuditConfig{ + Repo: mockRepo, + })) + + app.Post("/test", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + mockRepo.On("Create", mock.MatchedBy(func(log *domain.AuditLog) bool { + return log.IPAddress == "203.0.113.25" + })).Return(nil) + + req := httptest.NewRequest("POST", "/test", nil) + req.Header.Set("X-Forwarded-For", "100.100.100.1, 203.0.113.25") + + resp, _ := app.Test(req) + assert.Equal(t, fiber.StatusOK, resp.StatusCode) + mockRepo.AssertExpectations(t) + }) + t.Run("POST request - Sync Failure (Strict Mode)", func(t *testing.T) { app := fiber.New() mockRepo := new(MockAuditRepository) diff --git a/backend/internal/utils/client_ip.go b/backend/internal/utils/client_ip.go new file mode 100644 index 00000000..897cc1e6 --- /dev/null +++ b/backend/internal/utils/client_ip.go @@ -0,0 +1,87 @@ +package utils + +import ( + "net" + "strings" +) + +// ResolveClientIP selects the best client IP from proxy headers and the remote address. +// It prefers a public IP from X-Forwarded-For, then X-Real-IP, and finally the remote IP. +func ResolveClientIP(forwardedFor, realIP, remoteIP string) string { + forwardedCandidates := splitClientIPs(forwardedFor) + if ip := firstPublicIP(forwardedCandidates); ip != "" { + return ip + } + if ip := normalizeIP(realIP); ip != "" && !IsPrivateOrReservedIP(ip) { + return ip + } + if ip := normalizeIP(remoteIP); ip != "" && !IsPrivateOrReservedIP(ip) { + return ip + } + if len(forwardedCandidates) > 0 { + return forwardedCandidates[0] + } + if ip := normalizeIP(realIP); ip != "" { + return ip + } + return normalizeIP(remoteIP) +} + +// IsPrivateOrReservedIP reports whether the IP is private or from a non-public network range. +func IsPrivateOrReservedIP(raw string) bool { + ip := net.ParseIP(strings.TrimSpace(raw)) + if ip == nil { + return false + } + if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() { + return true + } + for _, cidr := range []string{ + "100.64.0.0/10", + "fc00::/7", + } { + _, network, err := net.ParseCIDR(cidr) + if err == nil && network.Contains(ip) { + return true + } + } + return false +} + +func splitClientIPs(forwardedFor string) []string { + if strings.TrimSpace(forwardedFor) == "" { + return nil + } + parts := strings.Split(forwardedFor, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + if ip := normalizeIP(part); ip != "" { + result = append(result, ip) + } + } + return result +} + +func firstPublicIP(candidates []string) string { + for _, candidate := range candidates { + if !IsPrivateOrReservedIP(candidate) { + return candidate + } + } + return "" +} + +func normalizeIP(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + if host, _, err := net.SplitHostPort(raw); err == nil { + raw = host + } + ip := net.ParseIP(raw) + if ip == nil { + return "" + } + return ip.String() +} diff --git a/backend/internal/utils/client_ip_test.go b/backend/internal/utils/client_ip_test.go new file mode 100644 index 00000000..8128fc87 --- /dev/null +++ b/backend/internal/utils/client_ip_test.go @@ -0,0 +1,24 @@ +package utils + +import "testing" + +func TestResolveClientIP_PrefersPublicForwardedIP(t *testing.T) { + got := ResolveClientIP("100.100.100.1, 203.0.113.25, 10.0.0.2", "", "172.18.0.5") + if got != "203.0.113.25" { + t.Fatalf("expected public forwarded IP, got %q", got) + } +} + +func TestResolveClientIP_FallsBackToFirstForwardedWhenAllPrivate(t *testing.T) { + got := ResolveClientIP("100.100.100.1, 10.0.0.2", "192.168.0.10", "172.18.0.5") + if got != "100.100.100.1" { + t.Fatalf("expected first forwarded private IP, got %q", got) + } +} + +func TestResolveClientIP_PrefersPublicRealIPOverPrivateForwarded(t *testing.T) { + got := ResolveClientIP("100.100.100.1, 10.0.0.2", "198.51.100.7", "172.18.0.5") + if got != "198.51.100.7" { + t.Fatalf("expected public real IP, got %q", got) + } +} diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart index 57eec236..2e3ae269 100644 --- a/userfront/lib/core/services/auth_proxy_service.dart +++ b/userfront/lib/core/services/auth_proxy_service.dart @@ -264,6 +264,41 @@ class AuthProxyService { } } + static Future fetchCurrentSessionId() async { + final url = Uri.parse('$_baseUrl/api/v1/user/sessions'); + final useCookie = AuthTokenStore.usesCookie(); + final token = AuthTokenStore.getToken(); + final client = createHttpClient(withCredentials: useCookie); + try { + final headers = {'Content-Type': 'application/json'}; + if (!useCookie && token != null && token.isNotEmpty) { + headers['Authorization'] = 'Bearer $token'; + } + final response = await client.get(url, headers: headers); + if (response.statusCode != 200) { + throw _error( + 'err.userfront.dashboard.sessions.load', + '활성 세션을 불러오지 못했습니다: {{error}}', + detail: response.body, + ); + } + + final body = jsonDecode(response.body) as Map; + final items = (body['items'] as List?) ?? const []; + for (final item in items.whereType>()) { + if (item['is_current'] == true) { + final sessionId = item['session_id']?.toString().trim() ?? ''; + if (sessionId.isNotEmpty) { + return sessionId; + } + } + } + return null; + } finally { + client.close(); + } + } + static Future> verifyLoginShortCode( String shortCode, { bool verifyOnly = false, diff --git a/userfront/lib/core/services/logout_service.dart b/userfront/lib/core/services/logout_service.dart new file mode 100644 index 00000000..38de877d --- /dev/null +++ b/userfront/lib/core/services/logout_service.dart @@ -0,0 +1,39 @@ +import '../notifiers/auth_notifier.dart'; +import 'auth_proxy_service.dart'; +import 'auth_token_store.dart'; + +typedef CurrentSessionLoader = Future Function(); +typedef SessionRevoker = Future Function(String sessionId); +typedef LogoutCallback = void Function(); + +class LogoutService { + LogoutService({ + CurrentSessionLoader? loadCurrentSessionId, + SessionRevoker? revokeSession, + LogoutCallback? clearAuth, + LogoutCallback? notifyAuthChanged, + }) : _loadCurrentSessionId = + loadCurrentSessionId ?? AuthProxyService.fetchCurrentSessionId, + _revokeSession = revokeSession ?? AuthProxyService.revokeSession, + _clearAuth = clearAuth ?? AuthTokenStore.clear, + _notifyAuthChanged = notifyAuthChanged ?? AuthNotifier.instance.notify; + + final CurrentSessionLoader _loadCurrentSessionId; + final SessionRevoker _revokeSession; + final LogoutCallback _clearAuth; + final LogoutCallback _notifyAuthChanged; + + Future logout() async { + try { + final currentSessionId = await _loadCurrentSessionId(); + if (currentSessionId != null && currentSessionId.isNotEmpty) { + await _revokeSession(currentSessionId); + } + } catch (_) { + // 서버 세션 종료는 best-effort로 처리하고, 로컬 로그아웃은 계속 진행합니다. + } finally { + _clearAuth(); + _notifyAuthChanged(); + } + } +} diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 40490b52..cbbbc9fa 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -12,6 +12,7 @@ import '../domain/providers/linked_rps_provider.dart'; import '../domain/providers/user_sessions_provider.dart'; import '../../../../core/notifiers/auth_notifier.dart'; import '../../../../core/services/auth_proxy_service.dart'; +import '../../../../core/services/logout_service.dart'; import '../../../../core/services/auth_token_store.dart'; import '../../../../core/services/http_client.dart'; import '../../../../core/i18n/locale_utils.dart'; @@ -74,8 +75,7 @@ class _DashboardScreenState extends ConsumerState { } Future _logout() async { - AuthTokenStore.clear(); - AuthNotifier.instance.notify(); + await LogoutService().logout(); } Future _onRevokeLink(String clientId, String appName) async { diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index 80b80ae3..bf8f6d50 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -6,6 +6,7 @@ import 'package:userfront/i18n.dart'; import '../../../../core/notifiers/auth_notifier.dart'; import '../../../../core/i18n/locale_utils.dart'; import '../../../../core/services/auth_proxy_service.dart'; +import '../../../../core/services/logout_service.dart'; import '../../../../core/services/auth_token_store.dart'; import '../../../../core/ui/layout_breakpoints.dart'; import '../../../../core/ui/toast_service.dart'; @@ -164,8 +165,7 @@ class _ProfilePageState extends ConsumerState { } Future _logout() async { - AuthTokenStore.clear(); - AuthNotifier.instance.notify(); + await LogoutService().logout(); } void _ensureControllers(UserProfile profile) { diff --git a/userfront/test/logout_service_test.dart b/userfront/test/logout_service_test.dart new file mode 100644 index 00000000..b9cdc5ec --- /dev/null +++ b/userfront/test/logout_service_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:userfront/core/services/logout_service.dart'; + +void main() { + test('현재 세션이 있으면 서버 세션 종료 후 로컬 로그아웃을 진행한다', () async { + final events = []; + final service = LogoutService( + loadCurrentSessionId: () async { + events.add('load'); + return 'current-sid'; + }, + revokeSession: (sessionId) async { + events.add('revoke:$sessionId'); + }, + clearAuth: () { + events.add('clear'); + }, + notifyAuthChanged: () { + events.add('notify'); + }, + ); + + await service.logout(); + + expect(events, ['load', 'revoke:current-sid', 'clear', 'notify']); + }); + + test('현재 세션이 없으면 서버 세션 종료 없이 로컬 로그아웃만 진행한다', () async { + final events = []; + final service = LogoutService( + loadCurrentSessionId: () async { + events.add('load'); + return null; + }, + revokeSession: (sessionId) async { + events.add('revoke:$sessionId'); + }, + clearAuth: () { + events.add('clear'); + }, + notifyAuthChanged: () { + events.add('notify'); + }, + ); + + await service.logout(); + + expect(events, ['load', 'clear', 'notify']); + }); + + test('서버 세션 종료가 실패해도 로컬 로그아웃은 계속 진행한다', () async { + final events = []; + final service = LogoutService( + loadCurrentSessionId: () async { + events.add('load'); + return 'current-sid'; + }, + revokeSession: (sessionId) async { + events.add('revoke:$sessionId'); + throw Exception('revoke failed'); + }, + clearAuth: () { + events.add('clear'); + }, + notifyAuthChanged: () { + events.add('notify'); + }, + ); + + await service.logout(); + + expect(events, ['load', 'revoke:current-sid', 'clear', 'notify']); + }); +} From 4ad75183280ed9bda102c746f4bae8d09843c6ed Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 14:20:20 +0900 Subject: [PATCH 08/39] =?UTF-8?q?devfront=20=EC=84=B8=EC=85=98=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/lib/apiClient.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/devfront/src/lib/apiClient.ts b/devfront/src/lib/apiClient.ts index 49b83cee..5a28cc72 100644 --- a/devfront/src/lib/apiClient.ts +++ b/devfront/src/lib/apiClient.ts @@ -27,17 +27,23 @@ apiClient.interceptors.request.use(async (config) => { apiClient.interceptors.response.use( (response) => response, async (error) => { - if (error.response?.status === 401) { - // 401 발생 시 로그인 페이지로 리다이렉트 - const isAuthPath = window.location.pathname.startsWith("/auth/callback"); - const isLoginPath = window.location.pathname === "/login"; - const user = await userManager.getUser(); - // 인증 토큰이 없는 경우에만 로그인으로 보낸다. - // 토큰이 있는데 401이면 권한/백엔드 정책 이슈로 간주하고 화면에서 에러를 노출한다. - const hasAccessToken = Boolean(user?.access_token); - if (!hasAccessToken && !isAuthPath && !isLoginPath) { - window.location.href = "/login"; - } + const status = error.response?.status; + const message = + error.response?.data?.error?.toString().toLowerCase() ?? + error.response?.data?.message?.toString().toLowerCase() ?? + ""; + const isAuthPath = window.location.pathname.startsWith("/auth/callback"); + const isLoginPath = window.location.pathname === "/login"; + const shouldRedirectToLogin = + status === 401 || + (status === 403 && + (message.includes("authentication required") || + message.includes("invalid session") || + message.includes("token is not active"))); + + if (shouldRedirectToLogin && !isAuthPath && !isLoginPath) { + await userManager.removeUser(); + window.location.href = "/login"; } return Promise.reject(error); }, From 890ddd9b3c2bec131a89ea652c4a0d25133cd50d Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 14:22:00 +0900 Subject: [PATCH 09/39] =?UTF-8?q?=EC=84=B8=EC=85=98=20=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=20=EC=8B=9C=20=EB=AA=A8=EB=93=A0=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 5 +- .../handler/auth_handler_sessions_test.go | 144 ++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 2dcac48c..a4993396 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -7219,6 +7219,9 @@ func (h *AuthHandler) loadSessionClientBindings(ctx context.Context, userID stri logs, err := h.AuditRepo.FindByUserAndEvents(ctx, userID, []string{ "consent.granted", "POST /api/v1/auth/oidc/login/accept", + "POST /api/v1/auth/password/login", + "password_login_success", + "login_success", }, 200) if err != nil { return bindings @@ -7265,7 +7268,7 @@ func (h *AuthHandler) revokeHydraSessionAccess(ctx context.Context, userID strin clientIDs := h.loadSessionClientBindings(ctx, userID)[strings.TrimSpace(sessionID)] if len(clientIDs) == 0 { - return h.Hydra.RevokeConsentSessions(ctx, userID, "") + return nil } for _, clientID := range clientIDs { if err := h.Hydra.RevokeConsentSessions(ctx, userID, clientID); err != nil { diff --git a/backend/internal/handler/auth_handler_sessions_test.go b/backend/internal/handler/auth_handler_sessions_test.go index 928468ee..7dfc0129 100644 --- a/backend/internal/handler/auth_handler_sessions_test.go +++ b/backend/internal/handler/auth_handler_sessions_test.go @@ -430,6 +430,150 @@ func TestDeleteMySession_Success(t *testing.T) { mockKratos.AssertExpectations(t) } +func TestDeleteMySession_DoesNotRevokeAllHydraSessionsWhenClientBindingMissing(t *testing.T) { + t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") + var hydraRevokeCalls int + client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { + switch r.URL.Host { + case "kratos.test": + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": time.Now().UTC().Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, + }, + }), nil + } + case "hydra.test": + if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" { + hydraRevokeCalls++ + return httpResponse(r, http.StatusNoContent, ""), nil + } + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })} + setDefaultHTTPClientForTest(t, client.Transport) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{ + {ID: "target-sid", Active: true}, + }, nil).Once() + mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{ + ID: "target-sid", + Active: true, + }, nil).Once() + mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once() + + auditRepo := &mockAuditRepo{} + h := &AuthHandler{ + KratosAdmin: mockKratos, + AuditRepo: auditRepo, + Hydra: &service.HydraAdminService{ + AdminURL: "http://hydra.test", + HTTPClient: client, + }, + } + + app := fiber.New() + app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession) + + req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + req.Header.Set("User-Agent", "session-test-agent") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 0, hydraRevokeCalls) + if assert.Len(t, auditRepo.logs, 1) { + assert.Equal(t, "session.revoked", auditRepo.logs[0].EventType) + assert.Equal(t, "user-123", auditRepo.logs[0].UserID) + assert.Contains(t, auditRepo.logs[0].Details, "target-sid") + } + + mockKratos.AssertExpectations(t) +} + +func TestDeleteMySession_RevokesHydraClientBoundFromPasswordLoginAudit(t *testing.T) { + t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") + var hydraRevokeCalls int + var revokedClient string + client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { + switch r.URL.Host { + case "kratos.test": + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": time.Now().UTC().Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, + }, + }), nil + } + case "hydra.test": + if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" { + revokedClient = r.URL.Query().Get("client") + hydraRevokeCalls++ + return httpResponse(r, http.StatusNoContent, ""), nil + } + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })} + setDefaultHTTPClientForTest(t, client.Transport) + + mockKratos := new(MockKratosAdminService) + mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{ + {ID: "target-sid", Active: true}, + }, nil).Once() + mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{ + ID: "target-sid", + Active: true, + }, nil).Once() + mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once() + + auditRepo := &mockAuditRepo{} + h := &AuthHandler{ + KratosAdmin: mockKratos, + AuditRepo: auditRepo, + Hydra: &service.HydraAdminService{ + AdminURL: "http://hydra.test", + HTTPClient: client, + }, + } + auditRepo.logs = append(auditRepo.logs, domain.AuditLog{ + UserID: "user-123", + EventType: "POST /api/v1/auth/password/login", + SessionID: "target-sid", + Details: `{"client_id":"adminfront","client_name":"AdminFront","session_id":"target-sid"}`, + }) + + app := fiber.New() + app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession) + + req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + req.Header.Set("User-Agent", "session-test-agent") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, 1, hydraRevokeCalls) + assert.Equal(t, "adminfront", revokedClient) + + mockKratos.AssertExpectations(t) +} + func TestGetHydraProfile_RejectsInactiveLinkedSession(t *testing.T) { client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { if r.URL.Host == "hydra.test" && r.URL.Path == "/oauth2/introspect" { From e3d279cb83f2449d2f275e95f0ef97493e12c59d Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 14:54:57 +0900 Subject: [PATCH 10/39] =?UTF-8?q?code=20check=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/tests/helpers/devfront-fixtures.ts | 16 + locales/template.toml | 300 +++++++++++++++++- userfront/assets/translations/template.toml | 157 +++++++++ .../presentation/pages/profile_page.dart | 2 - 4 files changed, 472 insertions(+), 3 deletions(-) diff --git a/devfront/tests/helpers/devfront-fixtures.ts b/devfront/tests/helpers/devfront-fixtures.ts index d3643940..e90806bd 100644 --- a/devfront/tests/helpers/devfront-fixtures.ts +++ b/devfront/tests/helpers/devfront-fixtures.ts @@ -196,6 +196,22 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) { }); }; + await page.route("**/api/v1/user/me", async (route) => { + return json(route, { + id: "playwright-user", + loginId: "playwright@example.com", + email: "playwright@example.com", + name: "Playwright User", + phoneNumber: "", + department: "QA", + tenantId: "tenant-a", + tenantName: "Tenant A", + role: "rp_admin", + createdAt: "2026-03-03T00:00:00.000Z", + updatedAt: "2026-03-03T00:00:00.000Z", + }); + }); + await page.route("**/api/v1/dev/**", async (route) => { const request = route.request(); const url = new URL(request.url()); diff --git a/locales/template.toml b/locales/template.toml index adaca6bf..ff8d60a1 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -73,7 +73,280 @@ scope_admin = "" session_ttl = "" tenant_headers = "" -[msg.admin.api_keys] +[msg.userfront.error] +detail_contact = "" +detail_generic = "" +detail_request = "" +id = "" +title = "" +title_generic = "" +title_with_code = "" +type = "" + +[msg.userfront.forgot] +description = "" +dry_send = "" +error = "" +input_required = "" +sent = "" + +[msg.userfront.login] +cookie_check_failed = "" +dry_send = "" +link_failed = "" +link_send_failed = "" +link_sent_email = "" +link_sent_phone = "" +link_timeout = "" +no_account = "" +oidc_failed = "" +qr_expired = "" +qr_init_failed = "" +qr_login_required = "" +token_missing = "" +verification_failed = "" + +[msg.userfront.login_success] +subtitle = "" + +[msg.userfront.consent] +accept_error = "" +client_id = "" +client_unknown = "" +description = "" +load_error = "" +missing_redirect = "" +redirect_notice = "" +scope_count = "" + +[msg.userfront.profile] +department_missing = "" +department_required = "" +email_missing = "" +greeting = "" +load_failed = "" +name_missing = "" +name_required = "" +phone_required = "" +phone_verify_required = "" +update_failed = "" +update_success = "" + +[msg.userfront.qr] +camera_error = "" +permission_error = "" +permission_required = "" + +[msg.userfront.reset] +invalid_body = "" +invalid_link = "" +invalid_title = "" +policy_loading = "" +success = "" + +[msg.userfront.sections] +apps_subtitle = "" +audit_subtitle = "" +sessions_subtitle = "" + +[msg.userfront.settings] +disabled = "" + +[msg.userfront.signup] +failed = "" +privacy_full = "" +tos_full = "" + +[ui.admin.audit] +export_csv = "" +load_more = "" +target = "" +title = "" + +[ui.admin.groups] +import_csv = "" + +[ui.admin.header] +plane = "" +subtitle = "" + +[ui.admin.nav] +api_keys = "" +audit_logs = "" +auth_guard = "" +logout = "" +overview = "" +relying_parties = "" +tenant_dashboard = "" +user_groups = "" +tenants = "" +users = "" + +[ui.admin.org] +download_template = "" +import_btn = "" +import_title = "" +start_import = "" + +[ui.admin.overview] +kicker = "" +title = "" + +[ui.admin.profile] +manageable_tenants = "" + +[ui.admin.role] +rp_admin = "" +super_admin = "" +tenant_admin = "" +user = "" + +[ui.admin.tenants] +add = "" +title = "" + +[ui.common.badge] +admin_only = "" +command_only = "" +system = "" + +[ui.common.status] +active = "" +blocked = "" +failure = "" +inactive = "" +ok = "" +pending = "" +success = "" + +[ui.dev.nav] +clients = "" +logout = "" + +[ui.dev.tenant] +single_notice = "" +switch_success = "" +workspace = "" +workspace_desc = "" + +[ui.dev.audit] +load_more = "" +title = "" + +[ui.dev.profile] +menu_aria = "" +menu_title = "" +unknown_email = "" +unknown_name = "" +title = "" +subtitle = "" +loading = "" +error = "" + +[ui.dev.clients] +new = "" +search_placeholder = "" +tenant_scoped = "" +untitled = "" + +[ui.dev.dashboard] +ready_badge = "" + +[ui.dev.header] +plane = "" +subtitle = "" + +[ui.dev.session] +auto_extend = "" +active = "" +disabled = "" +unknown = "" +expired = "" +expiring = "" +remaining = "" +refresh = "" +refreshing = "" + +[ui.userfront.app_label] +admin_console = "" +baron = "" +dev_console = "" + +[ui.userfront.auth_method] +ory = "" +session = "" + +[ui.userfront.dashboard] +last_auth_label = "" +status_history = "" + +[ui.userfront.device] +android = "" +ios = "" +linux = "" +macos = "" +windows = "" + +[ui.userfront.error] +go_home = "" +go_login = "" + +[ui.userfront.forgot] +heading = "" +input_label = "" +submit = "" +title = "" + +[ui.userfront.login] +forgot_password = "" +signup = "" + +[ui.userfront.login_success] +later = "" +qr = "" +title = "" + +[ui.userfront.consent] +accept = "" +requested_scopes = "" +title = "" + +[ui.userfront.nav] +dashboard = "" +logout = "" +profile = "" +qr_scan = "" + +[ui.userfront.profile] +department_empty = "" +manage = "" +user_fallback = "" + +[ui.userfront.qr] +rescan = "" +result_success = "" +title = "" + +[ui.userfront.reset] +confirm_password = "" +new_password = "" +submit = "" +subtitle = "" +title = "" + +[ui.userfront.sections] +apps = "" +audit = "" +sessions = "" + +[ui.userfront.session] +active = "" +unknown = "" + +[ui.userfront.signup] +complete = "" +next_step = "" +title = "" [msg.admin.api_keys.create] error = "" @@ -559,6 +832,20 @@ empty = "" empty_detail = "" error = "" +[msg.userfront.dashboard.sessions] +browser = "" +empty = "" +empty_detail = "" +error = "" +os = "" +recent_app = "" +session_id = "" + +[msg.userfront.dashboard.sessions.revoke] +confirm = "" +error = "" +success = "" + [msg.userfront.dashboard.approved_session] copy_click = "" copy_tap = "" @@ -2070,6 +2357,17 @@ status_history = "" [ui.userfront.dashboard.activity] linked = "" +[ui.userfront.dashboard.sessions] +active_badge = "" +current_badge = "" +current_disabled = "" +unknown_device = "" +unknown_session = "" + +[ui.userfront.dashboard.sessions.revoke] +action = "" +title = "" + [ui.userfront.dashboard.approved_session] default = "" userfront = "" diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index 18c8594b..e569bcfc 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -72,7 +72,139 @@ email = "" offline_access = "" openid = "" phone = "" + +[msg.userfront.profile] +department_missing = "" +department_required = "" +email_missing = "" +greeting = "" +load_failed = "" +name_missing = "" +name_required = "" +phone_required = "" +phone_verify_required = "" +update_failed = "" +update_success = "" + +[msg.userfront.qr] +camera_error = "" +permission_error = "" +permission_required = "" + +[msg.userfront.reset] +invalid_body = "" +invalid_link = "" +invalid_title = "" +policy_loading = "" +success = "" + +[msg.userfront.sections] +apps_subtitle = "" +audit_subtitle = "" +sessions_subtitle = "" + +[msg.userfront.settings] +disabled = "" + +[msg.userfront.signup] +failed = "" +privacy_full = "" +tos_full = "" + +[ui.common.badge] +admin_only = "" +command_only = "" +system = "" + +[ui.common.status] +active = "" +blocked = "" +failure = "" +inactive = "" +ok = "" +pending = "" +success = "" + +[ui.userfront.app_label] +admin_console = "" +baron = "" +dev_console = "" + +[ui.userfront.auth_method] +ory = "" +session = "" + +[ui.userfront.dashboard] +last_auth_label = "" +status_history = "" + +[ui.userfront.device] +android = "" +ios = "" +linux = "" +macos = "" +windows = "" + +[ui.userfront.error] +go_home = "" +go_login = "" + +[ui.userfront.forgot] +heading = "" +input_label = "" +submit = "" +title = "" + +[ui.userfront.login] +forgot_password = "" +signup = "" + +[ui.userfront.login_success] +later = "" +qr = "" +title = "" + +[ui.userfront.consent] +accept = "" +requested_scopes = "" +title = "" + +[ui.userfront.nav] +dashboard = "" +logout = "" profile = "" +qr_scan = "" + +[ui.userfront.profile] +department_empty = "" +manage = "" +user_fallback = "" + +[ui.userfront.qr] +rescan = "" +result_success = "" +title = "" + +[ui.userfront.reset] +confirm_password = "" +new_password = "" +submit = "" +subtitle = "" +title = "" + +[ui.userfront.sections] +apps = "" +audit = "" +sessions = "" + +[ui.userfront.session] +active = "" +unknown = "" + +[ui.userfront.signup] +complete = "" +next_step = "" +title = "" [msg.userfront.dashboard] approved_device = "" @@ -94,6 +226,20 @@ empty = "" empty_detail = "" error = "" +[msg.userfront.dashboard.sessions] +browser = "" +empty = "" +empty_detail = "" +error = "" +os = "" +recent_app = "" +session_id = "" + +[msg.userfront.dashboard.sessions.revoke] +confirm = "" +error = "" +success = "" + [msg.userfront.dashboard.approved_session] copy_click = "" copy_tap = "" @@ -450,6 +596,17 @@ status_history = "" [ui.userfront.dashboard.activity] linked = "" +[ui.userfront.dashboard.sessions] +active_badge = "" +current_badge = "" +current_disabled = "" +unknown_device = "" +unknown_session = "" + +[ui.userfront.dashboard.sessions.revoke] +action = "" +title = "" + [ui.userfront.dashboard.approved_session] default = "" userfront = "" diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index bf8f6d50..39987e19 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -3,11 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:userfront/i18n.dart'; -import '../../../../core/notifiers/auth_notifier.dart'; import '../../../../core/i18n/locale_utils.dart'; import '../../../../core/services/auth_proxy_service.dart'; import '../../../../core/services/logout_service.dart'; -import '../../../../core/services/auth_token_store.dart'; import '../../../../core/ui/layout_breakpoints.dart'; import '../../../../core/ui/toast_service.dart'; import '../../../../core/widgets/language_selector.dart'; From 1b8dc2c4abe00e24387566e9675b4465ac0e7b9e Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 16:03:49 +0900 Subject: [PATCH 11/39] =?UTF-8?q?dev=20=EB=B8=8C=EB=9F=B0=EC=B9=98=20?= =?UTF-8?q?=EB=B3=91=ED=95=A9=20=ED=9B=84=20code=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/headless_login_e2e_test.go | 12 ++ backend/internal/handler/auth_handler.go | 4 + .../handler/auth_handler_async_test.go | 2 + .../internal/service/tenant_service_test.go | 1 + .../tests/password-and-reset.spec.ts | 72 +++++-- userfront/assets/translations/ko.toml | 183 +++++++++--------- userfront/assets/translations/template.toml | 89 +++++++-- .../auth/presentation/login_screen.dart | 11 +- 8 files changed, 245 insertions(+), 129 deletions(-) diff --git a/backend/cmd/server/headless_login_e2e_test.go b/backend/cmd/server/headless_login_e2e_test.go index f91a5b53..89a1822b 100644 --- a/backend/cmd/server/headless_login_e2e_test.go +++ b/backend/cmd/server/headless_login_e2e_test.go @@ -121,6 +121,18 @@ func (m *e2eMockKratosAdminService) DeleteIdentity(ctx context.Context, identity return nil } +func (m *e2eMockKratosAdminService) ListIdentitySessions(ctx context.Context, identityID string) ([]service.KratosSession, error) { + return nil, nil +} + +func (m *e2eMockKratosAdminService) GetSession(ctx context.Context, sessionID string) (*service.KratosSession, error) { + return nil, nil +} + +func (m *e2eMockKratosAdminService) DeleteSession(ctx context.Context, sessionID string) error { + return nil +} + func newHeadlessLoginE2EApp(h *authhandler.AuthHandler, appEnv string) *fiber.App { app := fiber.New(fiber.Config{ DisableStartupMessage: true, diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index a4993396..28259b32 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -7126,6 +7126,10 @@ func isPrivateIPAddress(raw string) bool { return utils.IsPrivateOrReservedIP(raw) } +func parseAuditDetails(details string) (map[string]any, error) { + return utils.ParseAuditDetails(details) +} + func deriveSessionClientInfo(log domain.AuditLog) (string, string) { details, _ := parseAuditDetails(log.Details) clientID := "" diff --git a/backend/internal/handler/auth_handler_async_test.go b/backend/internal/handler/auth_handler_async_test.go index d3fee7f9..b2dee1ce 100644 --- a/backend/internal/handler/auth_handler_async_test.go +++ b/backend/internal/handler/auth_handler_async_test.go @@ -80,6 +80,7 @@ func (m *AsyncMockUserRepo) Create(ctx context.Context, user *domain.User) error } return args.Error(0) } + func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error { args := m.Called(ctx, user) if m.createCalled != nil { @@ -87,6 +88,7 @@ func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error } return args.Error(0) } + func (m *AsyncMockUserRepo) Delete(ctx context.Context, id string) error { return nil } func (m *AsyncMockUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) { return nil, nil diff --git a/backend/internal/service/tenant_service_test.go b/backend/internal/service/tenant_service_test.go index 37c68b9b..4b24ad34 100644 --- a/backend/internal/service/tenant_service_test.go +++ b/backend/internal/service/tenant_service_test.go @@ -137,6 +137,7 @@ func (m *MockUserRepoForTenant) CountByTenantIDs(ctx context.Context, tenantIDs } return args.Get(0).(map[string]int64), args.Error(1) } + func (m *MockUserRepoForTenant) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) { args := m.Called(ctx, codes) if args.Get(0) == nil { diff --git a/userfront-e2e/tests/password-and-reset.spec.ts b/userfront-e2e/tests/password-and-reset.spec.ts index 09728c53..e722ef6d 100644 --- a/userfront-e2e/tests/password-and-reset.spec.ts +++ b/userfront-e2e/tests/password-and-reset.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page, type Route } from '@playwright/test'; +import { expect, test, type Locator, type Page, type Route } from '@playwright/test'; type RequestCapture = { loginBody?: Record; @@ -7,15 +7,26 @@ type RequestCapture = { clientLogs: string[]; }; +const resetNewPasswordName = /^(새 비밀번호|ui\.userfront\.reset\.new_password)$/; +const resetConfirmPasswordName = + /^(새 비밀번호 확인|ui\.userfront\.reset\.confirm_password)$/; +const resetSubmitButtonName = /^(비밀번호 변경|ui\.userfront\.reset\.submit)$/; + async function enableFlutterAccessibility(page: Page): Promise { - await page.waitForTimeout(300); const button = page.getByRole('button', { name: 'Enable accessibility' }); if (await button.count()) { - await button.click({ force: true }); - const placeholder = page.locator('flt-semantics-placeholder'); - if (await placeholder.count()) { - await placeholder.first().click({ force: true }); - } + await button.first().evaluate((node) => { + (node as HTMLElement).click(); + }); + await page.waitForTimeout(200); + return; + } + await page.waitForTimeout(300); + const placeholder = page.locator('flt-semantics-placeholder').first(); + if (await placeholder.count()) { + await placeholder.evaluate((node) => { + (node as HTMLElement).click(); + }); await page.waitForTimeout(800); } } @@ -109,6 +120,18 @@ async function fillAt(page: Page, x: number, y: number, value: string): Promise< await page.keyboard.type(value); } +async function typeIntoAccessibleField( + page: Page, + field: Locator, + value: string, +): Promise { + await field.click({ force: true }); + await page.waitForTimeout(100); + await page.keyboard.press('Control+A'); + await page.keyboard.press('Backspace'); + await page.keyboard.type(value); +} + async function fillPasswordLoginForm( page: Page, loginId: string, @@ -128,25 +151,29 @@ async function fillPasswordLoginForm( async function submitPasswordLogin(page: Page): Promise { if (isMobileProject(page)) { + await enableFlutterAccessibility(page); await page.getByRole('button', { name: '로그인' }).click({ force: true }); return; } - const coords = coordsFor(page); - await page.locator('flt-glass-pane').click({ - position: { x: coords.signinSubmitX, y: coords.signinSubmitY }, - force: true, - }); + await page.keyboard.press('Enter'); } async function fillResetPasswordForm(page: Page, password: string): Promise { + await enableFlutterAccessibility(page); + const newPasswordInput = page.getByRole('textbox', { + name: resetNewPasswordName, + }); + const confirmPasswordInput = page.getByRole('textbox', { + name: resetConfirmPasswordName, + }); + if ((await newPasswordInput.count()) > 0 && (await confirmPasswordInput.count()) > 0) { + await typeIntoAccessibleField(page, newPasswordInput, password); + await typeIntoAccessibleField(page, confirmPasswordInput, password); + return; + } if (isMobileProject(page)) { - await enableFlutterAccessibility(page); - await page - .getByRole('textbox', { name: /^새 비밀번호$/ }) - .fill(password); - await page - .getByRole('textbox', { name: /^새 비밀번호 확인$/ }) - .fill(password); + await page.getByRole('textbox', { name: resetNewPasswordName }).fill(password); + await page.getByRole('textbox', { name: resetConfirmPasswordName }).fill(password); return; } const coords = coordsFor(page); @@ -160,8 +187,13 @@ async function fillResetPasswordForm(page: Page, password: string): Promise { + await enableFlutterAccessibility(page); + const submitButton = page.getByRole('button', { name: resetSubmitButtonName }); + if ((await submitButton.count()) > 0) { + await submitButton.click({ force: true }); + return; + } if (isMobileProject(page)) { - await page.getByRole('button', { name: '비밀번호 변경' }).click({ force: true }); return; } const coords = coordsFor(page); diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 3c565d55..9fc24973 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -40,9 +40,6 @@ verify_code_failed = "인증 실패: {error}" [err.userfront.session] missing = "활성 세션이 없습니다." -[msg.userfront] -greeting = "안녕하세요, {name}님" - [msg.userfront.audit] date = "접속일자: {value}" device = "접속환경: {value}" @@ -53,27 +50,6 @@ result = "인증결과: {value}" session_id = "Session ID: {value}" status = "현황: (준비중)" -[msg.userfront.consent] -accept_error = "동의 처리에 실패했습니다: {error}" -client_id = "클라이언트 ID: {id}" -client_unknown = "알 수 없는 앱" -description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\n계속 진행하려면 동의 여부를 선택해 주세요." -load_error = "동의 정보를 불러오는데 실패했습니다: {error}" -missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다." -redirect_notice = "동의 후 자동으로 서비스로 이동합니다." -scope_count = "총 {count}개" - -[msg.userfront.consent.cancel] -confirm = "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?" -error = "취소 처리 중 오류가 발생했습니다: {error}" - -[msg.userfront.consent.scope] -email = "이메일 주소 (계정 식별 및 알림 용도)" -offline_access = "오프라인 접근 (로그인 유지)" -openid = "OpenID 인증 정보 (로그인 상태 확인)" -phone = "휴대폰 번호 (본인 인증 및 알림)" -profile = "기본 프로필 정보 (이름, 사용자 식별자)" - [msg.userfront.dashboard] approved_device = "승인 기기: {device}" approved_ip = "승인 IP: {ip}" @@ -89,27 +65,6 @@ link_open_error = "해당 링크를 열 수 없습니다." render_error = "대시보드 렌더링 오류: {error}" session_id_copied = "세션 ID가 복사되었습니다." -[msg.userfront.dashboard.activities] -empty = "연동된 앱이 없습니다." -empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." -error = "연동 정보를 불러오지 못했습니다." - -[msg.userfront.dashboard.approved_session] -copy_click = "{label}: {id}\\\\n클릭하면 복사됩니다." -copy_tap = "{label}: {id}\\\\n탭하면 복사됩니다." -none = "{label} 없음" - -[msg.userfront.dashboard.revoke] -confirm = "{app} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다." -error = "해지 실패: {error}" -success = "{app} 연동이 해지되었습니다." - -[msg.userfront.dashboard.scopes] -empty = "요청된 권한이 없습니다." - -[msg.userfront.dashboard.timeline] -load_error = "접속이력을 불러오지 못했습니다." - [msg.userfront.error] detail_contact = "관리자에게 문의해 주세요." detail_generic = "오류가 발생했습니다." @@ -120,34 +75,6 @@ title_generic = "오류가 발생했습니다" title_with_code = "오류: {code}" type = "오류 종류: {type}" -[msg.userfront.error.ory] -"$normalizedCode" = "{error}" -access_denied = "사용자가 동의를 거부했습니다." -consent_required = "앱 접근 동의가 필요합니다." -interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요." -invalid_client = "클라이언트 인증 정보가 유효하지 않습니다." -invalid_grant = "인증 요청이 만료되었거나 유효하지 않습니다." -invalid_request = "잘못된 요청입니다." -invalid_scope = "요청한 권한 범위가 유효하지 않습니다." -login_required = "로그인이 필요합니다." -request_forbidden = "요청이 거부되었습니다." -server_error = "인증 서버 오류가 발생했습니다." -temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습니다." -unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다." -unsupported_response_type = "지원하지 않는 응답 타입입니다." - -[msg.userfront.error.whitelist] -"$normalizedCode" = "{error}" -bad_request = "입력값을 확인해 주세요." -invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요." -not_found = "요청한 페이지를 찾을 수 없습니다." -password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다." -rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요." -recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요." -recovery_invalid = "재설정 링크가 유효하지 않습니다." -settings_disabled = "현재 계정 설정 화면은 준비 중입니다." -verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요." - [msg.userfront.forgot] description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다." dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다." @@ -317,6 +244,55 @@ complete = "가입 완료" next_step = "다음 단계" title = "회원가입" +[msg.userfront] +greeting = "안녕하세요, {name}님" + +[msg.userfront.audit] +date = "접속일자: {value}" +device = "접속환경: {value}" +end = "더 이상 항목이 없습니다." +ip = "접속 IP: {value}" +load_more_error = "더 불러오지 못했습니다." +result = "인증결과: {value}" +session_id = "Session ID: {value}" +status = "현황: (준비중)" + +[msg.userfront.consent] +accept_error = "동의 처리에 실패했습니다: {error}" +client_id = "클라이언트 ID: {id}" +client_unknown = "알 수 없는 앱" +description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\n계속 진행하려면 동의 여부를 선택해 주세요." +load_error = "동의 정보를 불러오는데 실패했습니다: {error}" +missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다." +redirect_notice = "동의 후 자동으로 서비스로 이동합니다." +scope_count = "총 {count}개" + +[msg.userfront.consent.cancel] +confirm = "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?" +error = "취소 처리 중 오류가 발생했습니다: {error}" + +[msg.userfront.consent.scope] +email = "이메일 주소 (계정 식별 및 알림 용도)" +offline_access = "오프라인 접근 (로그인 유지)" +openid = "OpenID 인증 정보 (로그인 상태 확인)" +phone = "휴대폰 번호 (본인 인증 및 알림)" +profile = "기본 프로필 정보 (이름, 사용자 식별자)" + +[msg.userfront.dashboard] +approved_device = "승인 기기: {device}" +approved_ip = "승인 IP: {ip}" +audit_empty = "최근 접속 이력이 없습니다." +audit_load_error = "접속이력을 불러오지 못했습니다." +auth_method = "인증수단: {method}" +client_id = "Client ID: {id}" +client_id_missing = "Client ID 없음" +current_status = "현재 상태: {status}" +last_auth = "최근 인증: {value}" +link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다." +link_open_error = "해당 링크를 열 수 없습니다." +render_error = "대시보드 렌더링 오류: {error}" +session_id_copied = "세션 ID가 복사되었습니다." + [msg.userfront.dashboard.activities] empty = "연동된 앱이 없습니다." empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." @@ -337,12 +313,12 @@ error = "세션 종료 실패: {error}" success = "세션이 종료되었습니다." [msg.userfront.dashboard.approved_session] -copy_click = "{label}: {id}\n클릭하면 복사됩니다." -copy_tap = "{label}: {id}\n탭하면 복사됩니다." +copy_click = "{label}: {id}\\\\n클릭하면 복사됩니다." +copy_tap = "{label}: {id}\\\\n탭하면 복사됩니다." none = "{label} 없음" [msg.userfront.dashboard.revoke] -confirm = "{app} 앱과의 연동을 해지하시겠습니까?\n해지하면 다음 로그인 시 다시 동의가 필요합니다." +confirm = "{app} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다." error = "해지 실패: {error}" success = "{app} 연동이 해지되었습니다." @@ -352,17 +328,15 @@ empty = "요청된 권한이 없습니다." [msg.userfront.dashboard.timeline] load_error = "접속이력을 불러오지 못했습니다." -[msg.userfront.error.whitelist] -"$normalizedCode" = "{error}" -bad_request = "입력값을 확인해 주세요." -invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요." -not_found = "요청한 페이지를 찾을 수 없습니다." -password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다." -rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요." -recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요." -recovery_invalid = "재설정 링크가 유효하지 않습니다." -settings_disabled = "현재 계정 설정 화면은 준비 중입니다." -verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요." +[msg.userfront.error] +detail_contact = "관리자에게 문의해 주세요." +detail_generic = "오류가 발생했습니다." +detail_request = "요청을 처리하는 중 문제가 발생했습니다." +id = "오류 ID: {id}" +title = "인증 과정에서 오류가 발생했습니다" +title_generic = "오류가 발생했습니다" +title_with_code = "오류: {code}" +type = "오류 종류: {type}" [msg.userfront.error.ory] "$normalizedCode" = "{error}" @@ -380,6 +354,41 @@ temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습 unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다." unsupported_response_type = "지원하지 않는 응답 타입입니다." +[msg.userfront.error.whitelist] +"$normalizedCode" = "{error}" +bad_request = "입력값을 확인해 주세요." +invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요." +not_found = "요청한 페이지를 찾을 수 없습니다." +password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다." +rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요." +recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요." +recovery_invalid = "재설정 링크가 유효하지 않습니다." +settings_disabled = "현재 계정 설정 화면은 준비 중입니다." +verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요." + +[msg.userfront.forgot] +description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다." +dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다." +error = "전송에 실패했습니다: {error}" +input_required = "이메일 또는 휴대폰 번호를 입력해주세요." +sent = "비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요." + +[msg.userfront.login] +cookie_check_failed = "로그인 확인 실패: {error}" +dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다." +link_failed = "오류: {error}" +link_send_failed = "전송 실패: {error}" +link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다." +link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다." +link_timeout = "시간이 경과되었습니다." +no_account = "계정이 없으신가요?" +oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요." +qr_expired = "시간이 경과되었습니다." +qr_init_failed = "QR 초기화에 실패했습니다: {error}" +qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다" +token_missing = "로그인 토큰을 확인할 수 없습니다." +verification_failed = "승인 처리에 실패했습니다: {error}" + [msg.userfront.login.link] approved = "msg.userfront.login.link.approved" helper = "입력하신 정보로 로그인 링크를 전송합니다." diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index e569bcfc..c902ac09 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -40,18 +40,41 @@ verify_code_failed = "" [err.userfront.session] missing = "" -[msg.userfront] -greeting = "" +[msg.userfront.error] +detail_contact = "" +detail_generic = "" +detail_request = "" +id = "" +title = "" +title_generic = "" +title_with_code = "" +type = "" -[msg.userfront.audit] -date = "" -device = "" -end = "" -ip = "" -load_more_error = "" -result = "" -session_id = "" -status = "" +[msg.userfront.forgot] +description = "" +dry_send = "" +error = "" +input_required = "" +sent = "" + +[msg.userfront.login] +cookie_check_failed = "" +dry_send = "" +link_failed = "" +link_send_failed = "" +link_sent_email = "" +link_sent_phone = "" +link_timeout = "" +no_account = "" +oidc_failed = "" +qr_expired = "" +qr_init_failed = "" +qr_login_required = "" +token_missing = "" +verification_failed = "" + +[msg.userfront.login_success] +subtitle = "" [msg.userfront.consent] accept_error = "" @@ -63,16 +86,6 @@ missing_redirect = "" redirect_notice = "" scope_count = "" -[msg.userfront.consent.cancel] -confirm = "" -error = "" - -[msg.userfront.consent.scope] -email = "" -offline_access = "" -openid = "" -phone = "" - [msg.userfront.profile] department_missing = "" department_required = "" @@ -206,6 +219,40 @@ complete = "" next_step = "" title = "" +[msg.userfront] +greeting = "" + +[msg.userfront.audit] +date = "" +device = "" +end = "" +ip = "" +load_more_error = "" +result = "" +session_id = "" +status = "" + +[msg.userfront.consent] +accept_error = "" +client_id = "" +client_unknown = "" +description = "" +load_error = "" +missing_redirect = "" +redirect_notice = "" +scope_count = "" + +[msg.userfront.consent.cancel] +confirm = "" +error = "" + +[msg.userfront.consent.scope] +email = "" +offline_access = "" +openid = "" +phone = "" +profile = "" + [msg.userfront.dashboard] approved_device = "" approved_ip = "" diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index ec300fe3..460e56db 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -1032,13 +1032,22 @@ class _LoginScreenState extends ConsumerState webWindow.redirectTo(redirectTo); } else {} } catch (e) { + final errorMessage = e.toString().replaceFirst('Exception: ', ''); + try { + await AuthProxyService.logError( + '[PasswordLogin] $errorMessage', + error: e, + ); + } catch (_) { + // Ignore client-log relay failures and continue with user feedback. + } if (e.toString().contains("User not registered")) { _showUnregisteredDialog(); } else { _showError( tr( 'msg.userfront.login.password.failed', - params: {'error': e.toString().replaceFirst('Exception: ', '')}, + params: {'error': errorMessage}, ), ); } From 69d7f053be68cfd23757bb5e740377e0c82977f4 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 17:07:26 +0900 Subject: [PATCH 12/39] =?UTF-8?q?i18n=20=EA=B2=BD=EB=A1=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EB=B0=8F=20placeholder=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/core/i18n/toml_asset_loader.dart | 59 +- .../auth/presentation/consent_screen.dart | 50 +- userfront/lib/i18n_data.dart | 2361 ++++++++++++----- 3 files changed, 1754 insertions(+), 716 deletions(-) diff --git a/userfront/lib/core/i18n/toml_asset_loader.dart b/userfront/lib/core/i18n/toml_asset_loader.dart index 5170eccb..956c4faf 100644 --- a/userfront/lib/core/i18n/toml_asset_loader.dart +++ b/userfront/lib/core/i18n/toml_asset_loader.dart @@ -1,22 +1,59 @@ import 'dart:ui'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:toml/toml.dart'; + +import '../../i18n_data.dart'; class TomlAssetLoader extends AssetLoader { const TomlAssetLoader(); @override Future> load(String path, Locale locale) async { - final assetPath = '$path/${locale.languageCode}.toml'; - try { - final content = await rootBundle.loadString(assetPath); - final document = TomlDocument.parse(content); - return document.toMap(); - } catch (e) { - // 로딩 실패 시 빈 맵을 반환해 렌더링을 지속합니다. - return {}; - } + final languageCode = locale.languageCode.toLowerCase(); + final source = switch (languageCode) { + 'ko' => koStrings, + 'en' => enStrings, + _ => enStrings, + }; + return _expandFlatTranslations(source); } } + +Map _expandFlatTranslations(Map flatMap) { + final nested = {}; + for (final entry in flatMap.entries) { + final key = entry.key; + if (key.isEmpty) { + continue; + } + final segments = key.split('.'); + Map cursor = nested; + for (var index = 0; index < segments.length; index++) { + final segment = segments[index]; + if (segment.isEmpty) { + continue; + } + final isLeaf = index == segments.length - 1; + if (isLeaf) { + cursor[segment] = _normalizeLocalizationValue(entry.value); + continue; + } + final next = cursor.putIfAbsent(segment, () => {}); + if (next is Map) { + cursor = next; + continue; + } + final replacement = {}; + cursor[segment] = replacement; + cursor = replacement; + } + } + return nested; +} + +String _normalizeLocalizationValue(String value) { + return value.replaceAllMapped( + RegExp(r'\{\{[[:space:]]*([a-zA-Z0-9_]+)[[:space:]]*\}\}'), + (match) => '{${match.group(1)}}', + ); +} diff --git a/userfront/lib/features/auth/presentation/consent_screen.dart b/userfront/lib/features/auth/presentation/consent_screen.dart index 0d3acd05..dcf54939 100644 --- a/userfront/lib/features/auth/presentation/consent_screen.dart +++ b/userfront/lib/features/auth/presentation/consent_screen.dart @@ -57,6 +57,40 @@ class _ConsentScreenState extends State { }; } + String _renderConsentText(String key, {String? fallback}) { + return tr(key, fallback: fallback) + .replaceAll(r'\\n', '\n') + .replaceAll(r'\n', '\n') + .replaceAll('\\\n', '\n'); + } + + String _renderScopeCountLabel(int count) { + return tr( + 'msg.userfront.consent.scope_count', + fallback: 'Total {{count}}', + params: {'count': '$count'}, + ).replaceAll('{$count}', '$count'); + } + + String _scopeDisplayLabel(String scope) { + if (scope == 'offline_access') { + return 'offline access'; + } + return scope.replaceAll('_', ' '); + } + + String _renderClientIdLabel(String clientId) { + final raw = tr( + 'msg.userfront.consent.client_id', + fallback: 'Client ID: {{id}}', + ); + final normalized = raw + .replaceAll('{{id}}', '') + .replaceAll('{id}', '') + .trimRight(); + return '$normalized $clientId'; + } + Future _fetchConsentInfo() async { try { final info = await AuthProxyService.getConsentInfo( @@ -271,7 +305,7 @@ class _ConsentScreenState extends State { ), const SizedBox(height: 12), Text( - tr('msg.userfront.consent.description'), + _renderConsentText('msg.userfront.consent.description'), style: TextStyle(fontSize: 14, color: Colors.grey[600]), textAlign: TextAlign.center, ), @@ -318,11 +352,7 @@ class _ConsentScreenState extends State { ), const SizedBox(height: 4), Text( - tr( - 'msg.userfront.consent.client_id', - fallback: 'Client ID: {{id}}', - params: {'id': clientId}, - ), + _renderClientIdLabel(clientId), style: TextStyle( fontSize: 12, color: Colors.grey[500], @@ -349,11 +379,7 @@ class _ConsentScreenState extends State { ), ), Text( - tr( - 'msg.userfront.consent.scope_count', - fallback: 'Total {{count}}', - params: {'count': '${requestedScopes.length}'}, - ), + _renderScopeCountLabel(requestedScopes.length), style: TextStyle( fontSize: 14, color: Theme.of(context).primaryColor, @@ -371,7 +397,7 @@ class _ConsentScreenState extends State { return CheckboxListTile( title: Text( - scope, // 스코프 키 (예: openid) + _scopeDisplayLabel(scope), style: const TextStyle(fontWeight: FontWeight.w500), ), subtitle: Text(description), diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index ced42124..c5639e50 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -49,25 +49,25 @@ const Map koStrings = { "msg.admin.api_keys.create.scopes_count": "총 {{count}}개의 권한이 할당됩니다.", "msg.admin.api_keys.create.scopes_hint": "생성 즉시 활성화되어 사용 가능합니다.", "msg.admin.api_keys.create.subtitle": "내부 시스템 연동을 위한 보안 인증 키를 구성합니다.", - "msg.admin.api_keys.create.success.copy_hint": - "복사 버튼을 눌러 안전한 곳(비밀번호 관리자 등)에 저장하세요.", + "msg.admin.api_keys.create.success.copy_hint": "복사 버튼을 눌러 안전한 곳(비밀번호 관리자 등)에 저장하세요.", "msg.admin.api_keys.create.success.notice": "아래의 비밀번호(Secret)는 보안을 위해 ", "msg.admin.api_keys.create.success.notice_emphasis": "지금 한 번만", "msg.admin.api_keys.create.success.notice_suffix": "표시됩니다.", - "msg.admin.api_keys.list.delete_confirm": "API 키 \"{{name}}\"를 삭제할까요?", + "msg.admin.api_keys.list.delete_confirm": "API 키 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?", "msg.admin.api_keys.list.empty": "등록된 API 키가 없습니다.", "msg.admin.api_keys.list.fetch_error": "API 키 목록 조회에 실패했습니다.", "msg.admin.api_keys.list.registry.count": "총 {{count}}개 API 키", - "msg.admin.api_keys.list.subtitle": - "서버 간 통신(Machine-to-Machine)을 위한 API 키를 발급하고 관리합니다.", + "msg.admin.api_keys.list.subtitle": "서버 간 통신(Machine-to-Machine)을 위한 API 키를 발급하고 관리합니다.", "msg.admin.audit.empty": "아직 수집된 감사 로그가 없습니다.", - "msg.admin.audit.end": "End of audit feed", + "msg.admin.audit.end": "감사 로그의 마지막입니다.", "msg.admin.audit.filters.empty": "필터 없음", - "msg.admin.audit.load_error": "Error loading logs: {{error}}", - "msg.admin.audit.loading": "Loading audit logs...", + "msg.admin.audit.load_error": "감사 로그를 불러오지 못했습니다: {{error}}", + "msg.admin.audit.loading": "감사 로그를 불러오는 중...", "msg.admin.audit.registry.count": "로드된 로그 {{count}}건", - "msg.admin.audit.subtitle": - "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", + "msg.admin.audit.subtitle": "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", + "msg.admin.common.forbidden": "이 작업을 수행할 권한이 없습니다.", + "msg.admin.groups.create.description": "부서나 팀과 같은 새로운 조직 단위를 추가합니다.", + "msg.admin.groups.create.title": "새 조직 단위 생성", "msg.admin.groups.list.create_error": "생성 실패", "msg.admin.groups.list.create_success": "조직 단위가 생성되었습니다.", "msg.admin.groups.list.delete_confirm": "정말로 삭제하시겠습니까?", @@ -88,165 +88,250 @@ const Map koStrings = { "msg.admin.groups.roles.assign_success": "역할이 할당되었습니다.", "msg.admin.groups.roles.description": "이 조직의 구성원들이 대상 테넌트에서 상속받을 역할을 선택하세요.", "msg.admin.groups.roles.empty": "할당된 역할이 없습니다.", - "msg.admin.groups.roles.remove_confirm": - "msg.admin.groups.roles.remove_confirm", + "msg.admin.groups.roles.remove_confirm": "역할을 회수하시겠습니까?", "msg.admin.groups.roles.remove_success": "역할이 회수되었습니다.", "msg.admin.header.subtitle": "Tenant isolation & least privilege by default", "msg.admin.idp_env_prod": "IDP env: prod", "msg.admin.logout_confirm": "로그아웃 하시겠습니까?", - "msg.admin.notice.idp_policy": - "IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다.", + "msg.admin.notice.idp_policy": "IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다.", "msg.admin.notice.scope": "관리 기능은 /admin 네임스페이스에서만 노출합니다.", + "msg.admin.org.hover_member_info": "마우스를 올리면 상세 정보를 확인할 수 있습니다.", + "msg.admin.org.import_description": "CSV 파일을 업로드하여 조직도를 일괄 등록합니다.", + "msg.admin.org.import_error": "조직도 임포트 중 오류가 발생했습니다.", + "msg.admin.org.import_success": "조직도가 성공적으로 임포트되었습니다.", "msg.admin.overview.description": "모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다.", "msg.admin.overview.idp_fallback": "Fallback: Descope", "msg.admin.overview.idp_primary": "IDP: Ory primary", - "msg.admin.overview.playbook.description": - "운영 정책, 레이트리밋, 감사 로그의 기본 룰을 요약합니다.", - "msg.admin.overview.playbook.idp_body": - "모든 IDP 호출은 backend를 통해서만 수행하며, Hydra/Kratos admin 포트는 외부에 노출하지 않습니다.", + "msg.admin.overview.playbook.description": "운영 정책, 레이트리밋, 감사 로그의 기본 룰을 요약합니다.", + "msg.admin.overview.playbook.idp_body": "모든 IDP 호출은 backend를 통해서만 수행하며, Hydra/Kratos admin 포트는 외부에 노출하지 않습니다.", "msg.admin.overview.playbook.idp_title": "Backend-only IDP access", - "msg.admin.overview.playbook.tenant_body": - "Tenant 헤더와 감사 로그 규칙을 기본 적용하며, 향후 Keto 정책으로 확장 예정입니다.", + "msg.admin.overview.playbook.tenant_body": "Tenant 헤더와 감사 로그 규칙을 기본 적용하며, 향후 Keto 정책으로 확장 예정입니다.", "msg.admin.overview.playbook.tenant_title": "Tenant isolation", "msg.admin.overview.quick_links.description": "주요 운영 화면으로 바로 이동합니다.", + "msg.admin.overview.summary.audit_events_24h": "최근 24시간 감사 로그", + "msg.admin.overview.summary.oidc_clients": "등록된 OIDC 클라이언트", + "msg.admin.overview.summary.policy_gate": "정책 가이트 상태", + "msg.admin.overview.summary.total_tenants": "전체 테넌트 수", "msg.admin.scope_admin": "Scoped to /admin", "msg.admin.session_ttl": "Session TTL: 15m admin", "msg.admin.tenant_headers": "Tenant-aware headers", "msg.admin.tenants.admins.add_success": "관리자가 추가되었습니다.", "msg.admin.tenants.admins.empty": "등록된 관리자가 없습니다.", "msg.admin.tenants.admins.remove_confirm": "관리자를 삭제하시겠습니까?", + "msg.admin.tenants.admins.remove_last": "마지막 관리자는 회수할 수 없습니다.", + "msg.admin.tenants.admins.remove_self": "본인의 권한은 회수할 수 없습니다.", "msg.admin.tenants.admins.remove_success": "권한이 회수되었습니다.", "msg.admin.tenants.admins.subtitle": "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.", "msg.admin.tenants.approve_confirm": "이 테넌트를 승인하시겠습니까?", "msg.admin.tenants.approve_success": "테넌트가 승인되었습니다.", - "msg.admin.tenants.create.form.domains_help": - "Users with these email domains will be automatically assigned to this tenant.", - "msg.admin.tenants.create.memo.body": - "생성 직후에는 기본 활성 상태로 부여되며, 필요 시 상태를 수정하세요.", - "msg.admin.tenants.create.memo.subtitle": - "Tenant 권한 정책은 추후 Keto 연계로 확장 예정입니다.", - "msg.admin.tenants.create.profile.subtitle": - "필수 정보만 입력해도 생성 가능합니다. Slug는 없으면 자동 생성됩니다.", + "msg.admin.tenants.create.form.domains_help": "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.", + "msg.admin.tenants.create.memo.body": "생성 직후에는 기본 활성 상태로 부여되며, 필요 시 상태를 수정하세요.", + "msg.admin.tenants.create.memo.subtitle": "Tenant 권한 정책은 추후 Keto 연계로 확장 예정입니다.", + "msg.admin.tenants.create.profile.subtitle": "필수 정보만 입력해도 생성 가능합니다. Slug는 없으면 자동 생성됩니다.", "msg.admin.tenants.create.subtitle": "글로벌 운영 기준의 신규 테넌트를 등록합니다.", - "msg.admin.tenants.delete_confirm": "테넌트 \"{{name}}\"를 삭제할까요?", + "msg.admin.tenants.delete_confirm": "테넌트 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?", "msg.admin.tenants.delete_success": "테넌트가 삭제되었습니다.", "msg.admin.tenants.empty": "아직 등록된 테넌트가 없습니다.", "msg.admin.tenants.fetch_error": "테넌트 목록 조회에 실패했습니다.", + "msg.admin.tenants.members.desc": "조직에 소속된 사용자 목록을 확인합니다.", "msg.admin.tenants.members.empty": "소속된 사용자가 없습니다.", + "msg.admin.tenants.members.limit_notice": "하위 조직이 많아 상위 10개 조직의 멤버만 표시됩니다.", "msg.admin.tenants.missing_id": "테넌트 ID가 없습니다.", + "msg.admin.tenants.not_found": "테넌트를 찾을 수 없습니다.", + "msg.admin.tenants.owners.add_success": "소유자가 추가되었습니다.", + "msg.admin.tenants.owners.empty": "등록된 소유자가 없습니다.", + "msg.admin.tenants.owners.remove_confirm": "소유자를 삭제하시겠습니까?", + "msg.admin.tenants.owners.remove_last": "마지막 소유자는 회수할 수 없습니다.", + "msg.admin.tenants.owners.remove_self": "본인의 권한은 회수할 수 없습니다.", + "msg.admin.tenants.owners.remove_success": "소유자 권한이 회수되었습니다.", + "msg.admin.tenants.owners.subtitle": "이 테넌트의 최상위 권한을 가진 소유자(조직장) 목록입니다.", "msg.admin.tenants.registry.count": "총 {{count}}개 테넌트", - "msg.admin.tenants.schema.empty": - "No custom fields defined. Click \"Add Field\" to begin.", - "msg.admin.tenants.schema.missing_id": "Tenant ID missing", - "msg.admin.tenants.schema.subtitle": - "Define custom attributes for users in this tenant.", - "msg.admin.tenants.schema.update_error": "Failed to update schema", - "msg.admin.tenants.schema.update_success": "Schema updated successfully", + "msg.admin.tenants.remove_sub_confirm": "테넌트 \\\\\\\"{{name}}\\\\\\\"을(를) 하위 조직에서 제외할까요?", + "msg.admin.tenants.schema.empty": "등록된 커스텀 필드가 없습니다. 필드 추가를 눌러 시작하세요.", + "msg.admin.tenants.schema.forbidden_desc": "사용자 스키마 설정은 관리자만 접근할 수 있습니다.", + "msg.admin.tenants.schema.missing_id": "테넌트 ID가 없습니다.", + "msg.admin.tenants.schema.subtitle": "이 테넌트의 사용자에게 적용할 커스텀 속성을 정의합니다.", + "msg.admin.tenants.schema.update_error": "스키마 업데이트에 실패했습니다.", + "msg.admin.tenants.schema.update_success": "스키마가 성공적으로 업데이트되었습니다.", "msg.admin.tenants.sub.empty": "하위 테넌트가 없습니다.", "msg.admin.tenants.sub.subtitle": "현재 테넌트 하위에 생성된 조직입니다.", "msg.admin.tenants.subtitle": "현재 등록된 테넌트를 확인하고 상태를 관리합니다.", + "msg.admin.users.bulk.delete_confirm": "선택한 {{count}}명의 사용자를 정말로 삭제하시겠습니까?", + "msg.admin.users.bulk.delete_success": "{{count}}명의 사용자가 삭제되었습니다.", + "msg.admin.users.bulk.description": "CSV 파일을 통해 사용자를 일괄 등록하거나 관리합니다.", + "msg.admin.users.bulk.move_description": "선택한 사용자를 다른 테넌트로 일괄 이동합니다.", + "msg.admin.users.bulk.move_error": "사용자 이동 중 오류가 발생했습니다.", + "msg.admin.users.bulk.move_success": "{{count}}명의 사용자가 성공적으로 이동되었습니다.", + "msg.admin.users.bulk.parsed_count": "{{count}}행의 데이터가 파싱되었습니다.", + "msg.admin.users.bulk.schema_incompatible": "대상 테넌트 스키마에 없는 필드는 유실될 수 있습니다:", + "msg.admin.users.bulk.schema_missing": "대상 테넌트의 필수 필드가 누락되어 있습니다:", + "msg.admin.users.bulk.update_success": "사용자 정보가 일괄 업데이트되었습니다.", "msg.admin.users.create.account.subtitle": "새로운 사용자를 시스템에 등록합니다.", "msg.admin.users.create.error": "사용자 생성에 실패했습니다.", "msg.admin.users.create.form.email_required": "이메일은 필수입니다.", + "msg.admin.users.create.form.field_invalid": "{{label}} 형식이 올바르지 않습니다.", + "msg.admin.users.create.form.field_required": "{{label}}은(는) 필수입니다.", + "msg.admin.users.create.form.login_id_help": "msg.admin.users.create.form.login_id_help", "msg.admin.users.create.form.name_required": "이름은 필수입니다.", - "msg.admin.users.create.form.password_auto_help": - "비워두면 시스템이 초기 비밀번호를 자동 생성합니다.", + "msg.admin.users.create.form.password_auto_help": "비워두면 시스템이 초기 비밀번호를 자동 생성합니다.", "msg.admin.users.create.form.password_manual_help": "초기 비밀번호를 직접 설정합니다.", "msg.admin.users.create.form.role_help": "시스템 접근 권한을 결정합니다.", "msg.admin.users.create.password_generated.default": "초기 비밀번호가 생성되었습니다.", - "msg.admin.users.create.password_generated.with_email": - "{{email}} 계정의 초기 비밀번호입니다.", + "msg.admin.users.create.password_generated.with_email": "{{email}} 계정의 초기 비밀번호입니다.", "msg.admin.users.create.password_required": "비밀번호를 입력하거나 자동 생성을 사용해 주세요.", + "msg.admin.users.create.success": "사용자가 성공적으로 생성되었습니다.", + "msg.admin.users.detail.delete_confirm": "삭제하시겠습니까?", + "msg.admin.users.detail.delete_error": "msg.admin.users.detail.delete_error", + "msg.admin.users.detail.delete_success": "사용자가 삭제되었습니다.", "msg.admin.users.detail.edit_subtitle": "{{email}} 계정의 정보를 수정합니다.", + "msg.admin.users.detail.form.field_required": "필수입니다.", + "msg.admin.users.detail.form.invalid_format": "형식이 올바르지 않습니다.", "msg.admin.users.detail.form.name_required": "이름은 필수입니다.", + "msg.admin.users.detail.history_desc": "최근 로그인한 연동 서비스(RP) 목록입니다.", + "msg.admin.users.detail.no_history": "아직 이용한 서비스가 없습니다.", + "msg.admin.users.detail.no_tenants": "소속된 테넌트 정보가 없습니다.", "msg.admin.users.detail.not_found": "사용자를 찾을 수 없습니다.", - "msg.admin.users.detail.security.password_hint": - "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다.", + "msg.admin.users.detail.password_generated": "안전한 비밀번호가 생성되었습니다.", + "msg.admin.users.detail.password_generated_help": "보안 기준에 맞는 임시 비밀번호를 자동 생성해 즉시 적용합니다.", + "msg.admin.users.detail.password_manual_required": "비밀번호를 입력해 주세요.", + "msg.admin.users.detail.reset_auto_desc": "해킹이 어려운 복잡한 임시 비밀번호를 시스템이 즉시 생성합니다.", + "msg.admin.users.detail.reset_password_confirm": "msg.admin.users.detail.reset_password_confirm", + "msg.admin.users.detail.reset_password_help": "사용자의 비밀번호를 강제로 재설정하고 자동 생성하거나 직접 입력한 비밀번호를 적용합니다.", + "msg.admin.users.detail.security.password_hint": "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다.", + "msg.admin.users.detail.security_desc": "비밀번호 초기화 및 보안 설정을 관리합니다.", + "msg.admin.users.detail.self_password_reset_blocked": "본인 계정의 비밀번호는 사용자 포털(UserFront) 설정에서 변경해 주세요.", + "msg.admin.users.detail.tenant_slug_help": "사용자의 주된 정체성을 결정하는 대표 조직을 지정합니다.", + "msg.admin.users.detail.tenants_desc": "각 테넌트별로 정의된 커스텀 스키마 정보를 관리합니다.", "msg.admin.users.detail.update_error": "사용자 수정에 실패했습니다.", "msg.admin.users.detail.update_success": "사용자 정보가 수정되었습니다.", - "msg.admin.users.list.delete_confirm": "사용자 \"{{name}}\"을(를) 정말 삭제하시겠습니까?", + "msg.admin.users.list.columns.description": "테이블에 표시할 컬럼을 선택합니다.", + "msg.admin.users.list.columns.no_custom": "이 테넌트에 정의된 커스텀 필드가 없습니다.", + "msg.admin.users.list.delete_confirm": "사용자 \\\\\\\"{{name}}\\\\\\\"을(를) 정말 삭제하시겠습니까?", "msg.admin.users.list.empty": "검색 결과가 없습니다.", "msg.admin.users.list.fetch_error": "사용자 목록 조회에 실패했습니다.", "msg.admin.users.list.registry.count": "총 {{count}}명의 사용자가 등록되어 있습니다.", "msg.admin.users.list.subtitle": "시스템 사용자를 조회하고 관리합니다. (Local DB)", + "msg.common.copied": "복사되었습니다.", + "msg.common.copied_to_clipboard": "클립보드에 복사되었습니다.", "msg.common.error": "오류가 발생했습니다.", + "msg.common.forbidden": "접근 권한이 없습니다.", "msg.common.loading": "로딩 중...", "msg.common.no_description": "설명이 없습니다.", + "msg.common.parsing": "데이터 파싱 중...", "msg.common.requesting": "요청 중...", "msg.common.saving": "저장 중...", - "msg.common.unknown_error": "unknown error", - "msg.dev.clients.consents.empty": "No consents found.", - "msg.dev.clients.consents.load_error": "Error loading consents: {{error}}", - "msg.dev.clients.consents.loading": "Loading consents...", - "msg.dev.clients.consents.showing": - "Showing {{from}} to {{to}} of {{total}} users", + "msg.common.unknown_error": "알 수 없는 오류", + "msg.dev.audit.empty": "조회된 감사 로그가 없습니다.", + "msg.dev.audit.forbidden": "감사 로그를 조회할 권한이 없습니다. 관리자에게 권한을 요청해주세요.", + "msg.dev.audit.load_error": "감사 로그 조회 실패: {{error}}", + "msg.dev.audit.loaded_count": "로드된 로그 {{count}}건", + "msg.dev.audit.loading": "감사 로그를 불러오는 중...", + "msg.dev.audit.subtitle": "현재 테넌트/앱 범위의 DevFront 작업 이력을 조회합니다.", + "msg.dev.auth.access_denied_description": "DevFront는 관리자 전용 화면입니다. 권한이 필요하면 관리자에게 요청해 주세요.", + "msg.dev.auth.access_denied_title": "접근 권한이 없습니다.", + "msg.dev.clients.consents.empty": "등록된 동의 내역이 없습니다.", + "msg.dev.clients.consents.load_error": "동의 내역을 불러오지 못했습니다: {{error}}", + "msg.dev.clients.consents.loading": "동의 내역을 불러오는 중...", + "msg.dev.clients.consents.revoke_confirm": "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다.", + "msg.dev.clients.consents.showing": "전체 {{total}}명 중 {{from}}번째부터 {{to}}번째 사용자를 표시합니다.", "msg.dev.clients.consents.subtitle": "OIDC Relying Party 사용자 권한을 검토·관리합니다.", - "msg.dev.clients.copy_client_id": "Client ID가 복사되었습니다.", "msg.dev.clients.delete_confirm": "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "msg.dev.clients.delete_error": "삭제 실패: {{error}}", "msg.dev.clients.deleted": "앱이 삭제되었습니다.", "msg.dev.clients.details.copy_client_id": "Client ID가 복사되었습니다.", "msg.dev.clients.details.copy_client_secret": "Client Secret이 복사되었습니다.", "msg.dev.clients.details.copy_endpoint": "{{label}}가 복사되었습니다.", - "msg.dev.clients.details.load_error": "Error loading client: {{error}}", - "msg.dev.clients.details.loading": "Loading client...", + "msg.dev.clients.details.load_error": "앱 상세 정보를 불러오지 못했습니다: {{error}}", + "msg.dev.clients.details.loading": "앱 상세 정보를 불러오는 중...", "msg.dev.clients.details.missing_id": "Client ID가 필요합니다.", - "msg.dev.clients.details.redirect.description": - "인증 성공 후 사용자를 리다이렉트할 허용된 URL 목록입니다. 콤마(,)로 구분하여 여러 개 입력할 수 있습니다.", + "msg.dev.clients.details.redirect.description": "인증 성공 후 사용자를 리다이렉트할 허용된 URL 목록입니다. 콤마(,)로 구분하여 여러 개 입력할 수 있습니다.", "msg.dev.clients.details.redirect_saved": "Redirect URIs가 저장되었습니다.", - "msg.dev.clients.details.rotate_confirm": - "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?", + "msg.dev.clients.details.rotate_confirm": "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\\\\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?", "msg.dev.clients.details.rotate_error": "재발급 실패: {{error}}", "msg.dev.clients.details.save_error": "저장 실패: {{error}}", "msg.dev.clients.details.secret_rotated": "Client Secret이 재발급되었습니다.", "msg.dev.clients.details.secret_unavailable": "SECRET_NOT_AVAILABLE", - "msg.dev.clients.details.security.footer": - "비밀키 재발행 작업에는 관리자 세션 TTL 확인과 레이트리밋, 알림 연동을 권장합니다.", - "msg.dev.clients.details.security.note": - "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행/복사는 감사 로그와 연계하세요.", + "msg.dev.clients.details.security.footer": "비밀키 재발행 작업에는 관리자 세션 TTL 확인과 레이트리밋, 알림 연동을 권장합니다.", + "msg.dev.clients.details.security.note": "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행/복사는 감사 로그와 연계하세요.", "msg.dev.clients.details.subtitle": "OIDC 자격 증명과 엔드포인트를 관리합니다.", "msg.dev.clients.federation.add_subtitle": "외부 OIDC 제공자를 연결합니다.", "msg.dev.clients.federation.empty": "등록된 IdP 설정이 없습니다.", "msg.dev.clients.federation.subtitle": "이 애플리케이션의 외부 IdP 설정을 관리합니다.", "msg.dev.clients.general.identity.logo_help": "인증 화면에 표시될 PNG/SVG URL입니다.", "msg.dev.clients.general.identity.subtitle": "앱 이름과 설명, 로고를 설정합니다.", - "msg.dev.clients.general.load_error": "Error loading client: {{error}}", - "msg.dev.clients.general.loading": "Loading client...", - "msg.dev.clients.general.redirect.help": - "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다.", + "msg.dev.clients.general.load_error": "앱 설정을 불러오지 못했습니다: {{error}}", + "msg.dev.clients.general.loading": "앱 설정을 불러오는 중...", + "msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "허용 알고리즘: {{algorithms}}", + "msg.dev.clients.general.public_key.auth_method_client_secret_basic_help": "일반적인 서버 사이드 앱 인증 방식입니다.", + "msg.dev.clients.general.public_key.auth_method_none_help": "PKCE 기반 public client에 사용하는 방식입니다.", + "msg.dev.clients.general.public_key.auth_method_private_key_jwt_help": "Trusted RP bootstrap과 JAR 검증에 필요한 서명 키 기반 인증 방식입니다.", + "msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "알고리즘 미선언", + "msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "이 키는 `alg`가 비어 있어서 저장할 수 없습니다.", + "msg.dev.clients.general.public_key.cache.missing_algorithms_help": "저장 전 JWKS 각 키에 `alg`를 명시해 주세요: {{details}}", + "msg.dev.clients.general.public_key.cache.missing_algorithms_title": "알고리즘이 선언되지 않았습니다.", + "msg.dev.clients.general.public_key.cache.parsed_keys_empty": "No parsed JWKS keys are available yet.", + "msg.dev.clients.general.public_key.cache.parsed_keys_help": "Raw JWKS stays hidden. Only parsed key metadata is shown here.", + "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "이 알고리즘은 Headless Login에서 지원되지 않습니다.", + "msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "저장 전 JWKS를 수정해 주세요: {{details}}", + "msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "지원하지 않는 알고리즘이 감지되었습니다.", + "msg.dev.clients.general.public_key.cache_empty": "아직 캐시된 JWKS가 없습니다. Refresh를 눌러 백엔드 캐시 상태를 조회하세요.", + "msg.dev.clients.general.public_key.cache_help": "백엔드가 마지막으로 검증한 공개키 캐시 상태입니다.", + "msg.dev.clients.general.public_key.cache_refresh_failed": "JWKS 캐시 새로고침에 실패했습니다: {{error}}", + "msg.dev.clients.general.public_key.cache_refreshed": "JWKS 캐시를 새로 고쳤습니다.", + "msg.dev.clients.general.public_key.cache_revoke_confirm": "JWKS 캐시를 삭제하면 다음 검증 전에 다시 갱신해야 합니다. 계속할까요?", + "msg.dev.clients.general.public_key.cache_revoke_failed": "JWKS 캐시 삭제에 실패했습니다: {{error}}", + "msg.dev.clients.general.public_key.cache_revoked": "JWKS 캐시를 삭제했습니다.", + "msg.dev.clients.general.public_key.guide_example": "권장 예시: https://rp.example.com/.well-known/jwks.json", + "msg.dev.clients.general.public_key.guide_intro": "JWKS URI는 Baron이 만드는 값이 아니라 RP backend가 공개키를 노출하는 URL입니다.", + "msg.dev.clients.general.public_key.guide_step_1": "RP 서버에서 key pair를 생성하고 private key는 RP backend에만 보관합니다.", + "msg.dev.clients.general.public_key.guide_step_2": "RP backend가 public key를 JWKS(JSON Web Key Set) 형태로 제공하는 endpoint를 준비합니다.", + "msg.dev.clients.general.public_key.guide_step_3": "예: https://rp.example.com/.well-known/jwks.json 같은 URL을 DevFront에 입력합니다.", + "msg.dev.clients.general.public_key.headless_help": "애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다.", + "msg.dev.clients.general.public_key.jwks_inline_help": "SSH-RSA 공개키 형식을 우선 권장합니다. 'ssh-rsa AAA...' 형식으로 입력하면 Baron이 OIDC 표준인 JWKS(JSON)로 자동 변환하여 저장합니다.", + "msg.dev.clients.general.public_key.jwks_uri_help": "RP backend가 제공하는 공개키 endpoint URL을 입력하세요. 예: https://rp.example.com/.well-known/jwks.json", + "msg.dev.clients.general.public_key.request_object_alg_help": "Headless Login을 사용할 때 JAR(Request Object) 서명 알고리즘을 명시합니다.", + "msg.dev.clients.general.public_key.source_help": "애플리케이션의 공개키(SSH-RSA)를 직접 등록하거나, 운영 환경이라면 JWKS URI를 통해 자동으로 검증할 수 있습니다.", + "msg.dev.clients.general.public_key.subtitle": "Trusted RP 판정에 필요한 공개키와 headless login 관련 설정을 관리합니다.", + "msg.dev.clients.general.public_key.validation.headless_requires_alg": "Headless Login을 사용하려면 Request Object Signing Algorithm을 입력해야 합니다.", + "msg.dev.clients.general.public_key.validation.headless_requires_private_key_jwt": "Headless Login을 사용하려면 token endpoint auth method가 private_key_jwt여야 합니다.", + "msg.dev.clients.general.public_key.validation.headless_requires_public_key": "Headless Login을 사용하려면 JWKS URI가 필요합니다.", + "msg.dev.clients.general.public_key.validation.invalid_jwks_inline": "입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다.", + "msg.dev.clients.general.public_key.validation.invalid_jwks_uri": "JWKS URI 형식이 올바르지 않습니다.", + "msg.dev.clients.general.public_key.validation.missing_jwks_inline": "공개키(SSH-RSA 또는 JWKS)를 입력해야 합니다.", + "msg.dev.clients.general.public_key.validation.missing_jwks_uri": "JWKS URI를 입력해야 합니다.", + "msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "JWKS에 알고리즘(`alg`)이 선언되지 않은 키가 있습니다: {{details}}", + "msg.dev.clients.general.public_key.validation.private_key_jwt_requires_public_key": "서명 키 기반 인증을 사용하려면 JWKS URI가 필요합니다.", + "msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "JWKS에 지원하지 않는 알고리즘이 있습니다: {{details}}", + "msg.dev.clients.general.redirect.help": "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다.", "msg.dev.clients.general.save_error": "저장 실패: {{error}}", "msg.dev.clients.general.saved": "설정이 저장되었습니다.", "msg.dev.clients.general.scopes.empty": "등록된 스코프가 없습니다.", "msg.dev.clients.general.scopes.subtitle": "이 앱이 요청할 수 있는 권한 범위를 정의합니다.", - "msg.dev.clients.general.security.pkce_help": - "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다.", - "msg.dev.clients.general.security.private_help": - "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다.", - "msg.dev.clients.general.security.subtitle": - "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다.", + "msg.dev.clients.general.security.pkce_help": "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다.", + "msg.dev.clients.general.security.private_help": "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다.", + "msg.dev.clients.general.security.subtitle": "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다.", "msg.dev.clients.general.status_changed": "상태가 {{status}}로 변경되었습니다.", - "msg.dev.clients.help.docs_body": - "Includes PKCE, client_secret_basic, redirect URI validation tips.", - "msg.dev.clients.help.subtitle": - "Developer guides for Confidential/Public clients, redirect URIs, and auth methods.", - "msg.dev.clients.load_error": "Error loading clients: {{error}}", - "msg.dev.clients.loading": "Loading apps...", - "msg.dev.clients.registry.description": - "OIDC 앱, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.", + "msg.dev.clients.help.docs_body": "Includes PKCE, client_secret_basic, redirect URI validation tips.", + "msg.dev.clients.help.subtitle": "Developer guides for Confidential/Public clients, redirect URIs, and auth methods.", + "msg.dev.clients.load_error": "앱 정보를 불러오지 못했습니다: {{error}}", + "msg.dev.clients.loading": "앱 정보를 불러오는 중...", + "msg.dev.clients.registry.description": "OIDC 앱, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.", "msg.dev.clients.scopes.email": "이메일 주소 접근", "msg.dev.clients.scopes.openid": "OIDC 인증 필수 스코프", "msg.dev.clients.scopes.profile": "기본 프로필 정보 접근", - "msg.dev.clients.showing": "Showing {{shown}} of {{total}} apps", - "msg.dev.clients.status_update_error": "Failed to update client status", - "msg.dev.clients.status_updated": "앱이 {{status}}되었습니다.", - "msg.dev.dashboard.hero.body": - "Hydra Admin API와 동기화된 RP 목록, 상태 토글, Consent 회수까지 devfront에서 처리하도록 준비합니다.", + "msg.dev.clients.showing": "전체 {{total}}개 중 {{shown}}개를 표시하는 중입니다.", + "msg.dev.dashboard.hero.body": "Hydra Admin API와 동기화된 RP 목록, 상태 토글, Consent 회수까지 devfront에서 처리하도록 준비합니다.", "msg.dev.dashboard.hero.title_emphasis": " 하나의 화면", "msg.dev.dashboard.hero.title_prefix": "RP 등록 현황과 Consent 상태를", "msg.dev.dashboard.hero.title_suffix": "에서 관리합니다.", "msg.dev.dashboard.notice.consent_audit": "Consent 회수는 감사 로그와 연계", "msg.dev.dashboard.notice.dev_scope": "RP 정책은 dev scope에서만 적용", "msg.dev.dashboard.notice.hydra_health": "Hydra Admin 상태 체크 준비", + "msg.dev.forbidden.default": "해당 리소스에 접근할 권한이 없습니다. 관리자에게 문의하세요.", + "msg.dev.forbidden.rp_admin": "RP 관리자는 담당 앱의 리소스만 조회할 수 있습니다.", + "msg.dev.forbidden.tenant_admin": "테넌트 관리자 권한이 올바르게 설정되지 않았거나 만료되었습니다.", + "msg.dev.forbidden.title": "{{resource}} 접근 권한 없음", + "msg.dev.forbidden.user": "일반 사용자는 관리자 화면에 접근할 수 없습니다.", "msg.dev.logout_confirm": "로그아웃 하시겠습니까?", "msg.dev.sidebar.notice": "개발자 전용 콘솔입니다.", "msg.dev.sidebar.notice_detail": "연동 앱 등록 및 관리를 수행할 수 있습니다.", @@ -259,16 +344,28 @@ const Map koStrings = { "msg.userfront.audit.result": "인증결과: {{value}}", "msg.userfront.audit.session_id": "Session ID: {{value}}", "msg.userfront.audit.status": "현황: (준비중)", + "msg.userfront.consent.accept_error": "동의 처리에 실패했습니다: {{error}}", + "msg.userfront.consent.cancel.confirm": "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?", + "msg.userfront.consent.cancel.error": "취소 처리 중 오류가 발생했습니다: {{error}}", + "msg.userfront.consent.client_id": "클라이언트 ID: {{id}}", + "msg.userfront.consent.client_unknown": "알 수 없는 앱", + "msg.userfront.consent.description": "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\n계속 진행하려면 동의 여부를 선택해 주세요.", + "msg.userfront.consent.load_error": "동의 정보를 불러오는데 실패했습니다: {{error}}", + "msg.userfront.consent.missing_redirect": "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다.", + "msg.userfront.consent.redirect_notice": "동의 후 자동으로 서비스로 이동합니다.", + "msg.userfront.consent.scope.email": "이메일 주소 (계정 식별 및 알림 용도)", + "msg.userfront.consent.scope.offline_access": "오프라인 접근 (로그인 유지)", + "msg.userfront.consent.scope.openid": "OpenID 인증 정보 (로그인 상태 확인)", + "msg.userfront.consent.scope.phone": "휴대폰 번호 (본인 인증 및 알림)", + "msg.userfront.consent.scope.profile": "기본 프로필 정보 (이름, 사용자 식별자)", + "msg.userfront.consent.scope_count": "총 {{count}}개", "msg.userfront.dashboard.activities.empty": "연동된 앱이 없습니다.", - "msg.userfront.dashboard.activities.empty_detail": - "앱을 연동하면 최근 활동과 상태가 표시됩니다.", + "msg.userfront.dashboard.activities.empty_detail": "앱을 연동하면 최근 활동과 상태가 표시됩니다.", "msg.userfront.dashboard.activities.error": "연동 정보를 불러오지 못했습니다.", "msg.userfront.dashboard.approved_device": "승인 기기: {{device}}", "msg.userfront.dashboard.approved_ip": "승인 IP: {{ip}}", - "msg.userfront.dashboard.approved_session.copy_click": - "{{label}}: {{id}}\n클릭하면 복사됩니다.", - "msg.userfront.dashboard.approved_session.copy_tap": - "{{label}}: {{id}}\n탭하면 복사됩니다.", + "msg.userfront.dashboard.approved_session.copy_click": "{{label}}: {{id}}\\\\n클릭하면 복사됩니다.", + "msg.userfront.dashboard.approved_session.copy_tap": "{{label}}: {{id}}\\\\n탭하면 복사됩니다.", "msg.userfront.dashboard.approved_session.none": "{{label}} 없음", "msg.userfront.dashboard.audit_empty": "최근 접속 이력이 없습니다.", "msg.userfront.dashboard.audit_load_error": "접속이력을 불러오지 못했습니다.", @@ -280,14 +377,23 @@ const Map koStrings = { "msg.userfront.dashboard.link_missing": "이동할 페이지 주소(Client URI)가 설정되지 않았습니다.", "msg.userfront.dashboard.link_open_error": "해당 링크를 열 수 없습니다.", "msg.userfront.dashboard.render_error": "대시보드 렌더링 오류: {{error}}", - "msg.userfront.dashboard.revoke.confirm": - "{{app}} 앱과의 연동을 해지하시겠습니까?\n해지하면 다음 로그인 시 다시 동의가 필요합니다.", + "msg.userfront.dashboard.revoke.confirm": "{{app}} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.", "msg.userfront.dashboard.revoke.error": "해지 실패: {{error}}", "msg.userfront.dashboard.revoke.success": "{{app}} 연동이 해지되었습니다.", "msg.userfront.dashboard.scopes.empty": "요청된 권한이 없습니다.", "msg.userfront.dashboard.session_id_copied": "세션 ID가 복사되었습니다.", + "msg.userfront.dashboard.sessions.browser": "브라우저: {{value}}", + "msg.userfront.dashboard.sessions.empty": "활성 세션이 없습니다.", + "msg.userfront.dashboard.sessions.empty_detail": "같은 계정으로 로그인한 기기가 여기에 표시됩니다.", + "msg.userfront.dashboard.sessions.error": "세션 정보를 불러오지 못했습니다.", + "msg.userfront.dashboard.sessions.os": "OS: {{value}}", + "msg.userfront.dashboard.sessions.recent_app": "최근 접속 앱: {{app}}", + "msg.userfront.dashboard.sessions.revoke.confirm": "{{target}} 세션을 종료하시겠습니까?\n대상 기기에서는 다시 로그인이 필요합니다.", + "msg.userfront.dashboard.sessions.revoke.error": "세션 종료 실패: {{error}}", + "msg.userfront.dashboard.sessions.revoke.success": "세션이 종료되었습니다.", + "msg.userfront.dashboard.sessions.session_id": "세션 ID: {{id}}", "msg.userfront.dashboard.timeline.load_error": "접속이력을 불러오지 못했습니다.", - "msg.userfront.error.detail_contact": "msg.userfront.error.detail_contact", + "msg.userfront.error.detail_contact": "관리자에게 문의해 주세요.", "msg.userfront.error.detail_generic": "오류가 발생했습니다.", "msg.userfront.error.detail_request": "요청을 처리하는 중 문제가 발생했습니다.", "msg.userfront.error.id": "오류 ID: {{id}}", @@ -313,17 +419,13 @@ const Map koStrings = { "msg.userfront.error.whitelist.bad_request": "입력값을 확인해 주세요.", "msg.userfront.error.whitelist.invalid_session": "세션이 만료되었습니다. 다시 로그인해 주세요.", "msg.userfront.error.whitelist.not_found": "요청한 페이지를 찾을 수 없습니다.", - "msg.userfront.error.whitelist.password_or_email_mismatch": - "이메일 혹은 비밀번호가 일치하지 않습니다.", + "msg.userfront.error.whitelist.password_or_email_mismatch": "이메일 혹은 비밀번호가 일치하지 않습니다.", "msg.userfront.error.whitelist.rate_limited": "요청이 많습니다. 잠시 후 다시 시도해 주세요.", - "msg.userfront.error.whitelist.recovery_expired": - "재설정 링크가 만료되었습니다. 다시 요청해 주세요.", + "msg.userfront.error.whitelist.recovery_expired": "재설정 링크가 만료되었습니다. 다시 요청해 주세요.", "msg.userfront.error.whitelist.recovery_invalid": "재설정 링크가 유효하지 않습니다.", "msg.userfront.error.whitelist.settings_disabled": "현재 계정 설정 화면은 준비 중입니다.", - "msg.userfront.error.whitelist.verification_required": - "추가 인증이 필요합니다. 안내에 따라 진행해 주세요.", - "msg.userfront.forgot.description": - "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.", + "msg.userfront.error.whitelist.verification_required": "추가 인증이 필요합니다. 안내에 따라 진행해 주세요.", + "msg.userfront.forgot.description": "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.", "msg.userfront.forgot.dry_send": "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.", "msg.userfront.forgot.error": "전송에 실패했습니다: {{error}}", "msg.userfront.forgot.input_required": "이메일 또는 휴대폰 번호를 입력해주세요.", @@ -331,13 +433,12 @@ const Map koStrings = { "msg.userfront.greeting": "안녕하세요, {{name}}님", "msg.userfront.login.cookie_check_failed": "로그인 확인 실패: {{error}}", "msg.userfront.login.dry_send": "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.", - "msg.userfront.login.link.approved": "링크로 로그인 되었습니다. 잠시 후 로그인 화면으로 이동합니다.", + "msg.userfront.login.link.approved": "msg.userfront.login.link.approved", "msg.userfront.login.link.helper": "입력하신 정보로 로그인 링크를 전송합니다.", "msg.userfront.login.link.missing_login_id": "이메일 또는 휴대폰 번호를 입력해 주세요.", "msg.userfront.login.link.missing_phone": "휴대폰 번호를 입력해 주세요.", "msg.userfront.login.link.resend_wait": "재발송은 {{time}} 후 가능합니다.", - "msg.userfront.login.link.short_code_help": - "링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.", + "msg.userfront.login.link.short_code_help": "링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.", "msg.userfront.login.link_failed": "오류: {{error}}", "msg.userfront.login.link_send_failed": "전송 실패: {{error}}", "msg.userfront.login.link_sent_email": "입력하신 이메일로 로그인 링크를 보냈습니다.", @@ -346,8 +447,7 @@ const Map koStrings = { "msg.userfront.login.no_account": "계정이 없으신가요?", "msg.userfront.login.oidc_failed": "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요.", "msg.userfront.login.password.failed": "로그인 실패: {{error}}", - "msg.userfront.login.password.missing_credentials": - "이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.", + "msg.userfront.login.password.missing_credentials": "이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.", "msg.userfront.login.qr.load_failed": "QR 코드를 불러오지 못했습니다.", "msg.userfront.login.qr.scan_hint": "모바일 앱으로 스캔하세요", "msg.userfront.login.qr_expired": "시간이 경과되었습니다.", @@ -355,10 +455,9 @@ const Map koStrings = { "msg.userfront.login.qr_login_required": "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다", "msg.userfront.login.short_code.invalid": "문자 2개와 숫자 6자리를 입력해 주세요.", "msg.userfront.login.token_missing": "로그인 토큰을 확인할 수 없습니다.", - "msg.userfront.login.unregistered.body": "가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요.", + "msg.userfront.login.unregistered.body": "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주세요.", "msg.userfront.login.verification.approved": "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.", - "msg.userfront.login.verification.approved_local": - "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다", + "msg.userfront.login.verification.approved_local": "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다", "msg.userfront.login.verification.success": "로그인 승인에 성공했습니다.", "msg.userfront.login.verification_failed": "승인 처리에 실패했습니다: {{error}}", "msg.userfront.login_success.subtitle": "성공적으로 로그인되었습니다.", @@ -387,8 +486,8 @@ const Map koStrings = { "msg.userfront.profile.section.security": "비밀번호를 안전하게 관리합니다.", "msg.userfront.profile.update_failed": "수정 실패: {{error}}", "msg.userfront.profile.update_success": "정보가 수정되었습니다.", - "msg.userfront.qr.approve_error": "QR 승인 실패: {{error}}", - "msg.userfront.qr.approve_success": "QR 승인 완료! PC 화면에서 로그인이 진행됩니다.", + "msg.userfront.qr.approve_error": "msg.userfront.qr.approve_error", + "msg.userfront.qr.approve_success": "msg.userfront.qr.approve_success", "msg.userfront.qr.camera_error": "카메라 오류: {{error}}", "msg.userfront.qr.permission_error": "카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.", "msg.userfront.qr.permission_required": "카메라 권한이 필요합니다.", @@ -396,8 +495,7 @@ const Map koStrings = { "msg.userfront.reset.error.generic": "비밀번호 변경에 실패했습니다: {{error}}", "msg.userfront.reset.error.lowercase": "최소 1개 이상의 소문자를 포함해야 합니다.", "msg.userfront.reset.error.min_length": "비밀번호는 최소 {{count}}자 이상이어야 합니다.", - "msg.userfront.reset.error.min_types": - "비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.", + "msg.userfront.reset.error.min_types": "비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.", "msg.userfront.reset.error.mismatch": "비밀번호가 일치하지 않습니다.", "msg.userfront.reset.error.number": "최소 1개 이상의 숫자를 포함해야 합니다.", "msg.userfront.reset.error.symbol": "최소 1개 이상의 특수문자를 포함해야 합니다.", @@ -415,11 +513,16 @@ const Map koStrings = { "msg.userfront.reset.success": "비밀번호가 성공적으로 변경되었습니다. 다시 로그인해주세요.", "msg.userfront.sections.apps_subtitle": "현재 연결된 앱과 최근 인증 상태입니다.", "msg.userfront.sections.audit_subtitle": "Baron 로그인 기준의 최근 접근 기록입니다.", + "msg.userfront.sections.sessions_subtitle": "현재 로그인된 기기와 브라우저 세션입니다.", "msg.userfront.settings.disabled": "현재 계정 설정 화면은 준비 중입니다.", - "msg.userfront.signup.agreement.title": "서비스 이용을 위해\n약관에 동의해주세요", - "msg.userfront.signup.auth.affiliate_notice": - "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.", - "msg.userfront.signup.auth.title": "본인 확인을 위해\n인증을 진행해주세요", + "msg.userfront.signup.agreement.all_hint": "필수 약관 2개를 모두 확인하고 동의하면 다음 단계로 진행할 수 있습니다.", + "msg.userfront.signup.agreement.description": "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요.", + "msg.userfront.signup.agreement.privacy_summary": "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다.", + "msg.userfront.signup.agreement.progress": "필수 약관 {{total}}개 중 {{count}}개 동의 완료", + "msg.userfront.signup.agreement.title": "서비스 이용을 위해\\\\n약관에 동의해주세요", + "msg.userfront.signup.agreement.tos_summary": "서비스 이용 조건과 책임 범위를 확인할 수 있습니다.", + "msg.userfront.signup.auth.affiliate_notice": "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.", + "msg.userfront.signup.auth.title": "본인 확인을 위해\\\\n인증을 진행해주세요", "msg.userfront.signup.email.code_mismatch": "인증코드가 일치하지 않습니다.", "msg.userfront.signup.email.duplicate": "이미 가입된 이메일입니다.", "msg.userfront.signup.email.invalid": "유효한 이메일 형식이 아닙니다.", @@ -428,8 +531,7 @@ const Map koStrings = { "msg.userfront.signup.email.verify_failed": "인증 실패: {{error}}", "msg.userfront.signup.failed": "가입 실패: {{error}}", "msg.userfront.signup.password.length_required": "비밀번호는 최소 12자 이상이어야 합니다.", - "msg.userfront.signup.password.lowercase_required": - "소문자가 최소 1개 이상 포함되어야 합니다.", + "msg.userfront.signup.password.lowercase_required": "소문자가 최소 1개 이상 포함되어야 합니다.", "msg.userfront.signup.password.mismatch": "비밀번호가 일치하지 않습니다.", "msg.userfront.signup.password.number_required": "숫자가 최소 1개 이상 포함되어야 합니다.", "msg.userfront.signup.password.rule.lowercase": "소문자", @@ -439,9 +541,8 @@ const Map koStrings = { "msg.userfront.signup.password.rule.symbol": "특수문자", "msg.userfront.signup.password.rule.uppercase": "대문자", "msg.userfront.signup.password.symbol_required": "특수문자가 최소 1개 이상 포함되어야 합니다.", - "msg.userfront.signup.password.title": "마지막으로\n비밀번호를 설정해주세요", - "msg.userfront.signup.password.uppercase_required": - "대문자가 최소 1개 이상 포함되어야 합니다.", + "msg.userfront.signup.password.title": "마지막으로\\\\n비밀번호를 설정해주세요", + "msg.userfront.signup.password.uppercase_required": "대문자가 최소 1개 이상 포함되어야 합니다.", "msg.userfront.signup.phone.code_mismatch": "인증코드가 일치하지 않습니다.", "msg.userfront.signup.phone.send_failed": "발송 실패: {{error}}", "msg.userfront.signup.phone.verified": "✅ 휴대폰 인증 완료", @@ -454,17 +555,16 @@ const Map koStrings = { "msg.userfront.signup.policy.summary": "보안 정책: {{rules}}", "msg.userfront.signup.policy.symbol": "특수문자", "msg.userfront.signup.policy.uppercase": "대문자", - "msg.userfront.signup.privacy_full": - "\n개인정보 수집 및 이용 동의\n\n바론서비스 개인정보처리방침\n\n제1조 (목적)\n바론컨설턴트(이하 \"회사\")는 바론서비스(이하 \"서비스\")를 이용하는 고객(이하 \"이용자\")의 개인정보를 보호하고, 「개인정보 보호법」에 따라 책임과 의무를 다하기 위해 본 개인정보처리방침을 마련했습니다. 본 방침은 이용자가 제공한 개인정보가 어떻게 수집, 이용, 보관, 보호되는지를 설명합니다.\n제2조 (개인정보의 처리목적)\n회사는 다음의 목적을 위해 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\n- 본인확인: 회원가입 및 관리를 위한 본인 확인, 전화 또는 이메일을 통한 연락\n- 서비스 제공: 각종 통보 및 서비스 제공을 위한 업무 처리\n- 제품소개서 다운로드: 설명자료 전달\n- 상담 및 데모 신청: 상담 제공 및 데모 제공, 계약 처리자 정보 수집\n- 행사 참가 신청: 참석 안내 및 세미나/설명회/교육 제공\n- 보안가이드 제공: 안내자료 전달\n- 기술지원 문의: 서비스 사용 지원\n- 서비스 개선 의견 접수: 서비스 품질 개선\n- 마케팅 활동: 동의한 고객에 한해 뉴스레터 및 매거진 발송\n제3조 (개인정보의 처리 및 보유 기간)\n① 회사는 법령에 따른 개인정보 보유 및 이용기간 또는 정보주체로부터 개인정보를 수집 시 동의받은 개인정보 보유 및 이용기간 내에서 개인정보를 처리 및 보유합니다.\n② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다:\n- 회원정보: 회원가입일부터 회원탈퇴 후 1년까지\n- 홍보, 상담, 계약용 개인정보: 2년\n제4조 (개인정보의 제3자 제공)\n① 회사는 정보주체의 개인정보를 제2조에서 명시한 범위 내에서만 처리하며, 정보 주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.\n② 회사는 다음과 같이 개인정보를 제3자에게 제공하고 있습니다:\n- 제공받는 자: 수사기관 및 유관기관, 피신고업체\n- 이용 목적: 개인정보 침해 민원 처리\n- 제공하는 개인정보 항목: 성명, 연락처, 이메일\n- 보유 및 이용기간: 법령에서 정한 보존기간 및 제공목적 달성 시 파기\n제5조 (개인정보 처리 위탁)\n① 회사는 개인정보 처리업무를 외부 업체에 위탁하지 않으며, 자체적으로 처리하고 있습니다.\n② 회사가 특정 업무(예: 채용 업무)를 외부 업체에 위탁할 경우, 개인정보 처리방침 시행 전 회사 홈페이지에서 공지한 후 정보주체의 동의를 받은 후 위탁합니다.\n제6조 (정보주체의 권리·의무 및 행사 방법)\n① 정보주체는 회사에 대해 언제든지 개인정보 열람, 정정, 삭제, 처리정지 요구 등의 권리를 행사할 수 있습니다.\n② 권리 행사는 다음과 같은 방법으로 할 수 있습니다:\n- 서면: 회사 주소로 서면 제출\n- 전자우편: 회사 이메일로 요청\n- 모사전송(FAX): 회사 FAX로 요청\n③ 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자를 통해 대리로도 가능합니다. 이 경우 “개인정보 처리 방법에 관한 고시” 별지 제11호 서식에 따른 위임장을 제출해야 합니다.\n④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 따라 제한될 수 있습니다.\n⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에 따라 수집된 개인정보인 경우 제한될 수 있습니다.\n⑥ 회사는 권리 행사를 요청한 자가 본인 또는 정당한 대리인인지를 확인합니다.\n제7조 (처리하는 개인정보의 항목)\n회사는 다음의 개인정보 항목을 처리합니다:\n- 수집 항목:\n- 필수 항목: 성명, 휴대전화번호, 이메일\n- 선택 항목: 회사전화번호, 문의사항\n- 수집 방법:\n- 홈페이지, 전화, 이메일을 통해 수집\n제8조 (개인정보의 파기)\n① 회사는 개인정보 보유 기간의 경과, 처리 목적 달성 등 개인정보가 불필요하게 되었을 때 지체 없이 해당 개인정보를 파기합니다.\n② 정보주체로부터 동의받은 개인정보 보유 기간이 경과하거나 처리 목적이 달성된 경우에도 다른 법령에 따라 개인정보를 계속 보존해야 할 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관 장소를 달리하여 보존합니다.\n③ 개인정보 파기의 절차 및 방법은 다음과 같습니다:\n- 파기 절차: 회사는 파기 사유가 발생한 개인정보를 선정하고, 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.\n- 파기 방법: 전자적 파일 형태로 기록된 개인정보는 복구할 수 없도록 기술적 방법을 사용해 삭제하며, 종이 문서에 기록된 개인정보는 분쇄기로 분쇄하거나 소각하여 파기합니다.\n제9조 (개인정보의 안전성 확보 조치)\n회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취합니다:\n- 관리적 조치: 내부관리계획 수립·시행, 정기적 직원 교육\n- 기술적 조치: 개인정보처리시스템 접근 권한 관리, 접근통제시스템 설치, 고유식별정보 암호화, 보안 프로그램 설치\n- 물리적 조치: 전산실 및 자료보관실 접근 통제\n제10조 (개인정보 자동 수집 장치의 설치·운영 및 거부에 관한 사항)\n회사는 쿠키(Cookie)를 사용하지 않습니다. 쿠키는 이용자의 이용 정보를 저장하고 수시로 불러오는 작은 파일로, 바론서비스에서는 쿠키를 사용하지 않습니다.\n제11조 (개인정보 보호책임자)\n회사는 개인정보 처리에 관한 업무를 총괄하여 책임지고, 개인정보 처리와 관련된 정보주체의 불만처리 및 피해구제를 위해 개인정보 보호책임자를 지정하고 있습니다.\n개인정보 보호책임자:\n- 성명: 염승호\n- 직책: 수석연구원\n- 연락처: 02-2141-7448\n- 팩스번호: 02-2141-7599\n- 이메일: b23008@baroncs.co.kr\n제12조 (개인정보 열람청구)\n정보주체는 「개인정보 보호법」 제35조에 따른 개인정보 열람 청구를 아래 부서에 할 수 있습니다. 회사는 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.\n개인정보 열람청구 접수·처리 부서:\n- 부서명: 총괄기획실\n- 담당자: 권혁진\n- 연락처: 02-2141-7465\n- 팩스번호: 02-2141-7599\n- 이메일: baroncs@baroncs.co.kr\n제13조 (권익침해 구제방법)\n정보주체는 개인정보 침해로 인한 구제를 위해 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보해신고센터 등에 분쟁 해결이나 상담을 신청할 수 있습니다.\n- 개인정보분쟁조정위원회: (국번없이) 1833-6972 (www.kopico.go.kr)\n- 개인정보침해신고센터: (국번없이) 118 (privacy.kisa.or.kr)\n- 대검찰청: (국번없이) 1301 (www.spo.go.kr)\n- 경찰청: (국번없이) 182 (www.police.go.kr)\n제14조 (개인정보 처리방침의 변경)\n본 개인정보처리방침은 법령, 정책 또는 보안 기술의 변경에 따라 내용의 추가, 삭제 및 수정이 있을 시, 개정 최소 7일 전에 홈페이지를 통해 사전 공지합니다.\n\n부칙\n제1조 (시행일자)\n이 개인정보처리방침은 2024년 10월 1일부터 시행됩니다.\n제2조 (개정 및 고지의 의무)\n회사는 개인정보처리방침을 변경하는 경우, 변경사항을 시행일자 7일 전부터 서비스 내 공지사항 페이지를 통해 고지할 것입니다. 다만, 이용자의 권리나 의무에 중대한 변경이 발생하는 경우에는 시행일자 30일 전부터 고지합니다.\n제3조 (유효성)\n본 개인정보처리방침의 일부 조항이 법적 또는 기타 사유로 인해 무효화되거나 시행할 수 없는 경우, 나머지 조항들은 계속해서 유효합니다. 무효화된 조항은 관련 법령에 부합하는 방식으로 수정되어 효력을 지속합니다.\n제4조 (변경 통지의 방법)\n회사는 개인정보처리방침의 변경 시, 다음의 방법으로 이용자에게 고지합니다:\n- 서비스 초기화면 또는 팝업 공지\n- 이메일 발송\n- 회사 홈페이지 공지사항\n제5조 (비회원의 개인정보 보호)\n회사는 비회원의 개인정보도 회원과 동일한 수준으로 보호합니다. 비회원이 개인정보 제공을 거부할 경우 일부 서비스 이용에 제한이 있을 수 있습니다.\n제6조 (14세 미만 아동의 개인정보 보호)\n회사는 14세 미만 아동의 개인정보를 수집하지 않습니다. 만일 14세 미만 아동의 개인정보가 수집된 경우, 법정 대리인의 동의를 받아야 하며, 법정 대리인의 동의 없이 수집된 경우 이를 지체 없이 파기합니다.\n제7조 (개인정보의 국외 이전)\n회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.\n제8조 (기타)\n본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.\n", + "msg.userfront.signup.privacy_full": "개인정보 수집 및 이용 동의 전문...", "msg.userfront.signup.profile.affiliate_hint": "가족사 이메일 사용 시 자동으로 선택됩니다.", - "msg.userfront.signup.profile.title": "회원님의\n소속 정보를 알려주세요", + "msg.userfront.signup.profile.title": "회원님의\\\\n소속 정보를 알려주세요", "msg.userfront.signup.success.body": "성공적으로 가입되었습니다.", "msg.userfront.signup.success.title": "회원가입 완료", - "msg.userfront.signup.tos_full": - "\n바론 소프트웨어 이용약관\n\n제1장 총칙\n제1조 (목적)\n이 약관은 바론컨설턴트(이하 \"회사\"라 합니다)가 제공하는 바론소프트웨어(이하 \"서비스\"라 합니다)를 이용함에 있어 회사와 이용자 간의 권리, 의무 및 책임사항과 기타 필요한 사항을 정하는 것을 목적으로 합니다.\n제2조 (용어의 정의)\n① 본 약관에서 사용하는 용어의 정의는 다음과 같습니다:\n- “서비스”란 회사가 제공하는 소프트웨어 및 관련 제반 서비스를 의미합니다.\n- “이용자”란 회사의 서비스에 접속하여 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.\n- “회원”이란 본 약관에 동의하고 회사와 이용계약을 체결한 자를 의미합니다.\n- “비회원”이란 회원가입을 하지 않고 회사가 제공하는 일부 서비스를 이용하는 자를 말합니다.\n제3조 (약관의 효력 및 변경)\n① 본 약관은 이용자가 본 약관에 동의하고, 회사가 이에 대한 승낙을 완료함으로써 효력이 발생합니다. ② 회사는 필요한 경우 본 약관을 변경할 수 있으며, 변경된 약관은 서비스 화면에 공지된 후 효력이 발생합니다.\n제4조 (약관 외 준칙)\n본 약관에 명시되지 않은 사항에 대해서는 대한민국의 관련 법령과 상관습에 따릅니다.\n제2장 서비스 이용계약\n제5조 (이용계약의 성립)\n이용계약은 이용자가 약관의 내용에 동의하고, 회사가 제공하는 소정의 회원가입 신청서를 작성하여 가입을 완료한 후, 회사가 이를 승인함으로써 성립합니다.\n제6조 (이용계약의 유보와 거절)\n① 회사는 다음 각 호에 해당하는 경우 이용계약의 성립을 유보하거나 거절할 수 있습니다: - 신청서의 내용이 허위로 판명된 경우 - 서비스 제공이 기술적으로 어려운 경우\n제7조 (계약사항의 변경)\n회원은 개인정보 관리 메뉴를 통해 언제든지 자신의 정보를 열람하고 수정할 수 있습니다. 회원의 정보가 변경된 경우 즉시 수정해야 하며, 수정하지 않아 발생하는 문제의 책임은 회원에게 있습니다.\n제3장 개인정보 보호\n제8조 (개인정보 보호의 원칙)\n① 회원의 개인정보는 관련 법령에 따라 보호됩니다. ② 회사는 개인정보 보호와 관련된 세부 사항을 별도로 마련한 개인정보처리방침에 따라 관리하며, 이용자는 언제든지 해당 방침을 통해 개인정보 관리에 대한 자세한 내용을 확인할 수 있습니다.\n제9조 (개인정보처리방침 준수)\n① 회사는 개인정보 보호와 관련된 구체적인 사항을 개인정보처리방침에 따라 관리합니다. ② 개인정보의 수집, 이용, 제공, 보관, 보호 등에 관한 사항은 회사의 개인정보처리방침을 따르며, 이용자는 회사 웹사이트에서 이를 확인할 수 있습니다. ③ 회사는 개인정보 보호를 위해 최선을 다하며, 관련 법령에 따라 이용자의 개인정보를 안전하게 관리합니다.\n제10조 (14세 미만 아동의 개인정보 보호)\n① 회사는 14세 미만 아동의 개인정보를 수집할 경우, 반드시 법정대리인의 동의를 받아야 합니다. ② 법정대리인은 아동의 개인정보 열람, 수정, 삭제를 요청할 수 있으며, 회사는 이를 신속하게 처리합니다. ③ 14세 미만 아동의 개인정보 보호와 관련된 구체적인 사항은 개인정보처리방침에 명시되어 있습니다.\n제4장 서비스 제공 및 이용\n제11조 (서비스 제공)\n회사는 회원의 이용 신청을 승인한 때부터 서비스를 개시합니다. 서비스 이용은 연중무휴 24시간을 원칙으로 합니다.\n제12조 (서비스의 변경 및 중단)\n회사는 서비스 제공이 어려운 경우 사전 고지 후 서비스를 변경하거나 중단할 수 있습니다.\n제5장 정보 제공 및 광고\n제13조 (정보 제공 및 광고)\n① 회사는 서비스 이용 중 필요하다고 인정되는 정보 및 광고를 제공할 수 있습니다. ② 회원은 원치 않는 정보를 수신 거부할 수 있습니다.\n제6장 게시물 관리\n제14조 (게시물의 관리)\n회사는 회원이 게시한 내용이 불법적이거나 약관에 위배될 경우 이를 삭제할 수 있습니다.\n제15조 (게시물의 저작권)\n게시물의 저작권은 회원에게 있으며, 회사는 이를 서비스 홍보 및 개선 목적으로 사용할 수 있습니다.\n제7장 계약 해지 및 이용 제한\n제16조 (계약 해지)\n회원은 언제든지 계약 해지를 요청할 수 있으며, 회사는 신속하게 처리합니다.\n제17조 (이용 제한)\n회사는 회원이 약관을 위반할 경우 서비스 이용을 제한할 수 있습니다.\n제8장 손해 배상 및 면책 조항\n제18조 (손해 배상)\n회사는 무료로 제공되는 서비스와 관련하여 회원에게 발생한 손해에 대해 책임을 지지 않습니다.\n제19조 (면책 조항)\n회사는 천재지변 등 불가항력적인 사유로 인해 서비스를 제공하지 못하는 경우 책임을 지지 않습니다.\n제9장 유료 서비스\n20조 (유료 서비스의 이용)\n① 회사는 회원에게 특정 서비스에 대해 유료로 제공할 수 있습니다. ② 유료 서비스의 이용 요금, 결제 방식, 환불 절차 등에 대한 상세 내용은 서비스 안내 페이지와 결제 화면에 명시합니다. ③ 유료 서비스 이용 요금은 회사가 정한 결제 방식에 따라 결제됩니다. 회원은 신용카드, 계좌이체, 휴대전화 결제 등 회사가 제공하는 다양한 결제 방식을 통해 요금을 납부할 수 있습니다. ④ 유료 서비스의 이용 요금은 선불 결제를 원칙으로 하며, 이용 기간 중 서비스 중지 및 해지 시 남은 이용 기간에 대한 환불은 회사의 환불 정책에 따라 처리됩니다. ⑤ 회사는 회원의 유료 서비스 이용과 관련하여 발생한 문제에 대해 최선을 다해 해결하도록 노력합니다. 다만, 회사의 고의 또는 중대한 과실이 없는 한 회원이 유료 서비스 이용 중 입은 손해에 대해서는 책임을 지지 않습니다.\n제21조(환불 정책)\n① 회원은 결제 후 7일 이내에 서비스 이용을 시작하지 않은 경우, 요금 전액을 환불받을 수 있습니다. ② 유료 서비스 이용 중 부득이한 사유로 서비스가 중지된 경우, 회사는 이용하지 않은 부분에 대해 환불 절차를 밟습니다. ③ 회원의 귀책사유로 인해 서비스 이용이 중지된 경우, 환불이 불가능합니다. ④ 환불은 회원이 지정한 계좌로 환불 절차를 거치며, 환불 요청 후 7일 이내에 처리됩니다.\n제22조 (유료 서비스의 중지 및 해지)\n① 회원이 유료 서비스를 해지하고자 하는 경우, 회사의 고객 지원 센터에 해지 신청을 해야 합니다. ② 회사는 회원이 약관을 위반하거나 부정한 방법으로 유료 서비스를 이용한 경우, 유료 서비스 이용을 즉시 중지하고 계약을 해지할 수 있습니다.\n제10장 양도 금지\n제23조 (양도 금지)\n회원은 서비스 이용권한, 기타 이용계약상의 지위를 제3자에게 양도, 증여할 수 없으며, 이를 담보로 제공할 수 없습니다.\n제11장 관할 법원\n제24조 (분쟁 해결)\n서비스 이용과 관련하여 분쟁이 발생한 경우, 회사와 회원은 성실히 협의하여 해결합니다.\n제25조 (관할 법원)\n본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.\n부칙\n본 약관은 2024년 10월 1일부터 시행됩니다.\n", + "msg.userfront.signup.tos_full": "서비스 이용약관 전문...", + "non.existent.key": "존재하지 않는 키", + "test.key": "테스트", "ui.admin.api_keys.create.name_label": "서비스 또는 목적 식별 이름", - "ui.admin.api_keys.create.name_placeholder": - "예: Jenkins-CI, Grafana-Dashboard", + "ui.admin.api_keys.create.name_placeholder": "예: Jenkins-CI, Grafana-Dashboard", "ui.admin.api_keys.create.section_name": "키 이름 지정", "ui.admin.api_keys.create.section_scopes": "권한 범위(Scopes) 선택", "ui.admin.api_keys.create.submit": "API 키 발급하기", @@ -515,7 +615,7 @@ const Map koStrings = { "ui.admin.audit.title": "감사 로그", "ui.admin.brand": "Baron 로그인", "ui.admin.dev_role_switcher": "🛠 DEV Role Switcher", - "ui.admin.groups.add_unit": "조직 추가", + "ui.admin.dev_role_switcher_real": "실제 역할 사용", "ui.admin.groups.create.description": "부서나 팀과 같은 새로운 조직 단위를 추가합니다.", "ui.admin.groups.create.title": "새 그룹 생성", "ui.admin.groups.detail.breadcrumb_org": "조직 관리 목록으로 돌아가기", @@ -530,7 +630,6 @@ const Map koStrings = { "ui.admin.groups.form.name_label": "그룹 이름", "ui.admin.groups.form.name_placeholder": "예: 개발팀, 인사팀", "ui.admin.groups.form.parent_label": "상위 조직", - "ui.admin.groups.form.parent_none": "없음 (최상위)", "ui.admin.groups.form.submit": "생성하기", "ui.admin.groups.form.unit_level_label": "조직 레벨", "ui.admin.groups.form.unit_level_placeholder": "예: 본부, 팀", @@ -540,11 +639,10 @@ const Map koStrings = { "ui.admin.groups.members.table.name": "이름", "ui.admin.groups.members.table.remove": "제거", "ui.admin.groups.table.actions": "ACTIONS", - "ui.admin.groups.table.created_at": "생성일", - "ui.admin.groups.table.level": "레벨", "ui.admin.groups.table.members": "MEMBERS", "ui.admin.groups.table.name": "NAME", "ui.admin.header.plane": "Admin Plane", + "ui.admin.header.subtitle": "관리 및 정책 운영", "ui.admin.nav.api_keys": "API 키", "ui.admin.nav.audit_logs": "감사 로그", "ui.admin.nav.auth_guard": "인증 가드", @@ -555,17 +653,27 @@ const Map koStrings = { "ui.admin.nav.tenants": "테넌트", "ui.admin.nav.user_groups": "유저 그룹", "ui.admin.nav.users": "사용자", + "ui.admin.org.download_template": "템플릿 다운로드", + "ui.admin.org.import_btn": "임포트", + "ui.admin.org.import_title": "조직도 대량 등록", + "ui.admin.org.start_import": "임포트 시작", "ui.admin.overview.kicker": "Global Overview", "ui.admin.overview.playbook.title": "Admin playbook", "ui.admin.overview.quick_links.add_tenant": "테넌트 추가", - "ui.admin.overview.quick_links.tenant_dashboard": "테넌트 대시보드", + "ui.admin.overview.quick_links.api_key_management": "API 키 관리", "ui.admin.overview.quick_links.title": "빠른 이동", + "ui.admin.overview.quick_links.user_management": "사용자 관리", "ui.admin.overview.quick_links.view_audit_logs": "감사 로그 보기", - "ui.admin.overview.title": "Tenant-independent control plane", + "ui.admin.overview.summary.audit_events_24h": "24시간 이벤트", + "ui.admin.overview.summary.oidc_clients": "OIDC 클라이언트", + "ui.admin.overview.summary.policy_gate": "정책 게이트", + "ui.admin.overview.summary.total_tenants": "전체 테넌트 수", + "ui.admin.overview.title": "통합 대시보드", + "ui.admin.profile.manageable_tenants": "관리 가능한 테넌트", "ui.admin.role.rp_admin": "RP ADMIN", "ui.admin.role.super_admin": "SUPER ADMIN", "ui.admin.role.tenant_admin": "TENANT ADMIN", - "ui.admin.role.tenant_member": "TENANT MEMBER", + "ui.admin.role.user": "TENANT MEMBER", "ui.admin.tenants.add": "테넌트 추가", "ui.admin.tenants.admins.add_button": "관리자 추가", "ui.admin.tenants.admins.already_admin": "이미 관리자", @@ -583,39 +691,55 @@ const Map koStrings = { "ui.admin.tenants.breadcrumb.section": "Tenants", "ui.admin.tenants.create.breadcrumb.action": "Create", "ui.admin.tenants.create.breadcrumb.section": "Tenants", - "ui.admin.tenants.create.form.description": "Description", - "ui.admin.tenants.create.form.domains_label": - "Allowed Domains (Comma separated)", + "ui.admin.tenants.create.form.description": "설명", + "ui.admin.tenants.create.form.domains_label": "Allowed Domains (Comma separated)", "ui.admin.tenants.create.form.domains_placeholder": "example.com, example.kr", - "ui.admin.tenants.create.form.name": "Tenant name", - "ui.admin.tenants.create.form.parent": "상위 테넌트 (선택)", + "ui.admin.tenants.create.form.name": "테넌트 이름", + "ui.admin.tenants.create.form.name_placeholder": "테넌트 이름을 입력하세요", + "ui.admin.tenants.create.form.parent": "상위 테넌트", "ui.admin.tenants.create.form.slug": "Slug", "ui.admin.tenants.create.form.slug_placeholder": "tenant-slug", - "ui.admin.tenants.create.form.status": "Status", - "ui.admin.tenants.create.form.type": "테넌트 유형", + "ui.admin.tenants.create.form.status": "상태", + "ui.admin.tenants.create.form.type": "유형", "ui.admin.tenants.create.memo.title": "정책 메모", "ui.admin.tenants.create.profile.title": "Tenant Profile", "ui.admin.tenants.create.title": "테넌트 추가", "ui.admin.tenants.detail.breadcrumb_list": "테넌트 목록", "ui.admin.tenants.detail.header_subtitle": "테넌트 정보를 수정하거나 연동 설정을 관리합니다.", "ui.admin.tenants.detail.loading": "불러오는 중...", - "ui.admin.tenants.detail.tab_admins": "관리자 설정", "ui.admin.tenants.detail.tab_federation": "외부 연동", "ui.admin.tenants.detail.tab_organization": "조직 관리", + "ui.admin.tenants.detail.tab_permissions": "권한", "ui.admin.tenants.detail.tab_profile": "프로필", "ui.admin.tenants.detail.tab_schema": "사용자 스키마", "ui.admin.tenants.detail.title": "상세", "ui.admin.tenants.list.select_placeholder": "테넌트를 선택하세요", + "ui.admin.tenants.members.descendants": "하위 조직 멤버", + "ui.admin.tenants.members.direct": "소속 멤버", + "ui.admin.tenants.members.direct_label": "직속", + "ui.admin.tenants.members.list_title": "구성원 관리", "ui.admin.tenants.members.table.email": "EMAIL", "ui.admin.tenants.members.table.name": "NAME", "ui.admin.tenants.members.table.role": "ROLE", "ui.admin.tenants.members.table.status": "STATUS", - "ui.admin.tenants.members.title": "Tenant Members ({{count}})", + "ui.admin.tenants.members.title": "테넌트 구성원 ({{count}})", + "ui.admin.tenants.members.total": "전체", + "ui.admin.tenants.members.total_label": "전체", + "ui.admin.tenants.owners.add_button": "소유자 추가", + "ui.admin.tenants.owners.already_owner": "이미 소유자", + "ui.admin.tenants.owners.dialog_description": "이름 또는 이메일로 사용자를 검색하세요.", + "ui.admin.tenants.owners.dialog_title": "새 소유자 추가", + "ui.admin.tenants.owners.remove_title": "소유자 권한 회수", + "ui.admin.tenants.owners.table_actions": "액션", + "ui.admin.tenants.owners.table_email": "이메일", + "ui.admin.tenants.owners.table_name": "이름", + "ui.admin.tenants.owners.title": "테넌트 소유자", "ui.admin.tenants.profile.allowed_domains": "허용된 도메인 (콤마로 구분)", - "ui.admin.tenants.profile.allowed_domains_help": - "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.", + "ui.admin.tenants.profile.allowed_domains_help": "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.", "ui.admin.tenants.profile.approve_button": "테넌트 승인", "ui.admin.tenants.profile.description": "설명", + "ui.admin.tenants.profile.form.parent": "상위 테넌트 (선택)", + "ui.admin.tenants.profile.form.parent_help": "가족사 테넌트나 하위 조직을 종속시킬 경우 상위 테넌트를 선택해주세요.", "ui.admin.tenants.profile.name": "테넌트 이름", "ui.admin.tenants.profile.slug": "슬러그 (Slug)", "ui.admin.tenants.profile.status": "상태", @@ -623,32 +747,58 @@ const Map koStrings = { "ui.admin.tenants.profile.title": "테넌트 프로필", "ui.admin.tenants.profile.type": "테넌트 유형", "ui.admin.tenants.registry.title": "Tenant registry", - "ui.admin.tenants.schema.add_field": "Add Field", + "ui.admin.tenants.schema.add_field": "필드 추가", + "ui.admin.tenants.schema.field.admin_only": "관리자 전용", + "ui.admin.tenants.schema.field.is_login_id": "로그인 ID로 사용", "ui.admin.tenants.schema.field.key": "Field Key (ID)", "ui.admin.tenants.schema.field.key_placeholder": "e.g. employee_id", - "ui.admin.tenants.schema.field.label": "Display Label", - "ui.admin.tenants.schema.field.label_placeholder": "e.g. 사번", - "ui.admin.tenants.schema.field.type": "Type", + "ui.admin.tenants.schema.field.label": "표시 레이블", + "ui.admin.tenants.schema.field.label_placeholder": "예: 사번", + "ui.admin.tenants.schema.field.required": "필수 여부", + "ui.admin.tenants.schema.field.type": "타입", "ui.admin.tenants.schema.field.type_boolean": "Boolean", + "ui.admin.tenants.schema.field.type_date": "Date", + "ui.admin.tenants.schema.field.type_datetime": "일시 (DateTime)", + "ui.admin.tenants.schema.field.type_float": "실수 (Float)", "ui.admin.tenants.schema.field.type_number": "Number", - "ui.admin.tenants.schema.field.type_text": "Text", - "ui.admin.tenants.schema.save": "Save Schema Changes", + "ui.admin.tenants.schema.field.type_text": "텍스트", + "ui.admin.tenants.schema.field.unsigned": "음수 불가", + "ui.admin.tenants.schema.field.validation_placeholder": "정규표현식 (선택 사항)", + "ui.admin.tenants.schema.save": "스키마 저장", "ui.admin.tenants.schema.title": "User Schema Extension", "ui.admin.tenants.sub.add": "하위 테넌트 추가", + "ui.admin.tenants.sub.add_dialog_desc": "하위 테넌트로 추가할 테넌트를 선택하세요.", + "ui.admin.tenants.sub.add_dialog_title": "하위 테넌트 추가", + "ui.admin.tenants.sub.add_existing": "기존 테넌트 추가", "ui.admin.tenants.sub.manage": "관리", + "ui.admin.tenants.sub.no_candidates": "추가 가능한 테넌트가 없습니다.", + "ui.admin.tenants.sub.search_placeholder": "검색...", "ui.admin.tenants.sub.table.action": "ACTION", "ui.admin.tenants.sub.table.name": "NAME", "ui.admin.tenants.sub.table.slug": "SLUG", "ui.admin.tenants.sub.table.status": "STATUS", - "ui.admin.tenants.sub.title": "Sub-tenants ({{count}})", + "ui.admin.tenants.sub.title": "하위 테넌트 ({{count}})", + "ui.admin.tenants.sub.tree_search_placeholder": "트리에서 검색...", "ui.admin.tenants.table.actions": "ACTIONS", + "ui.admin.tenants.table.members": "멤버수", "ui.admin.tenants.table.name": "NAME", "ui.admin.tenants.table.slug": "SLUG", "ui.admin.tenants.table.status": "STATUS", - "ui.admin.tenants.table.type": "TYPE", + "ui.admin.tenants.table.type": "유형", "ui.admin.tenants.table.updated": "UPDATED", "ui.admin.tenants.title": "테넌트 목록", "ui.admin.title": "Admin Control", + "ui.admin.users.bulk.acknowledge_warning": "경고를 확인했으며 계속 진행합니다.", + "ui.admin.users.bulk.do_move": "이동 실행", + "ui.admin.users.bulk.download_template": "템플릿 받기", + "ui.admin.users.bulk.move_group": "테넌트 일괄 이동", + "ui.admin.users.bulk.move_title": "사용자 일괄 이동", + "ui.admin.users.bulk.no_department": "부서 없음", + "ui.admin.users.bulk.schema_warning": "스키마 호환성 경고", + "ui.admin.users.bulk.select_group": "대상 테넌트 선택", + "ui.admin.users.bulk.selected_count": "{{count}}명 선택됨", + "ui.admin.users.bulk.start_upload": "업로드 시작", + "ui.admin.users.bulk.title": "일괄 작업", "ui.admin.users.create.account.title": "계정 정보", "ui.admin.users.create.back": "목록으로 돌아가기", "ui.admin.users.create.breadcrumb.new": "New", @@ -659,8 +809,11 @@ const Map koStrings = { "ui.admin.users.create.form.department_placeholder": "개발팀", "ui.admin.users.create.form.email": "이메일", "ui.admin.users.create.form.email_placeholder": "user@example.com", + "ui.admin.users.create.form.is_login_id": "로그인 ID", "ui.admin.users.create.form.job_title": "직무", "ui.admin.users.create.form.job_title_placeholder": "프론트엔드 개발", + "ui.admin.users.create.form.login_id": "로그인 ID (선택)", + "ui.admin.users.create.form.login_id_placeholder": "사번 또는 아이디", "ui.admin.users.create.form.name": "이름", "ui.admin.users.create.form.name_placeholder": "홍길동", "ui.admin.users.create.form.password": "비밀번호", @@ -669,59 +822,281 @@ const Map koStrings = { "ui.admin.users.create.form.phone_placeholder": "010-1234-5678", "ui.admin.users.create.form.position": "직급", "ui.admin.users.create.form.position_placeholder": "수석/책임/선임", - "ui.admin.users.create.form.role": "역할 (Role)", - "ui.admin.users.create.form.tenant": "테넌트 (Tenant)", - "ui.admin.users.create.form.tenant_global": "시스템 전역 (소속 없음)", + "ui.admin.users.create.form.role": "역할", + "ui.admin.users.create.form.tenant": "테넌트", + "ui.admin.users.create.form.tenant_global": "시스템 전역", "ui.admin.users.create.go_list": "목록으로 이동", "ui.admin.users.create.password_generated.title": "초기 비밀번호 생성 완료", "ui.admin.users.create.submit": "사용자 생성", "ui.admin.users.create.title": "사용자 추가", "ui.admin.users.detail.back": "목록으로 돌아가기", "ui.admin.users.detail.breadcrumb.section": "Users", - "ui.admin.users.detail.custom_fields.title": "테넌트 확장 정보 (Custom Fields)", + "ui.admin.users.detail.contact_title": "ui.admin.users.detail.contact_title", + "ui.admin.users.detail.created_at": "가입일", + "ui.admin.users.detail.custom_fields.multi_title": "테넌트별 프로필 관리", + "ui.admin.users.detail.delete": "사용자 삭제", "ui.admin.users.detail.edit_title": "정보 수정", + "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", "ui.admin.users.detail.form.department": "부서", "ui.admin.users.detail.form.department_placeholder": "개발팀", + "ui.admin.users.detail.form.email": "이메일", + "ui.admin.users.detail.form.is_login_id": "로그인 ID", "ui.admin.users.detail.form.job_title": "직무", - "ui.admin.users.detail.form.job_title_placeholder": "프론트엔드 개발", + "ui.admin.users.detail.form.job_title_placeholder": "ui.admin.users.detail.form.job_title_placeholder", + "ui.admin.users.detail.form.login_id": "로그인 ID", + "ui.admin.users.detail.form.login_id_placeholder": "사번 또는 아이디", + "ui.admin.users.detail.form.msg.admin.users.detail.history_desc": "ui.admin.users.detail.form.msg.admin.users.detail.history_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.no_history": "ui.admin.users.detail.form.msg.admin.users.detail.no_history", + "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants": "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants", + "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc": "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.security_desc": "ui.admin.users.detail.form.msg.admin.users.detail.security_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help": "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help", + "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc": "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc", + "ui.admin.users.detail.form.msg.common.copied": "ui.admin.users.detail.form.msg.common.copied", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", "ui.admin.users.detail.form.name": "이름", "ui.admin.users.detail.form.name_placeholder": "홍길동", "ui.admin.users.detail.form.phone": "전화번호", "ui.admin.users.detail.form.phone_placeholder": "010-1234-5678", "ui.admin.users.detail.form.position": "직급", - "ui.admin.users.detail.form.position_placeholder": "수석/책임/선임", - "ui.admin.users.detail.form.role": "역할 (Role)", + "ui.admin.users.detail.form.position_placeholder": "ui.admin.users.detail.form.position_placeholder", + "ui.admin.users.detail.form.role": "역할", + "ui.admin.users.detail.form.role_rp_admin": "RP 관리자", + "ui.admin.users.detail.form.role_super_admin": "시스템 전역 관리자", + "ui.admin.users.detail.form.role_tenant_admin": "테넌트 관리자", + "ui.admin.users.detail.form.role_user": "사용자", "ui.admin.users.detail.form.status": "상태", - "ui.admin.users.detail.form.tenant": "테넌트 (Tenant)", - "ui.admin.users.detail.form.tenant_global": "시스템 전역 (소속 없음)", + "ui.admin.users.detail.form.status_active": "ui.admin.users.detail.form.status_active", + "ui.admin.users.detail.form.status_inactive": "ui.admin.users.detail.form.status_inactive", + "ui.admin.users.detail.form.tenant": "대표 소속 테넌트", + "ui.admin.users.detail.form.tenant_global": "시스템 전역", + "ui.admin.users.detail.form.tenant_slug": "대표 소속 (Tenant Slug)", + "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id": "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id", + "ui.admin.users.detail.form.ui.admin.users.detail.form.email": "ui.admin.users.detail.form.ui.admin.users.detail.form.email", + "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id": "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id", + "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin": "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin", + "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug": "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug", + "ui.admin.users.detail.form.ui.admin.users.detail.generate_button": "ui.admin.users.detail.form.ui.admin.users.detail.generate_button", + "ui.admin.users.detail.form.ui.admin.users.detail.history_title": "ui.admin.users.detail.form.ui.admin.users.detail.history_title", + "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm": "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm", + "ui.admin.users.detail.form.ui.admin.users.detail.manual_password": "ui.admin.users.detail.form.ui.admin.users.detail.manual_password", + "ui.admin.users.detail.form.ui.admin.users.detail.password_done": "ui.admin.users.detail.form.ui.admin.users.detail.password_done", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto": "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute": "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual": "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual", + "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants": "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants", + "ui.admin.users.detail.form.ui.admin.users.detail.updated_at": "ui.admin.users.detail.form.ui.admin.users.detail.updated_at", + "ui.admin.users.detail.form.ui.common.generate": "ui.admin.users.detail.form.ui.common.generate", + "ui.admin.users.detail.form.ui.common.status.blocked": "ui.admin.users.detail.form.ui.common.status.blocked", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri", + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable": "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable", + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help": "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.admin.users.detail.form.ui.dev.clients.help.docs_body": "ui.admin.users.detail.form.ui.dev.clients.help.docs_body", + "ui.admin.users.detail.form.ui.dev.clients.help.subtitle": "ui.admin.users.detail.form.ui.dev.clients.help.subtitle", + "ui.admin.users.detail.form.ui.dev.clients.registry.description": "ui.admin.users.detail.form.ui.dev.clients.registry.description", + "ui.admin.users.detail.form.ui.dev.clients.scopes.email": "ui.admin.users.detail.form.ui.dev.clients.scopes.email", + "ui.admin.users.detail.form.ui.dev.clients.scopes.openid": "ui.admin.users.detail.form.ui.dev.clients.scopes.openid", + "ui.admin.users.detail.form.ui.dev.clients.scopes.profile": "ui.admin.users.detail.form.ui.dev.clients.scopes.profile", + "ui.admin.users.detail.form.ui.dev.session.refresh": "ui.admin.users.detail.form.ui.dev.session.refresh", + "ui.admin.users.detail.form.ui.dev.session.refreshing": "ui.admin.users.detail.form.ui.dev.session.refreshing", + "ui.admin.users.detail.generate_button": "랜덤 비밀번호 생성", + "ui.admin.users.detail.generate_password": "자동 생성", + "ui.admin.users.detail.go_list": "목록으로 이동", + "ui.admin.users.detail.history_title": "서비스 이용 내역", + "ui.admin.users.detail.manual_confirm": "비밀번호 확인", + "ui.admin.users.detail.manual_password": "새 비밀번호", + "ui.admin.users.detail.password_done": "성공적으로 초기화됨", + "ui.admin.users.detail.password_mode_generated": "자동 생성", + "ui.admin.users.detail.password_mode_manual": "수동 입력", + "ui.admin.users.detail.password_result_title": "Reset Password", + "ui.admin.users.detail.password_title": "비밀번호 관리", + "ui.admin.users.detail.reset_auto": "자동 생성", + "ui.admin.users.detail.reset_execute": "재설정 완료", + "ui.admin.users.detail.reset_manual": "직접 입력", + "ui.admin.users.detail.reset_password": "초기화 도구", + "ui.admin.users.detail.reset_password_apply": "비밀번호 적용", + "ui.admin.users.detail.reset_password_label": "비밀번호 초기화", + "ui.admin.users.detail.save": "저장하기", + "ui.admin.users.detail.save_tenants": "모든 테넌트 프로필 저장", "ui.admin.users.detail.security.password": "비밀번호 변경", "ui.admin.users.detail.security.password_placeholder": "변경할 경우에만 입력", "ui.admin.users.detail.security.title": "보안 설정", + "ui.admin.users.detail.status_title": "ui.admin.users.detail.status_title", + "ui.admin.users.detail.tabs.info": "기본 정보", + "ui.admin.users.detail.tabs.security": "보안 & 활동", + "ui.admin.users.detail.tabs.tenants": "테넌트 프로필", + "ui.admin.users.detail.tenants_section.additional": "추가 소속/관리 테넌트", + "ui.admin.users.detail.tenants_section.primary": "대표 소속 테넌트", + "ui.admin.users.detail.tenants_section.title": "소속 및 조직 정보", "ui.admin.users.detail.title": "사용자 상세", + "ui.admin.users.detail.toggle_password_visibility": "비밀번호 표시 전환", + "ui.admin.users.detail.updated_at": "최근 수정", "ui.admin.users.list.add": "사용자 추가", "ui.admin.users.list.breadcrumb.list": "List", "ui.admin.users.list.breadcrumb.section": "Users", - "ui.admin.users.list.delete_aria": "사용자 삭제: {{name}}", - "ui.admin.users.list.edit_aria": "사용자 수정: {{name}}", - "ui.admin.users.list.registry.title": "User Registry", + "ui.admin.users.list.bulk_import": "일괄 임포트", + "ui.admin.users.list.columns.title": "컬럼 설정", + "ui.admin.users.list.empty": "검색 결과가 없습니다.", + "ui.admin.users.list.fetch_error": "사용자 목록 조회에 실패했습니다.", + "ui.admin.users.list.filter.tenant": "테넌트 필터", + "ui.admin.users.list.registry.count": "총 {{count}}명의 사용자가 등록되어 있습니다.", + "ui.admin.users.list.registry.title": "사용자 레지스트리", "ui.admin.users.list.search_placeholder": "이름 또는 이메일 검색...", + "ui.admin.users.list.subtitle": "시스템 사용자를 조회하고 관리합니다.", "ui.admin.users.list.table.actions": "ACTIONS", "ui.admin.users.list.table.created": "CREATED", + "ui.admin.users.list.table.login_id": "LOGIN ID", + "ui.admin.users.list.table.msg.admin.users.detail.history_desc": "ui.admin.users.list.table.msg.admin.users.detail.history_desc", + "ui.admin.users.list.table.msg.admin.users.detail.no_history": "ui.admin.users.list.table.msg.admin.users.detail.no_history", + "ui.admin.users.list.table.msg.admin.users.detail.no_tenants": "ui.admin.users.list.table.msg.admin.users.detail.no_tenants", + "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc": "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc", + "ui.admin.users.list.table.msg.admin.users.detail.security_desc": "ui.admin.users.list.table.msg.admin.users.detail.security_desc", + "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help": "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help", + "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc": "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc", + "ui.admin.users.list.table.msg.common.copied": "ui.admin.users.list.table.msg.common.copied", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", "ui.admin.users.list.table.name_email": "NAME / EMAIL", - "ui.admin.users.list.table.position_job": "POSITION / JOB", "ui.admin.users.list.table.role": "ROLE", "ui.admin.users.list.table.status": "STATUS", "ui.admin.users.list.table.tenant_dept": "TENANT / DEPT", - "ui.admin.users.list.tenant_slug": "Slug: {{slug}}", + "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id": "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id", + "ui.admin.users.list.table.ui.admin.users.detail.form.email": "ui.admin.users.list.table.ui.admin.users.detail.form.email", + "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id": "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id", + "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin": "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin", + "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug": "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug", + "ui.admin.users.list.table.ui.admin.users.detail.generate_button": "ui.admin.users.list.table.ui.admin.users.detail.generate_button", + "ui.admin.users.list.table.ui.admin.users.detail.history_title": "ui.admin.users.list.table.ui.admin.users.detail.history_title", + "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm": "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm", + "ui.admin.users.list.table.ui.admin.users.detail.manual_password": "ui.admin.users.list.table.ui.admin.users.detail.manual_password", + "ui.admin.users.list.table.ui.admin.users.detail.password_done": "ui.admin.users.list.table.ui.admin.users.detail.password_done", + "ui.admin.users.list.table.ui.admin.users.detail.reset_auto": "ui.admin.users.list.table.ui.admin.users.detail.reset_auto", + "ui.admin.users.list.table.ui.admin.users.detail.reset_execute": "ui.admin.users.list.table.ui.admin.users.detail.reset_execute", + "ui.admin.users.list.table.ui.admin.users.detail.reset_manual": "ui.admin.users.list.table.ui.admin.users.detail.reset_manual", + "ui.admin.users.list.table.ui.admin.users.detail.save_tenants": "ui.admin.users.list.table.ui.admin.users.detail.save_tenants", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.info": "ui.admin.users.list.table.ui.admin.users.detail.tabs.info", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.security": "ui.admin.users.list.table.ui.admin.users.detail.tabs.security", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants": "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants", + "ui.admin.users.list.table.ui.admin.users.detail.updated_at": "ui.admin.users.list.table.ui.admin.users.detail.updated_at", + "ui.admin.users.list.table.ui.common.generate": "ui.admin.users.list.table.ui.common.generate", + "ui.admin.users.list.table.ui.common.status.blocked": "ui.admin.users.list.table.ui.common.status.blocked", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info": "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle": "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled": "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled": "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline": "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg": "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source": "ui.admin.users.list.table.ui.dev.clients.general.public_key.source", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri": "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri", + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable": "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable", + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help": "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.admin.users.list.table.ui.dev.clients.help.docs_body": "ui.admin.users.list.table.ui.dev.clients.help.docs_body", + "ui.admin.users.list.table.ui.dev.clients.help.subtitle": "ui.admin.users.list.table.ui.dev.clients.help.subtitle", + "ui.admin.users.list.table.ui.dev.clients.registry.description": "ui.admin.users.list.table.ui.dev.clients.registry.description", + "ui.admin.users.list.table.ui.dev.clients.scopes.email": "ui.admin.users.list.table.ui.dev.clients.scopes.email", + "ui.admin.users.list.table.ui.dev.clients.scopes.openid": "ui.admin.users.list.table.ui.dev.clients.scopes.openid", + "ui.admin.users.list.table.ui.dev.clients.scopes.profile": "ui.admin.users.list.table.ui.dev.clients.scopes.profile", + "ui.admin.users.list.table.ui.dev.session.refresh": "ui.admin.users.list.table.ui.dev.session.refresh", + "ui.admin.users.list.table.ui.dev.session.refreshing": "ui.admin.users.list.table.ui.dev.session.refreshing", "ui.admin.users.list.title": "사용자 관리", + "ui.admin.users.table.email": "이메일", + "ui.admin.users.table.name": "이름", + "ui.admin.users.table.role": "역할", "ui.common.add": "추가", "ui.common.admin_only": "관리자 전용", + "ui.common.all": "전체", "ui.common.assign": "할당", "ui.common.back": "돌아가기", + "ui.common.back_to_login": "로그인으로 돌아가기", "ui.common.badge.admin_only": "Admin only", "ui.common.badge.command_only": "Command only", "ui.common.badge.system": "System", "ui.common.cancel": "취소", + "ui.common.change_file": "파일 변경", + "ui.common.clear_search": "검색 초기화", "ui.common.close": "닫기", "ui.common.collapse": "접기", "ui.common.confirm": "확인", @@ -730,42 +1105,60 @@ const Map koStrings = { "ui.common.delete": "삭제", "ui.common.details": "상세정보", "ui.common.edit": "편집", + "ui.common.enabled": "사용", + "ui.common.export": "내보내기", + "ui.common.fail": "실패", + "ui.common.generate": "ui.common.generate", + "ui.common.go_home": "홈으로", "ui.common.hyphen": "-", "ui.common.language": "언어", "ui.common.language_en": "English", "ui.common.language_ko": "한국어", + "ui.common.manage": "관리", "ui.common.na": "N/A", "ui.common.never": "Never", - "ui.common.next": "Next", + "ui.common.next": "다음", "ui.common.none": "없음", "ui.common.page_of": "Page {{page}} of {{total}}", "ui.common.prev": "이전", - "ui.common.previous": "Previous", + "ui.common.previous": "이전", "ui.common.qr": "QR", "ui.common.read_only": "읽기 전용", "ui.common.refresh": "새로고침", - "ui.common.requesting": "요청 중...", + "ui.common.remove": "제외", "ui.common.resend": "재발송", + "ui.common.reset": "초기화", "ui.common.retry": "다시 시도", - "ui.common.role.admin": "Admin", - "ui.common.role.user": "User", "ui.common.save": "저장", "ui.common.search": "검색", - "ui.common.select": "사용자 선택", - "ui.common.select_placeholder": "사용자를 선택하세요", + "ui.common.select": "선택", + "ui.common.select_file": "파일 선택", + "ui.common.select_placeholder": "선택하세요", "ui.common.show_more": "+ 더보기", - "ui.common.status.active": "Active", - "ui.common.status.blocked": "Blocked", + "ui.common.status.active": "활성", + "ui.common.status.blocked": "ui.common.status.blocked", "ui.common.status.failure": "실패", - "ui.common.status.inactive": "Inactive", + "ui.common.status.inactive": "비활성", "ui.common.status.ok": "정상", "ui.common.status.pending": "준비 중", "ui.common.status.success": "성공", + "ui.common.success": "성공", "ui.common.theme_dark": "Dark", "ui.common.theme_light": "Light", "ui.common.theme_toggle": "테마 전환", "ui.common.unknown": "Unknown", "ui.common.view": "보기", + "ui.dev.audit.filter.action": "액션으로 필터 (예: ROTATE_SECRET)", + "ui.dev.audit.filter.client_id": "Client ID로 필터", + "ui.dev.audit.filter.status_all": "모든 상태", + "ui.dev.audit.load_more": "더 보기", + "ui.dev.audit.registry.title": "Audit registry", + "ui.dev.audit.table.action": "액션", + "ui.dev.audit.table.actor": "수행자", + "ui.dev.audit.table.status": "상태", + "ui.dev.audit.table.target": "대상", + "ui.dev.audit.table.time": "시간", + "ui.dev.audit.title": "감사 로그", "ui.dev.brand": "Baron 로그인", "ui.dev.clients.badge.admin_session": "관리자 세션", "ui.dev.clients.badge.tenant_selected": "테넌트: 선택됨", @@ -773,36 +1166,34 @@ const Map koStrings = { "ui.dev.clients.consents.breadcrumb.current": "User Consent Grants", "ui.dev.clients.consents.breadcrumb.home": "Home", "ui.dev.clients.consents.export_csv": "Export CSV", - "ui.dev.clients.consents.filters.advanced": "Advanced Filters", + "ui.dev.clients.consents.filters.advanced": "고급 필터", "ui.dev.clients.consents.revoke": "Revoke", + "ui.dev.clients.consents.revoked_at": "철회일: ", + "ui.dev.clients.consents.scope_label": "권한:", "ui.dev.clients.consents.search_placeholder": "사용자 ID, 이름, 이메일로 검색", - "ui.dev.clients.consents.stats.active_grants": "Active Grants", - "ui.dev.clients.consents.stats.avg_scopes": "Avg. Scopes per User", - "ui.dev.clients.consents.stats.total_scopes": "Total Scopes Issued", - "ui.dev.clients.consents.status_all": "All Statuses", + "ui.dev.clients.consents.stats.active_grants": "활성 동의 건수", + "ui.dev.clients.consents.stats.avg_scopes": "사용자당 평균 스코프", + "ui.dev.clients.consents.stats.total_scopes": "총 발급 스코프 수", + "ui.dev.clients.consents.status_all": "ui.dev.clients.consents.status_all", "ui.dev.clients.consents.status_label": "Status:", "ui.dev.clients.consents.status_revoked": "Revoked", "ui.dev.clients.consents.subject": "Subject", - "ui.dev.clients.consents.table.action": "Action", - "ui.dev.clients.consents.table.first_granted": "First Granted", - "ui.dev.clients.consents.table.last_auth": "Last Authenticated", - "ui.dev.clients.consents.table.scopes": "Granted Scopes", - "ui.dev.clients.consents.table.status": "Status", - "ui.dev.clients.consents.table.tenant": "Tenant", - "ui.dev.clients.consents.table.user": "User", + "ui.dev.clients.consents.table.action": "동작", + "ui.dev.clients.consents.table.first_granted": "최초 동의 시각", + "ui.dev.clients.consents.table.last_auth": "마지막 인증 시각", + "ui.dev.clients.consents.table.scopes": "승인된 스코프", + "ui.dev.clients.consents.table.status": "상태", + "ui.dev.clients.consents.table.tenant": "테넌트", + "ui.dev.clients.consents.table.user": "사용자", "ui.dev.clients.consents.title": "User Consent Grants", - "ui.dev.clients.copy_client_id": "Copy client id", - "ui.dev.clients.details.breadcrumb.current": "연동 앱 상세", - "ui.dev.clients.details.breadcrumb.section": "연동 앱", - "ui.dev.clients.details.credentials.client_id": "Client ID", - "ui.dev.clients.details.credentials.client_secret": "Client Secret", + "ui.dev.clients.details.credentials.client_id": "클라이언트 ID", + "ui.dev.clients.details.credentials.client_secret": "클라이언트 시크릿", "ui.dev.clients.details.credentials.title": "앱 자격 증명", "ui.dev.clients.details.endpoints.read_only": "읽기 전용", "ui.dev.clients.details.endpoints.title": "OIDC 엔드포인트", "ui.dev.clients.details.redirect.callback_label": "인증 콜백 URL", - "ui.dev.clients.details.redirect.label": "Redirect URIs", - "ui.dev.clients.details.redirect.placeholder": - "https://your-app.com/callback, http://localhost:3000/auth/callback", + "ui.dev.clients.details.redirect.label": "리디렉션 URI", + "ui.dev.clients.details.redirect.placeholder": "https://your-app.com/callback, http://localhost:3000/auth/callback", "ui.dev.clients.details.redirect.save": "Redirect URIs 저장", "ui.dev.clients.details.redirect.title": "리디렉션 URI 설정", "ui.dev.clients.details.secret.hide": "비밀키 숨기기", @@ -810,43 +1201,92 @@ const Map koStrings = { "ui.dev.clients.details.secret.show": "비밀키 보기", "ui.dev.clients.details.security.title": "보안 메모", "ui.dev.clients.details.tab.connection": "연동 설정", - "ui.dev.clients.details.tab.consents": "Consent & Users", - "ui.dev.clients.details.tab.settings": "Settings", + "ui.dev.clients.details.tab.consents": "동의 및 사용자", + "ui.dev.clients.details.tab.settings": "설정", "ui.dev.clients.federation.add_btn": "Add Provider", "ui.dev.clients.federation.add_title": "Add Identity Provider", "ui.dev.clients.federation.title": "Identity Federation", - "ui.dev.clients.general.breadcrumb.section": "Applications", + "ui.dev.clients.filter.status_all": "모든 상태", + "ui.dev.clients.filter.type_all": "모든 유형", + "ui.dev.clients.filter.type_label": "유형:", "ui.dev.clients.general.create": "앱 생성", "ui.dev.clients.general.display_new": "연동 앱 추가", - "ui.dev.clients.general.footer.client_id": "Client ID", - "ui.dev.clients.general.footer.created_on": "Created On", - "ui.dev.clients.general.identity.description": "Description", - "ui.dev.clients.general.identity.description_placeholder": - "앱에 대한 간단한 설명을 입력하세요.", - "ui.dev.clients.general.identity.logo": "App Logo URL", - "ui.dev.clients.general.identity.logo_placeholder": - "https://example.com/logo.png", - "ui.dev.clients.general.identity.logo_preview": "Logo Preview", + "ui.dev.clients.general.identity.description": "설명", + "ui.dev.clients.general.identity.description_placeholder": "앱에 대한 간단한 설명을 입력하세요.", + "ui.dev.clients.general.identity.logo": "앱 로고 URL", + "ui.dev.clients.general.identity.logo_placeholder": "https://example.com/logo.png", + "ui.dev.clients.general.identity.logo_preview": "로고 미리보기", "ui.dev.clients.general.identity.name": "앱 이름", - "ui.dev.clients.general.identity.name_placeholder": "My Awesome Application", - "ui.dev.clients.general.identity.title": "Application Identity", - "ui.dev.clients.general.redirect.label": "Redirect URIs", - "ui.dev.clients.general.redirect.placeholder": - "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)", + "ui.dev.clients.general.identity.name_placeholder": "예: 멋진 애플리케이션", + "ui.dev.clients.general.identity.title": "애플리케이션 정보", + "ui.dev.clients.general.public_key.allowed_algorithms_info": "Headless Login 허용 알고리즘 정보", + "ui.dev.clients.general.public_key.auth_method": "ui.dev.clients.general.public_key.auth_method", + "ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.dev.clients.general.public_key.auth_method_none": "ui.dev.clients.general.public_key.auth_method_none", + "ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.dev.clients.general.public_key.cache.cached_at": "Cached At", + "ui.dev.clients.general.public_key.cache.error": "Last Error", + "ui.dev.clients.general.public_key.cache.expires_at": "Expires At", + "ui.dev.clients.general.public_key.cache.failures": "Consecutive Failures", + "ui.dev.clients.general.public_key.cache.kids": "Cached KIDs", + "ui.dev.clients.general.public_key.cache.last_checked_at": "Last Checked", + "ui.dev.clients.general.public_key.cache.last_success": "Last Successful Verification", + "ui.dev.clients.general.public_key.cache.parsed_key_n": "N", + "ui.dev.clients.general.public_key.cache.parsed_keys": "Parsed Keys", + "ui.dev.clients.general.public_key.cache.status": "상태", + "ui.dev.clients.general.public_key.cache.title": "JWKS Cache", + "ui.dev.clients.general.public_key.cache.uri": "JWKS URI", + "ui.dev.clients.general.public_key.guide_toggle": "ui.dev.clients.general.public_key.guide_toggle", + "ui.dev.clients.general.public_key.headless_disabled": "ui.dev.clients.general.public_key.headless_disabled", + "ui.dev.clients.general.public_key.headless_enabled": "ui.dev.clients.general.public_key.headless_enabled", + "ui.dev.clients.general.public_key.headless_toggle": "Headless Login", + "ui.dev.clients.general.public_key.jwks_inline": "ui.dev.clients.general.public_key.jwks_inline", + "ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.dev.clients.general.public_key.jwks_uri": "JWKS URI", + "ui.dev.clients.general.public_key.jwks_uri_placeholder": "https://rp.example.com/.well-known/jwks.json", + "ui.dev.clients.general.public_key.request_object_alg": "ui.dev.clients.general.public_key.request_object_alg", + "ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.dev.clients.general.public_key.revoke_cache": "Revoke Cache", + "ui.dev.clients.general.public_key.source": "ui.dev.clients.general.public_key.source", + "ui.dev.clients.general.public_key.source_uri": "ui.dev.clients.general.public_key.source_uri", + "ui.dev.clients.general.public_key.title": "공개키 등록", + "ui.dev.clients.general.public_key.validation_title": "저장 전 확인 필요", + "ui.dev.clients.general.redirect.label": "리디렉션 URI", + "ui.dev.clients.general.redirect.placeholder": "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)", "ui.dev.clients.general.scopes.add": "Scope 추가", "ui.dev.clients.general.scopes.description_placeholder": "권한에 대한 설명", "ui.dev.clients.general.scopes.name_placeholder": "e.g. profile", - "ui.dev.clients.general.scopes.table.delete": "Delete", - "ui.dev.clients.general.scopes.table.description": "Description", - "ui.dev.clients.general.scopes.table.mandatory": "Mandatory", - "ui.dev.clients.general.scopes.table.name": "Scope Name", - "ui.dev.clients.general.scopes.title": "Scopes", + "ui.dev.clients.general.scopes.table.contact_title": "ui.dev.clients.general.scopes.table.contact_title", + "ui.dev.clients.general.scopes.table.delete": "삭제", + "ui.dev.clients.general.scopes.table.description": "설명", + "ui.dev.clients.general.scopes.table.invalid_format": "ui.dev.clients.general.scopes.table.invalid_format", + "ui.dev.clients.general.scopes.table.login_id_help": "ui.dev.clients.general.scopes.table.login_id_help", + "ui.dev.clients.general.scopes.table.mandatory": "필수", + "ui.dev.clients.general.scopes.table.name": "스코프 이름", + "ui.dev.clients.general.scopes.table.password_title": "ui.dev.clients.general.scopes.table.password_title", + "ui.dev.clients.general.scopes.table.reset_password": "ui.dev.clients.general.scopes.table.reset_password", + "ui.dev.clients.general.scopes.table.reset_password_confirm": "ui.dev.clients.general.scopes.table.reset_password_confirm", + "ui.dev.clients.general.scopes.table.reset_password_label": "ui.dev.clients.general.scopes.table.reset_password_label", + "ui.dev.clients.general.scopes.table.role_super_admin": "ui.dev.clients.general.scopes.table.role_super_admin", + "ui.dev.clients.general.scopes.table.role_tenant_admin": "ui.dev.clients.general.scopes.table.role_tenant_admin", + "ui.dev.clients.general.scopes.table.role_user": "ui.dev.clients.general.scopes.table.role_user", + "ui.dev.clients.general.scopes.table.status_active": "ui.dev.clients.general.scopes.table.status_active", + "ui.dev.clients.general.scopes.table.status_inactive": "ui.dev.clients.general.scopes.table.status_inactive", + "ui.dev.clients.general.scopes.table.status_title": "ui.dev.clients.general.scopes.table.status_title", + "ui.dev.clients.general.scopes.title": "스코프", + "ui.dev.clients.general.security.headless_login_enable": "Headless Login (자체 로그인 UI 사용)", + "ui.dev.clients.general.security.headless_login_enable_help": "Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다.", "ui.dev.clients.general.security.pkce": "PKCE", "ui.dev.clients.general.security.private": "Server side App", "ui.dev.clients.general.security.title": "보안 설정", - "ui.dev.clients.general.title_create": "Create Client", - "ui.dev.clients.general.title_edit": "Client Settings", + "ui.dev.clients.general.security.trusted_rp_enable": "ui.dev.clients.general.security.trusted_rp_enable", + "ui.dev.clients.general.security.trusted_rp_enable_help": "ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.dev.clients.general.subtitle": "앱 설정과 보안 구성을 관리합니다.", + "ui.dev.clients.general.title_create": "연동 앱 생성", + "ui.dev.clients.general.title_edit": "연동 앱 설정", + "ui.dev.clients.help.docs_body": "ui.dev.clients.help.docs_body", "ui.dev.clients.help.docs_title": "Docs & Examples", + "ui.dev.clients.help.subtitle": "ui.dev.clients.help.subtitle", "ui.dev.clients.help.title": "Need help with OIDC configuration?", "ui.dev.clients.help.view_guides": "View guides", "ui.dev.clients.list.title": "연동 앱 목록", @@ -858,17 +1298,22 @@ const Map koStrings = { "ui.dev.clients.owner.scope": "Scope: TENANT-12", "ui.dev.clients.owner.subtitle": "Tenant admin on-call", "ui.dev.clients.owner.title": "Owner", + "ui.dev.clients.registry.description": "ui.dev.clients.registry.description", "ui.dev.clients.registry.subtitle": "연동 앱", "ui.dev.clients.registry.title": "RP registry", + "ui.dev.clients.scopes.email": "ui.dev.clients.scopes.email", + "ui.dev.clients.scopes.openid": "ui.dev.clients.scopes.openid", + "ui.dev.clients.scopes.profile": "ui.dev.clients.scopes.profile", "ui.dev.clients.search_placeholder": "연동 앱 이름/ID로 검색...", "ui.dev.clients.table.actions": "액션", "ui.dev.clients.table.application": "애플리케이션", - "ui.dev.clients.table.client_id": "Client ID", + "ui.dev.clients.table.client_id": "클라이언트 ID", "ui.dev.clients.table.created_at": "생성일", "ui.dev.clients.table.status": "상태", "ui.dev.clients.table.type": "유형", "ui.dev.clients.tenant_scoped": "Tenant-scoped", "ui.dev.clients.type.pkce": "PKCE", + "ui.dev.clients.type.pkce_headless": "Headless PKCE", "ui.dev.clients.type.private": "Server side App", "ui.dev.clients.untitled": "Untitled", "ui.dev.console_title": "Developer Console", @@ -893,18 +1338,41 @@ const Map koStrings = { "ui.dev.header.subtitle": "Manage your applications", "ui.dev.nav.clients": "연동 앱", "ui.dev.nav.logout": "로그아웃", + "ui.dev.profile.basic.email": "이메일", + "ui.dev.profile.basic.id": "사용자 ID", + "ui.dev.profile.basic.name": "이름", + "ui.dev.profile.basic.phone": "전화번호", + "ui.dev.profile.basic.title": "사용자 정보", + "ui.dev.profile.error": "프로필 정보를 불러오지 못했습니다.", + "ui.dev.profile.loading": "프로필 정보를 불러오는 중...", "ui.dev.profile.menu_aria": "계정 메뉴 열기", "ui.dev.profile.menu_title": "계정", + "ui.dev.profile.org.company_code": "회사 코드", + "ui.dev.profile.org.tenant": "테넌트", + "ui.dev.profile.org.title": "조직 정보", + "ui.dev.profile.role.current": "현재 역할", + "ui.dev.profile.role.description": "현재 계정에 부여된 권한 등급입니다.", + "ui.dev.profile.role.title": "시스템 역할", + "ui.dev.profile.subtitle": "사용자 상세 정보 및 할당된 역할(Role)을 확인합니다.", + "ui.dev.profile.tab.basic": "기본 정보", + "ui.dev.profile.tab.role": "권한 및 역할", + "ui.dev.profile.title": "내 정보", "ui.dev.profile.unknown_email": "unknown@example.com", "ui.dev.profile.unknown_name": "Unknown User", "ui.dev.scope_badge": "Scoped to /dev", - "ui.dev.session.active": "만료 시간 확인 중...", + "ui.dev.session.active": "세션 활성", + "ui.dev.session.auto_extend": "세션 만료 관리", + "ui.dev.session.disabled": "자동 연장 비활성화", "ui.dev.session.expired": "세션 만료", "ui.dev.session.expiring": "만료 임박: {{minutes}}분 {{seconds}}초 남음", - "ui.dev.session.refresh": "세션 만료 시간 갱신", - "ui.dev.session.refreshing": "세션 만료 시간 갱신 중...", + "ui.dev.session.refresh": "ui.dev.session.refresh", + "ui.dev.session.refreshing": "ui.dev.session.refreshing", "ui.dev.session.remaining": "만료 예정: {{minutes}}분 {{seconds}}초 남음", - "ui.dev.session.unknown": "확인 불가", + "ui.dev.session.unknown": "알 수 없음", + "ui.dev.tenant.single_notice": "단일 테넌트에 소속되어 전환할 필요가 없습니다.", + "ui.dev.tenant.switch_success": "테넌트 전환 완료", + "ui.dev.tenant.workspace": "작업 테넌트 (컨텍스트)", + "ui.dev.tenant.workspace_desc": "현재 작업 중인 테넌트를 선택하고 저장하여 API 요청 컨텍스트를 변경합니다.", "ui.userfront.app_label.admin_console": "Admin Console", "ui.userfront.app_label.baron": "Baron 로그인", "ui.userfront.app_label.dev_console": "Dev Console", @@ -920,6 +1388,11 @@ const Map koStrings = { "ui.userfront.audit.table.status": "현황", "ui.userfront.auth_method.ory": "Ory 세션", "ui.userfront.auth_method.session": "세션", + "ui.userfront.consent.accept": "동의하고 계속하기", + "ui.userfront.consent.cancel.confirm_button": "예, 취소합니다", + "ui.userfront.consent.cancel.title": "동의 취소", + "ui.userfront.consent.requested_scopes": "요청된 권한", + "ui.userfront.consent.title": "접근 권한 요청", "ui.userfront.dashboard.activity.linked": "연동됨", "ui.userfront.dashboard.approved_session.default": "승인한 세션 ID", "ui.userfront.dashboard.approved_session.userfront": "승인한 Userfront 세션 ID", @@ -927,6 +1400,13 @@ const Map koStrings = { "ui.userfront.dashboard.revoke.confirm_button": "해지하기", "ui.userfront.dashboard.revoke.title": "연동 해지", "ui.userfront.dashboard.scopes.title": "권한 (Scopes)", + "ui.userfront.dashboard.sessions.active_badge": "활성화", + "ui.userfront.dashboard.sessions.current_badge": "현재 접속중", + "ui.userfront.dashboard.sessions.current_disabled": "현재 세션", + "ui.userfront.dashboard.sessions.revoke.action": "세션 종료", + "ui.userfront.dashboard.sessions.revoke.title": "세션 종료", + "ui.userfront.dashboard.sessions.unknown_device": "알 수 없는 기기", + "ui.userfront.dashboard.sessions.unknown_session": "세션 정보", "ui.userfront.dashboard.status.revoked": "해지됨", "ui.userfront.dashboard.status_history": "상태 이력", "ui.userfront.device.android": "Mobile(Android)", @@ -944,12 +1424,12 @@ const Map koStrings = { "ui.userfront.login.field.login_id": "이메일 또는 휴대폰 번호", "ui.userfront.login.field.password": "비밀번호", "ui.userfront.login.forgot_password": "비밀번호를 잊으셨나요?", - "ui.userfront.login.link.action_label": "로그인 화면으로 이동", + "ui.userfront.login.link.action_label": "ui.userfront.login.link.action_label", "ui.userfront.login.link.code_only": "코드만 받기({{time}})", - "ui.userfront.login.link.page_title": "링크 로그인", + "ui.userfront.login.link.page_title": "ui.userfront.login.link.page_title", "ui.userfront.login.link.resend_with_time": "재발송 ({{time}})", "ui.userfront.login.link.send": "로그인 링크 전송", - "ui.userfront.login.link.title": "링크 로그인 완료", + "ui.userfront.login.link.title": "ui.userfront.login.link.title", "ui.userfront.login.qr.expired": "QR 코드 만료됨", "ui.userfront.login.qr.refresh": "QR 코드 새로고침", "ui.userfront.login.qr.remaining": "남은 시간: {{time}}", @@ -993,10 +1473,10 @@ const Map koStrings = { "ui.userfront.profile.section.basic": "기본 정보", "ui.userfront.profile.section.organization": "조직 정보", "ui.userfront.profile.section.security": "보안", - "ui.userfront.profile.user_fallback": "User", - "ui.userfront.qr.request_permission": "카메라 권한 요청하기", + "ui.userfront.profile.user_fallback": "사용자", + "ui.userfront.qr.request_permission": "ui.userfront.qr.request_permission", "ui.userfront.qr.rescan": "다시 스캔", - "ui.userfront.qr.result_failure": "승인 실패", + "ui.userfront.qr.result_failure": "ui.userfront.qr.result_failure", "ui.userfront.qr.result_success": "승인 완료", "ui.userfront.qr.title": "Scan QR Code", "ui.userfront.reset.confirm_password": "새 비밀번호 확인", @@ -1006,10 +1486,12 @@ const Map koStrings = { "ui.userfront.reset.title": "새 비밀번호 설정", "ui.userfront.sections.apps": "나의 App 현황", "ui.userfront.sections.audit": "접속이력", + "ui.userfront.sections.sessions": "활성 세션", "ui.userfront.session.active": "세션 활성", "ui.userfront.session.unknown": "알 수 없음", "ui.userfront.signup.agreement.all": "모두 동의합니다", "ui.userfront.signup.agreement.privacy_title": "개인정보 수집 및 이용 동의 (필수)", + "ui.userfront.signup.agreement.required": "필수", "ui.userfront.signup.agreement.tos_title": "바론 소프트웨어 이용약관 (필수)", "ui.userfront.signup.auth.code_label": "인증코드 6자리", "ui.userfront.signup.auth.email.label": "이메일 주소", @@ -1047,170 +1529,230 @@ const Map enStrings = { "domain.tenant_type.company_group": "Company Group", "domain.tenant_type.personal": "Personal", "domain.tenant_type.user_group": "User Group", - "err.backend.authorization_pending": - "Authentication approval is still pending.", + "err.backend.authorization_pending": "Authentication approval is still pending.", "err.backend.bad_request": "Please check your request.", "err.backend.conflict": "The request conflicts with the current state.", "err.backend.expired_token": "The token has expired.", "err.backend.forbidden": "This request is not allowed.", - "err.backend.internal_error": - "An internal error occurred while processing the request.", + "err.backend.internal_error": "An internal error occurred while processing the request.", "err.backend.invalid_code": "The verification code is invalid.", - "err.backend.invalid_or_expired_code": - "The verification code is invalid or expired.", + "err.backend.invalid_or_expired_code": "The verification code is invalid or expired.", "err.backend.invalid_session": "The session is invalid.", "err.backend.invalid_session_reference": "The session reference is invalid.", "err.backend.not_found": "The requested authentication flow was not found.", "err.backend.not_supported": "This login method is not supported.", "err.backend.password_or_email_mismatch": "Email or password does not match.", "err.backend.rate_limited": "Too many requests. Please try again later.", - "err.backend.service_unavailable": - "The authentication service is currently unavailable.", - "err.backend.slow_down": - "Requests are too frequent. Please try again shortly.", + "err.backend.service_unavailable": "The authentication service is currently unavailable.", + "err.backend.slow_down": "Requests are too frequent. Please try again shortly.", "err.common.unknown": "An unknown error occurred.", - "err.userfront.auth_proxy.consent_accept": "Consent Accept", - "err.userfront.auth_proxy.consent_fetch": "Consent Fetch", - "err.userfront.auth_proxy.consent_reject": "Consent Reject", - "err.userfront.auth_proxy.linked_app_revoke": "Linked App Revoke", - "err.userfront.auth_proxy.login_failed": "Login Failed", + "err.userfront.auth_proxy.consent_accept": "Failed to accept the consent request.", + "err.userfront.auth_proxy.consent_fetch": "Failed to load consent details.", + "err.userfront.auth_proxy.consent_reject": "Failed to reject the consent request.", + "err.userfront.auth_proxy.linked_app_revoke": "Failed to revoke the linked application.", + "err.userfront.auth_proxy.login_failed": "Login failed.", "err.userfront.auth_proxy.oidc_accept": "OIDC Accept", - "err.userfront.auth_proxy.password_reset_complete": "Password Reset Complete", - "err.userfront.auth_proxy.password_reset_init": "Password Reset Init", - "err.userfront.profile.load_failed": "Load Failed", + "err.userfront.auth_proxy.password_reset_complete": "Failed to complete the password reset.", + "err.userfront.auth_proxy.password_reset_init": "Failed to start the password reset.", + "err.userfront.profile.load_failed": "Failed to load the profile.", "err.userfront.profile.password_change_failed": "Password Change Failed", - "err.userfront.profile.send_code_failed": "Send Code Failed", - "err.userfront.profile.update_failed": "Update Failed", - "err.userfront.profile.verify_code_failed": "Verify Code Failed", - "err.userfront.session.missing": "Missing", - "msg.admin.api_keys.create.error": "Error", - "msg.admin.api_keys.create.name_required": "Name Required", - "msg.admin.api_keys.create.scope_required": "Scope Required", - "msg.admin.api_keys.create.scopes_count": "Scopes Count", - "msg.admin.api_keys.create.scopes_hint": "Scopes Hint", - "msg.admin.api_keys.create.subtitle": "Subtitle", - "msg.admin.api_keys.create.success.copy_hint": "Copy Hint", - "msg.admin.api_keys.create.success.notice": "Notice", - "msg.admin.api_keys.create.success.notice_emphasis": "Notice Emphasis", - "msg.admin.api_keys.create.success.notice_suffix": "Notice Suffix", - "msg.admin.api_keys.list.delete_confirm": "Delete Confirm", - "msg.admin.api_keys.list.empty": "Empty", - "msg.admin.api_keys.list.fetch_error": "Fetch Error", - "msg.admin.api_keys.list.registry.count": "Count", - "msg.admin.api_keys.list.subtitle": "Subtitle", - "msg.admin.audit.empty": "Empty", + "err.userfront.profile.send_code_failed": "Failed to send the verification code.", + "err.userfront.profile.update_failed": "Failed to update the profile.", + "err.userfront.profile.verify_code_failed": "Verification failed.", + "err.userfront.session.missing": "No active session was found.", + "msg.admin.api_keys.create.error": "Failed to create the API key.", + "msg.admin.api_keys.create.name_required": "Name is required.", + "msg.admin.api_keys.create.scope_required": "Select at least one scope.", + "msg.admin.api_keys.create.scopes_count": "{{count}} scopes will be assigned.", + "msg.admin.api_keys.create.scopes_hint": "Choose the scopes to grant to this API key.", + "msg.admin.api_keys.create.subtitle": "Create and issue an API key for machine-to-machine communication.", + "msg.admin.api_keys.create.success.copy_hint": "Copy the secret now. It will not be shown again.", + "msg.admin.api_keys.create.success.notice": "The generated secret is displayed only once.", + "msg.admin.api_keys.create.success.notice_emphasis": "Store it in a secure location.", + "msg.admin.api_keys.create.success.notice_suffix": "Rotate the key immediately if you think it has been exposed.", + "msg.admin.api_keys.list.delete_confirm": "Are you sure you want to delete this API key?", + "msg.admin.api_keys.list.empty": "No API keys have been issued yet.", + "msg.admin.api_keys.list.fetch_error": "Failed to load the API key list.", + "msg.admin.api_keys.list.registry.count": "{{count}} API keys loaded.", + "msg.admin.api_keys.list.subtitle": "View and manage the API keys issued for server-to-server communication.", + "msg.admin.audit.empty": "No audit logs have been collected yet.", "msg.admin.audit.end": "End of audit feed", - "msg.admin.audit.filters.empty": "Empty", + "msg.admin.audit.filters.empty": "No filters applied.", "msg.admin.audit.load_error": "Error loading logs: {{error}}", "msg.admin.audit.loading": "Loading audit logs...", - "msg.admin.audit.registry.count": "Count", - "msg.admin.audit.subtitle": "Subtitle", - "msg.admin.groups.list.create_error": "Create Failed", - "msg.admin.groups.list.create_success": "Create Success", - "msg.admin.groups.list.delete_confirm": "Delete Confirm", - "msg.admin.groups.list.delete_error": "Delete Error", - "msg.admin.groups.list.delete_success": "Delete Success", - "msg.admin.groups.list.empty": "Empty", + "msg.admin.audit.registry.count": "{{count}} logs loaded.", + "msg.admin.audit.subtitle": "Review command-driven ClickHouse audit logs from the admin workspace.", + "msg.admin.common.forbidden": "You do not have permission to perform this action.", + "msg.admin.groups.create.description": "Adds a new organization unit such as a department or team.", + "msg.admin.groups.create.title": "Create New Organization Unit", + "msg.admin.groups.list.create_error": "Failed to create the organization unit.", + "msg.admin.groups.list.create_success": "Organization unit created successfully.", + "msg.admin.groups.list.delete_confirm": "Are you sure you want to delete this organization unit?", + "msg.admin.groups.list.delete_error": "Failed to delete the organization unit.", + "msg.admin.groups.list.delete_success": "Organization unit deleted successfully.", + "msg.admin.groups.list.empty": "No organization units have been registered yet.", "msg.admin.groups.list.import_error": "Import Error", "msg.admin.groups.list.import_success": "Import Success", "msg.admin.groups.list.loading": "Loading...", - "msg.admin.groups.list.subtitle": "Subtitle", - "msg.admin.groups.members.add_success": "Add Success", - "msg.admin.groups.members.count": "Count", - "msg.admin.groups.members.empty": "Empty", - "msg.admin.groups.members.remove_confirm": "Remove Confirm", - "msg.admin.groups.members.remove_success": "Remove Success", - "msg.admin.groups.members.title": "Title", - "msg.admin.groups.prompt.user_id": "User Id", + "msg.admin.groups.list.subtitle": "Manage departments and teams under the current tenant.", + "msg.admin.groups.members.add_success": "Member added successfully.", + "msg.admin.groups.members.count": "{{count}} members loaded.", + "msg.admin.groups.members.empty": "No members are assigned to this organization unit.", + "msg.admin.groups.members.remove_confirm": "Are you sure you want to remove this member?", + "msg.admin.groups.members.remove_success": "Member removed successfully.", + "msg.admin.groups.members.title": "Member Management", + "msg.admin.groups.prompt.user_id": "Enter the user's UUID to add:", "msg.admin.groups.roles.assign_success": "Assign Success", - "msg.admin.groups.roles.description": "Description", - "msg.admin.groups.roles.empty": "Empty", - "msg.admin.groups.roles.remove_confirm": - "msg.admin.groups.roles.remove_confirm", - "msg.admin.groups.roles.remove_success": "Remove Success", + "msg.admin.groups.roles.description": "Assign or revoke roles for members of this organization unit.", + "msg.admin.groups.roles.empty": "No roles have been assigned yet.", + "msg.admin.groups.roles.remove_confirm": "Are you sure you want to revoke this role?", + "msg.admin.groups.roles.remove_success": "Role revoked successfully.", "msg.admin.header.subtitle": "Tenant isolation & least privilege by default", "msg.admin.idp_env_prod": "IDP env: prod", "msg.admin.logout_confirm": "Are you sure you want to log out?", - "msg.admin.notice.idp_policy": "IDP Policy", - "msg.admin.notice.scope": "Scope", - "msg.admin.overview.description": "Description", + "msg.admin.notice.idp_policy": "IDP management keys are only used through server-side wrapper APIs with audit logging and rate limits enabled.", + "msg.admin.notice.scope": "Administrative features are exposed only within the /admin namespace.", + "msg.admin.org.hover_member_info": "Hover to see member details.", + "msg.admin.org.import_description": "Upload a CSV file to bulk register the organization chart.", + "msg.admin.org.import_error": "An error occurred during organization chart import.", + "msg.admin.org.import_success": "Organization chart imported successfully.", + "msg.admin.overview.description": "Review shared metrics and policy status across all tenants in one place.", "msg.admin.overview.idp_fallback": "Fallback: Descope", "msg.admin.overview.idp_primary": "IDP: Ory primary", - "msg.admin.overview.playbook.description": "Description", - "msg.admin.overview.playbook.idp_body": "IDP Body", + "msg.admin.overview.playbook.description": "Operational guardrails and architecture decisions for the admin control plane.", + "msg.admin.overview.playbook.idp_body": "All IDP calls are routed through the backend only. Hydra and Kratos admin ports are never exposed publicly.", "msg.admin.overview.playbook.idp_title": "Backend-only IDP access", - "msg.admin.overview.playbook.tenant_body": "Tenant Body", + "msg.admin.overview.playbook.tenant_body": "Tenant headers and audit logging are enabled by default and can later be extended with Keto policies.", "msg.admin.overview.playbook.tenant_title": "Tenant isolation", - "msg.admin.overview.quick_links.description": "Description", + "msg.admin.overview.quick_links.description": "Jump to the most frequently used administrative workflows.", + "msg.admin.overview.summary.audit_events_24h": "24h Audit Events", + "msg.admin.overview.summary.oidc_clients": "OIDC Clients", + "msg.admin.overview.summary.policy_gate": "Policy Gate Status", + "msg.admin.overview.summary.total_tenants": "Total Tenants", "msg.admin.scope_admin": "Scoped to /admin", "msg.admin.session_ttl": "Session TTL: 15m admin", "msg.admin.tenant_headers": "Tenant-aware headers", - "msg.admin.tenants.admins.add_success": "Add Success", - "msg.admin.tenants.admins.empty": "Empty", - "msg.admin.tenants.admins.remove_confirm": "Remove Confirm", - "msg.admin.tenants.admins.remove_success": "Remove Success", - "msg.admin.tenants.admins.subtitle": "Subtitle", - "msg.admin.tenants.approve_confirm": "Approve Confirm", - "msg.admin.tenants.approve_success": "Approve Success", - "msg.admin.tenants.create.form.domains_help": - "Users with these email domains will be automatically assigned to this tenant.", - "msg.admin.tenants.create.memo.body": "Body", - "msg.admin.tenants.create.memo.subtitle": "Subtitle", - "msg.admin.tenants.create.profile.subtitle": "Subtitle", - "msg.admin.tenants.create.subtitle": "Subtitle", - "msg.admin.tenants.delete_confirm": "Delete Tenant \"{{name}}\"?", + "msg.admin.tenants.admins.add_success": "Tenant admin added successfully.", + "msg.admin.tenants.admins.empty": "No tenant admins are assigned yet.", + "msg.admin.tenants.admins.remove_confirm": "Are you sure you want to remove this tenant admin?", + "msg.admin.tenants.admins.remove_last": "Cannot remove the last admin.", + "msg.admin.tenants.admins.remove_self": "Cannot remove yourself.", + "msg.admin.tenants.admins.remove_success": "Tenant admin removed successfully.", + "msg.admin.tenants.admins.subtitle": "Manage the administrators assigned to this tenant.", + "msg.admin.tenants.approve_confirm": "Do you want to approve this tenant?", + "msg.admin.tenants.approve_success": "Tenant approved successfully.", + "msg.admin.tenants.create.form.domains_help": "Users with these email domains will be automatically assigned to this tenant.", + "msg.admin.tenants.create.memo.body": "Leave operational notes or policy reminders for this tenant.", + "msg.admin.tenants.create.memo.subtitle": "Capture internal policy notes for administrators.", + "msg.admin.tenants.create.profile.subtitle": "Set the basic tenant profile information.", + "msg.admin.tenants.create.subtitle": "Enter the minimum required information to create a tenant.", + "msg.admin.tenants.delete_confirm": "Delete Tenant \\\\\\\"{{name}}\\\\\\\"?", "msg.admin.tenants.delete_success": "Tenant deleted.", - "msg.admin.tenants.empty": "Empty", - "msg.admin.tenants.fetch_error": "Fetch Error", - "msg.admin.tenants.members.empty": "Empty", + "msg.admin.tenants.empty": "No tenants have been registered yet.", + "msg.admin.tenants.fetch_error": "Failed to load the tenant list.", + "msg.admin.tenants.members.desc": "View the list of users belonging to this organization.", + "msg.admin.tenants.members.empty": "No members found.", + "msg.admin.tenants.members.limit_notice": "Showing members from the first 10 descendant organizations due to size limits.", "msg.admin.tenants.missing_id": "No Tenant ID.", - "msg.admin.tenants.registry.count": "Count", - "msg.admin.tenants.schema.empty": - "No custom fields defined. Click \"Add Field\" to begin.", + "msg.admin.tenants.not_found": "Tenant not found.", + "msg.admin.tenants.owners.add_success": "Owner added successfully.", + "msg.admin.tenants.owners.empty": "No owners registered.", + "msg.admin.tenants.owners.remove_confirm": "Are you sure you want to remove this owner?", + "msg.admin.tenants.owners.remove_last": "Cannot remove the last owner.", + "msg.admin.tenants.owners.remove_self": "Cannot remove yourself.", + "msg.admin.tenants.owners.remove_success": "Owner permission revoked.", + "msg.admin.tenants.owners.subtitle": "List of owners with top-level permissions for this tenant.", + "msg.admin.tenants.registry.count": "{{count}} tenants loaded.", + "msg.admin.tenants.remove_sub_confirm": "Remove tenant \\\"{{name}}\\\" from sub-tenants?", + "msg.admin.tenants.schema.empty": "No custom fields defined. Click \\\\\\\"Add Field\\\\\\\" to begin.", + "msg.admin.tenants.schema.forbidden_desc": "Only administrators can access user schema settings.", "msg.admin.tenants.schema.missing_id": "Tenant ID missing", - "msg.admin.tenants.schema.subtitle": - "Define custom attributes for users in this tenant.", + "msg.admin.tenants.schema.subtitle": "Define custom attributes for users in this tenant.", "msg.admin.tenants.schema.update_error": "Failed to update schema", "msg.admin.tenants.schema.update_success": "Schema updated successfully", - "msg.admin.tenants.sub.empty": "Empty", - "msg.admin.tenants.sub.subtitle": "Subtitle", - "msg.admin.tenants.subtitle": "Subtitle", - "msg.admin.users.create.account.subtitle": "Subtitle", + "msg.admin.tenants.sub.empty": "No child tenants are connected.", + "msg.admin.tenants.sub.subtitle": "Review and manage child tenants linked under this tenant.", + "msg.admin.tenants.subtitle": "Review registered tenants and manage their current status.", + "msg.admin.users.bulk.delete_confirm": "Are you sure you want to delete the selected {{count}} users?", + "msg.admin.users.bulk.delete_success": "{{count}} users have been deleted.", + "msg.admin.users.bulk.description": "Bulk register or manage users via CSV file.", + "msg.admin.users.bulk.move_description": "Bulk move selected users to another tenant.", + "msg.admin.users.bulk.move_error": "Error moving users.", + "msg.admin.users.bulk.move_success": "{{count}} users moved successfully.", + "msg.admin.users.bulk.parsed_count": "Parsed {{count}} rows.", + "msg.admin.users.bulk.schema_incompatible": "Fields not in target schema may be lost:", + "msg.admin.users.bulk.schema_missing": "Missing required fields for target tenant:", + "msg.admin.users.bulk.update_success": "User info updated successfully.", + "msg.admin.users.create.account.subtitle": "Fill in the account details required to create the user.", "msg.admin.users.create.error": "Failed to User Create.", "msg.admin.users.create.form.email_required": "Email Required", - "msg.admin.users.create.form.name_required": "Name Required", + "msg.admin.users.create.form.field_invalid": "Invalid {{label}} format.", + "msg.admin.users.create.form.field_required": "{{label}} is required.", + "msg.admin.users.create.form.login_id_help": "msg.admin.users.create.form.login_id_help", + "msg.admin.users.create.form.name_required": "Name is required.", "msg.admin.users.create.form.password_auto_help": "Password Auto Help", "msg.admin.users.create.form.password_manual_help": "Password Manual Help", "msg.admin.users.create.form.role_help": "Role Help", "msg.admin.users.create.password_generated.default": "Default", "msg.admin.users.create.password_generated.with_email": "With Email", "msg.admin.users.create.password_required": "Password Required", + "msg.admin.users.create.success": "User created successfully.", + "msg.admin.users.detail.delete_confirm": "Are you sure you want to delete this user?", + "msg.admin.users.detail.delete_error": "msg.admin.users.detail.delete_error", + "msg.admin.users.detail.delete_success": "User deleted.", "msg.admin.users.detail.edit_subtitle": "Edit Subtitle", - "msg.admin.users.detail.form.name_required": "Name Required", + "msg.admin.users.detail.form.field_required": "Required.", + "msg.admin.users.detail.form.invalid_format": "Invalid Format", + "msg.admin.users.detail.form.name_required": "Name is required.", + "msg.admin.users.detail.history_desc": "History Desc", + "msg.admin.users.detail.no_history": "No History", + "msg.admin.users.detail.no_tenants": "No Tenants", "msg.admin.users.detail.not_found": "Not Found", + "msg.admin.users.detail.password_generated": "A secure password has been generated.", + "msg.admin.users.detail.password_generated_help": "Generate a temporary password that meets the security policy and apply it immediately.", + "msg.admin.users.detail.password_manual_required": "Please enter a password.", + "msg.admin.users.detail.reset_auto_desc": "Reset Auto Desc", + "msg.admin.users.detail.reset_password_confirm": "msg.admin.users.detail.reset_password_confirm", + "msg.admin.users.detail.reset_password_help": "Force-reset the user's password and apply either an auto-generated password or a manually entered one.", "msg.admin.users.detail.security.password_hint": "Password Hint", + "msg.admin.users.detail.security_desc": "Security Desc", + "msg.admin.users.detail.self_password_reset_blocked": "Please change your own password from the UserFront settings page.", + "msg.admin.users.detail.tenant_slug_help": "Tenant Slug Help", + "msg.admin.users.detail.tenants_desc": "Tenants Desc", "msg.admin.users.detail.update_error": "Failed to User Edit.", "msg.admin.users.detail.update_success": "Update Success", - "msg.admin.users.list.delete_confirm": "Delete Confirm", - "msg.admin.users.list.empty": "Empty", - "msg.admin.users.list.fetch_error": "Fetch Error", - "msg.admin.users.list.registry.count": "Count", - "msg.admin.users.list.subtitle": "Subtitle", - "msg.common.error": "Error", + "msg.admin.users.list.columns.description": "Select columns to display in the table.", + "msg.admin.users.list.columns.no_custom": "No custom fields defined for this tenant.", + "msg.admin.users.list.delete_confirm": "Are you sure you want to delete the selected user?", + "msg.admin.users.list.empty": "No users match the current filters.", + "msg.admin.users.list.fetch_error": "Failed to load the user list.", + "msg.admin.users.list.registry.count": "{{count}} users loaded.", + "msg.admin.users.list.subtitle": "Search and manage users registered in the current tenant.", + "msg.common.copied": "Copied", + "msg.common.copied_to_clipboard": "Copied to clipboard.", + "msg.common.error": "An error occurred.", + "msg.common.forbidden": "Access Denied.", "msg.common.loading": "Loading...", "msg.common.no_description": "No Description.", + "msg.common.parsing": "Parsing data...", "msg.common.requesting": "Requesting...", "msg.common.saving": "Saving...", "msg.common.unknown_error": "unknown error", + "msg.dev.audit.empty": "No audit logs found.", + "msg.dev.audit.forbidden": "You do not have permission to view audit logs. Please request access from an administrator.", + "msg.dev.audit.load_error": "Error loading audit logs: {{error}}", + "msg.dev.audit.loaded_count": "Loaded {{count}} rows", + "msg.dev.audit.loading": "Loading audit logs...", + "msg.dev.audit.subtitle": "Shows DevFront activity history within current tenant/app scope.", + "msg.dev.auth.access_denied_description": "DevFront is for administrators only. Request access from your administrator.", + "msg.dev.auth.access_denied_title": "Access denied.", "msg.dev.clients.consents.empty": "No consents found.", "msg.dev.clients.consents.load_error": "Error loading consents: {{error}}", "msg.dev.clients.consents.loading": "Loading consents...", - "msg.dev.clients.consents.showing": - "Showing {{from}} to {{to}} of {{total}} users", - "msg.dev.clients.consents.subtitle": "Subtitle", - "msg.dev.clients.copy_client_id": "Copy Client Id", - "msg.dev.clients.delete_confirm": - "Are you sure you want to delete this app? This action cannot be undone.", + "msg.dev.clients.consents.revoke_confirm": "Are you sure you want to revoke this user's permissions? After revocation, the user must consent again on next login.", + "msg.dev.clients.consents.showing": "Showing {{from}} to {{to}} of {{total}} users", + "msg.dev.clients.consents.subtitle": "Review consent grants and users who have approved this application.", + "msg.dev.clients.delete_confirm": "Are you sure you want to delete this app? This action cannot be undone.", "msg.dev.clients.delete_error": "Failed to delete: {{error}}", "msg.dev.clients.deleted": "App deleted.", "msg.dev.clients.details.copy_client_id": "Client ID copied.", @@ -1219,274 +1761,325 @@ const Map enStrings = { "msg.dev.clients.details.load_error": "Error loading client: {{error}}", "msg.dev.clients.details.loading": "Loading client...", "msg.dev.clients.details.missing_id": "Client ID is required.", - "msg.dev.clients.details.redirect.description": "Description", + "msg.dev.clients.details.redirect.description": "List the allowed URLs that users can be redirected to after authentication. Separate multiple values with commas.", "msg.dev.clients.details.redirect_saved": "Redirect URIs saved.", "msg.dev.clients.details.rotate_confirm": "Rotate Confirm", "msg.dev.clients.details.rotate_error": "Rotate Error", "msg.dev.clients.details.save_error": "Save Error", "msg.dev.clients.details.secret_rotated": "Secret Rotated", - "msg.dev.clients.details.secret_unavailable": "SECRET_NOT_AVAILABLE", - "msg.dev.clients.details.security.footer": "Footer", - "msg.dev.clients.details.security.note": "Note", - "msg.dev.clients.details.subtitle": "Subtitle", - "msg.dev.clients.federation.add_subtitle": - "Connect an external OIDC provider.", + "msg.dev.clients.details.secret_unavailable": "The client secret is not available.", + "msg.dev.clients.details.security.footer": "When rotating a secret, confirm the admin session TTL, rate limits, and notification flow.", + "msg.dev.clients.details.security.note": "Keep endpoints read-only and link secret copy or rotation actions to audit logs.", + "msg.dev.clients.details.subtitle": "Inspect this application's credentials, endpoints, and security settings.", + "msg.dev.clients.federation.add_subtitle": "Connect an external OIDC provider.", "msg.dev.clients.federation.empty": "No IdP configurations found.", - "msg.dev.clients.federation.subtitle": - "Manage external identity providers for this application.", + "msg.dev.clients.federation.subtitle": "Manage external identity providers for this application.", "msg.dev.clients.general.identity.logo_help": "Logo Help", - "msg.dev.clients.general.identity.subtitle": "Subtitle", + "msg.dev.clients.general.identity.subtitle": "Manage the OIDC identity, branding, and basic metadata for this application.", "msg.dev.clients.general.load_error": "Error loading client: {{error}}", "msg.dev.clients.general.loading": "Loading client...", - "msg.dev.clients.general.redirect.help": - "Enter the redirect URIs. You can modify them in the Federation tab after creation.", + "msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "Allowed Algorithms Tooltip", + "msg.dev.clients.general.public_key.auth_method_client_secret_basic_help": "Standard authentication method for server-side applications.", + "msg.dev.clients.general.public_key.auth_method_none_help": "Use this for PKCE-based public clients.", + "msg.dev.clients.general.public_key.auth_method_private_key_jwt_help": "Signed key-based client authentication recommended for trusted RP bootstrap and JAR verification.", + "msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "Missing Algorithm Badge", + "msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "Missing Algorithm Reason", + "msg.dev.clients.general.public_key.cache.missing_algorithms_help": "Missing Algorithms Help", + "msg.dev.clients.general.public_key.cache.missing_algorithms_title": "Missing Algorithms Title", + "msg.dev.clients.general.public_key.cache.parsed_keys_empty": "No parsed JWKS keys are available yet.", + "msg.dev.clients.general.public_key.cache.parsed_keys_help": "Raw JWKS stays hidden. Only parsed key metadata is shown here.", + "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "Unsupported Algorithm Reason", + "msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "Unsupported Algorithms Help", + "msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "Unsupported Algorithms Title", + "msg.dev.clients.general.public_key.cache_empty": "Cache Empty", + "msg.dev.clients.general.public_key.cache_help": "Cache Help", + "msg.dev.clients.general.public_key.cache_refresh_failed": "Cache Refresh Failed", + "msg.dev.clients.general.public_key.cache_refreshed": "Cache Refreshed", + "msg.dev.clients.general.public_key.cache_revoke_confirm": "Cache Revoke Confirm", + "msg.dev.clients.general.public_key.cache_revoke_failed": "Cache Revoke Failed", + "msg.dev.clients.general.public_key.cache_revoked": "Cache Revoked", + "msg.dev.clients.general.public_key.guide_example": "Recommended example: https://rp.example.com/.well-known/jwks.json", + "msg.dev.clients.general.public_key.guide_intro": "A JWKS URI is not created by Baron. It is the URL where the RP backend exposes its public key.", + "msg.dev.clients.general.public_key.guide_step_1": "Generate a key pair on the RP server and keep the private key only in the RP backend.", + "msg.dev.clients.general.public_key.guide_step_2": "Expose the public key from the RP backend through a JWKS (JSON Web Key Set) endpoint.", + "msg.dev.clients.general.public_key.guide_step_3": "Enter a URL such as https://rp.example.com/.well-known/jwks.json in DevFront.", + "msg.dev.clients.general.public_key.headless_help": "You can design your own login UI within the application. While the UI is yours, the actual identity verification and security checks are handled in the background via Baron's API.", + "msg.dev.clients.general.public_key.jwks_inline_help": "Prefer the SSH-RSA public key format first. If you paste an 'ssh-rsa AAA...' key, Baron converts it to OIDC-standard JWKS (JSON) before saving.", + "msg.dev.clients.general.public_key.jwks_uri_help": "Enter the public key endpoint URL exposed by the RP backend. Example: https://rp.example.com/.well-known/jwks.json", + "msg.dev.clients.general.public_key.request_object_alg_help": "Specify the JAR (Request Object) signing algorithm used for headless login.", + "msg.dev.clients.general.public_key.source_help": "Register the JWKS URI served by the RP so Baron can verify the public key.", + "msg.dev.clients.general.public_key.subtitle": "Manage the public key and headless login settings required for trusted RP evaluation.", + "msg.dev.clients.general.public_key.validation.headless_requires_alg": "Headless login requires a Request Object Signing Algorithm.", + "msg.dev.clients.general.public_key.validation.headless_requires_private_key_jwt": "Headless login requires token endpoint auth method to be private_key_jwt.", + "msg.dev.clients.general.public_key.validation.headless_requires_public_key": "Headless login requires a JWKS URI.", + "msg.dev.clients.general.public_key.validation.invalid_jwks_inline": "The input must be valid JSON (JWKS). For SSH-RSA input, it must start with 'ssh-rsa'.", + "msg.dev.clients.general.public_key.validation.invalid_jwks_uri": "JWKS URI format is invalid.", + "msg.dev.clients.general.public_key.validation.missing_jwks_inline": "Enter a public key in SSH-RSA or JWKS format.", + "msg.dev.clients.general.public_key.validation.missing_jwks_uri": "JWKS URI is required.", + "msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "Missing Parsed Algorithms", + "msg.dev.clients.general.public_key.validation.private_key_jwt_requires_public_key": "Signed key-based authentication requires a JWKS URI.", + "msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "Unsupported Parsed Algorithms", + "msg.dev.clients.general.redirect.help": "Enter the redirect URIs. You can modify them in the Federation tab after creation.", "msg.dev.clients.general.save_error": "Failed to save: {{error}}", "msg.dev.clients.general.saved": "Saved", - "msg.dev.clients.general.scopes.empty": "Empty", - "msg.dev.clients.general.scopes.subtitle": "Subtitle", - "msg.dev.clients.general.security.pkce_help": - "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory.", - "msg.dev.clients.general.security.private_help": - "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers.", - "msg.dev.clients.general.security.subtitle": - "Select application type. Security level determines authentication method.", + "msg.dev.clients.general.scopes.empty": "No custom scopes have been added yet.", + "msg.dev.clients.general.scopes.subtitle": "Define the scopes this application can request.", + "msg.dev.clients.general.security.pkce_help": "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory.", + "msg.dev.clients.general.security.private_help": "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers.", + "msg.dev.clients.general.security.subtitle": "Select application type. Security level determines authentication method.", "msg.dev.clients.general.status_changed": "Status changed to {{status}}.", - "msg.dev.clients.help.docs_body": - "Includes PKCE, client_secret_basic, redirect URI validation tips.", - "msg.dev.clients.help.subtitle": - "Developer guides for Confidential/Public clients, redirect URIs, and auth methods.", + "msg.dev.clients.help.docs_body": "Includes PKCE, client_secret_basic, redirect URI validation tips.", + "msg.dev.clients.help.subtitle": "Developer guides for Confidential/Public clients, redirect URIs, and auth methods.", "msg.dev.clients.load_error": "Error loading clients: {{error}}", "msg.dev.clients.loading": "Loading apps...", - "msg.dev.clients.registry.description": "Description", + "msg.dev.clients.registry.description": "Manage OIDC applications, authentication methods, redirect URIs, and secret key reissue with audit logs.", "msg.dev.clients.scopes.email": "Email", "msg.dev.clients.scopes.openid": "Openid", "msg.dev.clients.scopes.profile": "Profile", "msg.dev.clients.showing": "Showing {{shown}} of {{total}} apps", - "msg.dev.clients.status_update_error": "Failed to update client status", - "msg.dev.clients.status_updated": "The app has been {{status}}.", - "msg.dev.dashboard.hero.body": "Body", + "msg.dev.dashboard.hero.body": "Monitor RP readiness, consent activity, and operational status for the current developer workspace.", "msg.dev.dashboard.hero.title_emphasis": "Title Emphasis", "msg.dev.dashboard.hero.title_prefix": "Title Prefix", "msg.dev.dashboard.hero.title_suffix": "Title Suffix", "msg.dev.dashboard.notice.consent_audit": "Consent Audit", "msg.dev.dashboard.notice.dev_scope": "Dev Scope", "msg.dev.dashboard.notice.hydra_health": "Hydra Health", + "msg.dev.forbidden.default": "You do not have permission to access this resource. Please contact an administrator.", + "msg.dev.forbidden.rp_admin": "RP administrators can only access resources for the apps they manage.", + "msg.dev.forbidden.tenant_admin": "Tenant administrator permissions are not configured correctly or have expired.", + "msg.dev.forbidden.title": "Access Denied: {{resource}}", + "msg.dev.forbidden.user": "Regular users cannot access the developer console.", "msg.dev.logout_confirm": "Are you sure you want to log out?", - "msg.dev.sidebar.notice": "Notice", - "msg.dev.sidebar.notice_detail": "Notice Detail", + "msg.dev.sidebar.notice": "Developer Console", + "msg.dev.sidebar.notice_detail": "Register and manage client applications.", "msg.info.saved_success": "Saved successfully.", - "msg.userfront.audit.date": "Date", - "msg.userfront.audit.device": "Device", - "msg.userfront.audit.end": "End", - "msg.userfront.audit.ip": "Ip", - "msg.userfront.audit.load_more_error": "Load More Error", - "msg.userfront.audit.result": "Result", + "msg.userfront.audit.date": "Date: {{value}}", + "msg.userfront.audit.device": "Device: {{value}}", + "msg.userfront.audit.end": "No more items to show.", + "msg.userfront.audit.ip": "IP address: {{value}}", + "msg.userfront.audit.load_more_error": "Could not load more history.", + "msg.userfront.audit.result": "Result: {{value}}", "msg.userfront.audit.session_id": "Session ID: {{value}}", - "msg.userfront.audit.status": "Status", - "msg.userfront.dashboard.activities.empty": "Empty", - "msg.userfront.dashboard.activities.empty_detail": "Empty Detail", - "msg.userfront.dashboard.activities.error": "Error", - "msg.userfront.dashboard.approved_device": "Approved Device", - "msg.userfront.dashboard.approved_ip": "Approve IP: {{ip}}", - "msg.userfront.dashboard.approved_session.copy_click": "Copy Click", - "msg.userfront.dashboard.approved_session.copy_tap": "Copy Tap", - "msg.userfront.dashboard.approved_session.none": "None", - "msg.userfront.dashboard.audit_empty": "Audit Empty", - "msg.userfront.dashboard.audit_load_error": "Audit Load Error", - "msg.userfront.dashboard.auth_method": "Auth Method", + "msg.userfront.audit.status": "Status: pending", + "msg.userfront.consent.accept_error": "Failed to process consent: {{error}}", + "msg.userfront.consent.cancel.confirm": "If you cancel consent, you will not be able to use this service. Do you want to cancel?", + "msg.userfront.consent.cancel.error": "An error occurred while cancelling consent: {{error}}", + "msg.userfront.consent.client_id": "Client ID: {{id}}", + "msg.userfront.consent.client_unknown": "Unknown application", + "msg.userfront.consent.description": "The service below is requesting access to your account information.\\\\nPlease choose whether to continue.", + "msg.userfront.consent.load_error": "Failed to load consent information: {{error}}", + "msg.userfront.consent.missing_redirect": "Consent was processed, but the redirect URL was missing.", + "msg.userfront.consent.redirect_notice": "After consent, you will be redirected automatically.", + "msg.userfront.consent.scope.email": "Email address (account identification and notifications)", + "msg.userfront.consent.scope.offline_access": "Offline access (keep signed in)", + "msg.userfront.consent.scope.openid": "OpenID authentication information (signin session check)", + "msg.userfront.consent.scope.phone": "Phone number (identity verification and notifications)", + "msg.userfront.consent.scope.profile": "Basic profile information (name, user identifier)", + "msg.userfront.consent.scope_count": "Total {{count}}", + "msg.userfront.dashboard.activities.empty": "No linked apps yet.", + "msg.userfront.dashboard.activities.empty_detail": "Linked apps and their latest activity will appear here.", + "msg.userfront.dashboard.activities.error": "Could not load linked apps.", + "msg.userfront.dashboard.approved_device": "Approved device: {{device}}", + "msg.userfront.dashboard.approved_ip": "Approved IP: {{ip}}", + "msg.userfront.dashboard.approved_session.copy_click": "{{label}}: {{id}}\\\\\\\\\\\\\\\\nClick to copy.", + "msg.userfront.dashboard.approved_session.copy_tap": "{{label}}: {{id}}\\\\\\\\\\\\\\\\nTap to copy.", + "msg.userfront.dashboard.approved_session.none": "No {{label}}", + "msg.userfront.dashboard.audit_empty": "No recent sign-in activity.", + "msg.userfront.dashboard.audit_load_error": "Could not load sign-in history.", + "msg.userfront.dashboard.auth_method": "Auth method: {{method}}", "msg.userfront.dashboard.client_id": "Client ID: {{id}}", - "msg.userfront.dashboard.client_id_missing": "Client Id Missing", - "msg.userfront.dashboard.current_status": "Current Status", - "msg.userfront.dashboard.last_auth": "Last Auth", - "msg.userfront.dashboard.link_missing": "Link Missing", - "msg.userfront.dashboard.link_open_error": "Link Open Error", + "msg.userfront.dashboard.client_id_missing": "No client ID available.", + "msg.userfront.dashboard.current_status": "Current status: {{status}}", + "msg.userfront.dashboard.last_auth": "Last signed in: {{value}}", + "msg.userfront.dashboard.link_missing": "This app does not have a launch URL configured.", + "msg.userfront.dashboard.link_open_error": "Could not open the app link.", "msg.userfront.dashboard.render_error": "Dashboard render error: {{error}}", - "msg.userfront.dashboard.revoke.confirm": "Confirm", - "msg.userfront.dashboard.revoke.error": "Error", - "msg.userfront.dashboard.revoke.success": "Success", - "msg.userfront.dashboard.scopes.empty": "Empty", - "msg.userfront.dashboard.session_id_copied": "Session Id Copied", - "msg.userfront.dashboard.timeline.load_error": "Load Error", - "msg.userfront.error.detail_contact": "msg.userfront.error.detail_contact", - "msg.userfront.error.detail_generic": "Detail Generic", - "msg.userfront.error.detail_request": "Detail Request", - "msg.userfront.error.id": "Id", + "msg.userfront.dashboard.revoke.confirm": "Disconnect {{app}}?\\\\\\\\\\\\\\\\nYou will need to grant access again the next time you sign in.", + "msg.userfront.dashboard.revoke.error": "Could not disconnect the app: {{error}}", + "msg.userfront.dashboard.revoke.success": "{{app}} has been disconnected.", + "msg.userfront.dashboard.scopes.empty": "No scopes were requested.", + "msg.userfront.dashboard.session_id_copied": "Session ID copied.", + "msg.userfront.dashboard.sessions.browser": "Browser: {{value}}", + "msg.userfront.dashboard.sessions.empty": "No active sessions.", + "msg.userfront.dashboard.sessions.empty_detail": "Devices signed in with this account will appear here.", + "msg.userfront.dashboard.sessions.error": "Could not load sessions.", + "msg.userfront.dashboard.sessions.os": "OS: {{value}}", + "msg.userfront.dashboard.sessions.recent_app": "Recent app: {{app}}", + "msg.userfront.dashboard.sessions.revoke.confirm": "End the session for {{target}}?\nThat device will need to sign in again.", + "msg.userfront.dashboard.sessions.revoke.error": "Could not end the session: {{error}}", + "msg.userfront.dashboard.sessions.revoke.success": "The session has been ended.", + "msg.userfront.dashboard.sessions.session_id": "Session ID: {{id}}", + "msg.userfront.dashboard.timeline.load_error": "Could not load sign-in history.", + "msg.userfront.error.detail_contact": "If the problem continues, please contact your administrator.", + "msg.userfront.error.detail_generic": "Something went wrong.", + "msg.userfront.error.detail_request": "We had trouble processing your request.", + "msg.userfront.error.id": "Error ID: {{id}}", "msg.userfront.error.ory.\"\$normalizedCode\"": "{{error}}", - "msg.userfront.error.ory.access_denied": - "The user denied the consent request.", - "msg.userfront.error.ory.consent_required": - "Consent is required to continue.", - "msg.userfront.error.ory.interaction_required": - "Additional interaction is required. Please try again.", + "msg.userfront.error.ory.access_denied": "The user denied the consent request.", + "msg.userfront.error.ory.consent_required": "Consent is required to continue.", + "msg.userfront.error.ory.interaction_required": "Additional interaction is required. Please try again.", "msg.userfront.error.ory.invalid_client": "Client authentication failed.", - "msg.userfront.error.ory.invalid_grant": - "The authorization grant is invalid or expired.", + "msg.userfront.error.ory.invalid_grant": "The authorization grant is invalid or expired.", "msg.userfront.error.ory.invalid_request": "The request is invalid.", "msg.userfront.error.ory.invalid_scope": "The requested scope is invalid.", "msg.userfront.error.ory.login_required": "Login is required.", "msg.userfront.error.ory.request_forbidden": "The request was forbidden.", - "msg.userfront.error.ory.server_error": - "An authentication server error occurred.", - "msg.userfront.error.ory.temporarily_unavailable": - "The authentication server is temporarily unavailable.", - "msg.userfront.error.ory.unauthorized_client": - "The client is not authorized for this request.", - "msg.userfront.error.ory.unsupported_response_type": - "The response type is not supported.", - "msg.userfront.error.title": "Title", - "msg.userfront.error.title_generic": "Title Generic", - "msg.userfront.error.title_with_code": "Title With Code", - "msg.userfront.error.type": "Type", + "msg.userfront.error.ory.server_error": "An authentication server error occurred.", + "msg.userfront.error.ory.temporarily_unavailable": "The authentication server is temporarily unavailable.", + "msg.userfront.error.ory.unauthorized_client": "The client is not authorized for this request.", + "msg.userfront.error.ory.unsupported_response_type": "The response type is not supported.", + "msg.userfront.error.title": "An error occurred during authentication.", + "msg.userfront.error.title_generic": "An error occurred.", + "msg.userfront.error.title_with_code": "Error: {{code}}", + "msg.userfront.error.type": "Error type: {{type}}", "msg.userfront.error.whitelist.\"\$normalizedCode\"": "{{error}}", "msg.userfront.error.whitelist.bad_request": "Please check your input.", - "msg.userfront.error.whitelist.invalid_session": - "Your session has expired. Please sign in again.", - "msg.userfront.error.whitelist.not_found": - "The requested page could not be found.", - "msg.userfront.error.whitelist.password_or_email_mismatch": - "Email or password does not match.", - "msg.userfront.error.whitelist.rate_limited": - "Too many requests. Please try again later.", - "msg.userfront.error.whitelist.recovery_expired": - "The recovery link has expired. Please request a new one.", - "msg.userfront.error.whitelist.recovery_invalid": - "The recovery link is invalid.", - "msg.userfront.error.whitelist.settings_disabled": - "Account settings are currently unavailable.", - "msg.userfront.error.whitelist.verification_required": - "Additional verification is required. Please follow the instructions.", - "msg.userfront.forgot.description": "Description", - "msg.userfront.forgot.dry_send": "Dry Send", - "msg.userfront.forgot.error": "Error", - "msg.userfront.forgot.input_required": "Input Required", - "msg.userfront.forgot.sent": "Sent", - "msg.userfront.greeting": "Greeting", - "msg.userfront.login.cookie_check_failed": "Cookie Check Failed", - "msg.userfront.login.dry_send": "Dry Send", - "msg.userfront.login.link.approved": "Approved", - "msg.userfront.login.link.helper": "Sending you a login link", - "msg.userfront.login.link.missing_login_id": "Missing Login Id", - "msg.userfront.login.link.missing_phone": "Missing Phone", - "msg.userfront.login.link.resend_wait": "Resend Wait", - "msg.userfront.login.link.short_code_help": "Short Code Help", - "msg.userfront.login.link_failed": "Link Failed", - "msg.userfront.login.link_send_failed": "Link Send Failed", - "msg.userfront.login.link_sent_email": "Link Sent Email", - "msg.userfront.login.link_sent_phone": "Link Sent Phone", + "msg.userfront.error.whitelist.invalid_session": "Your session has expired. Please sign in again.", + "msg.userfront.error.whitelist.not_found": "The requested page could not be found.", + "msg.userfront.error.whitelist.password_or_email_mismatch": "Email or password does not match.", + "msg.userfront.error.whitelist.rate_limited": "Too many requests. Please try again later.", + "msg.userfront.error.whitelist.recovery_expired": "The recovery link has expired. Please request a new one.", + "msg.userfront.error.whitelist.recovery_invalid": "The recovery link is invalid.", + "msg.userfront.error.whitelist.settings_disabled": "Account settings are currently unavailable.", + "msg.userfront.error.whitelist.verification_required": "Additional verification is required. Please follow the instructions.", + "msg.userfront.forgot.description": "Enter the email address or phone number linked to your account and we will send you a password reset link.", + "msg.userfront.forgot.dry_send": "Dry-send mode: no email or SMS was actually sent.", + "msg.userfront.forgot.error": "Failed to send the reset link: {{error}}", + "msg.userfront.forgot.input_required": "Enter your email address or phone number.", + "msg.userfront.forgot.sent": "A password reset link has been sent. Check your email or SMS.", + "msg.userfront.greeting": "Hello, {{name}}.", + "msg.userfront.login.cookie_check_failed": "Could not verify your sign-in state: {{error}}", + "msg.userfront.login.dry_send": "Dry-send mode: no email or SMS was actually sent.", + "msg.userfront.login.link.approved": "Sign-in approved. You will be redirected to the sign-in page shortly.", + "msg.userfront.login.link.helper": "We will send a sign-in link using the information you enter.", + "msg.userfront.login.link.missing_login_id": "Enter your email address or phone number.", + "msg.userfront.login.link.missing_phone": "Enter your phone number.", + "msg.userfront.login.link.resend_wait": "You can resend in {{time}}.", + "msg.userfront.login.link.short_code_help": "You can also sign in with the last 2 letters and 6 digits from the link you received.", + "msg.userfront.login.link_failed": "Could not complete link sign-in: {{error}}", + "msg.userfront.login.link_send_failed": "Failed to send the sign-in link: {{error}}", + "msg.userfront.login.link_sent_email": "We sent a sign-in link to your email address.", + "msg.userfront.login.link_sent_phone": "We sent a sign-in link to your phone number.", "msg.userfront.login.link_timeout": "Time expired.", "msg.userfront.login.no_account": "New to Baron?", - "msg.userfront.login.oidc_failed": "OIDC Failed", - "msg.userfront.login.password.failed": "Failed", - "msg.userfront.login.password.missing_credentials": "Missing Credentials", - "msg.userfront.login.qr.load_failed": "Load Failed", - "msg.userfront.login.qr.scan_hint": "Scan Hint", + "msg.userfront.login.oidc_failed": "OIDC sign-in failed. Please try again.", + "msg.userfront.login.password.failed": "Sign-in failed: {{error}}", + "msg.userfront.login.password.missing_credentials": "Enter both your email or phone number and your password.", + "msg.userfront.login.qr.load_failed": "Could not load the QR code.", + "msg.userfront.login.qr.scan_hint": "Scan it with the mobile app.", "msg.userfront.login.qr_expired": "Time expired.", - "msg.userfront.login.qr_init_failed": "QR Init Failed", - "msg.userfront.login.qr_login_required": "QR Login Required", - "msg.userfront.login.short_code.invalid": "Invalid", - "msg.userfront.login.token_missing": "Token Missing", - "msg.userfront.login.unregistered.body": "Body", - "msg.userfront.login.verification.approved": "Approved", - "msg.userfront.login.verification.approved_local": "Approved Local", - "msg.userfront.login.verification.success": "Success", - "msg.userfront.login.verification_failed": "Verification Failed", - "msg.userfront.login_success.subtitle": "Subtitle", - "msg.userfront.profile.department_missing": "Department Missing", - "msg.userfront.profile.department_required": "Department Required", - "msg.userfront.profile.email_missing": "Email Missing", - "msg.userfront.profile.greeting": "Greeting", - "msg.userfront.profile.load_failed": "Load Failed", - "msg.userfront.profile.name_missing": "Name Missing", - "msg.userfront.profile.name_required": "Name Required", - "msg.userfront.profile.password.change_failed": "Change Failed", - "msg.userfront.profile.password.changed": "Changed", - "msg.userfront.profile.password.current_required": "Current Required", - "msg.userfront.profile.password.mismatch": "Mismatch", - "msg.userfront.profile.password.new_required": "New Required", - "msg.userfront.profile.password.subtitle": "Subtitle", - "msg.userfront.profile.phone.code_sent": "Code Sent", - "msg.userfront.profile.phone.send_failed": "Send Failed", - "msg.userfront.profile.phone.verified": "Verified", - "msg.userfront.profile.phone.verify_failed": "Verify Failed", - "msg.userfront.profile.phone.verify_notice": "Verify Notice", - "msg.userfront.profile.phone_required": "Phone Required", - "msg.userfront.profile.phone_verify_required": "Phone Verify Required", - "msg.userfront.profile.section.basic": "Basic", - "msg.userfront.profile.section.organization": "Organization", - "msg.userfront.profile.section.security": "Security", - "msg.userfront.profile.update_failed": "Update Failed", - "msg.userfront.profile.update_success": "Update Success", - "msg.userfront.qr.approve_error": "Approve Error", - "msg.userfront.qr.approve_success": "Approve Success", - "msg.userfront.qr.camera_error": "Camera Error", - "msg.userfront.qr.permission_error": "Permission Error", - "msg.userfront.qr.permission_required": "Permission Required", + "msg.userfront.login.qr_init_failed": "Failed to initialize QR sign-in: {{error}}", + "msg.userfront.login.qr_login_required": "You need to be signed in to approve a QR sign-in.", + "msg.userfront.login.short_code.invalid": "Enter the 2 letters and 6 digits from your code.", + "msg.userfront.login.token_missing": "Could not find the sign-in token.", + "msg.userfront.login.unregistered.body": "We could not find an account for that information.\\\\\\\\\\\\\\\\nPlease sign up before continuing.", + "msg.userfront.login.verification.approved": "Approved. Complete sign-in in the original window.", + "msg.userfront.login.verification.approved_local": "Approved. This device is already signed in, and the remote window will be signed in shortly.", + "msg.userfront.login.verification.success": "Sign-in approval completed.", + "msg.userfront.login.verification_failed": "Failed to approve the sign-in request: {{error}}", + "msg.userfront.login_success.subtitle": "You have signed in successfully.", + "msg.userfront.profile.department_missing": "No department information", + "msg.userfront.profile.department_required": "Enter your department.", + "msg.userfront.profile.email_missing": "No email address", + "msg.userfront.profile.greeting": "Hello, {{name}}.", + "msg.userfront.profile.load_failed": "Could not load your profile.", + "msg.userfront.profile.name_missing": "No name provided", + "msg.userfront.profile.name_required": "Enter your name.", + "msg.userfront.profile.password.change_failed": "Failed to change your password: {{error}}", + "msg.userfront.profile.password.changed": "Your password has been changed.", + "msg.userfront.profile.password.current_required": "Enter your current password.", + "msg.userfront.profile.password.mismatch": "The new passwords do not match.", + "msg.userfront.profile.password.new_required": "Enter a new password.", + "msg.userfront.profile.password.subtitle": "Verify your current password before setting a new one.", + "msg.userfront.profile.phone.code_sent": "A verification code has been sent.", + "msg.userfront.profile.phone.send_failed": "Failed to send the code: {{error}}", + "msg.userfront.profile.phone.verified": "Phone number verified.", + "msg.userfront.profile.phone.verify_failed": "Verification failed: {{error}}", + "msg.userfront.profile.phone.verify_notice": "SMS verification is required to change your phone number.", + "msg.userfront.profile.phone_required": "Enter your phone number.", + "msg.userfront.profile.phone_verify_required": "Phone verification is required.", + "msg.userfront.profile.section.basic": "Manage your basic account information.", + "msg.userfront.profile.section.organization": "Your organization and affiliation details.", + "msg.userfront.profile.section.security": "Keep your password secure.", + "msg.userfront.profile.update_failed": "Failed to update your profile: {{error}}", + "msg.userfront.profile.update_success": "Your profile has been updated.", + "msg.userfront.qr.approve_error": "QR approval failed: {{error}}", + "msg.userfront.qr.approve_success": "QR approval complete. Continue on your desktop.", + "msg.userfront.qr.camera_error": "Camera error: {{error}}", + "msg.userfront.qr.permission_error": "Could not request camera access. Check your browser or OS settings.", + "msg.userfront.qr.permission_required": "Camera access is required.", "msg.userfront.reset.error.empty_password": "Please enter Password.", - "msg.userfront.reset.error.generic": "Generic", - "msg.userfront.reset.error.lowercase": "Lowercase", - "msg.userfront.reset.error.min_length": "Min Length", - "msg.userfront.reset.error.min_types": "Min Types", - "msg.userfront.reset.error.mismatch": "Mismatch", - "msg.userfront.reset.error.number": "Number", - "msg.userfront.reset.error.symbol": "Symbol", - "msg.userfront.reset.error.uppercase": "Uppercase", - "msg.userfront.reset.invalid_body": "Invalid Body", - "msg.userfront.reset.invalid_link": "Invalid Link", - "msg.userfront.reset.invalid_title": "Invalid Title", - "msg.userfront.reset.policy.lowercase": "Lowercase", - "msg.userfront.reset.policy.min_length": "Min Length", - "msg.userfront.reset.policy.min_types": "Min Types", - "msg.userfront.reset.policy.number": "Number", - "msg.userfront.reset.policy.symbol": "Symbol", - "msg.userfront.reset.policy.uppercase": "Uppercase", - "msg.userfront.reset.policy_loading": "Policy Loading", - "msg.userfront.reset.success": "Success", - "msg.userfront.sections.apps_subtitle": "Apps Subtitle", - "msg.userfront.sections.audit_subtitle": "Audit Subtitle", - "msg.userfront.settings.disabled": "Disabled", - "msg.userfront.signup.agreement.title": "Title", - "msg.userfront.signup.auth.affiliate_notice": "Affiliate Notice", - "msg.userfront.signup.auth.title": "Title", - "msg.userfront.signup.email.code_mismatch": "Code Mismatch", - "msg.userfront.signup.email.duplicate": "Duplicate", - "msg.userfront.signup.email.invalid": "Invalid", - "msg.userfront.signup.email.send_failed": "Send Failed", - "msg.userfront.signup.email.verified": "Verified", - "msg.userfront.signup.email.verify_failed": "Verify Failed", + "msg.userfront.reset.error.generic": "Failed to change your password: {{error}}", + "msg.userfront.reset.error.lowercase": "Include at least one lowercase letter.", + "msg.userfront.reset.error.min_length": "Use at least {{count}} characters.", + "msg.userfront.reset.error.min_types": "Use at least {{count}} character types: uppercase, lowercase, number, or symbol.", + "msg.userfront.reset.error.mismatch": "The passwords do not match.", + "msg.userfront.reset.error.number": "Include at least one number.", + "msg.userfront.reset.error.symbol": "Include at least one symbol.", + "msg.userfront.reset.error.uppercase": "Include at least one uppercase letter.", + "msg.userfront.reset.invalid_body": "This password reset link is invalid or has expired. Please request a new one.", + "msg.userfront.reset.invalid_link": "This reset link is invalid. Missing loginId or token.", + "msg.userfront.reset.invalid_title": "Invalid reset link", + "msg.userfront.reset.policy.lowercase": "At least one lowercase letter", + "msg.userfront.reset.policy.min_length": "At least {{count}} characters", + "msg.userfront.reset.policy.min_types": "At least {{count}} character types", + "msg.userfront.reset.policy.number": "At least one number", + "msg.userfront.reset.policy.symbol": "At least one symbol", + "msg.userfront.reset.policy.uppercase": "At least one uppercase letter", + "msg.userfront.reset.policy_loading": "Loading the password policy...", + "msg.userfront.reset.success": "Your password has been changed successfully. Please sign in again.", + "msg.userfront.sections.apps_subtitle": "Your linked apps and their latest sign-in status.", + "msg.userfront.sections.audit_subtitle": "Recent access history for Baron sign-in.", + "msg.userfront.sections.sessions_subtitle": "Your currently signed-in devices and browser sessions.", + "msg.userfront.settings.disabled": "Account settings are currently unavailable.", + "msg.userfront.signup.agreement.all_hint": "Agree to both required documents to continue to the next step.", + "msg.userfront.signup.agreement.description": "Review the service terms and privacy collection notice, then agree to continue.", + "msg.userfront.signup.agreement.privacy_summary": "Review what personal data is collected, why it is used, and how it is retained.", + "msg.userfront.signup.agreement.progress": "{{count}} of {{total}} required agreements completed", + "msg.userfront.signup.agreement.title": "Please review and agree to the terms to continue.", + "msg.userfront.signup.agreement.tos_summary": "Review the service terms, usage conditions, and responsibilities.", + "msg.userfront.signup.auth.affiliate_notice": "If you are an affiliate employee, use your official company email address.", + "msg.userfront.signup.auth.title": "Verify your email and phone number.", + "msg.userfront.signup.email.code_mismatch": "The verification code does not match.", + "msg.userfront.signup.email.duplicate": "This email address is already registered.", + "msg.userfront.signup.email.invalid": "Enter a valid email address.", + "msg.userfront.signup.email.send_failed": "Failed to send the email: {{error}}", + "msg.userfront.signup.email.verified": "Email verified.", + "msg.userfront.signup.email.verify_failed": "Email verification failed: {{error}}", "msg.userfront.signup.failed": "Failed", - "msg.userfront.signup.password.length_required": "Length Required", - "msg.userfront.signup.password.lowercase_required": "Lowercase Required", - "msg.userfront.signup.password.mismatch": "Mismatch", - "msg.userfront.signup.password.number_required": "Number Required", - "msg.userfront.signup.password.rule.lowercase": "Lowercase", - "msg.userfront.signup.password.rule.min_length": "Min Length", - "msg.userfront.signup.password.rule.min_types": "Min Types", + "msg.userfront.signup.password.length_required": "Your password must be at least 12 characters long.", + "msg.userfront.signup.password.lowercase_required": "Include at least one lowercase letter.", + "msg.userfront.signup.password.mismatch": "The passwords do not match.", + "msg.userfront.signup.password.number_required": "Include at least one number.", + "msg.userfront.signup.password.rule.lowercase": "Lowercase letter", + "msg.userfront.signup.password.rule.min_length": "At least {{count}} characters", + "msg.userfront.signup.password.rule.min_types": "At least {{count}} character types", "msg.userfront.signup.password.rule.number": "Number", "msg.userfront.signup.password.rule.symbol": "Symbol", - "msg.userfront.signup.password.rule.uppercase": "Uppercase", - "msg.userfront.signup.password.symbol_required": "Symbol Required", - "msg.userfront.signup.password.title": "Title", - "msg.userfront.signup.password.uppercase_required": "Uppercase Required", - "msg.userfront.signup.phone.code_mismatch": "Code Mismatch", - "msg.userfront.signup.phone.send_failed": "Send Failed", - "msg.userfront.signup.phone.verified": "Verified", - "msg.userfront.signup.phone.verify_failed": "Verify Failed", - "msg.userfront.signup.policy.loading": "Loading", - "msg.userfront.signup.policy.lowercase": "Lowercase", - "msg.userfront.signup.policy.min_length": "Min Length", - "msg.userfront.signup.policy.min_types": "Min Types", + "msg.userfront.signup.password.rule.uppercase": "Uppercase letter", + "msg.userfront.signup.password.symbol_required": "Include at least one symbol.", + "msg.userfront.signup.password.title": "Create a secure password to finish signing up.", + "msg.userfront.signup.password.uppercase_required": "Include at least one uppercase letter.", + "msg.userfront.signup.phone.code_mismatch": "The verification code does not match.", + "msg.userfront.signup.phone.send_failed": "Failed to send the SMS: {{error}}", + "msg.userfront.signup.phone.verified": "Phone number verified.", + "msg.userfront.signup.phone.verify_failed": "Phone verification failed: {{error}}", + "msg.userfront.signup.policy.loading": "Loading the password policy...", + "msg.userfront.signup.policy.lowercase": "Lowercase letter", + "msg.userfront.signup.policy.min_length": "At least {{count}} characters", + "msg.userfront.signup.policy.min_types": "At least {{count}} character types", "msg.userfront.signup.policy.number": "Number", - "msg.userfront.signup.policy.summary": "Summary", + "msg.userfront.signup.policy.summary": "Security policy: {{rules}}", "msg.userfront.signup.policy.symbol": "Symbol", - "msg.userfront.signup.policy.uppercase": "Uppercase", - "msg.userfront.signup.privacy_full": - "\n개인정보 수집 및 이용 동의\n\n바론서비스 개인정보처리방침\n\n제1조 (목적)\n바론컨설턴트(이하 \"회사\")는 바론서비스(이하 \"서비스\")를 이용하는 고객(이하 \"이용자\")의 개인정보를 보호하고, 「개인정보 보호법」에 따라 책임과 의무를 다하기 위해 본 개인정보처리방침을 마련했습니다. 본 방침은 이용자가 제공한 개인정보가 어떻게 수집, 이용, 보관, 보호되는지를 설명합니다.\n제2조 (개인정보의 처리목적)\n회사는 다음의 목적을 위해 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\n- 본인확인: 회원가입 및 관리를 위한 본인 확인, 전화 또는 이메일을 통한 연락\n- 서비스 제공: 각종 통보 및 서비스 제공을 위한 업무 처리\n- 제품소개서 다운로드: 설명자료 전달\n- 상담 및 데모 신청: 상담 제공 및 데모 제공, 계약 처리자 정보 수집\n- 행사 참가 신청: 참석 안내 및 세미나/설명회/교육 제공\n- 보안가이드 제공: 안내자료 전달\n- 기술지원 문의: 서비스 사용 지원\n- 서비스 개선 의견 접수: 서비스 품질 개선\n- 마케팅 활동: 동의한 고객에 한해 뉴스레터 및 매거진 발송\n제3조 (개인정보의 처리 및 보유 기간)\n① 회사는 법령에 따른 개인정보 보유 및 이용기간 또는 정보주체로부터 개인정보를 수집 시 동의받은 개인정보 보유 및 이용기간 내에서 개인정보를 처리 및 보유합니다.\n② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다:\n- 회원정보: 회원가입일부터 회원탈퇴 후 1년까지\n- 홍보, 상담, 계약용 개인정보: 2년\n제4조 (개인정보의 제3자 제공)\n① 회사는 정보주체의 개인정보를 제2조에서 명시한 범위 내에서만 처리하며, 정보 주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.\n② 회사는 다음과 같이 개인정보를 제3자에게 제공하고 있습니다:\n- 제공받는 자: 수사기관 및 유관기관, 피신고업체\n- 이용 목적: 개인정보 침해 민원 처리\n- 제공하는 개인정보 항목: 성명, 연락처, 이메일\n- 보유 및 이용기간: 법령에서 정한 보존기간 및 제공목적 달성 시 파기\n제5조 (개인정보 처리 위탁)\n① 회사는 개인정보 처리업무를 외부 업체에 위탁하지 않으며, 자체적으로 처리하고 있습니다.\n② 회사가 특정 업무(예: 채용 업무)를 외부 업체에 위탁할 경우, 개인정보 처리방침 시행 전 회사 홈페이지에서 공지한 후 정보주체의 동의를 받은 후 위탁합니다.\n제6조 (정보주체의 권리·의무 및 행사 방법)\n① 정보주체는 회사에 대해 언제든지 개인정보 열람, 정정, 삭제, 처리정지 요구 등의 권리를 행사할 수 있습니다.\n② 권리 행사는 다음과 같은 방법으로 할 수 있습니다:\n- 서면: 회사 주소로 서면 제출\n- 전자우편: 회사 이메일로 요청\n- 모사전송(FAX): 회사 FAX로 요청\n③ 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자를 통해 대리로도 가능합니다. 이 경우 “개인정보 처리 방법에 관한 고시” 별지 제11호 서식에 따른 위임장을 제출해야 합니다.\n④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 따라 제한될 수 있습니다.\n⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에 따라 수집된 개인정보인 경우 제한될 수 있습니다.\n⑥ 회사는 권리 행사를 요청한 자가 본인 또는 정당한 대리인인지를 확인합니다.\n제7조 (처리하는 개인정보의 항목)\n회사는 다음의 개인정보 항목을 처리합니다:\n- 수집 항목:\n- 필수 항목: 성명, 휴대전화번호, 이메일\n- 선택 항목: 회사전화번호, 문의사항\n- 수집 방법:\n- 홈페이지, 전화, 이메일을 통해 수집\n제8조 (개인정보의 파기)\n① 회사는 개인정보 보유 기간의 경과, 처리 목적 달성 등 개인정보가 불필요하게 되었을 때 지체 없이 해당 개인정보를 파기합니다.\n② 정보주체로부터 동의받은 개인정보 보유 기간이 경과하거나 처리 목적이 달성된 경우에도 다른 법령에 따라 개인정보를 계속 보존해야 할 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관 장소를 달리하여 보존합니다.\n③ 개인정보 파기의 절차 및 방법은 다음과 같습니다:\n- 파기 절차: 회사는 파기 사유가 발생한 개인정보를 선정하고, 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.\n- 파기 방법: 전자적 파일 형태로 기록된 개인정보는 복구할 수 없도록 기술적 방법을 사용해 삭제하며, 종이 문서에 기록된 개인정보는 분쇄기로 분쇄하거나 소각하여 파기합니다.\n제9조 (개인정보의 안전성 확보 조치)\n회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취합니다:\n- 관리적 조치: 내부관리계획 수립·시행, 정기적 직원 교육\n- 기술적 조치: 개인정보처리시스템 접근 권한 관리, 접근통제시스템 설치, 고유식별정보 암호화, 보안 프로그램 설치\n- 물리적 조치: 전산실 및 자료보관실 접근 통제\n제10조 (개인정보 자동 수집 장치의 설치·운영 및 거부에 관한 사항)\n회사는 쿠키(Cookie)를 사용하지 않습니다. 쿠키는 이용자의 이용 정보를 저장하고 수시로 불러오는 작은 파일로, 바론서비스에서는 쿠키를 사용하지 않습니다.\n제11조 (개인정보 보호책임자)\n회사는 개인정보 처리에 관한 업무를 총괄하여 책임지고, 개인정보 처리와 관련된 정보주체의 불만처리 및 피해구제를 위해 개인정보 보호책임자를 지정하고 있습니다.\n개인정보 보호책임자:\n- 성명: 염승호\n- 직책: 수석연구원\n- 연락처: 02-2141-7448\n- 팩스번호: 02-2141-7599\n- 이메일: b23008@baroncs.co.kr\n제12조 (개인정보 열람청구)\n정보주체는 「개인정보 보호법」 제35조에 따른 개인정보 열람 청구를 아래 부서에 할 수 있습니다. 회사는 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.\n개인정보 열람청구 접수·처리 부서:\n- 부서명: 총괄기획실\n- 담당자: 권혁진\n- 연락처: 02-2141-7465\n- 팩스번호: 02-2141-7599\n- 이메일: baroncs@baroncs.co.kr\n제13조 (권익침해 구제방법)\n정보주체는 개인정보 침해로 인한 구제를 위해 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보해신고센터 등에 분쟁 해결이나 상담을 신청할 수 있습니다.\n- 개인정보분쟁조정위원회: (국번없이) 1833-6972 (www.kopico.go.kr)\n- 개인정보침해신고센터: (국번없이) 118 (privacy.kisa.or.kr)\n- 대검찰청: (국번없이) 1301 (www.spo.go.kr)\n- 경찰청: (국번없이) 182 (www.police.go.kr)\n제14조 (개인정보 처리방침의 변경)\n본 개인정보처리방침은 법령, 정책 또는 보안 기술의 변경에 따라 내용의 추가, 삭제 및 수정이 있을 시, 개정 최소 7일 전에 홈페이지를 통해 사전 공지합니다.\n\n부칙\n제1조 (시행일자)\n이 개인정보처리방침은 2024년 10월 1일부터 시행됩니다.\n제2조 (개정 및 고지의 의무)\n회사는 개인정보처리방침을 변경하는 경우, 변경사항을 시행일자 7일 전부터 서비스 내 공지사항 페이지를 통해 고지할 것입니다. 다만, 이용자의 권리나 의무에 중대한 변경이 발생하는 경우에는 시행일자 30일 전부터 고지합니다.\n제3조 (유효성)\n본 개인정보처리방침의 일부 조항이 법적 또는 기타 사유로 인해 무효화되거나 시행할 수 없는 경우, 나머지 조항들은 계속해서 유효합니다. 무효화된 조항은 관련 법령에 부합하는 방식으로 수정되어 효력을 지속합니다.\n제4조 (변경 통지의 방법)\n회사는 개인정보처리방침의 변경 시, 다음의 방법으로 이용자에게 고지합니다:\n- 서비스 초기화면 또는 팝업 공지\n- 이메일 발송\n- 회사 홈페이지 공지사항\n제5조 (비회원의 개인정보 보호)\n회사는 비회원의 개인정보도 회원과 동일한 수준으로 보호합니다. 비회원이 개인정보 제공을 거부할 경우 일부 서비스 이용에 제한이 있을 수 있습니다.\n제6조 (14세 미만 아동의 개인정보 보호)\n회사는 14세 미만 아동의 개인정보를 수집하지 않습니다. 만일 14세 미만 아동의 개인정보가 수집된 경우, 법정 대리인의 동의를 받아야 하며, 법정 대리인의 동의 없이 수집된 경우 이를 지체 없이 파기합니다.\n제7조 (개인정보의 국외 이전)\n회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.\n제8조 (기타)\n본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.\n", - "msg.userfront.signup.profile.affiliate_hint": "Affiliate Hint", - "msg.userfront.signup.profile.title": "Title", - "msg.userfront.signup.success.body": "Body", - "msg.userfront.signup.success.title": "Title", - "msg.userfront.signup.tos_full": - "\n바론 소프트웨어 이용약관\n\n제1장 총칙\n제1조 (목적)\n이 약관은 바론컨설턴트(이하 \"회사\"라 합니다)가 제공하는 바론소프트웨어(이하 \"서비스\"라 합니다)를 이용함에 있어 회사와 이용자 간의 권리, 의무 및 책임사항과 기타 필요한 사항을 정하는 것을 목적으로 합니다.\n제2조 (용어의 정의)\n① 본 약관에서 사용하는 용어의 정의는 다음과 같습니다:\n- “서비스”란 회사가 제공하는 소프트웨어 및 관련 제반 서비스를 의미합니다.\n- “이용자”란 회사의 서비스에 접속하여 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.\n- “회원”이란 본 약관에 동의하고 회사와 이용계약을 체결한 자를 의미합니다.\n- “비회원”이란 회원가입을 하지 않고 회사가 제공하는 일부 서비스를 이용하는 자를 말합니다.\n제3조 (약관의 효력 및 변경)\n① 본 약관은 이용자가 본 약관에 동의하고, 회사가 이에 대한 승낙을 완료함으로써 효력이 발생합니다. ② 회사는 필요한 경우 본 약관을 변경할 수 있으며, 변경된 약관은 서비스 화면에 공지된 후 효력이 발생합니다.\n제4조 (약관 외 준칙)\n본 약관에 명시되지 않은 사항에 대해서는 대한민국의 관련 법령과 상관습에 따릅니다.\n제2장 서비스 이용계약\n제5조 (이용계약의 성립)\n이용계약은 이용자가 약관의 내용에 동의하고, 회사가 제공하는 소정의 회원가입 신청서를 작성하여 가입을 완료한 후, 회사가 이를 승인함으로써 성립합니다.\n제6조 (이용계약의 유보와 거절)\n① 회사는 다음 각 호에 해당하는 경우 이용계약의 성립을 유보하거나 거절할 수 있습니다: - 신청서의 내용이 허위로 판명된 경우 - 서비스 제공이 기술적으로 어려운 경우\n제7조 (계약사항의 변경)\n회원은 개인정보 관리 메뉴를 통해 언제든지 자신의 정보를 열람하고 수정할 수 있습니다. 회원의 정보가 변경된 경우 즉시 수정해야 하며, 수정하지 않아 발생하는 문제의 책임은 회원에게 있습니다.\n제3장 개인정보 보호\n제8조 (개인정보 보호의 원칙)\n① 회원의 개인정보는 관련 법령에 따라 보호됩니다. ② 회사는 개인정보 보호와 관련된 세부 사항을 별도로 마련한 개인정보처리방침에 따라 관리하며, 이용자는 언제든지 해당 방침을 통해 개인정보 관리에 대한 자세한 내용을 확인할 수 있습니다.\n제9조 (개인정보처리방침 준수)\n① 회사는 개인정보 보호와 관련된 구체적인 사항을 개인정보처리방침에 따라 관리합니다. ② 개인정보의 수집, 이용, 제공, 보관, 보호 등에 관한 사항은 회사의 개인정보처리방침을 따르며, 이용자는 회사 웹사이트에서 이를 확인할 수 있습니다. ③ 회사는 개인정보 보호를 위해 최선을 다하며, 관련 법령에 따라 이용자의 개인정보를 안전하게 관리합니다.\n제10조 (14세 미만 아동의 개인정보 보호)\n① 회사는 14세 미만 아동의 개인정보를 수집할 경우, 반드시 법정대리인의 동의를 받아야 합니다. ② 법정대리인은 아동의 개인정보 열람, 수정, 삭제를 요청할 수 있으며, 회사는 이를 신속하게 처리합니다. ③ 14세 미만 아동의 개인정보 보호와 관련된 구체적인 사항은 개인정보처리방침에 명시되어 있습니다.\n제4장 서비스 제공 및 이용\n제11조 (서비스 제공)\n회사는 회원의 이용 신청을 승인한 때부터 서비스를 개시합니다. 서비스 이용은 연중무휴 24시간을 원칙으로 합니다.\n제12조 (서비스의 변경 및 중단)\n회사는 서비스 제공이 어려운 경우 사전 고지 후 서비스를 변경하거나 중단할 수 있습니다.\n제5장 정보 제공 및 광고\n제13조 (정보 제공 및 광고)\n① 회사는 서비스 이용 중 필요하다고 인정되는 정보 및 광고를 제공할 수 있습니다. ② 회원은 원치 않는 정보를 수신 거부할 수 있습니다.\n제6장 게시물 관리\n제14조 (게시물의 관리)\n회사는 회원이 게시한 내용이 불법적이거나 약관에 위배될 경우 이를 삭제할 수 있습니다.\n제15조 (게시물의 저작권)\n게시물의 저작권은 회원에게 있으며, 회사는 이를 서비스 홍보 및 개선 목적으로 사용할 수 있습니다.\n제7장 계약 해지 및 이용 제한\n제16조 (계약 해지)\n회원은 언제든지 계약 해지를 요청할 수 있으며, 회사는 신속하게 처리합니다.\n제17조 (이용 제한)\n회사는 회원이 약관을 위반할 경우 서비스 이용을 제한할 수 있습니다.\n제8장 손해 배상 및 면책 조항\n제18조 (손해 배상)\n회사는 무료로 제공되는 서비스와 관련하여 회원에게 발생한 손해에 대해 책임을 지지 않습니다.\n제19조 (면책 조항)\n회사는 천재지변 등 불가항력적인 사유로 인해 서비스를 제공하지 못하는 경우 책임을 지지 않습니다.\n제9장 유료 서비스\n20조 (유료 서비스의 이용)\n① 회사는 회원에게 특정 서비스에 대해 유료로 제공할 수 있습니다. ② 유료 서비스의 이용 요금, 결제 방식, 환불 절차 등에 대한 상세 내용은 서비스 안내 페이지와 결제 화면에 명시합니다. ③ 유료 서비스 이용 요금은 회사가 정한 결제 방식에 따라 결제됩니다. 회원은 신용카드, 계좌이체, 휴대전화 결제 등 회사가 제공하는 다양한 결제 방식을 통해 요금을 납부할 수 있습니다. ④ 유료 서비스의 이용 요금은 선불 결제를 원칙으로 하며, 이용 기간 중 서비스 중지 및 해지 시 남은 이용 기간에 대한 환불은 회사의 환불 정책에 따라 처리됩니다. ⑤ 회사는 회원의 유료 서비스 이용과 관련하여 발생한 문제에 대해 최선을 다해 해결하도록 노력합니다. 다만, 회사의 고의 또는 중대한 과실이 없는 한 회원이 유료 서비스 이용 중 입은 손해에 대해서는 책임을 지지 않습니다.\n제21조(환불 정책)\n① 회원은 결제 후 7일 이내에 서비스 이용을 시작하지 않은 경우, 요금 전액을 환불받을 수 있습니다. ② 유료 서비스 이용 중 부득이한 사유로 서비스가 중지된 경우, 회사는 이용하지 않은 부분에 대해 환불 절차를 밟습니다. ③ 회원의 귀책사유로 인해 서비스 이용이 중지된 경우, 환불이 불가능합니다. ④ 환불은 회원이 지정한 계좌로 환불 절차를 거치며, 환불 요청 후 7일 이내에 처리됩니다.\n제22조 (유료 서비스의 중지 및 해지)\n① 회원이 유료 서비스를 해지하고자 하는 경우, 회사의 고객 지원 센터에 해지 신청을 해야 합니다. ② 회사는 회원이 약관을 위반하거나 부정한 방법으로 유료 서비스를 이용한 경우, 유료 서비스 이용을 즉시 중지하고 계약을 해지할 수 있습니다.\n제10장 양도 금지\n제23조 (양도 금지)\n회원은 서비스 이용권한, 기타 이용계약상의 지위를 제3자에게 양도, 증여할 수 없으며, 이를 담보로 제공할 수 없습니다.\n제11장 관할 법원\n제24조 (분쟁 해결)\n서비스 이용과 관련하여 분쟁이 발생한 경우, 회사와 회원은 성실히 협의하여 해결합니다.\n제25조 (관할 법원)\n본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.\n부칙\n본 약관은 2024년 10월 1일부터 시행됩니다.\n", + "msg.userfront.signup.policy.uppercase": "Uppercase letter", + "msg.userfront.signup.privacy_full": "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 수집 및 이용 동의\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론서비스 개인정보처리방침\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론컨설턴트(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"회사\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")는 바론서비스(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")를 이용하는 고객(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"이용자\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")의 개인정보를 보호하고, 「개인정보 보호법」에 따라 책임과 의무를 다하기 위해 본 개인정보처리방침을 마련했습니다. 본 방침은 이용자가 제공한 개인정보가 어떻게 수집, 이용, 보관, 보호되는지를 설명합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (개인정보의 처리목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 다음의 목적을 위해 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 본인확인: 회원가입 및 관리를 위한 본인 확인, 전화 또는 이메일을 통한 연락\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 제공: 각종 통보 및 서비스 제공을 위한 업무 처리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제품소개서 다운로드: 설명자료 전달\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 상담 및 데모 신청: 상담 제공 및 데모 제공, 계약 처리자 정보 수집\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 행사 참가 신청: 참석 안내 및 세미나/설명회/교육 제공\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 보안가이드 제공: 안내자료 전달\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 기술지원 문의: 서비스 사용 지원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 개선 의견 접수: 서비스 품질 개선\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 마케팅 활동: 동의한 고객에 한해 뉴스레터 및 매거진 발송\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (개인정보의 처리 및 보유 기간)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 법령에 따른 개인정보 보유 및 이용기간 또는 정보주체로부터 개인정보를 수집 시 동의받은 개인정보 보유 및 이용기간 내에서 개인정보를 처리 및 보유합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 회원정보: 회원가입일부터 회원탈퇴 후 1년까지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 홍보, 상담, 계약용 개인정보: 2년\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (개인정보의 제3자 제공)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 정보주체의 개인정보를 제2조에서 명시한 범위 내에서만 처리하며, 정보 주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 회사는 다음과 같이 개인정보를 제3자에게 제공하고 있습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제공받는 자: 수사기관 및 유관기관, 피신고업체\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이용 목적: 개인정보 침해 민원 처리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제공하는 개인정보 항목: 성명, 연락처, 이메일\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 보유 및 이용기간: 법령에서 정한 보존기간 및 제공목적 달성 시 파기\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (개인정보 처리 위탁)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 처리업무를 외부 업체에 위탁하지 않으며, 자체적으로 처리하고 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 회사가 특정 업무(예: 채용 업무)를 외부 업체에 위탁할 경우, 개인정보 처리방침 시행 전 회사 홈페이지에서 공지한 후 정보주체의 동의를 받은 후 위탁합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (정보주체의 권리·의무 및 행사 방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 정보주체는 회사에 대해 언제든지 개인정보 열람, 정정, 삭제, 처리정지 요구 등의 권리를 행사할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 권리 행사는 다음과 같은 방법으로 할 수 있습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서면: 회사 주소로 서면 제출\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 전자우편: 회사 이메일로 요청\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 모사전송(FAX): 회사 FAX로 요청\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n③ 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자를 통해 대리로도 가능합니다. 이 경우 “개인정보 처리 방법에 관한 고시” 별지 제11호 서식에 따른 위임장을 제출해야 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 따라 제한될 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에 따라 수집된 개인정보인 경우 제한될 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n⑥ 회사는 권리 행사를 요청한 자가 본인 또는 정당한 대리인인지를 확인합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (처리하는 개인정보의 항목)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 다음의 개인정보 항목을 처리합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 수집 항목:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 필수 항목: 성명, 휴대전화번호, 이메일\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 선택 항목: 회사전화번호, 문의사항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 수집 방법:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 홈페이지, 전화, 이메일을 통해 수집\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (개인정보의 파기)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 보유 기간의 경과, 처리 목적 달성 등 개인정보가 불필요하게 되었을 때 지체 없이 해당 개인정보를 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 정보주체로부터 동의받은 개인정보 보유 기간이 경과하거나 처리 목적이 달성된 경우에도 다른 법령에 따라 개인정보를 계속 보존해야 할 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관 장소를 달리하여 보존합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n③ 개인정보 파기의 절차 및 방법은 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 파기 절차: 회사는 파기 사유가 발생한 개인정보를 선정하고, 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 파기 방법: 전자적 파일 형태로 기록된 개인정보는 복구할 수 없도록 기술적 방법을 사용해 삭제하며, 종이 문서에 기록된 개인정보는 분쇄기로 분쇄하거나 소각하여 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9조 (개인정보의 안전성 확보 조치)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 관리적 조치: 내부관리계획 수립·시행, 정기적 직원 교육\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 기술적 조치: 개인정보처리시스템 접근 권한 관리, 접근통제시스템 설치, 고유식별정보 암호화, 보안 프로그램 설치\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 물리적 조치: 전산실 및 자료보관실 접근 통제\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10조 (개인정보 자동 수집 장치의 설치·운영 및 거부에 관한 사항)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 쿠키(Cookie)를 사용하지 않습니다. 쿠키는 이용자의 이용 정보를 저장하고 수시로 불러오는 작은 파일로, 바론서비스에서는 쿠키를 사용하지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11조 (개인정보 보호책임자)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보 처리에 관한 업무를 총괄하여 책임지고, 개인정보 처리와 관련된 정보주체의 불만처리 및 피해구제를 위해 개인정보 보호책임자를 지정하고 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 보호책임자:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 성명: 염승호\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 직책: 수석연구원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 연락처: 02-2141-7448\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 팩스번호: 02-2141-7599\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일: b23008@baroncs.co.kr\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제12조 (개인정보 열람청구)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n정보주체는 「개인정보 보호법」 제35조에 따른 개인정보 열람 청구를 아래 부서에 할 수 있습니다. 회사는 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 열람청구 접수·처리 부서:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 부서명: 총괄기획실\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 담당자: 권혁진\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 연락처: 02-2141-7465\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 팩스번호: 02-2141-7599\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일: baroncs@baroncs.co.kr\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제13조 (권익침해 구제방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n정보주체는 개인정보 침해로 인한 구제를 위해 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보해신고센터 등에 분쟁 해결이나 상담을 신청할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 개인정보분쟁조정위원회: (국번없이) 1833-6972 (www.kopico.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 개인정보침해신고센터: (국번없이) 118 (privacy.kisa.or.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 대검찰청: (국번없이) 1301 (www.spo.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 경찰청: (국번없이) 182 (www.police.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제14조 (개인정보 처리방침의 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 개인정보처리방침은 법령, 정책 또는 보안 기술의 변경에 따라 내용의 추가, 삭제 및 수정이 있을 시, 개정 최소 7일 전에 홈페이지를 통해 사전 공지합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n부칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (시행일자)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이 개인정보처리방침은 2024년 10월 1일부터 시행됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (개정 및 고지의 의무)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보처리방침을 변경하는 경우, 변경사항을 시행일자 7일 전부터 서비스 내 공지사항 페이지를 통해 고지할 것입니다. 다만, 이용자의 권리나 의무에 중대한 변경이 발생하는 경우에는 시행일자 30일 전부터 고지합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (유효성)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 개인정보처리방침의 일부 조항이 법적 또는 기타 사유로 인해 무효화되거나 시행할 수 없는 경우, 나머지 조항들은 계속해서 유효합니다. 무효화된 조항은 관련 법령에 부합하는 방식으로 수정되어 효력을 지속합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (변경 통지의 방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보처리방침의 변경 시, 다음의 방법으로 이용자에게 고지합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 초기화면 또는 팝업 공지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일 발송\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 회사 홈페이지 공지사항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (비회원의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 비회원의 개인정보도 회원과 동일한 수준으로 보호합니다. 비회원이 개인정보 제공을 거부할 경우 일부 서비스 이용에 제한이 있을 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (14세 미만 아동의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 14세 미만 아동의 개인정보를 수집하지 않습니다. 만일 14세 미만 아동의 개인정보가 수집된 경우, 법정 대리인의 동의를 받아야 하며, 법정 대리인의 동의 없이 수집된 경우 이를 지체 없이 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (개인정보의 국외 이전)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (기타)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n", + "msg.userfront.signup.profile.affiliate_hint": "This will be selected automatically when you use an affiliate email.", + "msg.userfront.signup.profile.title": "Tell us about your affiliation.", + "msg.userfront.signup.success.body": "Your account has been created successfully.", + "msg.userfront.signup.success.title": "Sign-up complete", + "msg.userfront.signup.tos_full": "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론 소프트웨어 이용약관\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1장 총칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이 약관은 바론컨설턴트(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"회사\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"라 합니다)가 제공하는 바론소프트웨어(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"라 합니다)를 이용함에 있어 회사와 이용자 간의 권리, 의무 및 책임사항과 기타 필요한 사항을 정하는 것을 목적으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (용어의 정의)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 본 약관에서 사용하는 용어의 정의는 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “서비스”란 회사가 제공하는 소프트웨어 및 관련 제반 서비스를 의미합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “이용자”란 회사의 서비스에 접속하여 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “회원”이란 본 약관에 동의하고 회사와 이용계약을 체결한 자를 의미합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “비회원”이란 회원가입을 하지 않고 회사가 제공하는 일부 서비스를 이용하는 자를 말합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (약관의 효력 및 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 본 약관은 이용자가 본 약관에 동의하고, 회사가 이에 대한 승낙을 완료함으로써 효력이 발생합니다. ② 회사는 필요한 경우 본 약관을 변경할 수 있으며, 변경된 약관은 서비스 화면에 공지된 후 효력이 발생합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (약관 외 준칙)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관에 명시되지 않은 사항에 대해서는 대한민국의 관련 법령과 상관습에 따릅니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2장 서비스 이용계약\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (이용계약의 성립)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이용계약은 이용자가 약관의 내용에 동의하고, 회사가 제공하는 소정의 회원가입 신청서를 작성하여 가입을 완료한 후, 회사가 이를 승인함으로써 성립합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (이용계약의 유보와 거절)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 다음 각 호에 해당하는 경우 이용계약의 성립을 유보하거나 거절할 수 있습니다: - 신청서의 내용이 허위로 판명된 경우 - 서비스 제공이 기술적으로 어려운 경우\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (계약사항의 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 개인정보 관리 메뉴를 통해 언제든지 자신의 정보를 열람하고 수정할 수 있습니다. 회원의 정보가 변경된 경우 즉시 수정해야 하며, 수정하지 않아 발생하는 문제의 책임은 회원에게 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3장 개인정보 보호\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (개인정보 보호의 원칙)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원의 개인정보는 관련 법령에 따라 보호됩니다. ② 회사는 개인정보 보호와 관련된 세부 사항을 별도로 마련한 개인정보처리방침에 따라 관리하며, 이용자는 언제든지 해당 방침을 통해 개인정보 관리에 대한 자세한 내용을 확인할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9조 (개인정보처리방침 준수)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 보호와 관련된 구체적인 사항을 개인정보처리방침에 따라 관리합니다. ② 개인정보의 수집, 이용, 제공, 보관, 보호 등에 관한 사항은 회사의 개인정보처리방침을 따르며, 이용자는 회사 웹사이트에서 이를 확인할 수 있습니다. ③ 회사는 개인정보 보호를 위해 최선을 다하며, 관련 법령에 따라 이용자의 개인정보를 안전하게 관리합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10조 (14세 미만 아동의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 14세 미만 아동의 개인정보를 수집할 경우, 반드시 법정대리인의 동의를 받아야 합니다. ② 법정대리인은 아동의 개인정보 열람, 수정, 삭제를 요청할 수 있으며, 회사는 이를 신속하게 처리합니다. ③ 14세 미만 아동의 개인정보 보호와 관련된 구체적인 사항은 개인정보처리방침에 명시되어 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4장 서비스 제공 및 이용\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11조 (서비스 제공)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원의 이용 신청을 승인한 때부터 서비스를 개시합니다. 서비스 이용은 연중무휴 24시간을 원칙으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제12조 (서비스의 변경 및 중단)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 서비스 제공이 어려운 경우 사전 고지 후 서비스를 변경하거나 중단할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5장 정보 제공 및 광고\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제13조 (정보 제공 및 광고)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 서비스 이용 중 필요하다고 인정되는 정보 및 광고를 제공할 수 있습니다. ② 회원은 원치 않는 정보를 수신 거부할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6장 게시물 관리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제14조 (게시물의 관리)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원이 게시한 내용이 불법적이거나 약관에 위배될 경우 이를 삭제할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제15조 (게시물의 저작권)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n게시물의 저작권은 회원에게 있으며, 회사는 이를 서비스 홍보 및 개선 목적으로 사용할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7장 계약 해지 및 이용 제한\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제16조 (계약 해지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 언제든지 계약 해지를 요청할 수 있으며, 회사는 신속하게 처리합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제17조 (이용 제한)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원이 약관을 위반할 경우 서비스 이용을 제한할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8장 손해 배상 및 면책 조항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제18조 (손해 배상)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 무료로 제공되는 서비스와 관련하여 회원에게 발생한 손해에 대해 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제19조 (면책 조항)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 천재지변 등 불가항력적인 사유로 인해 서비스를 제공하지 못하는 경우 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9장 유료 서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n20조 (유료 서비스의 이용)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 회원에게 특정 서비스에 대해 유료로 제공할 수 있습니다. ② 유료 서비스의 이용 요금, 결제 방식, 환불 절차 등에 대한 상세 내용은 서비스 안내 페이지와 결제 화면에 명시합니다. ③ 유료 서비스 이용 요금은 회사가 정한 결제 방식에 따라 결제됩니다. 회원은 신용카드, 계좌이체, 휴대전화 결제 등 회사가 제공하는 다양한 결제 방식을 통해 요금을 납부할 수 있습니다. ④ 유료 서비스의 이용 요금은 선불 결제를 원칙으로 하며, 이용 기간 중 서비스 중지 및 해지 시 남은 이용 기간에 대한 환불은 회사의 환불 정책에 따라 처리됩니다. ⑤ 회사는 회원의 유료 서비스 이용과 관련하여 발생한 문제에 대해 최선을 다해 해결하도록 노력합니다. 다만, 회사의 고의 또는 중대한 과실이 없는 한 회원이 유료 서비스 이용 중 입은 손해에 대해서는 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제21조(환불 정책)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원은 결제 후 7일 이내에 서비스 이용을 시작하지 않은 경우, 요금 전액을 환불받을 수 있습니다. ② 유료 서비스 이용 중 부득이한 사유로 서비스가 중지된 경우, 회사는 이용하지 않은 부분에 대해 환불 절차를 밟습니다. ③ 회원의 귀책사유로 인해 서비스 이용이 중지된 경우, 환불이 불가능합니다. ④ 환불은 회원이 지정한 계좌로 환불 절차를 거치며, 환불 요청 후 7일 이내에 처리됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제22조 (유료 서비스의 중지 및 해지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원이 유료 서비스를 해지하고자 하는 경우, 회사의 고객 지원 센터에 해지 신청을 해야 합니다. ② 회사는 회원이 약관을 위반하거나 부정한 방법으로 유료 서비스를 이용한 경우, 유료 서비스 이용을 즉시 중지하고 계약을 해지할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10장 양도 금지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제23조 (양도 금지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 서비스 이용권한, 기타 이용계약상의 지위를 제3자에게 양도, 증여할 수 없으며, 이를 담보로 제공할 수 없습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11장 관할 법원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제24조 (분쟁 해결)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n서비스 이용과 관련하여 분쟁이 발생한 경우, 회사와 회원은 성실히 협의하여 해결합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제25조 (관할 법원)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n부칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관은 2024년 10월 1일부터 시행됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n", + "non.existent.key": "Non-existent key", + "test.key": "Test", "ui.admin.api_keys.create.name_label": "Name Label", "ui.admin.api_keys.create.name_placeholder": "Name Placeholder", "ui.admin.api_keys.create.section_name": "Section Name", @@ -1494,8 +2087,8 @@ const Map enStrings = { "ui.admin.api_keys.create.submit": "Submit", "ui.admin.api_keys.create.success.copy_secret": "Copy Secret", "ui.admin.api_keys.create.success.go_list": "Go List", - "ui.admin.api_keys.create.success.title": "Title", - "ui.admin.api_keys.create.title": "Title", + "ui.admin.api_keys.create.success.title": "API Key Created", + "ui.admin.api_keys.create.title": "Create New API Key", "ui.admin.api_keys.list.add": "Add", "ui.admin.api_keys.list.breadcrumb.list": "List", "ui.admin.api_keys.list.breadcrumb.section": "API Keys", @@ -1505,7 +2098,7 @@ const Map enStrings = { "ui.admin.api_keys.list.table.last_used": "LAST USED", "ui.admin.api_keys.list.table.name": "NAME", "ui.admin.api_keys.list.table.scopes": "SCOPES", - "ui.admin.api_keys.list.title": "Title", + "ui.admin.api_keys.list.title": "API Key Management", "ui.admin.audit.breadcrumb.logs": "Logs", "ui.admin.audit.breadcrumb.section": "Audit", "ui.admin.audit.copy.actor_id": "Copy actor id", @@ -1536,12 +2129,12 @@ const Map enStrings = { "ui.admin.audit.table.status": "STATUS", "ui.admin.audit.table.time": "TIME", "ui.admin.audit.target": "Target · {{target}}", - "ui.admin.audit.title": "Title", + "ui.admin.audit.title": "Audit Logs", "ui.admin.brand": "Brand", "ui.admin.dev_role_switcher": "🛠 DEV Role Switcher", - "ui.admin.groups.add_unit": "Organization Add", - "ui.admin.groups.create.description": "Description", - "ui.admin.groups.create.title": "Title", + "ui.admin.dev_role_switcher_real": "Use real role", + "ui.admin.groups.create.description": "Adds a new organization unit such as a department or team.", + "ui.admin.groups.create.title": "Create Organization Unit", "ui.admin.groups.detail.breadcrumb_org": "Breadcrumb Org", "ui.admin.groups.detail.breadcrumb_tenant": "Tenant Details", "ui.admin.groups.detail.breadcrumb_unit": "Breadcrumb Unit", @@ -1549,12 +2142,11 @@ const Map enStrings = { "ui.admin.groups.detail.members_title": "Members Title", "ui.admin.groups.detail.permissions_subtitle": "Permissions Subtitle", "ui.admin.groups.detail.permissions_title": "Permission Manage", - "ui.admin.groups.form.desc_label": "Description", + "ui.admin.groups.form.desc_label": "Description Label", "ui.admin.groups.form.desc_placeholder": "Desc Placeholder", "ui.admin.groups.form.name_label": "Group Name", "ui.admin.groups.form.name_placeholder": "Name Placeholder", - "ui.admin.groups.form.parent_label": "Parent Label", - "ui.admin.groups.form.parent_none": "Parent None", + "ui.admin.groups.form.parent_label": "Parent Unit", "ui.admin.groups.form.submit": "Submit", "ui.admin.groups.form.unit_level_label": "Unit Level Label", "ui.admin.groups.form.unit_level_placeholder": "Unit Level Placeholder", @@ -1564,11 +2156,10 @@ const Map enStrings = { "ui.admin.groups.members.table.name": "Name", "ui.admin.groups.members.table.remove": "Remove", "ui.admin.groups.table.actions": "ACTIONS", - "ui.admin.groups.table.created_at": "Created At", - "ui.admin.groups.table.level": "Level", "ui.admin.groups.table.members": "MEMBERS", "ui.admin.groups.table.name": "NAME", "ui.admin.header.plane": "Admin Plane", + "ui.admin.header.subtitle": "Manage tenants, policies, and operators", "ui.admin.nav.api_keys": "API Keys", "ui.admin.nav.audit_logs": "Audit Logs", "ui.admin.nav.auth_guard": "Auth Guard", @@ -1579,112 +2170,167 @@ const Map enStrings = { "ui.admin.nav.tenants": "Tenants", "ui.admin.nav.user_groups": "User Groups", "ui.admin.nav.users": "Users", + "ui.admin.org.download_template": "Download Template", + "ui.admin.org.import_btn": "Import", + "ui.admin.org.import_title": "Bulk Organization Import", + "ui.admin.org.start_import": "Start Import", "ui.admin.overview.kicker": "Global Overview", "ui.admin.overview.playbook.title": "Admin playbook", "ui.admin.overview.quick_links.add_tenant": "Tenant Add", - "ui.admin.overview.quick_links.tenant_dashboard": "Tenant Dashboard", - "ui.admin.overview.quick_links.title": "Title", + "ui.admin.overview.quick_links.api_key_management": "API Key Management", + "ui.admin.overview.quick_links.title": "Quick Links", + "ui.admin.overview.quick_links.user_management": "User Management", "ui.admin.overview.quick_links.view_audit_logs": "View Audit Logs", + "ui.admin.overview.summary.audit_events_24h": "24h Events", + "ui.admin.overview.summary.oidc_clients": "OIDC Clients", + "ui.admin.overview.summary.policy_gate": "Policy Gate", + "ui.admin.overview.summary.total_tenants": "Total Tenants", "ui.admin.overview.title": "Tenant-independent control plane", + "ui.admin.profile.manageable_tenants": "Manageable Tenants", "ui.admin.role.rp_admin": "RP ADMIN", "ui.admin.role.super_admin": "SUPER ADMIN", "ui.admin.role.tenant_admin": "TENANT ADMIN", - "ui.admin.role.tenant_member": "TENANT MEMBER", - "ui.admin.tenants.add": "Tenant Add", + "ui.admin.role.user": "TENANT MEMBER", + "ui.admin.tenants.add": "Add Tenant", "ui.admin.tenants.admins.add_button": "Add Button", "ui.admin.tenants.admins.already_admin": "Already Admin", "ui.admin.tenants.admins.dialog_description": "Dialog Description", "ui.admin.tenants.admins.dialog_no_results": "Dialog No Results", "ui.admin.tenants.admins.dialog_search_hint": "Dialog Search Hint", - "ui.admin.tenants.admins.dialog_search_placeholder": - "Dialog Search Placeholder", + "ui.admin.tenants.admins.dialog_search_placeholder": "Dialog Search Placeholder", "ui.admin.tenants.admins.dialog_title": "Dialog Title", "ui.admin.tenants.admins.remove_title": "Remove Title", "ui.admin.tenants.admins.table_actions": "Table Actions", "ui.admin.tenants.admins.table_email": "Email", "ui.admin.tenants.admins.table_name": "Name", - "ui.admin.tenants.admins.title": "Title", + "ui.admin.tenants.admins.title": "Tenant Admins", "ui.admin.tenants.breadcrumb.list": "List", "ui.admin.tenants.breadcrumb.section": "Tenants", "ui.admin.tenants.create.breadcrumb.action": "Create", "ui.admin.tenants.create.breadcrumb.section": "Tenants", - "ui.admin.tenants.create.form.description": "Description", - "ui.admin.tenants.create.form.domains_label": - "Allowed Domains (Comma separated)", + "ui.admin.tenants.create.form.description": "Tenant Description", + "ui.admin.tenants.create.form.domains_label": "Allowed Domains (Comma separated)", "ui.admin.tenants.create.form.domains_placeholder": "example.com, example.kr", "ui.admin.tenants.create.form.name": "Tenant name", + "ui.admin.tenants.create.form.name_placeholder": "Enter tenant name", "ui.admin.tenants.create.form.parent": "Parent", "ui.admin.tenants.create.form.slug": "Slug", "ui.admin.tenants.create.form.slug_placeholder": "tenant-slug", "ui.admin.tenants.create.form.status": "Status", "ui.admin.tenants.create.form.type": "Type", - "ui.admin.tenants.create.memo.title": "Title", + "ui.admin.tenants.create.memo.title": "Policy Memo", "ui.admin.tenants.create.profile.title": "Tenant Profile", "ui.admin.tenants.create.title": "Tenant Add", "ui.admin.tenants.detail.breadcrumb_list": "Tenant List", "ui.admin.tenants.detail.header_subtitle": "Header Subtitle", - "ui.admin.tenants.detail.loading": "Loading", - "ui.admin.tenants.detail.tab_admins": "Tab Admins", + "ui.admin.tenants.detail.loading": "Loading tenant details...", "ui.admin.tenants.detail.tab_federation": "Tab Federation", "ui.admin.tenants.detail.tab_organization": "Organization Manage", + "ui.admin.tenants.detail.tab_permissions": "Permissions", "ui.admin.tenants.detail.tab_profile": "Profile", "ui.admin.tenants.detail.tab_schema": "Tab Schema", "ui.admin.tenants.detail.title": "Details", "ui.admin.tenants.list.select_placeholder": "Select Placeholder", + "ui.admin.tenants.members.descendants": "Descendant Members", + "ui.admin.tenants.members.direct": "Direct Members", + "ui.admin.tenants.members.direct_label": "Direct", + "ui.admin.tenants.members.list_title": "Member Management", "ui.admin.tenants.members.table.email": "EMAIL", "ui.admin.tenants.members.table.name": "NAME", "ui.admin.tenants.members.table.role": "ROLE", "ui.admin.tenants.members.table.status": "STATUS", "ui.admin.tenants.members.title": "Tenant Members ({{count}})", + "ui.admin.tenants.members.total": "Total", + "ui.admin.tenants.members.total_label": "Total", + "ui.admin.tenants.owners.add_button": "Add Owner", + "ui.admin.tenants.owners.already_owner": "Already Owner", + "ui.admin.tenants.owners.dialog_description": "Search users by name or email.", + "ui.admin.tenants.owners.dialog_title": "Add New Owner", + "ui.admin.tenants.owners.remove_title": "Revoke Owner Permission", + "ui.admin.tenants.owners.table_actions": "Actions", + "ui.admin.tenants.owners.table_email": "Email", + "ui.admin.tenants.owners.table_name": "Name", + "ui.admin.tenants.owners.title": "Tenant Owners", "ui.admin.tenants.profile.allowed_domains": "Allowed Domains", - "ui.admin.tenants.profile.allowed_domains_help": "Allowed Domains Help", - "ui.admin.tenants.profile.approve_button": "Tenant Approve", - "ui.admin.tenants.profile.description": "Description", + "ui.admin.tenants.profile.allowed_domains_help": "Users with these email domains will be automatically assigned to this tenant.", + "ui.admin.tenants.profile.approve_button": "Approve Tenant", + "ui.admin.tenants.profile.description": "Review and edit the tenant's basic profile information.", + "ui.admin.tenants.profile.form.parent": "Parent Tenant (Optional)", + "ui.admin.tenants.profile.form.parent_help": "Select a parent tenant if this is a subsidiary or sub-organization.", "ui.admin.tenants.profile.name": "Tenant Name", "ui.admin.tenants.profile.slug": "Slug", "ui.admin.tenants.profile.status": "Status", - "ui.admin.tenants.profile.subtitle": "Subtitle", + "ui.admin.tenants.profile.subtitle": "Slug and status changes are applied immediately.", "ui.admin.tenants.profile.title": "Tenant Profile", "ui.admin.tenants.profile.type": "Type", "ui.admin.tenants.registry.title": "Tenant registry", "ui.admin.tenants.schema.add_field": "Add Field", + "ui.admin.tenants.schema.field.admin_only": "Admin Only", + "ui.admin.tenants.schema.field.is_login_id": "Is Login Id", "ui.admin.tenants.schema.field.key": "Field Key (ID)", "ui.admin.tenants.schema.field.key_placeholder": "e.g. employee_id", "ui.admin.tenants.schema.field.label": "Display Label", "ui.admin.tenants.schema.field.label_placeholder": "Label Placeholder", + "ui.admin.tenants.schema.field.required": "Required", "ui.admin.tenants.schema.field.type": "Type", "ui.admin.tenants.schema.field.type_boolean": "Boolean", + "ui.admin.tenants.schema.field.type_date": "Date", + "ui.admin.tenants.schema.field.type_datetime": "DateTime", + "ui.admin.tenants.schema.field.type_float": "Float", "ui.admin.tenants.schema.field.type_number": "Number", - "ui.admin.tenants.schema.field.type_text": "Text", - "ui.admin.tenants.schema.save": "Save Schema Changes", + "ui.admin.tenants.schema.field.type_text": "Text Value", + "ui.admin.tenants.schema.field.unsigned": "Unsigned", + "ui.admin.tenants.schema.field.validation_placeholder": "Regex Pattern (Optional)", + "ui.admin.tenants.schema.save": "Save Schema", "ui.admin.tenants.schema.title": "User Schema Extension", "ui.admin.tenants.sub.add": "Add", + "ui.admin.tenants.sub.add_dialog_desc": "Select a tenant to add as a sub-tenant.", + "ui.admin.tenants.sub.add_dialog_title": "Add Sub-tenant", + "ui.admin.tenants.sub.add_existing": "Add Existing Tenant", "ui.admin.tenants.sub.manage": "Manage", + "ui.admin.tenants.sub.no_candidates": "No available tenants to add.", + "ui.admin.tenants.sub.search_placeholder": "Search...", "ui.admin.tenants.sub.table.action": "ACTION", "ui.admin.tenants.sub.table.name": "NAME", "ui.admin.tenants.sub.table.slug": "SLUG", "ui.admin.tenants.sub.table.status": "STATUS", "ui.admin.tenants.sub.title": "Sub-tenants ({{count}})", + "ui.admin.tenants.sub.tree_search_placeholder": "Search in tree...", "ui.admin.tenants.table.actions": "ACTIONS", + "ui.admin.tenants.table.members": "Members", "ui.admin.tenants.table.name": "NAME", "ui.admin.tenants.table.slug": "SLUG", "ui.admin.tenants.table.status": "STATUS", "ui.admin.tenants.table.type": "TYPE", "ui.admin.tenants.table.updated": "UPDATED", - "ui.admin.tenants.title": "Tenant List", + "ui.admin.tenants.title": "Tenant Registry", "ui.admin.title": "Admin Control", - "ui.admin.users.create.account.title": "Title", + "ui.admin.users.bulk.acknowledge_warning": "I acknowledge the warning and will proceed.", + "ui.admin.users.bulk.do_move": "Execute Move", + "ui.admin.users.bulk.download_template": "Download Template", + "ui.admin.users.bulk.move_group": "Bulk Tenant Move", + "ui.admin.users.bulk.move_title": "Bulk User Move", + "ui.admin.users.bulk.no_department": "No Department", + "ui.admin.users.bulk.schema_warning": "Schema Compatibility Warning", + "ui.admin.users.bulk.select_group": "Select Target Tenant", + "ui.admin.users.bulk.selected_count": "{{count}} users selected", + "ui.admin.users.bulk.start_upload": "Start Upload", + "ui.admin.users.bulk.title": "Bulk Actions", + "ui.admin.users.create.account.title": "Account Information", "ui.admin.users.create.back": "Back", "ui.admin.users.create.breadcrumb.new": "New", "ui.admin.users.create.breadcrumb.section": "Users", - "ui.admin.users.create.custom_fields.title": "Title", + "ui.admin.users.create.custom_fields.title": "Tenant Custom Fields", "ui.admin.users.create.form.auto_password": "Auto Password", "ui.admin.users.create.form.department": "Department", "ui.admin.users.create.form.department_placeholder": "Department Placeholder", "ui.admin.users.create.form.email": "Email", "ui.admin.users.create.form.email_placeholder": "user@example.com", + "ui.admin.users.create.form.is_login_id": "Sign in ID", "ui.admin.users.create.form.job_title": "Job Title", - "ui.admin.users.create.form.job_title_placeholder": "Job Title Placeholder", + "ui.admin.users.create.form.job_title_placeholder": "e.g. Frontend Developer", + "ui.admin.users.create.form.login_id": "Sign in ID (Optional)", + "ui.admin.users.create.form.login_id_placeholder": "Login Id Placeholder", "ui.admin.users.create.form.name": "Name", "ui.admin.users.create.form.name_placeholder": "Name Placeholder", "ui.admin.users.create.form.password": "Password", @@ -1692,60 +2338,282 @@ const Map enStrings = { "ui.admin.users.create.form.phone": "Phone number", "ui.admin.users.create.form.phone_placeholder": "010-1234-5678", "ui.admin.users.create.form.position": "Position", - "ui.admin.users.create.form.position_placeholder": "Position Placeholder", + "ui.admin.users.create.form.position_placeholder": "e.g. Senior", "ui.admin.users.create.form.role": "Role", - "ui.admin.users.create.form.tenant": "Tenant (Tenant)", + "ui.admin.users.create.form.tenant": "Tenant", "ui.admin.users.create.form.tenant_global": "Tenant Global", "ui.admin.users.create.go_list": "Go List", - "ui.admin.users.create.password_generated.title": "Title", + "ui.admin.users.create.password_generated.title": "Initial Password Generated", "ui.admin.users.create.submit": "User Create", "ui.admin.users.create.title": "User Add", "ui.admin.users.detail.back": "Back", "ui.admin.users.detail.breadcrumb.section": "Users", - "ui.admin.users.detail.custom_fields.title": "Title", + "ui.admin.users.detail.contact_title": "ui.admin.users.detail.contact_title", + "ui.admin.users.detail.created_at": "Created At", + "ui.admin.users.detail.custom_fields.multi_title": "Per-tenant Profile Management", + "ui.admin.users.detail.delete": "User Delete", "ui.admin.users.detail.edit_title": "Edit Title", + "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", "ui.admin.users.detail.form.department": "Department", "ui.admin.users.detail.form.department_placeholder": "Department Placeholder", + "ui.admin.users.detail.form.email": "Email", + "ui.admin.users.detail.form.is_login_id": "Sign in ID", "ui.admin.users.detail.form.job_title": "Job Title", - "ui.admin.users.detail.form.job_title_placeholder": "Job Title Placeholder", + "ui.admin.users.detail.form.job_title_placeholder": "ui.admin.users.detail.form.job_title_placeholder", + "ui.admin.users.detail.form.login_id": "Sign in ID", + "ui.admin.users.detail.form.login_id_placeholder": "Login Id Placeholder", + "ui.admin.users.detail.form.msg.admin.users.detail.history_desc": "ui.admin.users.detail.form.msg.admin.users.detail.history_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.no_history": "ui.admin.users.detail.form.msg.admin.users.detail.no_history", + "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants": "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants", + "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc": "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.security_desc": "ui.admin.users.detail.form.msg.admin.users.detail.security_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help": "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help", + "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc": "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc", + "ui.admin.users.detail.form.msg.common.copied": "ui.admin.users.detail.form.msg.common.copied", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", "ui.admin.users.detail.form.name": "Name", "ui.admin.users.detail.form.name_placeholder": "Name Placeholder", "ui.admin.users.detail.form.phone": "Phone number", "ui.admin.users.detail.form.phone_placeholder": "010-1234-5678", "ui.admin.users.detail.form.position": "Position", - "ui.admin.users.detail.form.position_placeholder": "Position Placeholder", + "ui.admin.users.detail.form.position_placeholder": "ui.admin.users.detail.form.position_placeholder", "ui.admin.users.detail.form.role": "Role", + "ui.admin.users.detail.form.role_rp_admin": "Role Rp Admin", + "ui.admin.users.detail.form.role_super_admin": "Role Super Admin", + "ui.admin.users.detail.form.role_tenant_admin": "Role Tenant Admin", + "ui.admin.users.detail.form.role_user": "User", "ui.admin.users.detail.form.status": "Status", - "ui.admin.users.detail.form.tenant": "Tenant (Tenant)", + "ui.admin.users.detail.form.status_active": "ui.admin.users.detail.form.status_active", + "ui.admin.users.detail.form.status_inactive": "ui.admin.users.detail.form.status_inactive", + "ui.admin.users.detail.form.tenant": "Representative Affiliated Tenant", "ui.admin.users.detail.form.tenant_global": "Tenant Global", + "ui.admin.users.detail.form.tenant_slug": "Tenant Slug", + "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id": "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id", + "ui.admin.users.detail.form.ui.admin.users.detail.form.email": "ui.admin.users.detail.form.ui.admin.users.detail.form.email", + "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id": "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id", + "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin": "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin", + "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug": "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug", + "ui.admin.users.detail.form.ui.admin.users.detail.generate_button": "ui.admin.users.detail.form.ui.admin.users.detail.generate_button", + "ui.admin.users.detail.form.ui.admin.users.detail.history_title": "ui.admin.users.detail.form.ui.admin.users.detail.history_title", + "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm": "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm", + "ui.admin.users.detail.form.ui.admin.users.detail.manual_password": "ui.admin.users.detail.form.ui.admin.users.detail.manual_password", + "ui.admin.users.detail.form.ui.admin.users.detail.password_done": "ui.admin.users.detail.form.ui.admin.users.detail.password_done", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto": "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute": "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual": "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual", + "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants": "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants", + "ui.admin.users.detail.form.ui.admin.users.detail.updated_at": "ui.admin.users.detail.form.ui.admin.users.detail.updated_at", + "ui.admin.users.detail.form.ui.common.generate": "ui.admin.users.detail.form.ui.common.generate", + "ui.admin.users.detail.form.ui.common.status.blocked": "ui.admin.users.detail.form.ui.common.status.blocked", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri", + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable": "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable", + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help": "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.admin.users.detail.form.ui.dev.clients.help.docs_body": "ui.admin.users.detail.form.ui.dev.clients.help.docs_body", + "ui.admin.users.detail.form.ui.dev.clients.help.subtitle": "ui.admin.users.detail.form.ui.dev.clients.help.subtitle", + "ui.admin.users.detail.form.ui.dev.clients.registry.description": "ui.admin.users.detail.form.ui.dev.clients.registry.description", + "ui.admin.users.detail.form.ui.dev.clients.scopes.email": "ui.admin.users.detail.form.ui.dev.clients.scopes.email", + "ui.admin.users.detail.form.ui.dev.clients.scopes.openid": "ui.admin.users.detail.form.ui.dev.clients.scopes.openid", + "ui.admin.users.detail.form.ui.dev.clients.scopes.profile": "ui.admin.users.detail.form.ui.dev.clients.scopes.profile", + "ui.admin.users.detail.form.ui.dev.session.refresh": "ui.admin.users.detail.form.ui.dev.session.refresh", + "ui.admin.users.detail.form.ui.dev.session.refreshing": "ui.admin.users.detail.form.ui.dev.session.refreshing", + "ui.admin.users.detail.generate_button": "Generate Button", + "ui.admin.users.detail.generate_password": "Auto Generate", + "ui.admin.users.detail.go_list": "Go List", + "ui.admin.users.detail.history_title": "History Title", + "ui.admin.users.detail.manual_confirm": "Password Confirm", + "ui.admin.users.detail.manual_password": "Manual Password", + "ui.admin.users.detail.password_done": "Password Done", + "ui.admin.users.detail.password_mode_generated": "Auto Generate", + "ui.admin.users.detail.password_mode_manual": "Manual Entry", + "ui.admin.users.detail.password_result_title": "Reset Password", + "ui.admin.users.detail.password_title": "Password Manage", + "ui.admin.users.detail.reset_auto": "Reset Auto", + "ui.admin.users.detail.reset_execute": "Reset Complete", + "ui.admin.users.detail.reset_manual": "Reset Manual", + "ui.admin.users.detail.reset_password": "Reset Password", + "ui.admin.users.detail.reset_password_apply": "Apply Password", + "ui.admin.users.detail.reset_password_label": "Reset Password Label", + "ui.admin.users.detail.save": "Save", + "ui.admin.users.detail.save_tenants": "Save Tenants", "ui.admin.users.detail.security.password": "Password", "ui.admin.users.detail.security.password_placeholder": "Password Placeholder", "ui.admin.users.detail.security.title": "Security Settings", + "ui.admin.users.detail.status_title": "ui.admin.users.detail.status_title", + "ui.admin.users.detail.tabs.info": "Info", + "ui.admin.users.detail.tabs.security": "Security", + "ui.admin.users.detail.tabs.tenants": "Tenant Profile", + "ui.admin.users.detail.tenants_section.additional": "Additional Affiliated/Manageable Tenants", + "ui.admin.users.detail.tenants_section.primary": "Representative Affiliated Tenant", + "ui.admin.users.detail.tenants_section.title": "Affiliation & Organization Info", "ui.admin.users.detail.title": "User Details", + "ui.admin.users.detail.toggle_password_visibility": "Toggle password visibility", + "ui.admin.users.detail.updated_at": "Updated At", "ui.admin.users.list.add": "User Add", "ui.admin.users.list.breadcrumb.list": "List", "ui.admin.users.list.breadcrumb.section": "Users", - "ui.admin.users.list.delete_aria": "User Delete: {{name}}", - "ui.admin.users.list.edit_aria": "User Edit: {{name}}", + "ui.admin.users.list.bulk_import": "Bulk Import", + "ui.admin.users.list.columns.title": "Column Settings", + "ui.admin.users.list.empty": "No users found.", + "ui.admin.users.list.fetch_error": "Failed to load the user list.", + "ui.admin.users.list.filter.tenant": "Tenant Filter", + "ui.admin.users.list.registry.count": "{{count}} users loaded.", "ui.admin.users.list.registry.title": "User Registry", "ui.admin.users.list.search_placeholder": "Search Placeholder", + "ui.admin.users.list.subtitle": "Browse and manage registered users.", "ui.admin.users.list.table.actions": "ACTIONS", "ui.admin.users.list.table.created": "CREATED", + "ui.admin.users.list.table.login_id": "LOGIN ID", + "ui.admin.users.list.table.msg.admin.users.detail.history_desc": "ui.admin.users.list.table.msg.admin.users.detail.history_desc", + "ui.admin.users.list.table.msg.admin.users.detail.no_history": "ui.admin.users.list.table.msg.admin.users.detail.no_history", + "ui.admin.users.list.table.msg.admin.users.detail.no_tenants": "ui.admin.users.list.table.msg.admin.users.detail.no_tenants", + "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc": "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc", + "ui.admin.users.list.table.msg.admin.users.detail.security_desc": "ui.admin.users.list.table.msg.admin.users.detail.security_desc", + "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help": "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help", + "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc": "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc", + "ui.admin.users.list.table.msg.common.copied": "ui.admin.users.list.table.msg.common.copied", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", "ui.admin.users.list.table.name_email": "NAME / EMAIL", - "ui.admin.users.list.table.position_job": "POSITION / JOB", "ui.admin.users.list.table.role": "ROLE", "ui.admin.users.list.table.status": "STATUS", "ui.admin.users.list.table.tenant_dept": "TENANT / DEPT", - "ui.admin.users.list.tenant_slug": "Slug: {{slug}}", + "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id": "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id", + "ui.admin.users.list.table.ui.admin.users.detail.form.email": "ui.admin.users.list.table.ui.admin.users.detail.form.email", + "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id": "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id", + "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin": "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin", + "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug": "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug", + "ui.admin.users.list.table.ui.admin.users.detail.generate_button": "ui.admin.users.list.table.ui.admin.users.detail.generate_button", + "ui.admin.users.list.table.ui.admin.users.detail.history_title": "ui.admin.users.list.table.ui.admin.users.detail.history_title", + "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm": "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm", + "ui.admin.users.list.table.ui.admin.users.detail.manual_password": "ui.admin.users.list.table.ui.admin.users.detail.manual_password", + "ui.admin.users.list.table.ui.admin.users.detail.password_done": "ui.admin.users.list.table.ui.admin.users.detail.password_done", + "ui.admin.users.list.table.ui.admin.users.detail.reset_auto": "ui.admin.users.list.table.ui.admin.users.detail.reset_auto", + "ui.admin.users.list.table.ui.admin.users.detail.reset_execute": "ui.admin.users.list.table.ui.admin.users.detail.reset_execute", + "ui.admin.users.list.table.ui.admin.users.detail.reset_manual": "ui.admin.users.list.table.ui.admin.users.detail.reset_manual", + "ui.admin.users.list.table.ui.admin.users.detail.save_tenants": "ui.admin.users.list.table.ui.admin.users.detail.save_tenants", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.info": "ui.admin.users.list.table.ui.admin.users.detail.tabs.info", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.security": "ui.admin.users.list.table.ui.admin.users.detail.tabs.security", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants": "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants", + "ui.admin.users.list.table.ui.admin.users.detail.updated_at": "ui.admin.users.list.table.ui.admin.users.detail.updated_at", + "ui.admin.users.list.table.ui.common.generate": "ui.admin.users.list.table.ui.common.generate", + "ui.admin.users.list.table.ui.common.status.blocked": "ui.admin.users.list.table.ui.common.status.blocked", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info": "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle": "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled": "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled": "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline": "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg": "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source": "ui.admin.users.list.table.ui.dev.clients.general.public_key.source", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri": "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri", + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable": "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable", + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help": "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.admin.users.list.table.ui.dev.clients.help.docs_body": "ui.admin.users.list.table.ui.dev.clients.help.docs_body", + "ui.admin.users.list.table.ui.dev.clients.help.subtitle": "ui.admin.users.list.table.ui.dev.clients.help.subtitle", + "ui.admin.users.list.table.ui.dev.clients.registry.description": "ui.admin.users.list.table.ui.dev.clients.registry.description", + "ui.admin.users.list.table.ui.dev.clients.scopes.email": "ui.admin.users.list.table.ui.dev.clients.scopes.email", + "ui.admin.users.list.table.ui.dev.clients.scopes.openid": "ui.admin.users.list.table.ui.dev.clients.scopes.openid", + "ui.admin.users.list.table.ui.dev.clients.scopes.profile": "ui.admin.users.list.table.ui.dev.clients.scopes.profile", + "ui.admin.users.list.table.ui.dev.session.refresh": "ui.admin.users.list.table.ui.dev.session.refresh", + "ui.admin.users.list.table.ui.dev.session.refreshing": "ui.admin.users.list.table.ui.dev.session.refreshing", "ui.admin.users.list.title": "User Manage", + "ui.admin.users.table.email": "Email", + "ui.admin.users.table.name": "Name", + "ui.admin.users.table.role": "Role", "ui.common.add": "Add", "ui.common.admin_only": "Admin Only", + "ui.common.all": "All", "ui.common.assign": "Assign", "ui.common.back": "Back", + "ui.common.back_to_login": "Back to login", "ui.common.badge.admin_only": "Admin only", "ui.common.badge.command_only": "Command only", "ui.common.badge.system": "System", "ui.common.cancel": "Cancel", + "ui.common.change_file": "Change File", + "ui.common.clear_search": "Clear Search", "ui.common.close": "Close", "ui.common.collapse": "Collapse", "ui.common.confirm": "Confirm", @@ -1754,10 +2622,16 @@ const Map enStrings = { "ui.common.delete": "Delete", "ui.common.details": "Details", "ui.common.edit": "Edit", + "ui.common.enabled": "Enabled", + "ui.common.export": "Export", + "ui.common.fail": "Fail", + "ui.common.generate": "ui.common.generate", + "ui.common.go_home": "Go Home", "ui.common.hyphen": "-", "ui.common.language": "Language", "ui.common.language_en": "English", - "ui.common.language_ko": "Language Ko", + "ui.common.language_ko": "Korean", + "ui.common.manage": "Manage", "ui.common.na": "N/A", "ui.common.never": "Never", "ui.common.next": "Next", @@ -1768,28 +2642,40 @@ const Map enStrings = { "ui.common.qr": "QR", "ui.common.read_only": "Read Only", "ui.common.refresh": "Refresh", - "ui.common.requesting": "Requesting", + "ui.common.remove": "Remove", "ui.common.resend": "Resend", + "ui.common.reset": "Reset", "ui.common.retry": "Retry", - "ui.common.role.admin": "Admin", - "ui.common.role.user": "User", "ui.common.save": "Save", "ui.common.search": "Search", - "ui.common.select": "User Optional", + "ui.common.select": "Select", + "ui.common.select_file": "Select File", "ui.common.select_placeholder": "Select Placeholder", "ui.common.show_more": "Show More", "ui.common.status.active": "Active", - "ui.common.status.blocked": "Blocked", + "ui.common.status.blocked": "ui.common.status.blocked", "ui.common.status.failure": "Failure", "ui.common.status.inactive": "Inactive", "ui.common.status.ok": "Ok", "ui.common.status.pending": "Pending", "ui.common.status.success": "Success", + "ui.common.success": "Success", "ui.common.theme_dark": "Dark", "ui.common.theme_light": "Light", "ui.common.theme_toggle": "Theme Toggle", "ui.common.unknown": "Unknown", "ui.common.view": "View", + "ui.dev.audit.filter.action": "Filter by Action (e.g. ROTATE_SECRET)", + "ui.dev.audit.filter.client_id": "Filter by Client ID", + "ui.dev.audit.filter.status_all": "All Status", + "ui.dev.audit.load_more": "Load more", + "ui.dev.audit.registry.title": "Audit registry", + "ui.dev.audit.table.action": "Action", + "ui.dev.audit.table.actor": "Actor", + "ui.dev.audit.table.status": "Status", + "ui.dev.audit.table.target": "Target", + "ui.dev.audit.table.time": "Time", + "ui.dev.audit.title": "Audit Logs", "ui.dev.brand": "Brand", "ui.dev.clients.badge.admin_session": "Admin Session", "ui.dev.clients.badge.tenant_selected": "Tenant Selected", @@ -1799,6 +2685,8 @@ const Map enStrings = { "ui.dev.clients.consents.export_csv": "Export CSV", "ui.dev.clients.consents.filters.advanced": "Advanced Filters", "ui.dev.clients.consents.revoke": "Revoke", + "ui.dev.clients.consents.revoked_at": "Revoked: ", + "ui.dev.clients.consents.scope_label": "Scope:", "ui.dev.clients.consents.search_placeholder": "Search Placeholder", "ui.dev.clients.consents.stats.active_grants": "Active Grants", "ui.dev.clients.consents.stats.avg_scopes": "Avg. Scopes per User", @@ -1815,62 +2703,107 @@ const Map enStrings = { "ui.dev.clients.consents.table.tenant": "Tenant", "ui.dev.clients.consents.table.user": "User", "ui.dev.clients.consents.title": "User Consent Grants", - "ui.dev.clients.copy_client_id": "Copy client id", - "ui.dev.clients.details.breadcrumb.current": "Current", - "ui.dev.clients.details.breadcrumb.section": "Applications", "ui.dev.clients.details.credentials.client_id": "Client ID", "ui.dev.clients.details.credentials.client_secret": "Client Secret", - "ui.dev.clients.details.credentials.title": "Title", + "ui.dev.clients.details.credentials.title": "Client Credentials", "ui.dev.clients.details.endpoints.read_only": "Read Only", - "ui.dev.clients.details.endpoints.title": "Title", + "ui.dev.clients.details.endpoints.title": "OIDC Endpoints", "ui.dev.clients.details.redirect.callback_label": "Callback Label", "ui.dev.clients.details.redirect.label": "Redirect URIs", - "ui.dev.clients.details.redirect.placeholder": - "https://your-app.com/callback, http://localhost:3000/auth/callback", + "ui.dev.clients.details.redirect.placeholder": "https://your-app.com/callback, http://localhost:3000/auth/callback", "ui.dev.clients.details.redirect.save": "Save", - "ui.dev.clients.details.redirect.title": "Title", + "ui.dev.clients.details.redirect.title": "Redirection Settings", "ui.dev.clients.details.secret.hide": "Hide", "ui.dev.clients.details.secret.rotate": "Rotate", "ui.dev.clients.details.secret.show": "Show", - "ui.dev.clients.details.security.title": "Title", + "ui.dev.clients.details.security.title": "Security Note", "ui.dev.clients.details.tab.connection": "Federation", "ui.dev.clients.details.tab.consents": "Consent & Users", "ui.dev.clients.details.tab.settings": "Settings", "ui.dev.clients.federation.add_btn": "Add Provider", "ui.dev.clients.federation.add_title": "Add Identity Provider", "ui.dev.clients.federation.title": "Identity Federation", - "ui.dev.clients.general.breadcrumb.section": "Applications", + "ui.dev.clients.filter.status_all": "All Statuses", + "ui.dev.clients.filter.type_all": "All Types", + "ui.dev.clients.filter.type_label": "Type:", "ui.dev.clients.general.create": "Create Application", "ui.dev.clients.general.display_new": "Add Connected Application", - "ui.dev.clients.general.footer.client_id": "Client ID", - "ui.dev.clients.general.footer.created_on": "Created On", - "ui.dev.clients.general.identity.description": "Description", - "ui.dev.clients.general.identity.description_placeholder": - "Description Placeholder", + "ui.dev.clients.general.identity.description": "Application Description", + "ui.dev.clients.general.identity.description_placeholder": "Description Placeholder", "ui.dev.clients.general.identity.logo": "App Logo URL", - "ui.dev.clients.general.identity.logo_placeholder": - "https://example.com/logo.png", + "ui.dev.clients.general.identity.logo_placeholder": "https://example.com/logo.png", "ui.dev.clients.general.identity.logo_preview": "Logo Preview", "ui.dev.clients.general.identity.name": "Name", "ui.dev.clients.general.identity.name_placeholder": "My Awesome Application", "ui.dev.clients.general.identity.title": "Application Identity", + "ui.dev.clients.general.public_key.allowed_algorithms_info": "Allowed Algorithms Info", + "ui.dev.clients.general.public_key.auth_method": "ui.dev.clients.general.public_key.auth_method", + "ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.dev.clients.general.public_key.auth_method_none": "ui.dev.clients.general.public_key.auth_method_none", + "ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.dev.clients.general.public_key.cache.cached_at": "Cached At", + "ui.dev.clients.general.public_key.cache.error": "Last Error", + "ui.dev.clients.general.public_key.cache.expires_at": "Expires At", + "ui.dev.clients.general.public_key.cache.failures": "Consecutive Failures", + "ui.dev.clients.general.public_key.cache.kids": "Cached KIDs", + "ui.dev.clients.general.public_key.cache.last_checked_at": "Last Checked", + "ui.dev.clients.general.public_key.cache.last_success": "Last Successful Verification", + "ui.dev.clients.general.public_key.cache.parsed_key_n": "N", + "ui.dev.clients.general.public_key.cache.parsed_keys": "Parsed Keys", + "ui.dev.clients.general.public_key.cache.status": "Status", + "ui.dev.clients.general.public_key.cache.title": "JWKS Cache", + "ui.dev.clients.general.public_key.cache.uri": "JWKS URI", + "ui.dev.clients.general.public_key.guide_toggle": "ui.dev.clients.general.public_key.guide_toggle", + "ui.dev.clients.general.public_key.headless_disabled": "ui.dev.clients.general.public_key.headless_disabled", + "ui.dev.clients.general.public_key.headless_enabled": "ui.dev.clients.general.public_key.headless_enabled", + "ui.dev.clients.general.public_key.headless_toggle": "Headless Login", + "ui.dev.clients.general.public_key.jwks_inline": "ui.dev.clients.general.public_key.jwks_inline", + "ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.dev.clients.general.public_key.jwks_uri": "JWKS URI", + "ui.dev.clients.general.public_key.jwks_uri_placeholder": "https://rp.example.com/.well-known/jwks.json", + "ui.dev.clients.general.public_key.request_object_alg": "ui.dev.clients.general.public_key.request_object_alg", + "ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.dev.clients.general.public_key.revoke_cache": "Revoke Cache", + "ui.dev.clients.general.public_key.source": "ui.dev.clients.general.public_key.source", + "ui.dev.clients.general.public_key.source_uri": "ui.dev.clients.general.public_key.source_uri", + "ui.dev.clients.general.public_key.title": "Public Key Registration", + "ui.dev.clients.general.public_key.validation_title": "Check before saving", "ui.dev.clients.general.redirect.label": "Redirect URIs", "ui.dev.clients.general.redirect.placeholder": "Placeholder", "ui.dev.clients.general.scopes.add": "Scope Add", - "ui.dev.clients.general.scopes.description_placeholder": - "Description Placeholder", + "ui.dev.clients.general.scopes.description_placeholder": "Description Placeholder", "ui.dev.clients.general.scopes.name_placeholder": "e.g. profile", + "ui.dev.clients.general.scopes.table.contact_title": "ui.dev.clients.general.scopes.table.contact_title", "ui.dev.clients.general.scopes.table.delete": "Delete", - "ui.dev.clients.general.scopes.table.description": "Description", + "ui.dev.clients.general.scopes.table.description": "Scope Description", + "ui.dev.clients.general.scopes.table.invalid_format": "ui.dev.clients.general.scopes.table.invalid_format", + "ui.dev.clients.general.scopes.table.login_id_help": "ui.dev.clients.general.scopes.table.login_id_help", "ui.dev.clients.general.scopes.table.mandatory": "Mandatory", "ui.dev.clients.general.scopes.table.name": "Scope Name", + "ui.dev.clients.general.scopes.table.password_title": "ui.dev.clients.general.scopes.table.password_title", + "ui.dev.clients.general.scopes.table.reset_password": "ui.dev.clients.general.scopes.table.reset_password", + "ui.dev.clients.general.scopes.table.reset_password_confirm": "ui.dev.clients.general.scopes.table.reset_password_confirm", + "ui.dev.clients.general.scopes.table.reset_password_label": "ui.dev.clients.general.scopes.table.reset_password_label", + "ui.dev.clients.general.scopes.table.role_super_admin": "ui.dev.clients.general.scopes.table.role_super_admin", + "ui.dev.clients.general.scopes.table.role_tenant_admin": "ui.dev.clients.general.scopes.table.role_tenant_admin", + "ui.dev.clients.general.scopes.table.role_user": "ui.dev.clients.general.scopes.table.role_user", + "ui.dev.clients.general.scopes.table.status_active": "ui.dev.clients.general.scopes.table.status_active", + "ui.dev.clients.general.scopes.table.status_inactive": "ui.dev.clients.general.scopes.table.status_inactive", + "ui.dev.clients.general.scopes.table.status_title": "ui.dev.clients.general.scopes.table.status_title", "ui.dev.clients.general.scopes.title": "Scopes", + "ui.dev.clients.general.security.headless_login_enable": "Headless Login (Custom Login UI)", + "ui.dev.clients.general.security.headless_login_enable_help": "Enable this if you want to implement your own login screen within the app instead of using the Baron SSO login page.", "ui.dev.clients.general.security.pkce": "PKCE", "ui.dev.clients.general.security.private": "Server Side App", "ui.dev.clients.general.security.title": "Security Settings", + "ui.dev.clients.general.security.trusted_rp_enable": "ui.dev.clients.general.security.trusted_rp_enable", + "ui.dev.clients.general.security.trusted_rp_enable_help": "ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.dev.clients.general.subtitle": "Manage application settings and security configuration.", "ui.dev.clients.general.title_create": "Create Client", "ui.dev.clients.general.title_edit": "Client Settings", + "ui.dev.clients.help.docs_body": "ui.dev.clients.help.docs_body", "ui.dev.clients.help.docs_title": "Docs & Examples", + "ui.dev.clients.help.subtitle": "ui.dev.clients.help.subtitle", "ui.dev.clients.help.title": "Need help with OIDC configuration?", "ui.dev.clients.help.view_guides": "View guides", "ui.dev.clients.list.title": "Connected Applications", @@ -1882,8 +2815,12 @@ const Map enStrings = { "ui.dev.clients.owner.scope": "Scope: TENANT-12", "ui.dev.clients.owner.subtitle": "Tenant admin on-call", "ui.dev.clients.owner.title": "Owner", + "ui.dev.clients.registry.description": "ui.dev.clients.registry.description", "ui.dev.clients.registry.subtitle": "Applications", "ui.dev.clients.registry.title": "RP registry", + "ui.dev.clients.scopes.email": "ui.dev.clients.scopes.email", + "ui.dev.clients.scopes.openid": "ui.dev.clients.scopes.openid", + "ui.dev.clients.scopes.profile": "ui.dev.clients.scopes.profile", "ui.dev.clients.search_placeholder": "Search by app name or ID...", "ui.dev.clients.table.actions": "Actions", "ui.dev.clients.table.application": "Application", @@ -1893,6 +2830,7 @@ const Map enStrings = { "ui.dev.clients.table.type": "Type", "ui.dev.clients.tenant_scoped": "Tenant-scoped", "ui.dev.clients.type.pkce": "PKCE", + "ui.dev.clients.type.pkce_headless": "Headless PKCE", "ui.dev.clients.type.private": "Server side App", "ui.dev.clients.untitled": "Untitled", "ui.dev.console_title": "Developer Console", @@ -1904,7 +2842,7 @@ const Map enStrings = { "ui.dev.dashboard.ops.card.consent_revoked": "Consent Revoked", "ui.dev.dashboard.ops.card.hydra_status": "Hydra Status", "ui.dev.dashboard.ops.card.rp_requests": "Rp Requests", - "ui.dev.dashboard.ops.subtitle": "Subtitle", + "ui.dev.dashboard.ops.subtitle": "Operational indicators for the current developer workspace.", "ui.dev.dashboard.ops.tag.consent": "Consent grants", "ui.dev.dashboard.ops.tag.rp_status": "RP status", "ui.dev.dashboard.ops.title": "Ops board", @@ -1917,18 +2855,41 @@ const Map enStrings = { "ui.dev.header.subtitle": "Manage your applications", "ui.dev.nav.clients": "Connected Application", "ui.dev.nav.logout": "Logout", + "ui.dev.profile.basic.email": "Email", + "ui.dev.profile.basic.id": "Account ID", + "ui.dev.profile.basic.name": "Name", + "ui.dev.profile.basic.phone": "Phone Number", + "ui.dev.profile.basic.title": "User Info", + "ui.dev.profile.error": "Failed to load profile.", + "ui.dev.profile.loading": "Loading profile...", "ui.dev.profile.menu_aria": "Open account menu", "ui.dev.profile.menu_title": "Account", + "ui.dev.profile.org.company_code": "Company Code", + "ui.dev.profile.org.tenant": "Tenant", + "ui.dev.profile.org.title": "Organization Info", + "ui.dev.profile.role.current": "Current Role", + "ui.dev.profile.role.description": "The permission level granted to this account.", + "ui.dev.profile.role.title": "System Role", + "ui.dev.profile.subtitle": "View user details and assigned roles.", + "ui.dev.profile.tab.basic": "Basic Info", + "ui.dev.profile.tab.role": "Roles & Permissions", + "ui.dev.profile.title": "My Profile", "ui.dev.profile.unknown_email": "unknown@example.com", "ui.dev.profile.unknown_name": "Unknown User", "ui.dev.scope_badge": "Scoped to /dev", "ui.dev.session.active": "Checking expiration...", + "ui.dev.session.auto_extend": "Session expiry controls", + "ui.dev.session.disabled": "Auto extend disabled", "ui.dev.session.expired": "Session expired", "ui.dev.session.expiring": "Expiring soon: {{minutes}}m {{seconds}}s left", - "ui.dev.session.refresh": "Refresh session expiry", - "ui.dev.session.refreshing": "Refreshing session expiry...", + "ui.dev.session.refresh": "ui.dev.session.refresh", + "ui.dev.session.refreshing": "ui.dev.session.refreshing", "ui.dev.session.remaining": "Expires in: {{minutes}}m {{seconds}}s", "ui.dev.session.unknown": "Unknown", + "ui.dev.tenant.single_notice": "You belong to a single tenant, so no switching is needed.", + "ui.dev.tenant.switch_success": "Tenant switch completed", + "ui.dev.tenant.workspace": "Workspace tenant (context)", + "ui.dev.tenant.workspace_desc": "Select and save the current working tenant to change API request context.", "ui.userfront.app_label.admin_console": "Admin Console", "ui.userfront.app_label.baron": "Baron", "ui.userfront.app_label.dev_console": "Dev Console", @@ -1944,15 +2905,27 @@ const Map enStrings = { "ui.userfront.audit.table.status": "Status", "ui.userfront.auth_method.ory": "Ory", "ui.userfront.auth_method.session": "Session", + "ui.userfront.consent.accept": "Agree and continue", + "ui.userfront.consent.cancel.confirm_button": "Yes, cancel", + "ui.userfront.consent.cancel.title": "Cancel consent", + "ui.userfront.consent.requested_scopes": "Requested permissions", + "ui.userfront.consent.title": "Permission request", "ui.userfront.dashboard.activity.linked": "Linked", "ui.userfront.dashboard.approved_session.default": "Default", - "ui.userfront.dashboard.approved_session.userfront": "Userfront", - "ui.userfront.dashboard.last_auth_label": "Last Auth Label", - "ui.userfront.dashboard.revoke.confirm_button": "Confirm Button", - "ui.userfront.dashboard.revoke.title": "Title", + "ui.userfront.dashboard.approved_session.userfront": "Approved UserFront session ID", + "ui.userfront.dashboard.last_auth_label": "Last sign-in", + "ui.userfront.dashboard.revoke.confirm_button": "Disconnect", + "ui.userfront.dashboard.revoke.title": "Disconnect app", "ui.userfront.dashboard.scopes.title": "Permission (Scopes)", + "ui.userfront.dashboard.sessions.active_badge": "Active", + "ui.userfront.dashboard.sessions.current_badge": "Current", + "ui.userfront.dashboard.sessions.current_disabled": "Current session", + "ui.userfront.dashboard.sessions.revoke.action": "End session", + "ui.userfront.dashboard.sessions.revoke.title": "End session", + "ui.userfront.dashboard.sessions.unknown_device": "Unknown device", + "ui.userfront.dashboard.sessions.unknown_session": "Session", "ui.userfront.dashboard.status.revoked": "Revoked", - "ui.userfront.dashboard.status_history": "Status History", + "ui.userfront.dashboard.status_history": "Activity history", "ui.userfront.device.android": "Mobile(Android)", "ui.userfront.device.ios": "Mobile(iOS)", "ui.userfront.device.linux": "Desktop(Linux)", @@ -1960,39 +2933,39 @@ const Map enStrings = { "ui.userfront.device.windows": "Desktop(Windows)", "ui.userfront.error.go_home": "Go Home", "ui.userfront.error.go_login": "Go Login", - "ui.userfront.forgot.heading": "Heading", - "ui.userfront.forgot.input_label": "Input Label", - "ui.userfront.forgot.submit": "Submit", - "ui.userfront.forgot.title": "Title", - "ui.userfront.login.action.submit": "Submit", - "ui.userfront.login.field.login_id": "Emain or Phone Number", + "ui.userfront.forgot.heading": "Forgot your password?", + "ui.userfront.forgot.input_label": "Email address or phone number", + "ui.userfront.forgot.submit": "Send reset link", + "ui.userfront.forgot.title": "Reset password", + "ui.userfront.login.action.submit": "Sign in", + "ui.userfront.login.field.login_id": "Email address or phone number", "ui.userfront.login.field.password": "Password", "ui.userfront.login.forgot_password": "Forgot Password", - "ui.userfront.login.link.action_label": "Action Label", - "ui.userfront.login.link.code_only": "Code Only", - "ui.userfront.login.link.page_title": "Page Title", - "ui.userfront.login.link.resend_with_time": "Resend With Time", + "ui.userfront.login.link.action_label": "Go to sign-in", + "ui.userfront.login.link.code_only": "Get code only ({{time}})", + "ui.userfront.login.link.page_title": "Link sign-in", + "ui.userfront.login.link.resend_with_time": "Resend ({{time}})", "ui.userfront.login.link.send": "Send", - "ui.userfront.login.link.title": "Title", + "ui.userfront.login.link.title": "Link sign-in complete", "ui.userfront.login.qr.expired": "Expired", "ui.userfront.login.qr.refresh": "Refresh", "ui.userfront.login.qr.remaining": "Remaining: {{time}}", - "ui.userfront.login.short_code.digits": "Digits", - "ui.userfront.login.short_code.expire_time": "Expire Time", - "ui.userfront.login.short_code.prefix": "Prefix", - "ui.userfront.login.short_code.submit": "Submit", - "ui.userfront.login.signup": "Signup", - "ui.userfront.login.tabs.link": "Link/Code", + "ui.userfront.login.short_code.digits": "6 digits", + "ui.userfront.login.short_code.expire_time": "Expires in {{time}}", + "ui.userfront.login.short_code.prefix": "2 letters", + "ui.userfront.login.short_code.submit": "Sign in with code", + "ui.userfront.login.signup": "Sign up", + "ui.userfront.login.tabs.link": "Sign-in link", "ui.userfront.login.tabs.password": "Password", "ui.userfront.login.tabs.qr": "QR Code", - "ui.userfront.login.unregistered.action": "Action", - "ui.userfront.login.unregistered.title": "Title", - "ui.userfront.login.verification.action_label": "Confirm", - "ui.userfront.login.verification.page_title": "Page Title", - "ui.userfront.login.verification.title": "Title", - "ui.userfront.login_success.later": "Later", - "ui.userfront.login_success.qr": "QR", - "ui.userfront.login_success.title": "Title", + "ui.userfront.login.unregistered.action": "Create an account", + "ui.userfront.login.unregistered.title": "Account not found", + "ui.userfront.login.verification.action_label": "Done", + "ui.userfront.login.verification.page_title": "Sign-in approval", + "ui.userfront.login.verification.title": "Approval complete", + "ui.userfront.login_success.later": "Do this later (go to dashboard)", + "ui.userfront.login_success.qr": "Use QR approval", + "ui.userfront.login_success.title": "Sign-in complete", "ui.userfront.nav.dashboard": "Dashboard", "ui.userfront.nav.logout": "Logout", "ui.userfront.nav.profile": "Profile", @@ -2004,56 +2977,58 @@ const Map enStrings = { "ui.userfront.profile.field.email": "Email", "ui.userfront.profile.field.name": "Name", "ui.userfront.profile.field.tenant": "Tenant", - "ui.userfront.profile.manage": "Manage", + "ui.userfront.profile.manage": "Manage profile", "ui.userfront.profile.password.change": "Change", "ui.userfront.profile.password.confirm": "Confirm", "ui.userfront.profile.password.current": "Current", "ui.userfront.profile.password.forgot": "Forgot", "ui.userfront.profile.password.new": "New", - "ui.userfront.profile.password.title": "Title", - "ui.userfront.profile.phone.code_hint": "Code Hint", - "ui.userfront.profile.phone.request_code": "Request Code", + "ui.userfront.profile.password.title": "Change password", + "ui.userfront.profile.phone.code_hint": "6-digit code", + "ui.userfront.profile.phone.request_code": "Send code", "ui.userfront.profile.phone.title": "Phone number", "ui.userfront.profile.section.basic": "Basic", "ui.userfront.profile.section.organization": "Organization", "ui.userfront.profile.section.security": "Security", "ui.userfront.profile.user_fallback": "User", - "ui.userfront.qr.request_permission": "Request Permission", + "ui.userfront.qr.request_permission": "Allow camera access", "ui.userfront.qr.rescan": "Rescan", - "ui.userfront.qr.result_failure": "Result Failure", - "ui.userfront.qr.result_success": "Result Success", + "ui.userfront.qr.result_failure": "Approval failed", + "ui.userfront.qr.result_success": "Approval complete", "ui.userfront.qr.title": "Scan QR Code", "ui.userfront.reset.confirm_password": "Confirm Password", "ui.userfront.reset.new_password": "New Password", "ui.userfront.reset.submit": "Submit", - "ui.userfront.reset.subtitle": "Subtitle", - "ui.userfront.reset.title": "Title", + "ui.userfront.reset.subtitle": "Set a new password", + "ui.userfront.reset.title": "Create a new password", "ui.userfront.sections.apps": "Apps", "ui.userfront.sections.audit": "Audit", - "ui.userfront.session.active": "Active", + "ui.userfront.sections.sessions": "Sessions", + "ui.userfront.session.active": "Active session", "ui.userfront.session.unknown": "Unknown", - "ui.userfront.signup.agreement.all": "All", - "ui.userfront.signup.agreement.privacy_title": "Privacy Title", - "ui.userfront.signup.agreement.tos_title": "Tos Title", - "ui.userfront.signup.auth.code_label": "Code Label", - "ui.userfront.signup.auth.email.label": "Label", - "ui.userfront.signup.auth.email.title": "Title", - "ui.userfront.signup.auth.request_code": "Request Code", - "ui.userfront.signup.complete": "Complete", - "ui.userfront.signup.next_step": "Next Step", + "ui.userfront.signup.agreement.all": "Agree to all", + "ui.userfront.signup.agreement.privacy_title": "Privacy Policy (Required)", + "ui.userfront.signup.agreement.required": "Required", + "ui.userfront.signup.agreement.tos_title": "Terms of Service (Required)", + "ui.userfront.signup.auth.code_label": "6-digit verification code", + "ui.userfront.signup.auth.email.label": "Email address", + "ui.userfront.signup.auth.email.title": "Email verification", + "ui.userfront.signup.auth.request_code": "Send code", + "ui.userfront.signup.complete": "Finish sign-up", + "ui.userfront.signup.next_step": "Next", "ui.userfront.signup.password.confirm_label": "Password Confirm", "ui.userfront.signup.password.label": "Password", - "ui.userfront.signup.phone.label": "Label", - "ui.userfront.signup.phone.title": "Title", + "ui.userfront.signup.phone.label": "Phone number (no hyphens)", + "ui.userfront.signup.phone.title": "Phone verification", "ui.userfront.signup.profile.affiliation_type": "Affiliation Type", "ui.userfront.signup.profile.company": "Company", "ui.userfront.signup.profile.department": "Department", - "ui.userfront.signup.profile.department_optional": "Department Optional", + "ui.userfront.signup.profile.department_optional": "Department (optional)", "ui.userfront.signup.profile.name": "Name", - "ui.userfront.signup.steps.agreement": "Agreement", + "ui.userfront.signup.steps.agreement": "Terms", "ui.userfront.signup.steps.password": "Password", "ui.userfront.signup.steps.profile": "Profile", - "ui.userfront.signup.steps.verify": "Verify", - "ui.userfront.signup.success.action": "Action", - "ui.userfront.signup.title": "Title", + "ui.userfront.signup.steps.verify": "Verification", + "ui.userfront.signup.success.action": "Go to sign-in", + "ui.userfront.signup.title": "Sign up", }; From 886e99bfa9e01f933c6fd3e753658d89451cf7d6 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 17:21:41 +0900 Subject: [PATCH 13/39] =?UTF-8?q?dev=20=EB=B3=91=ED=95=A9=20=ED=9B=84=20co?= =?UTF-8?q?de-check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/i18n-scanner/gen-flutter-i18n.js | 10 + .../auth/presentation/consent_screen.dart | 8 +- userfront/lib/i18n_data.dart | 2403 +++++++++++------ 3 files changed, 1616 insertions(+), 805 deletions(-) diff --git a/tools/i18n-scanner/gen-flutter-i18n.js b/tools/i18n-scanner/gen-flutter-i18n.js index 8239911f..a91ed502 100755 --- a/tools/i18n-scanner/gen-flutter-i18n.js +++ b/tools/i18n-scanner/gen-flutter-i18n.js @@ -3,6 +3,7 @@ const fs = require("fs"); const path = require("path"); +const { spawnSync } = require("child_process"); const ROOT = process.cwd(); const LOCALES_DIR = path.join(ROOT, "locales"); @@ -84,3 +85,12 @@ const output = [ ].join("\n"); fs.writeFileSync(OUT_PATH, output, "utf8"); + +const formatResult = spawnSync("dart", ["format", OUT_PATH], { + cwd: ROOT, + stdio: "inherit", +}); + +if (formatResult.status !== 0) { + process.exit(formatResult.status ?? 1); +} diff --git a/userfront/lib/features/auth/presentation/consent_screen.dart b/userfront/lib/features/auth/presentation/consent_screen.dart index dcf54939..46a0ed82 100644 --- a/userfront/lib/features/auth/presentation/consent_screen.dart +++ b/userfront/lib/features/auth/presentation/consent_screen.dart @@ -58,10 +58,10 @@ class _ConsentScreenState extends State { } String _renderConsentText(String key, {String? fallback}) { - return tr(key, fallback: fallback) - .replaceAll(r'\\n', '\n') - .replaceAll(r'\n', '\n') - .replaceAll('\\\n', '\n'); + return tr( + key, + fallback: fallback, + ).replaceAll(r'\\n', '\n').replaceAll(r'\n', '\n').replaceAll('\\\n', '\n'); } String _renderScopeCountLabel(int count) { diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index c5639e50..08155de5 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -49,22 +49,26 @@ const Map koStrings = { "msg.admin.api_keys.create.scopes_count": "총 {{count}}개의 권한이 할당됩니다.", "msg.admin.api_keys.create.scopes_hint": "생성 즉시 활성화되어 사용 가능합니다.", "msg.admin.api_keys.create.subtitle": "내부 시스템 연동을 위한 보안 인증 키를 구성합니다.", - "msg.admin.api_keys.create.success.copy_hint": "복사 버튼을 눌러 안전한 곳(비밀번호 관리자 등)에 저장하세요.", + "msg.admin.api_keys.create.success.copy_hint": + "복사 버튼을 눌러 안전한 곳(비밀번호 관리자 등)에 저장하세요.", "msg.admin.api_keys.create.success.notice": "아래의 비밀번호(Secret)는 보안을 위해 ", "msg.admin.api_keys.create.success.notice_emphasis": "지금 한 번만", "msg.admin.api_keys.create.success.notice_suffix": "표시됩니다.", - "msg.admin.api_keys.list.delete_confirm": "API 키 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?", + "msg.admin.api_keys.list.delete_confirm": + "API 키 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?", "msg.admin.api_keys.list.empty": "등록된 API 키가 없습니다.", "msg.admin.api_keys.list.fetch_error": "API 키 목록 조회에 실패했습니다.", "msg.admin.api_keys.list.registry.count": "총 {{count}}개 API 키", - "msg.admin.api_keys.list.subtitle": "서버 간 통신(Machine-to-Machine)을 위한 API 키를 발급하고 관리합니다.", + "msg.admin.api_keys.list.subtitle": + "서버 간 통신(Machine-to-Machine)을 위한 API 키를 발급하고 관리합니다.", "msg.admin.audit.empty": "아직 수집된 감사 로그가 없습니다.", "msg.admin.audit.end": "감사 로그의 마지막입니다.", "msg.admin.audit.filters.empty": "필터 없음", "msg.admin.audit.load_error": "감사 로그를 불러오지 못했습니다: {{error}}", "msg.admin.audit.loading": "감사 로그를 불러오는 중...", "msg.admin.audit.registry.count": "로드된 로그 {{count}}건", - "msg.admin.audit.subtitle": "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", + "msg.admin.audit.subtitle": + "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", "msg.admin.common.forbidden": "이 작업을 수행할 권한이 없습니다.", "msg.admin.groups.create.description": "부서나 팀과 같은 새로운 조직 단위를 추가합니다.", "msg.admin.groups.create.title": "새 조직 단위 생성", @@ -93,7 +97,8 @@ const Map koStrings = { "msg.admin.header.subtitle": "Tenant isolation & least privilege by default", "msg.admin.idp_env_prod": "IDP env: prod", "msg.admin.logout_confirm": "로그아웃 하시겠습니까?", - "msg.admin.notice.idp_policy": "IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다.", + "msg.admin.notice.idp_policy": + "IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다.", "msg.admin.notice.scope": "관리 기능은 /admin 네임스페이스에서만 노출합니다.", "msg.admin.org.hover_member_info": "마우스를 올리면 상세 정보를 확인할 수 있습니다.", "msg.admin.org.import_description": "CSV 파일을 업로드하여 조직도를 일괄 등록합니다.", @@ -102,10 +107,13 @@ const Map koStrings = { "msg.admin.overview.description": "모든 테넌트 공통 지표와 정책 상태를 한 곳에서 확인합니다.", "msg.admin.overview.idp_fallback": "Fallback: Descope", "msg.admin.overview.idp_primary": "IDP: Ory primary", - "msg.admin.overview.playbook.description": "운영 정책, 레이트리밋, 감사 로그의 기본 룰을 요약합니다.", - "msg.admin.overview.playbook.idp_body": "모든 IDP 호출은 backend를 통해서만 수행하며, Hydra/Kratos admin 포트는 외부에 노출하지 않습니다.", + "msg.admin.overview.playbook.description": + "운영 정책, 레이트리밋, 감사 로그의 기본 룰을 요약합니다.", + "msg.admin.overview.playbook.idp_body": + "모든 IDP 호출은 backend를 통해서만 수행하며, Hydra/Kratos admin 포트는 외부에 노출하지 않습니다.", "msg.admin.overview.playbook.idp_title": "Backend-only IDP access", - "msg.admin.overview.playbook.tenant_body": "Tenant 헤더와 감사 로그 규칙을 기본 적용하며, 향후 Keto 정책으로 확장 예정입니다.", + "msg.admin.overview.playbook.tenant_body": + "Tenant 헤더와 감사 로그 규칙을 기본 적용하며, 향후 Keto 정책으로 확장 예정입니다.", "msg.admin.overview.playbook.tenant_title": "Tenant isolation", "msg.admin.overview.quick_links.description": "주요 운영 화면으로 바로 이동합니다.", "msg.admin.overview.summary.audit_events_24h": "최근 24시간 감사 로그", @@ -124,10 +132,14 @@ const Map koStrings = { "msg.admin.tenants.admins.subtitle": "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.", "msg.admin.tenants.approve_confirm": "이 테넌트를 승인하시겠습니까?", "msg.admin.tenants.approve_success": "테넌트가 승인되었습니다.", - "msg.admin.tenants.create.form.domains_help": "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.", - "msg.admin.tenants.create.memo.body": "생성 직후에는 기본 활성 상태로 부여되며, 필요 시 상태를 수정하세요.", - "msg.admin.tenants.create.memo.subtitle": "Tenant 권한 정책은 추후 Keto 연계로 확장 예정입니다.", - "msg.admin.tenants.create.profile.subtitle": "필수 정보만 입력해도 생성 가능합니다. Slug는 없으면 자동 생성됩니다.", + "msg.admin.tenants.create.form.domains_help": + "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.", + "msg.admin.tenants.create.memo.body": + "생성 직후에는 기본 활성 상태로 부여되며, 필요 시 상태를 수정하세요.", + "msg.admin.tenants.create.memo.subtitle": + "Tenant 권한 정책은 추후 Keto 연계로 확장 예정입니다.", + "msg.admin.tenants.create.profile.subtitle": + "필수 정보만 입력해도 생성 가능합니다. Slug는 없으면 자동 생성됩니다.", "msg.admin.tenants.create.subtitle": "글로벌 운영 기준의 신규 테넌트를 등록합니다.", "msg.admin.tenants.delete_confirm": "테넌트 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?", "msg.admin.tenants.delete_success": "테넌트가 삭제되었습니다.", @@ -146,7 +158,8 @@ const Map koStrings = { "msg.admin.tenants.owners.remove_success": "소유자 권한이 회수되었습니다.", "msg.admin.tenants.owners.subtitle": "이 테넌트의 최상위 권한을 가진 소유자(조직장) 목록입니다.", "msg.admin.tenants.registry.count": "총 {{count}}개 테넌트", - "msg.admin.tenants.remove_sub_confirm": "테넌트 \\\\\\\"{{name}}\\\\\\\"을(를) 하위 조직에서 제외할까요?", + "msg.admin.tenants.remove_sub_confirm": + "테넌트 \\\\\\\"{{name}}\\\\\\\"을(를) 하위 조직에서 제외할까요?", "msg.admin.tenants.schema.empty": "등록된 커스텀 필드가 없습니다. 필드 추가를 눌러 시작하세요.", "msg.admin.tenants.schema.forbidden_desc": "사용자 스키마 설정은 관리자만 접근할 수 있습니다.", "msg.admin.tenants.schema.missing_id": "테넌트 ID가 없습니다.", @@ -171,13 +184,16 @@ const Map koStrings = { "msg.admin.users.create.form.email_required": "이메일은 필수입니다.", "msg.admin.users.create.form.field_invalid": "{{label}} 형식이 올바르지 않습니다.", "msg.admin.users.create.form.field_required": "{{label}}은(는) 필수입니다.", - "msg.admin.users.create.form.login_id_help": "msg.admin.users.create.form.login_id_help", + "msg.admin.users.create.form.login_id_help": + "msg.admin.users.create.form.login_id_help", "msg.admin.users.create.form.name_required": "이름은 필수입니다.", - "msg.admin.users.create.form.password_auto_help": "비워두면 시스템이 초기 비밀번호를 자동 생성합니다.", + "msg.admin.users.create.form.password_auto_help": + "비워두면 시스템이 초기 비밀번호를 자동 생성합니다.", "msg.admin.users.create.form.password_manual_help": "초기 비밀번호를 직접 설정합니다.", "msg.admin.users.create.form.role_help": "시스템 접근 권한을 결정합니다.", "msg.admin.users.create.password_generated.default": "초기 비밀번호가 생성되었습니다.", - "msg.admin.users.create.password_generated.with_email": "{{email}} 계정의 초기 비밀번호입니다.", + "msg.admin.users.create.password_generated.with_email": + "{{email}} 계정의 초기 비밀번호입니다.", "msg.admin.users.create.password_required": "비밀번호를 입력하거나 자동 생성을 사용해 주세요.", "msg.admin.users.create.success": "사용자가 성공적으로 생성되었습니다.", "msg.admin.users.detail.delete_confirm": "삭제하시겠습니까?", @@ -192,21 +208,28 @@ const Map koStrings = { "msg.admin.users.detail.no_tenants": "소속된 테넌트 정보가 없습니다.", "msg.admin.users.detail.not_found": "사용자를 찾을 수 없습니다.", "msg.admin.users.detail.password_generated": "안전한 비밀번호가 생성되었습니다.", - "msg.admin.users.detail.password_generated_help": "보안 기준에 맞는 임시 비밀번호를 자동 생성해 즉시 적용합니다.", + "msg.admin.users.detail.password_generated_help": + "보안 기준에 맞는 임시 비밀번호를 자동 생성해 즉시 적용합니다.", "msg.admin.users.detail.password_manual_required": "비밀번호를 입력해 주세요.", - "msg.admin.users.detail.reset_auto_desc": "해킹이 어려운 복잡한 임시 비밀번호를 시스템이 즉시 생성합니다.", - "msg.admin.users.detail.reset_password_confirm": "msg.admin.users.detail.reset_password_confirm", - "msg.admin.users.detail.reset_password_help": "사용자의 비밀번호를 강제로 재설정하고 자동 생성하거나 직접 입력한 비밀번호를 적용합니다.", - "msg.admin.users.detail.security.password_hint": "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다.", + "msg.admin.users.detail.reset_auto_desc": + "해킹이 어려운 복잡한 임시 비밀번호를 시스템이 즉시 생성합니다.", + "msg.admin.users.detail.reset_password_confirm": + "msg.admin.users.detail.reset_password_confirm", + "msg.admin.users.detail.reset_password_help": + "사용자의 비밀번호를 강제로 재설정하고 자동 생성하거나 직접 입력한 비밀번호를 적용합니다.", + "msg.admin.users.detail.security.password_hint": + "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다.", "msg.admin.users.detail.security_desc": "비밀번호 초기화 및 보안 설정을 관리합니다.", - "msg.admin.users.detail.self_password_reset_blocked": "본인 계정의 비밀번호는 사용자 포털(UserFront) 설정에서 변경해 주세요.", + "msg.admin.users.detail.self_password_reset_blocked": + "본인 계정의 비밀번호는 사용자 포털(UserFront) 설정에서 변경해 주세요.", "msg.admin.users.detail.tenant_slug_help": "사용자의 주된 정체성을 결정하는 대표 조직을 지정합니다.", "msg.admin.users.detail.tenants_desc": "각 테넌트별로 정의된 커스텀 스키마 정보를 관리합니다.", "msg.admin.users.detail.update_error": "사용자 수정에 실패했습니다.", "msg.admin.users.detail.update_success": "사용자 정보가 수정되었습니다.", "msg.admin.users.list.columns.description": "테이블에 표시할 컬럼을 선택합니다.", "msg.admin.users.list.columns.no_custom": "이 테넌트에 정의된 커스텀 필드가 없습니다.", - "msg.admin.users.list.delete_confirm": "사용자 \\\\\\\"{{name}}\\\\\\\"을(를) 정말 삭제하시겠습니까?", + "msg.admin.users.list.delete_confirm": + "사용자 \\\\\\\"{{name}}\\\\\\\"을(를) 정말 삭제하시겠습니까?", "msg.admin.users.list.empty": "검색 결과가 없습니다.", "msg.admin.users.list.fetch_error": "사용자 목록 조회에 실패했습니다.", "msg.admin.users.list.registry.count": "총 {{count}}명의 사용자가 등록되어 있습니다.", @@ -227,13 +250,16 @@ const Map koStrings = { "msg.dev.audit.loaded_count": "로드된 로그 {{count}}건", "msg.dev.audit.loading": "감사 로그를 불러오는 중...", "msg.dev.audit.subtitle": "현재 테넌트/앱 범위의 DevFront 작업 이력을 조회합니다.", - "msg.dev.auth.access_denied_description": "DevFront는 관리자 전용 화면입니다. 권한이 필요하면 관리자에게 요청해 주세요.", + "msg.dev.auth.access_denied_description": + "DevFront는 관리자 전용 화면입니다. 권한이 필요하면 관리자에게 요청해 주세요.", "msg.dev.auth.access_denied_title": "접근 권한이 없습니다.", "msg.dev.clients.consents.empty": "등록된 동의 내역이 없습니다.", "msg.dev.clients.consents.load_error": "동의 내역을 불러오지 못했습니다: {{error}}", "msg.dev.clients.consents.loading": "동의 내역을 불러오는 중...", - "msg.dev.clients.consents.revoke_confirm": "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다.", - "msg.dev.clients.consents.showing": "전체 {{total}}명 중 {{from}}번째부터 {{to}}번째 사용자를 표시합니다.", + "msg.dev.clients.consents.revoke_confirm": + "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다.", + "msg.dev.clients.consents.showing": + "전체 {{total}}명 중 {{from}}번째부터 {{to}}번째 사용자를 표시합니다.", "msg.dev.clients.consents.subtitle": "OIDC Relying Party 사용자 권한을 검토·관리합니다.", "msg.dev.clients.delete_confirm": "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "msg.dev.clients.delete_error": "삭제 실패: {{error}}", @@ -244,15 +270,19 @@ const Map koStrings = { "msg.dev.clients.details.load_error": "앱 상세 정보를 불러오지 못했습니다: {{error}}", "msg.dev.clients.details.loading": "앱 상세 정보를 불러오는 중...", "msg.dev.clients.details.missing_id": "Client ID가 필요합니다.", - "msg.dev.clients.details.redirect.description": "인증 성공 후 사용자를 리다이렉트할 허용된 URL 목록입니다. 콤마(,)로 구분하여 여러 개 입력할 수 있습니다.", + "msg.dev.clients.details.redirect.description": + "인증 성공 후 사용자를 리다이렉트할 허용된 URL 목록입니다. 콤마(,)로 구분하여 여러 개 입력할 수 있습니다.", "msg.dev.clients.details.redirect_saved": "Redirect URIs가 저장되었습니다.", - "msg.dev.clients.details.rotate_confirm": "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\\\\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?", + "msg.dev.clients.details.rotate_confirm": + "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\\\\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?", "msg.dev.clients.details.rotate_error": "재발급 실패: {{error}}", "msg.dev.clients.details.save_error": "저장 실패: {{error}}", "msg.dev.clients.details.secret_rotated": "Client Secret이 재발급되었습니다.", "msg.dev.clients.details.secret_unavailable": "SECRET_NOT_AVAILABLE", - "msg.dev.clients.details.security.footer": "비밀키 재발행 작업에는 관리자 세션 TTL 확인과 레이트리밋, 알림 연동을 권장합니다.", - "msg.dev.clients.details.security.note": "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행/복사는 감사 로그와 연계하세요.", + "msg.dev.clients.details.security.footer": + "비밀키 재발행 작업에는 관리자 세션 TTL 확인과 레이트리밋, 알림 연동을 권장합니다.", + "msg.dev.clients.details.security.note": + "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행/복사는 감사 로그와 연계하세요.", "msg.dev.clients.details.subtitle": "OIDC 자격 증명과 엔드포인트를 관리합니다.", "msg.dev.clients.federation.add_subtitle": "외부 OIDC 제공자를 연결합니다.", "msg.dev.clients.federation.empty": "등록된 IdP 설정이 없습니다.", @@ -261,66 +291,113 @@ const Map koStrings = { "msg.dev.clients.general.identity.subtitle": "앱 이름과 설명, 로고를 설정합니다.", "msg.dev.clients.general.load_error": "앱 설정을 불러오지 못했습니다: {{error}}", "msg.dev.clients.general.loading": "앱 설정을 불러오는 중...", - "msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "허용 알고리즘: {{algorithms}}", - "msg.dev.clients.general.public_key.auth_method_client_secret_basic_help": "일반적인 서버 사이드 앱 인증 방식입니다.", - "msg.dev.clients.general.public_key.auth_method_none_help": "PKCE 기반 public client에 사용하는 방식입니다.", - "msg.dev.clients.general.public_key.auth_method_private_key_jwt_help": "Trusted RP bootstrap과 JAR 검증에 필요한 서명 키 기반 인증 방식입니다.", - "msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "알고리즘 미선언", - "msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "이 키는 `alg`가 비어 있어서 저장할 수 없습니다.", - "msg.dev.clients.general.public_key.cache.missing_algorithms_help": "저장 전 JWKS 각 키에 `alg`를 명시해 주세요: {{details}}", - "msg.dev.clients.general.public_key.cache.missing_algorithms_title": "알고리즘이 선언되지 않았습니다.", - "msg.dev.clients.general.public_key.cache.parsed_keys_empty": "No parsed JWKS keys are available yet.", - "msg.dev.clients.general.public_key.cache.parsed_keys_help": "Raw JWKS stays hidden. Only parsed key metadata is shown here.", - "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "이 알고리즘은 Headless Login에서 지원되지 않습니다.", - "msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "저장 전 JWKS를 수정해 주세요: {{details}}", - "msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "지원하지 않는 알고리즘이 감지되었습니다.", - "msg.dev.clients.general.public_key.cache_empty": "아직 캐시된 JWKS가 없습니다. Refresh를 눌러 백엔드 캐시 상태를 조회하세요.", - "msg.dev.clients.general.public_key.cache_help": "백엔드가 마지막으로 검증한 공개키 캐시 상태입니다.", - "msg.dev.clients.general.public_key.cache_refresh_failed": "JWKS 캐시 새로고침에 실패했습니다: {{error}}", + "msg.dev.clients.general.public_key.allowed_algorithms_tooltip": + "허용 알고리즘: {{algorithms}}", + "msg.dev.clients.general.public_key.auth_method_client_secret_basic_help": + "일반적인 서버 사이드 앱 인증 방식입니다.", + "msg.dev.clients.general.public_key.auth_method_none_help": + "PKCE 기반 public client에 사용하는 방식입니다.", + "msg.dev.clients.general.public_key.auth_method_private_key_jwt_help": + "Trusted RP bootstrap과 JAR 검증에 필요한 서명 키 기반 인증 방식입니다.", + "msg.dev.clients.general.public_key.cache.missing_algorithm_badge": + "알고리즘 미선언", + "msg.dev.clients.general.public_key.cache.missing_algorithm_reason": + "이 키는 `alg`가 비어 있어서 저장할 수 없습니다.", + "msg.dev.clients.general.public_key.cache.missing_algorithms_help": + "저장 전 JWKS 각 키에 `alg`를 명시해 주세요: {{details}}", + "msg.dev.clients.general.public_key.cache.missing_algorithms_title": + "알고리즘이 선언되지 않았습니다.", + "msg.dev.clients.general.public_key.cache.parsed_keys_empty": + "No parsed JWKS keys are available yet.", + "msg.dev.clients.general.public_key.cache.parsed_keys_help": + "Raw JWKS stays hidden. Only parsed key metadata is shown here.", + "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": + "이 알고리즘은 Headless Login에서 지원되지 않습니다.", + "msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": + "저장 전 JWKS를 수정해 주세요: {{details}}", + "msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": + "지원하지 않는 알고리즘이 감지되었습니다.", + "msg.dev.clients.general.public_key.cache_empty": + "아직 캐시된 JWKS가 없습니다. Refresh를 눌러 백엔드 캐시 상태를 조회하세요.", + "msg.dev.clients.general.public_key.cache_help": + "백엔드가 마지막으로 검증한 공개키 캐시 상태입니다.", + "msg.dev.clients.general.public_key.cache_refresh_failed": + "JWKS 캐시 새로고침에 실패했습니다: {{error}}", "msg.dev.clients.general.public_key.cache_refreshed": "JWKS 캐시를 새로 고쳤습니다.", - "msg.dev.clients.general.public_key.cache_revoke_confirm": "JWKS 캐시를 삭제하면 다음 검증 전에 다시 갱신해야 합니다. 계속할까요?", - "msg.dev.clients.general.public_key.cache_revoke_failed": "JWKS 캐시 삭제에 실패했습니다: {{error}}", + "msg.dev.clients.general.public_key.cache_revoke_confirm": + "JWKS 캐시를 삭제하면 다음 검증 전에 다시 갱신해야 합니다. 계속할까요?", + "msg.dev.clients.general.public_key.cache_revoke_failed": + "JWKS 캐시 삭제에 실패했습니다: {{error}}", "msg.dev.clients.general.public_key.cache_revoked": "JWKS 캐시를 삭제했습니다.", - "msg.dev.clients.general.public_key.guide_example": "권장 예시: https://rp.example.com/.well-known/jwks.json", - "msg.dev.clients.general.public_key.guide_intro": "JWKS URI는 Baron이 만드는 값이 아니라 RP backend가 공개키를 노출하는 URL입니다.", - "msg.dev.clients.general.public_key.guide_step_1": "RP 서버에서 key pair를 생성하고 private key는 RP backend에만 보관합니다.", - "msg.dev.clients.general.public_key.guide_step_2": "RP backend가 public key를 JWKS(JSON Web Key Set) 형태로 제공하는 endpoint를 준비합니다.", - "msg.dev.clients.general.public_key.guide_step_3": "예: https://rp.example.com/.well-known/jwks.json 같은 URL을 DevFront에 입력합니다.", - "msg.dev.clients.general.public_key.headless_help": "애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다.", - "msg.dev.clients.general.public_key.jwks_inline_help": "SSH-RSA 공개키 형식을 우선 권장합니다. 'ssh-rsa AAA...' 형식으로 입력하면 Baron이 OIDC 표준인 JWKS(JSON)로 자동 변환하여 저장합니다.", - "msg.dev.clients.general.public_key.jwks_uri_help": "RP backend가 제공하는 공개키 endpoint URL을 입력하세요. 예: https://rp.example.com/.well-known/jwks.json", - "msg.dev.clients.general.public_key.request_object_alg_help": "Headless Login을 사용할 때 JAR(Request Object) 서명 알고리즘을 명시합니다.", - "msg.dev.clients.general.public_key.source_help": "애플리케이션의 공개키(SSH-RSA)를 직접 등록하거나, 운영 환경이라면 JWKS URI를 통해 자동으로 검증할 수 있습니다.", - "msg.dev.clients.general.public_key.subtitle": "Trusted RP 판정에 필요한 공개키와 headless login 관련 설정을 관리합니다.", - "msg.dev.clients.general.public_key.validation.headless_requires_alg": "Headless Login을 사용하려면 Request Object Signing Algorithm을 입력해야 합니다.", - "msg.dev.clients.general.public_key.validation.headless_requires_private_key_jwt": "Headless Login을 사용하려면 token endpoint auth method가 private_key_jwt여야 합니다.", - "msg.dev.clients.general.public_key.validation.headless_requires_public_key": "Headless Login을 사용하려면 JWKS URI가 필요합니다.", - "msg.dev.clients.general.public_key.validation.invalid_jwks_inline": "입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다.", - "msg.dev.clients.general.public_key.validation.invalid_jwks_uri": "JWKS URI 형식이 올바르지 않습니다.", - "msg.dev.clients.general.public_key.validation.missing_jwks_inline": "공개키(SSH-RSA 또는 JWKS)를 입력해야 합니다.", - "msg.dev.clients.general.public_key.validation.missing_jwks_uri": "JWKS URI를 입력해야 합니다.", - "msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "JWKS에 알고리즘(`alg`)이 선언되지 않은 키가 있습니다: {{details}}", - "msg.dev.clients.general.public_key.validation.private_key_jwt_requires_public_key": "서명 키 기반 인증을 사용하려면 JWKS URI가 필요합니다.", - "msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "JWKS에 지원하지 않는 알고리즘이 있습니다: {{details}}", - "msg.dev.clients.general.redirect.help": "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다.", + "msg.dev.clients.general.public_key.guide_example": + "권장 예시: https://rp.example.com/.well-known/jwks.json", + "msg.dev.clients.general.public_key.guide_intro": + "JWKS URI는 Baron이 만드는 값이 아니라 RP backend가 공개키를 노출하는 URL입니다.", + "msg.dev.clients.general.public_key.guide_step_1": + "RP 서버에서 key pair를 생성하고 private key는 RP backend에만 보관합니다.", + "msg.dev.clients.general.public_key.guide_step_2": + "RP backend가 public key를 JWKS(JSON Web Key Set) 형태로 제공하는 endpoint를 준비합니다.", + "msg.dev.clients.general.public_key.guide_step_3": + "예: https://rp.example.com/.well-known/jwks.json 같은 URL을 DevFront에 입력합니다.", + "msg.dev.clients.general.public_key.headless_help": + "애플리케이션 고유의 디자인으로 로그인 화면을 구성할 수 있습니다. 실제 아이디/비밀번호 확인 및 보안 검증 로직은 Baron API를 통해 백그라운드에서 처리됩니다.", + "msg.dev.clients.general.public_key.jwks_inline_help": + "SSH-RSA 공개키 형식을 우선 권장합니다. 'ssh-rsa AAA...' 형식으로 입력하면 Baron이 OIDC 표준인 JWKS(JSON)로 자동 변환하여 저장합니다.", + "msg.dev.clients.general.public_key.jwks_uri_help": + "RP backend가 제공하는 공개키 endpoint URL을 입력하세요. 예: https://rp.example.com/.well-known/jwks.json", + "msg.dev.clients.general.public_key.request_object_alg_help": + "Headless Login을 사용할 때 JAR(Request Object) 서명 알고리즘을 명시합니다.", + "msg.dev.clients.general.public_key.source_help": + "애플리케이션의 공개키(SSH-RSA)를 직접 등록하거나, 운영 환경이라면 JWKS URI를 통해 자동으로 검증할 수 있습니다.", + "msg.dev.clients.general.public_key.subtitle": + "Trusted RP 판정에 필요한 공개키와 headless login 관련 설정을 관리합니다.", + "msg.dev.clients.general.public_key.validation.headless_requires_alg": + "Headless Login을 사용하려면 Request Object Signing Algorithm을 입력해야 합니다.", + "msg.dev.clients.general.public_key.validation.headless_requires_private_key_jwt": + "Headless Login을 사용하려면 token endpoint auth method가 private_key_jwt여야 합니다.", + "msg.dev.clients.general.public_key.validation.headless_requires_public_key": + "Headless Login을 사용하려면 JWKS URI가 필요합니다.", + "msg.dev.clients.general.public_key.validation.invalid_jwks_inline": + "입력값이 유효한 JSON(JWKS) 형식이 아닙니다. SSH-RSA의 경우 'ssh-rsa'로 시작해야 합니다.", + "msg.dev.clients.general.public_key.validation.invalid_jwks_uri": + "JWKS URI 형식이 올바르지 않습니다.", + "msg.dev.clients.general.public_key.validation.missing_jwks_inline": + "공개키(SSH-RSA 또는 JWKS)를 입력해야 합니다.", + "msg.dev.clients.general.public_key.validation.missing_jwks_uri": + "JWKS URI를 입력해야 합니다.", + "msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": + "JWKS에 알고리즘(`alg`)이 선언되지 않은 키가 있습니다: {{details}}", + "msg.dev.clients.general.public_key.validation.private_key_jwt_requires_public_key": + "서명 키 기반 인증을 사용하려면 JWKS URI가 필요합니다.", + "msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": + "JWKS에 지원하지 않는 알고리즘이 있습니다: {{details}}", + "msg.dev.clients.general.redirect.help": + "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다.", "msg.dev.clients.general.save_error": "저장 실패: {{error}}", "msg.dev.clients.general.saved": "설정이 저장되었습니다.", "msg.dev.clients.general.scopes.empty": "등록된 스코프가 없습니다.", "msg.dev.clients.general.scopes.subtitle": "이 앱이 요청할 수 있는 권한 범위를 정의합니다.", - "msg.dev.clients.general.security.pkce_help": "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다.", - "msg.dev.clients.general.security.private_help": "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다.", - "msg.dev.clients.general.security.subtitle": "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다.", + "msg.dev.clients.general.security.pkce_help": + "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다.", + "msg.dev.clients.general.security.private_help": + "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다.", + "msg.dev.clients.general.security.subtitle": + "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다.", "msg.dev.clients.general.status_changed": "상태가 {{status}}로 변경되었습니다.", - "msg.dev.clients.help.docs_body": "Includes PKCE, client_secret_basic, redirect URI validation tips.", - "msg.dev.clients.help.subtitle": "Developer guides for Confidential/Public clients, redirect URIs, and auth methods.", + "msg.dev.clients.help.docs_body": + "Includes PKCE, client_secret_basic, redirect URI validation tips.", + "msg.dev.clients.help.subtitle": + "Developer guides for Confidential/Public clients, redirect URIs, and auth methods.", "msg.dev.clients.load_error": "앱 정보를 불러오지 못했습니다: {{error}}", "msg.dev.clients.loading": "앱 정보를 불러오는 중...", - "msg.dev.clients.registry.description": "OIDC 앱, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.", + "msg.dev.clients.registry.description": + "OIDC 앱, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.", "msg.dev.clients.scopes.email": "이메일 주소 접근", "msg.dev.clients.scopes.openid": "OIDC 인증 필수 스코프", "msg.dev.clients.scopes.profile": "기본 프로필 정보 접근", "msg.dev.clients.showing": "전체 {{total}}개 중 {{shown}}개를 표시하는 중입니다.", - "msg.dev.dashboard.hero.body": "Hydra Admin API와 동기화된 RP 목록, 상태 토글, Consent 회수까지 devfront에서 처리하도록 준비합니다.", + "msg.dev.dashboard.hero.body": + "Hydra Admin API와 동기화된 RP 목록, 상태 토글, Consent 회수까지 devfront에서 처리하도록 준비합니다.", "msg.dev.dashboard.hero.title_emphasis": " 하나의 화면", "msg.dev.dashboard.hero.title_prefix": "RP 등록 현황과 Consent 상태를", "msg.dev.dashboard.hero.title_suffix": "에서 관리합니다.", @@ -345,11 +422,13 @@ const Map koStrings = { "msg.userfront.audit.session_id": "Session ID: {{value}}", "msg.userfront.audit.status": "현황: (준비중)", "msg.userfront.consent.accept_error": "동의 처리에 실패했습니다: {{error}}", - "msg.userfront.consent.cancel.confirm": "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?", + "msg.userfront.consent.cancel.confirm": + "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?", "msg.userfront.consent.cancel.error": "취소 처리 중 오류가 발생했습니다: {{error}}", "msg.userfront.consent.client_id": "클라이언트 ID: {{id}}", "msg.userfront.consent.client_unknown": "알 수 없는 앱", - "msg.userfront.consent.description": "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\n계속 진행하려면 동의 여부를 선택해 주세요.", + "msg.userfront.consent.description": + "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\n계속 진행하려면 동의 여부를 선택해 주세요.", "msg.userfront.consent.load_error": "동의 정보를 불러오는데 실패했습니다: {{error}}", "msg.userfront.consent.missing_redirect": "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다.", "msg.userfront.consent.redirect_notice": "동의 후 자동으로 서비스로 이동합니다.", @@ -360,12 +439,15 @@ const Map koStrings = { "msg.userfront.consent.scope.profile": "기본 프로필 정보 (이름, 사용자 식별자)", "msg.userfront.consent.scope_count": "총 {{count}}개", "msg.userfront.dashboard.activities.empty": "연동된 앱이 없습니다.", - "msg.userfront.dashboard.activities.empty_detail": "앱을 연동하면 최근 활동과 상태가 표시됩니다.", + "msg.userfront.dashboard.activities.empty_detail": + "앱을 연동하면 최근 활동과 상태가 표시됩니다.", "msg.userfront.dashboard.activities.error": "연동 정보를 불러오지 못했습니다.", "msg.userfront.dashboard.approved_device": "승인 기기: {{device}}", "msg.userfront.dashboard.approved_ip": "승인 IP: {{ip}}", - "msg.userfront.dashboard.approved_session.copy_click": "{{label}}: {{id}}\\\\n클릭하면 복사됩니다.", - "msg.userfront.dashboard.approved_session.copy_tap": "{{label}}: {{id}}\\\\n탭하면 복사됩니다.", + "msg.userfront.dashboard.approved_session.copy_click": + "{{label}}: {{id}}\\\\n클릭하면 복사됩니다.", + "msg.userfront.dashboard.approved_session.copy_tap": + "{{label}}: {{id}}\\\\n탭하면 복사됩니다.", "msg.userfront.dashboard.approved_session.none": "{{label}} 없음", "msg.userfront.dashboard.audit_empty": "최근 접속 이력이 없습니다.", "msg.userfront.dashboard.audit_load_error": "접속이력을 불러오지 못했습니다.", @@ -377,18 +459,21 @@ const Map koStrings = { "msg.userfront.dashboard.link_missing": "이동할 페이지 주소(Client URI)가 설정되지 않았습니다.", "msg.userfront.dashboard.link_open_error": "해당 링크를 열 수 없습니다.", "msg.userfront.dashboard.render_error": "대시보드 렌더링 오류: {{error}}", - "msg.userfront.dashboard.revoke.confirm": "{{app}} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.", + "msg.userfront.dashboard.revoke.confirm": + "{{app}} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.", "msg.userfront.dashboard.revoke.error": "해지 실패: {{error}}", "msg.userfront.dashboard.revoke.success": "{{app}} 연동이 해지되었습니다.", "msg.userfront.dashboard.scopes.empty": "요청된 권한이 없습니다.", "msg.userfront.dashboard.session_id_copied": "세션 ID가 복사되었습니다.", "msg.userfront.dashboard.sessions.browser": "브라우저: {{value}}", "msg.userfront.dashboard.sessions.empty": "활성 세션이 없습니다.", - "msg.userfront.dashboard.sessions.empty_detail": "같은 계정으로 로그인한 기기가 여기에 표시됩니다.", + "msg.userfront.dashboard.sessions.empty_detail": + "같은 계정으로 로그인한 기기가 여기에 표시됩니다.", "msg.userfront.dashboard.sessions.error": "세션 정보를 불러오지 못했습니다.", "msg.userfront.dashboard.sessions.os": "OS: {{value}}", "msg.userfront.dashboard.sessions.recent_app": "최근 접속 앱: {{app}}", - "msg.userfront.dashboard.sessions.revoke.confirm": "{{target}} 세션을 종료하시겠습니까?\n대상 기기에서는 다시 로그인이 필요합니다.", + "msg.userfront.dashboard.sessions.revoke.confirm": + "{{target}} 세션을 종료하시겠습니까?\n대상 기기에서는 다시 로그인이 필요합니다.", "msg.userfront.dashboard.sessions.revoke.error": "세션 종료 실패: {{error}}", "msg.userfront.dashboard.sessions.revoke.success": "세션이 종료되었습니다.", "msg.userfront.dashboard.sessions.session_id": "세션 ID: {{id}}", @@ -419,13 +504,17 @@ const Map koStrings = { "msg.userfront.error.whitelist.bad_request": "입력값을 확인해 주세요.", "msg.userfront.error.whitelist.invalid_session": "세션이 만료되었습니다. 다시 로그인해 주세요.", "msg.userfront.error.whitelist.not_found": "요청한 페이지를 찾을 수 없습니다.", - "msg.userfront.error.whitelist.password_or_email_mismatch": "이메일 혹은 비밀번호가 일치하지 않습니다.", + "msg.userfront.error.whitelist.password_or_email_mismatch": + "이메일 혹은 비밀번호가 일치하지 않습니다.", "msg.userfront.error.whitelist.rate_limited": "요청이 많습니다. 잠시 후 다시 시도해 주세요.", - "msg.userfront.error.whitelist.recovery_expired": "재설정 링크가 만료되었습니다. 다시 요청해 주세요.", + "msg.userfront.error.whitelist.recovery_expired": + "재설정 링크가 만료되었습니다. 다시 요청해 주세요.", "msg.userfront.error.whitelist.recovery_invalid": "재설정 링크가 유효하지 않습니다.", "msg.userfront.error.whitelist.settings_disabled": "현재 계정 설정 화면은 준비 중입니다.", - "msg.userfront.error.whitelist.verification_required": "추가 인증이 필요합니다. 안내에 따라 진행해 주세요.", - "msg.userfront.forgot.description": "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.", + "msg.userfront.error.whitelist.verification_required": + "추가 인증이 필요합니다. 안내에 따라 진행해 주세요.", + "msg.userfront.forgot.description": + "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.", "msg.userfront.forgot.dry_send": "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.", "msg.userfront.forgot.error": "전송에 실패했습니다: {{error}}", "msg.userfront.forgot.input_required": "이메일 또는 휴대폰 번호를 입력해주세요.", @@ -438,7 +527,8 @@ const Map koStrings = { "msg.userfront.login.link.missing_login_id": "이메일 또는 휴대폰 번호를 입력해 주세요.", "msg.userfront.login.link.missing_phone": "휴대폰 번호를 입력해 주세요.", "msg.userfront.login.link.resend_wait": "재발송은 {{time}} 후 가능합니다.", - "msg.userfront.login.link.short_code_help": "링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.", + "msg.userfront.login.link.short_code_help": + "링크로 받은 값의 뒤 문자 2개와 숫자 6자리를 입력하셔도 로그인 할 수 있습니다.", "msg.userfront.login.link_failed": "오류: {{error}}", "msg.userfront.login.link_send_failed": "전송 실패: {{error}}", "msg.userfront.login.link_sent_email": "입력하신 이메일로 로그인 링크를 보냈습니다.", @@ -447,7 +537,8 @@ const Map koStrings = { "msg.userfront.login.no_account": "계정이 없으신가요?", "msg.userfront.login.oidc_failed": "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요.", "msg.userfront.login.password.failed": "로그인 실패: {{error}}", - "msg.userfront.login.password.missing_credentials": "이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.", + "msg.userfront.login.password.missing_credentials": + "이메일(또는 전화번호)와 비밀번호를 모두 입력해주세요.", "msg.userfront.login.qr.load_failed": "QR 코드를 불러오지 못했습니다.", "msg.userfront.login.qr.scan_hint": "모바일 앱으로 스캔하세요", "msg.userfront.login.qr_expired": "시간이 경과되었습니다.", @@ -457,7 +548,8 @@ const Map koStrings = { "msg.userfront.login.token_missing": "로그인 토큰을 확인할 수 없습니다.", "msg.userfront.login.unregistered.body": "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주세요.", "msg.userfront.login.verification.approved": "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다.", - "msg.userfront.login.verification.approved_local": "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다", + "msg.userfront.login.verification.approved_local": + "승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다", "msg.userfront.login.verification.success": "로그인 승인에 성공했습니다.", "msg.userfront.login.verification_failed": "승인 처리에 실패했습니다: {{error}}", "msg.userfront.login_success.subtitle": "성공적으로 로그인되었습니다.", @@ -495,7 +587,8 @@ const Map koStrings = { "msg.userfront.reset.error.generic": "비밀번호 변경에 실패했습니다: {{error}}", "msg.userfront.reset.error.lowercase": "최소 1개 이상의 소문자를 포함해야 합니다.", "msg.userfront.reset.error.min_length": "비밀번호는 최소 {{count}}자 이상이어야 합니다.", - "msg.userfront.reset.error.min_types": "비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.", + "msg.userfront.reset.error.min_types": + "비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.", "msg.userfront.reset.error.mismatch": "비밀번호가 일치하지 않습니다.", "msg.userfront.reset.error.number": "최소 1개 이상의 숫자를 포함해야 합니다.", "msg.userfront.reset.error.symbol": "최소 1개 이상의 특수문자를 포함해야 합니다.", @@ -515,13 +608,18 @@ const Map koStrings = { "msg.userfront.sections.audit_subtitle": "Baron 로그인 기준의 최근 접근 기록입니다.", "msg.userfront.sections.sessions_subtitle": "현재 로그인된 기기와 브라우저 세션입니다.", "msg.userfront.settings.disabled": "현재 계정 설정 화면은 준비 중입니다.", - "msg.userfront.signup.agreement.all_hint": "필수 약관 2개를 모두 확인하고 동의하면 다음 단계로 진행할 수 있습니다.", - "msg.userfront.signup.agreement.description": "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요.", - "msg.userfront.signup.agreement.privacy_summary": "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다.", - "msg.userfront.signup.agreement.progress": "필수 약관 {{total}}개 중 {{count}}개 동의 완료", + "msg.userfront.signup.agreement.all_hint": + "필수 약관 2개를 모두 확인하고 동의하면 다음 단계로 진행할 수 있습니다.", + "msg.userfront.signup.agreement.description": + "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요.", + "msg.userfront.signup.agreement.privacy_summary": + "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다.", + "msg.userfront.signup.agreement.progress": + "필수 약관 {{total}}개 중 {{count}}개 동의 완료", "msg.userfront.signup.agreement.title": "서비스 이용을 위해\\\\n약관에 동의해주세요", "msg.userfront.signup.agreement.tos_summary": "서비스 이용 조건과 책임 범위를 확인할 수 있습니다.", - "msg.userfront.signup.auth.affiliate_notice": "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.", + "msg.userfront.signup.auth.affiliate_notice": + "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요.", "msg.userfront.signup.auth.title": "본인 확인을 위해\\\\n인증을 진행해주세요", "msg.userfront.signup.email.code_mismatch": "인증코드가 일치하지 않습니다.", "msg.userfront.signup.email.duplicate": "이미 가입된 이메일입니다.", @@ -531,7 +629,8 @@ const Map koStrings = { "msg.userfront.signup.email.verify_failed": "인증 실패: {{error}}", "msg.userfront.signup.failed": "가입 실패: {{error}}", "msg.userfront.signup.password.length_required": "비밀번호는 최소 12자 이상이어야 합니다.", - "msg.userfront.signup.password.lowercase_required": "소문자가 최소 1개 이상 포함되어야 합니다.", + "msg.userfront.signup.password.lowercase_required": + "소문자가 최소 1개 이상 포함되어야 합니다.", "msg.userfront.signup.password.mismatch": "비밀번호가 일치하지 않습니다.", "msg.userfront.signup.password.number_required": "숫자가 최소 1개 이상 포함되어야 합니다.", "msg.userfront.signup.password.rule.lowercase": "소문자", @@ -542,7 +641,8 @@ const Map koStrings = { "msg.userfront.signup.password.rule.uppercase": "대문자", "msg.userfront.signup.password.symbol_required": "특수문자가 최소 1개 이상 포함되어야 합니다.", "msg.userfront.signup.password.title": "마지막으로\\\\n비밀번호를 설정해주세요", - "msg.userfront.signup.password.uppercase_required": "대문자가 최소 1개 이상 포함되어야 합니다.", + "msg.userfront.signup.password.uppercase_required": + "대문자가 최소 1개 이상 포함되어야 합니다.", "msg.userfront.signup.phone.code_mismatch": "인증코드가 일치하지 않습니다.", "msg.userfront.signup.phone.send_failed": "발송 실패: {{error}}", "msg.userfront.signup.phone.verified": "✅ 휴대폰 인증 완료", @@ -564,7 +664,8 @@ const Map koStrings = { "non.existent.key": "존재하지 않는 키", "test.key": "테스트", "ui.admin.api_keys.create.name_label": "서비스 또는 목적 식별 이름", - "ui.admin.api_keys.create.name_placeholder": "예: Jenkins-CI, Grafana-Dashboard", + "ui.admin.api_keys.create.name_placeholder": + "예: Jenkins-CI, Grafana-Dashboard", "ui.admin.api_keys.create.section_name": "키 이름 지정", "ui.admin.api_keys.create.section_scopes": "권한 범위(Scopes) 선택", "ui.admin.api_keys.create.submit": "API 키 발급하기", @@ -692,7 +793,8 @@ const Map koStrings = { "ui.admin.tenants.create.breadcrumb.action": "Create", "ui.admin.tenants.create.breadcrumb.section": "Tenants", "ui.admin.tenants.create.form.description": "설명", - "ui.admin.tenants.create.form.domains_label": "Allowed Domains (Comma separated)", + "ui.admin.tenants.create.form.domains_label": + "Allowed Domains (Comma separated)", "ui.admin.tenants.create.form.domains_placeholder": "example.com, example.kr", "ui.admin.tenants.create.form.name": "테넌트 이름", "ui.admin.tenants.create.form.name_placeholder": "테넌트 이름을 입력하세요", @@ -735,11 +837,13 @@ const Map koStrings = { "ui.admin.tenants.owners.table_name": "이름", "ui.admin.tenants.owners.title": "테넌트 소유자", "ui.admin.tenants.profile.allowed_domains": "허용된 도메인 (콤마로 구분)", - "ui.admin.tenants.profile.allowed_domains_help": "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.", + "ui.admin.tenants.profile.allowed_domains_help": + "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.", "ui.admin.tenants.profile.approve_button": "테넌트 승인", "ui.admin.tenants.profile.description": "설명", "ui.admin.tenants.profile.form.parent": "상위 테넌트 (선택)", - "ui.admin.tenants.profile.form.parent_help": "가족사 테넌트나 하위 조직을 종속시킬 경우 상위 테넌트를 선택해주세요.", + "ui.admin.tenants.profile.form.parent_help": + "가족사 테넌트나 하위 조직을 종속시킬 경우 상위 테넌트를 선택해주세요.", "ui.admin.tenants.profile.name": "테넌트 이름", "ui.admin.tenants.profile.slug": "슬러그 (Slug)", "ui.admin.tenants.profile.status": "상태", @@ -836,116 +940,205 @@ const Map koStrings = { "ui.admin.users.detail.custom_fields.multi_title": "테넌트별 프로필 관리", "ui.admin.users.detail.delete": "사용자 삭제", "ui.admin.users.detail.edit_title": "정보 수정", - "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": + "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", "ui.admin.users.detail.form.department": "부서", "ui.admin.users.detail.form.department_placeholder": "개발팀", "ui.admin.users.detail.form.email": "이메일", "ui.admin.users.detail.form.is_login_id": "로그인 ID", "ui.admin.users.detail.form.job_title": "직무", - "ui.admin.users.detail.form.job_title_placeholder": "ui.admin.users.detail.form.job_title_placeholder", + "ui.admin.users.detail.form.job_title_placeholder": + "ui.admin.users.detail.form.job_title_placeholder", "ui.admin.users.detail.form.login_id": "로그인 ID", "ui.admin.users.detail.form.login_id_placeholder": "사번 또는 아이디", - "ui.admin.users.detail.form.msg.admin.users.detail.history_desc": "ui.admin.users.detail.form.msg.admin.users.detail.history_desc", - "ui.admin.users.detail.form.msg.admin.users.detail.no_history": "ui.admin.users.detail.form.msg.admin.users.detail.no_history", - "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants": "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants", - "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc": "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc", - "ui.admin.users.detail.form.msg.admin.users.detail.security_desc": "ui.admin.users.detail.form.msg.admin.users.detail.security_desc", - "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help": "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help", - "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc": "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc", - "ui.admin.users.detail.form.msg.common.copied": "ui.admin.users.detail.form.msg.common.copied", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", + "ui.admin.users.detail.form.msg.admin.users.detail.history_desc": + "ui.admin.users.detail.form.msg.admin.users.detail.history_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.no_history": + "ui.admin.users.detail.form.msg.admin.users.detail.no_history", + "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants": + "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants", + "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc": + "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.security_desc": + "ui.admin.users.detail.form.msg.admin.users.detail.security_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help": + "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help", + "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc": + "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc", + "ui.admin.users.detail.form.msg.common.copied": + "ui.admin.users.detail.form.msg.common.copied", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", "ui.admin.users.detail.form.name": "이름", "ui.admin.users.detail.form.name_placeholder": "홍길동", "ui.admin.users.detail.form.phone": "전화번호", "ui.admin.users.detail.form.phone_placeholder": "010-1234-5678", "ui.admin.users.detail.form.position": "직급", - "ui.admin.users.detail.form.position_placeholder": "ui.admin.users.detail.form.position_placeholder", + "ui.admin.users.detail.form.position_placeholder": + "ui.admin.users.detail.form.position_placeholder", "ui.admin.users.detail.form.role": "역할", "ui.admin.users.detail.form.role_rp_admin": "RP 관리자", "ui.admin.users.detail.form.role_super_admin": "시스템 전역 관리자", "ui.admin.users.detail.form.role_tenant_admin": "테넌트 관리자", "ui.admin.users.detail.form.role_user": "사용자", "ui.admin.users.detail.form.status": "상태", - "ui.admin.users.detail.form.status_active": "ui.admin.users.detail.form.status_active", - "ui.admin.users.detail.form.status_inactive": "ui.admin.users.detail.form.status_inactive", + "ui.admin.users.detail.form.status_active": + "ui.admin.users.detail.form.status_active", + "ui.admin.users.detail.form.status_inactive": + "ui.admin.users.detail.form.status_inactive", "ui.admin.users.detail.form.tenant": "대표 소속 테넌트", "ui.admin.users.detail.form.tenant_global": "시스템 전역", "ui.admin.users.detail.form.tenant_slug": "대표 소속 (Tenant Slug)", - "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id": "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id", - "ui.admin.users.detail.form.ui.admin.users.detail.form.email": "ui.admin.users.detail.form.ui.admin.users.detail.form.email", - "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id": "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id", - "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin": "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin", - "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug": "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug", - "ui.admin.users.detail.form.ui.admin.users.detail.generate_button": "ui.admin.users.detail.form.ui.admin.users.detail.generate_button", - "ui.admin.users.detail.form.ui.admin.users.detail.history_title": "ui.admin.users.detail.form.ui.admin.users.detail.history_title", - "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm": "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm", - "ui.admin.users.detail.form.ui.admin.users.detail.manual_password": "ui.admin.users.detail.form.ui.admin.users.detail.manual_password", - "ui.admin.users.detail.form.ui.admin.users.detail.password_done": "ui.admin.users.detail.form.ui.admin.users.detail.password_done", - "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto": "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto", - "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute": "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute", - "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual": "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual", - "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants": "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants", - "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info", - "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security", - "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants", - "ui.admin.users.detail.form.ui.admin.users.detail.updated_at": "ui.admin.users.detail.form.ui.admin.users.detail.updated_at", - "ui.admin.users.detail.form.ui.common.generate": "ui.admin.users.detail.form.ui.common.generate", - "ui.admin.users.detail.form.ui.common.status.blocked": "ui.admin.users.detail.form.ui.common.status.blocked", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri", - "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable": "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable", - "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help": "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help", - "ui.admin.users.detail.form.ui.dev.clients.help.docs_body": "ui.admin.users.detail.form.ui.dev.clients.help.docs_body", - "ui.admin.users.detail.form.ui.dev.clients.help.subtitle": "ui.admin.users.detail.form.ui.dev.clients.help.subtitle", - "ui.admin.users.detail.form.ui.dev.clients.registry.description": "ui.admin.users.detail.form.ui.dev.clients.registry.description", - "ui.admin.users.detail.form.ui.dev.clients.scopes.email": "ui.admin.users.detail.form.ui.dev.clients.scopes.email", - "ui.admin.users.detail.form.ui.dev.clients.scopes.openid": "ui.admin.users.detail.form.ui.dev.clients.scopes.openid", - "ui.admin.users.detail.form.ui.dev.clients.scopes.profile": "ui.admin.users.detail.form.ui.dev.clients.scopes.profile", - "ui.admin.users.detail.form.ui.dev.session.refresh": "ui.admin.users.detail.form.ui.dev.session.refresh", - "ui.admin.users.detail.form.ui.dev.session.refreshing": "ui.admin.users.detail.form.ui.dev.session.refreshing", + "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id": + "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id", + "ui.admin.users.detail.form.ui.admin.users.detail.form.email": + "ui.admin.users.detail.form.ui.admin.users.detail.form.email", + "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id": + "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id", + "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin": + "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin", + "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug": + "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug", + "ui.admin.users.detail.form.ui.admin.users.detail.generate_button": + "ui.admin.users.detail.form.ui.admin.users.detail.generate_button", + "ui.admin.users.detail.form.ui.admin.users.detail.history_title": + "ui.admin.users.detail.form.ui.admin.users.detail.history_title", + "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm": + "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm", + "ui.admin.users.detail.form.ui.admin.users.detail.manual_password": + "ui.admin.users.detail.form.ui.admin.users.detail.manual_password", + "ui.admin.users.detail.form.ui.admin.users.detail.password_done": + "ui.admin.users.detail.form.ui.admin.users.detail.password_done", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto": + "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute": + "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual": + "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual", + "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants": + "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info": + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security": + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants": + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants", + "ui.admin.users.detail.form.ui.admin.users.detail.updated_at": + "ui.admin.users.detail.form.ui.admin.users.detail.updated_at", + "ui.admin.users.detail.form.ui.common.generate": + "ui.admin.users.detail.form.ui.common.generate", + "ui.admin.users.detail.form.ui.common.status.blocked": + "ui.admin.users.detail.form.ui.common.status.blocked", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri", + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable": + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable", + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help": + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.admin.users.detail.form.ui.dev.clients.help.docs_body": + "ui.admin.users.detail.form.ui.dev.clients.help.docs_body", + "ui.admin.users.detail.form.ui.dev.clients.help.subtitle": + "ui.admin.users.detail.form.ui.dev.clients.help.subtitle", + "ui.admin.users.detail.form.ui.dev.clients.registry.description": + "ui.admin.users.detail.form.ui.dev.clients.registry.description", + "ui.admin.users.detail.form.ui.dev.clients.scopes.email": + "ui.admin.users.detail.form.ui.dev.clients.scopes.email", + "ui.admin.users.detail.form.ui.dev.clients.scopes.openid": + "ui.admin.users.detail.form.ui.dev.clients.scopes.openid", + "ui.admin.users.detail.form.ui.dev.clients.scopes.profile": + "ui.admin.users.detail.form.ui.dev.clients.scopes.profile", + "ui.admin.users.detail.form.ui.dev.session.refresh": + "ui.admin.users.detail.form.ui.dev.session.refresh", + "ui.admin.users.detail.form.ui.dev.session.refreshing": + "ui.admin.users.detail.form.ui.dev.session.refreshing", "ui.admin.users.detail.generate_button": "랜덤 비밀번호 생성", "ui.admin.users.detail.generate_password": "자동 생성", "ui.admin.users.detail.go_list": "목록으로 이동", @@ -993,94 +1186,178 @@ const Map koStrings = { "ui.admin.users.list.table.actions": "ACTIONS", "ui.admin.users.list.table.created": "CREATED", "ui.admin.users.list.table.login_id": "LOGIN ID", - "ui.admin.users.list.table.msg.admin.users.detail.history_desc": "ui.admin.users.list.table.msg.admin.users.detail.history_desc", - "ui.admin.users.list.table.msg.admin.users.detail.no_history": "ui.admin.users.list.table.msg.admin.users.detail.no_history", - "ui.admin.users.list.table.msg.admin.users.detail.no_tenants": "ui.admin.users.list.table.msg.admin.users.detail.no_tenants", - "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc": "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc", - "ui.admin.users.list.table.msg.admin.users.detail.security_desc": "ui.admin.users.list.table.msg.admin.users.detail.security_desc", - "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help": "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help", - "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc": "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc", - "ui.admin.users.list.table.msg.common.copied": "ui.admin.users.list.table.msg.common.copied", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", + "ui.admin.users.list.table.msg.admin.users.detail.history_desc": + "ui.admin.users.list.table.msg.admin.users.detail.history_desc", + "ui.admin.users.list.table.msg.admin.users.detail.no_history": + "ui.admin.users.list.table.msg.admin.users.detail.no_history", + "ui.admin.users.list.table.msg.admin.users.detail.no_tenants": + "ui.admin.users.list.table.msg.admin.users.detail.no_tenants", + "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc": + "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc", + "ui.admin.users.list.table.msg.admin.users.detail.security_desc": + "ui.admin.users.list.table.msg.admin.users.detail.security_desc", + "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help": + "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help", + "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc": + "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc", + "ui.admin.users.list.table.msg.common.copied": + "ui.admin.users.list.table.msg.common.copied", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", "ui.admin.users.list.table.name_email": "NAME / EMAIL", "ui.admin.users.list.table.role": "ROLE", "ui.admin.users.list.table.status": "STATUS", "ui.admin.users.list.table.tenant_dept": "TENANT / DEPT", - "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id": "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id", - "ui.admin.users.list.table.ui.admin.users.detail.form.email": "ui.admin.users.list.table.ui.admin.users.detail.form.email", - "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id": "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id", - "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin": "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin", - "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug": "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug", - "ui.admin.users.list.table.ui.admin.users.detail.generate_button": "ui.admin.users.list.table.ui.admin.users.detail.generate_button", - "ui.admin.users.list.table.ui.admin.users.detail.history_title": "ui.admin.users.list.table.ui.admin.users.detail.history_title", - "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm": "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm", - "ui.admin.users.list.table.ui.admin.users.detail.manual_password": "ui.admin.users.list.table.ui.admin.users.detail.manual_password", - "ui.admin.users.list.table.ui.admin.users.detail.password_done": "ui.admin.users.list.table.ui.admin.users.detail.password_done", - "ui.admin.users.list.table.ui.admin.users.detail.reset_auto": "ui.admin.users.list.table.ui.admin.users.detail.reset_auto", - "ui.admin.users.list.table.ui.admin.users.detail.reset_execute": "ui.admin.users.list.table.ui.admin.users.detail.reset_execute", - "ui.admin.users.list.table.ui.admin.users.detail.reset_manual": "ui.admin.users.list.table.ui.admin.users.detail.reset_manual", - "ui.admin.users.list.table.ui.admin.users.detail.save_tenants": "ui.admin.users.list.table.ui.admin.users.detail.save_tenants", - "ui.admin.users.list.table.ui.admin.users.detail.tabs.info": "ui.admin.users.list.table.ui.admin.users.detail.tabs.info", - "ui.admin.users.list.table.ui.admin.users.detail.tabs.security": "ui.admin.users.list.table.ui.admin.users.detail.tabs.security", - "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants": "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants", - "ui.admin.users.list.table.ui.admin.users.detail.updated_at": "ui.admin.users.list.table.ui.admin.users.detail.updated_at", - "ui.admin.users.list.table.ui.common.generate": "ui.admin.users.list.table.ui.common.generate", - "ui.admin.users.list.table.ui.common.status.blocked": "ui.admin.users.list.table.ui.common.status.blocked", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info": "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle": "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled": "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled": "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline": "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg": "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.source": "ui.admin.users.list.table.ui.dev.clients.general.public_key.source", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri": "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri", - "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable": "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable", - "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help": "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help", - "ui.admin.users.list.table.ui.dev.clients.help.docs_body": "ui.admin.users.list.table.ui.dev.clients.help.docs_body", - "ui.admin.users.list.table.ui.dev.clients.help.subtitle": "ui.admin.users.list.table.ui.dev.clients.help.subtitle", - "ui.admin.users.list.table.ui.dev.clients.registry.description": "ui.admin.users.list.table.ui.dev.clients.registry.description", - "ui.admin.users.list.table.ui.dev.clients.scopes.email": "ui.admin.users.list.table.ui.dev.clients.scopes.email", - "ui.admin.users.list.table.ui.dev.clients.scopes.openid": "ui.admin.users.list.table.ui.dev.clients.scopes.openid", - "ui.admin.users.list.table.ui.dev.clients.scopes.profile": "ui.admin.users.list.table.ui.dev.clients.scopes.profile", - "ui.admin.users.list.table.ui.dev.session.refresh": "ui.admin.users.list.table.ui.dev.session.refresh", - "ui.admin.users.list.table.ui.dev.session.refreshing": "ui.admin.users.list.table.ui.dev.session.refreshing", + "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id": + "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id", + "ui.admin.users.list.table.ui.admin.users.detail.form.email": + "ui.admin.users.list.table.ui.admin.users.detail.form.email", + "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id": + "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id", + "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin": + "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin", + "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug": + "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug", + "ui.admin.users.list.table.ui.admin.users.detail.generate_button": + "ui.admin.users.list.table.ui.admin.users.detail.generate_button", + "ui.admin.users.list.table.ui.admin.users.detail.history_title": + "ui.admin.users.list.table.ui.admin.users.detail.history_title", + "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm": + "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm", + "ui.admin.users.list.table.ui.admin.users.detail.manual_password": + "ui.admin.users.list.table.ui.admin.users.detail.manual_password", + "ui.admin.users.list.table.ui.admin.users.detail.password_done": + "ui.admin.users.list.table.ui.admin.users.detail.password_done", + "ui.admin.users.list.table.ui.admin.users.detail.reset_auto": + "ui.admin.users.list.table.ui.admin.users.detail.reset_auto", + "ui.admin.users.list.table.ui.admin.users.detail.reset_execute": + "ui.admin.users.list.table.ui.admin.users.detail.reset_execute", + "ui.admin.users.list.table.ui.admin.users.detail.reset_manual": + "ui.admin.users.list.table.ui.admin.users.detail.reset_manual", + "ui.admin.users.list.table.ui.admin.users.detail.save_tenants": + "ui.admin.users.list.table.ui.admin.users.detail.save_tenants", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.info": + "ui.admin.users.list.table.ui.admin.users.detail.tabs.info", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.security": + "ui.admin.users.list.table.ui.admin.users.detail.tabs.security", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants": + "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants", + "ui.admin.users.list.table.ui.admin.users.detail.updated_at": + "ui.admin.users.list.table.ui.admin.users.detail.updated_at", + "ui.admin.users.list.table.ui.common.generate": + "ui.admin.users.list.table.ui.common.generate", + "ui.admin.users.list.table.ui.common.status.blocked": + "ui.admin.users.list.table.ui.common.status.blocked", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri", + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable": + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable", + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help": + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.admin.users.list.table.ui.dev.clients.help.docs_body": + "ui.admin.users.list.table.ui.dev.clients.help.docs_body", + "ui.admin.users.list.table.ui.dev.clients.help.subtitle": + "ui.admin.users.list.table.ui.dev.clients.help.subtitle", + "ui.admin.users.list.table.ui.dev.clients.registry.description": + "ui.admin.users.list.table.ui.dev.clients.registry.description", + "ui.admin.users.list.table.ui.dev.clients.scopes.email": + "ui.admin.users.list.table.ui.dev.clients.scopes.email", + "ui.admin.users.list.table.ui.dev.clients.scopes.openid": + "ui.admin.users.list.table.ui.dev.clients.scopes.openid", + "ui.admin.users.list.table.ui.dev.clients.scopes.profile": + "ui.admin.users.list.table.ui.dev.clients.scopes.profile", + "ui.admin.users.list.table.ui.dev.session.refresh": + "ui.admin.users.list.table.ui.dev.session.refresh", + "ui.admin.users.list.table.ui.dev.session.refreshing": + "ui.admin.users.list.table.ui.dev.session.refreshing", "ui.admin.users.list.title": "사용자 관리", "ui.admin.users.table.email": "이메일", "ui.admin.users.table.name": "이름", @@ -1193,7 +1470,8 @@ const Map koStrings = { "ui.dev.clients.details.endpoints.title": "OIDC 엔드포인트", "ui.dev.clients.details.redirect.callback_label": "인증 콜백 URL", "ui.dev.clients.details.redirect.label": "리디렉션 URI", - "ui.dev.clients.details.redirect.placeholder": "https://your-app.com/callback, http://localhost:3000/auth/callback", + "ui.dev.clients.details.redirect.placeholder": + "https://your-app.com/callback, http://localhost:3000/auth/callback", "ui.dev.clients.details.redirect.save": "Redirect URIs 저장", "ui.dev.clients.details.redirect.title": "리디렉션 URI 설정", "ui.dev.clients.details.secret.hide": "비밀키 숨기기", @@ -1212,75 +1490,111 @@ const Map koStrings = { "ui.dev.clients.general.create": "앱 생성", "ui.dev.clients.general.display_new": "연동 앱 추가", "ui.dev.clients.general.identity.description": "설명", - "ui.dev.clients.general.identity.description_placeholder": "앱에 대한 간단한 설명을 입력하세요.", + "ui.dev.clients.general.identity.description_placeholder": + "앱에 대한 간단한 설명을 입력하세요.", "ui.dev.clients.general.identity.logo": "앱 로고 URL", - "ui.dev.clients.general.identity.logo_placeholder": "https://example.com/logo.png", + "ui.dev.clients.general.identity.logo_placeholder": + "https://example.com/logo.png", "ui.dev.clients.general.identity.logo_preview": "로고 미리보기", "ui.dev.clients.general.identity.name": "앱 이름", "ui.dev.clients.general.identity.name_placeholder": "예: 멋진 애플리케이션", "ui.dev.clients.general.identity.title": "애플리케이션 정보", - "ui.dev.clients.general.public_key.allowed_algorithms_info": "Headless Login 허용 알고리즘 정보", - "ui.dev.clients.general.public_key.auth_method": "ui.dev.clients.general.public_key.auth_method", - "ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.dev.clients.general.public_key.auth_method_client_secret_basic", - "ui.dev.clients.general.public_key.auth_method_none": "ui.dev.clients.general.public_key.auth_method_none", - "ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.dev.clients.general.public_key.allowed_algorithms_info": + "Headless Login 허용 알고리즘 정보", + "ui.dev.clients.general.public_key.auth_method": + "ui.dev.clients.general.public_key.auth_method", + "ui.dev.clients.general.public_key.auth_method_client_secret_basic": + "ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.dev.clients.general.public_key.auth_method_none": + "ui.dev.clients.general.public_key.auth_method_none", + "ui.dev.clients.general.public_key.auth_method_private_key_jwt": + "ui.dev.clients.general.public_key.auth_method_private_key_jwt", "ui.dev.clients.general.public_key.cache.cached_at": "Cached At", "ui.dev.clients.general.public_key.cache.error": "Last Error", "ui.dev.clients.general.public_key.cache.expires_at": "Expires At", "ui.dev.clients.general.public_key.cache.failures": "Consecutive Failures", "ui.dev.clients.general.public_key.cache.kids": "Cached KIDs", "ui.dev.clients.general.public_key.cache.last_checked_at": "Last Checked", - "ui.dev.clients.general.public_key.cache.last_success": "Last Successful Verification", + "ui.dev.clients.general.public_key.cache.last_success": + "Last Successful Verification", "ui.dev.clients.general.public_key.cache.parsed_key_n": "N", "ui.dev.clients.general.public_key.cache.parsed_keys": "Parsed Keys", "ui.dev.clients.general.public_key.cache.status": "상태", "ui.dev.clients.general.public_key.cache.title": "JWKS Cache", "ui.dev.clients.general.public_key.cache.uri": "JWKS URI", - "ui.dev.clients.general.public_key.guide_toggle": "ui.dev.clients.general.public_key.guide_toggle", - "ui.dev.clients.general.public_key.headless_disabled": "ui.dev.clients.general.public_key.headless_disabled", - "ui.dev.clients.general.public_key.headless_enabled": "ui.dev.clients.general.public_key.headless_enabled", + "ui.dev.clients.general.public_key.guide_toggle": + "ui.dev.clients.general.public_key.guide_toggle", + "ui.dev.clients.general.public_key.headless_disabled": + "ui.dev.clients.general.public_key.headless_disabled", + "ui.dev.clients.general.public_key.headless_enabled": + "ui.dev.clients.general.public_key.headless_enabled", "ui.dev.clients.general.public_key.headless_toggle": "Headless Login", - "ui.dev.clients.general.public_key.jwks_inline": "ui.dev.clients.general.public_key.jwks_inline", - "ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.dev.clients.general.public_key.jwks_inline": + "ui.dev.clients.general.public_key.jwks_inline", + "ui.dev.clients.general.public_key.jwks_inline_placeholder": + "ui.dev.clients.general.public_key.jwks_inline_placeholder", "ui.dev.clients.general.public_key.jwks_uri": "JWKS URI", - "ui.dev.clients.general.public_key.jwks_uri_placeholder": "https://rp.example.com/.well-known/jwks.json", - "ui.dev.clients.general.public_key.request_object_alg": "ui.dev.clients.general.public_key.request_object_alg", - "ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.dev.clients.general.public_key.jwks_uri_placeholder": + "https://rp.example.com/.well-known/jwks.json", + "ui.dev.clients.general.public_key.request_object_alg": + "ui.dev.clients.general.public_key.request_object_alg", + "ui.dev.clients.general.public_key.request_object_alg_placeholder": + "ui.dev.clients.general.public_key.request_object_alg_placeholder", "ui.dev.clients.general.public_key.revoke_cache": "Revoke Cache", - "ui.dev.clients.general.public_key.source": "ui.dev.clients.general.public_key.source", - "ui.dev.clients.general.public_key.source_uri": "ui.dev.clients.general.public_key.source_uri", + "ui.dev.clients.general.public_key.source": + "ui.dev.clients.general.public_key.source", + "ui.dev.clients.general.public_key.source_uri": + "ui.dev.clients.general.public_key.source_uri", "ui.dev.clients.general.public_key.title": "공개키 등록", "ui.dev.clients.general.public_key.validation_title": "저장 전 확인 필요", "ui.dev.clients.general.redirect.label": "리디렉션 URI", - "ui.dev.clients.general.redirect.placeholder": "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)", + "ui.dev.clients.general.redirect.placeholder": + "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)", "ui.dev.clients.general.scopes.add": "Scope 추가", "ui.dev.clients.general.scopes.description_placeholder": "권한에 대한 설명", "ui.dev.clients.general.scopes.name_placeholder": "e.g. profile", - "ui.dev.clients.general.scopes.table.contact_title": "ui.dev.clients.general.scopes.table.contact_title", + "ui.dev.clients.general.scopes.table.contact_title": + "ui.dev.clients.general.scopes.table.contact_title", "ui.dev.clients.general.scopes.table.delete": "삭제", "ui.dev.clients.general.scopes.table.description": "설명", - "ui.dev.clients.general.scopes.table.invalid_format": "ui.dev.clients.general.scopes.table.invalid_format", - "ui.dev.clients.general.scopes.table.login_id_help": "ui.dev.clients.general.scopes.table.login_id_help", + "ui.dev.clients.general.scopes.table.invalid_format": + "ui.dev.clients.general.scopes.table.invalid_format", + "ui.dev.clients.general.scopes.table.login_id_help": + "ui.dev.clients.general.scopes.table.login_id_help", "ui.dev.clients.general.scopes.table.mandatory": "필수", "ui.dev.clients.general.scopes.table.name": "스코프 이름", - "ui.dev.clients.general.scopes.table.password_title": "ui.dev.clients.general.scopes.table.password_title", - "ui.dev.clients.general.scopes.table.reset_password": "ui.dev.clients.general.scopes.table.reset_password", - "ui.dev.clients.general.scopes.table.reset_password_confirm": "ui.dev.clients.general.scopes.table.reset_password_confirm", - "ui.dev.clients.general.scopes.table.reset_password_label": "ui.dev.clients.general.scopes.table.reset_password_label", - "ui.dev.clients.general.scopes.table.role_super_admin": "ui.dev.clients.general.scopes.table.role_super_admin", - "ui.dev.clients.general.scopes.table.role_tenant_admin": "ui.dev.clients.general.scopes.table.role_tenant_admin", - "ui.dev.clients.general.scopes.table.role_user": "ui.dev.clients.general.scopes.table.role_user", - "ui.dev.clients.general.scopes.table.status_active": "ui.dev.clients.general.scopes.table.status_active", - "ui.dev.clients.general.scopes.table.status_inactive": "ui.dev.clients.general.scopes.table.status_inactive", - "ui.dev.clients.general.scopes.table.status_title": "ui.dev.clients.general.scopes.table.status_title", + "ui.dev.clients.general.scopes.table.password_title": + "ui.dev.clients.general.scopes.table.password_title", + "ui.dev.clients.general.scopes.table.reset_password": + "ui.dev.clients.general.scopes.table.reset_password", + "ui.dev.clients.general.scopes.table.reset_password_confirm": + "ui.dev.clients.general.scopes.table.reset_password_confirm", + "ui.dev.clients.general.scopes.table.reset_password_label": + "ui.dev.clients.general.scopes.table.reset_password_label", + "ui.dev.clients.general.scopes.table.role_super_admin": + "ui.dev.clients.general.scopes.table.role_super_admin", + "ui.dev.clients.general.scopes.table.role_tenant_admin": + "ui.dev.clients.general.scopes.table.role_tenant_admin", + "ui.dev.clients.general.scopes.table.role_user": + "ui.dev.clients.general.scopes.table.role_user", + "ui.dev.clients.general.scopes.table.status_active": + "ui.dev.clients.general.scopes.table.status_active", + "ui.dev.clients.general.scopes.table.status_inactive": + "ui.dev.clients.general.scopes.table.status_inactive", + "ui.dev.clients.general.scopes.table.status_title": + "ui.dev.clients.general.scopes.table.status_title", "ui.dev.clients.general.scopes.title": "스코프", - "ui.dev.clients.general.security.headless_login_enable": "Headless Login (자체 로그인 UI 사용)", - "ui.dev.clients.general.security.headless_login_enable_help": "Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다.", + "ui.dev.clients.general.security.headless_login_enable": + "Headless Login (자체 로그인 UI 사용)", + "ui.dev.clients.general.security.headless_login_enable_help": + "Baron SSO 로그인 창을 거치지 않고 애플리케이션 내의 자체 로그인 화면을 직접 구현하고 싶은 경우 활성화합니다.", "ui.dev.clients.general.security.pkce": "PKCE", "ui.dev.clients.general.security.private": "Server side App", "ui.dev.clients.general.security.title": "보안 설정", - "ui.dev.clients.general.security.trusted_rp_enable": "ui.dev.clients.general.security.trusted_rp_enable", - "ui.dev.clients.general.security.trusted_rp_enable_help": "ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.dev.clients.general.security.trusted_rp_enable": + "ui.dev.clients.general.security.trusted_rp_enable", + "ui.dev.clients.general.security.trusted_rp_enable_help": + "ui.dev.clients.general.security.trusted_rp_enable_help", "ui.dev.clients.general.subtitle": "앱 설정과 보안 구성을 관리합니다.", "ui.dev.clients.general.title_create": "연동 앱 생성", "ui.dev.clients.general.title_edit": "연동 앱 설정", @@ -1424,7 +1738,8 @@ const Map koStrings = { "ui.userfront.login.field.login_id": "이메일 또는 휴대폰 번호", "ui.userfront.login.field.password": "비밀번호", "ui.userfront.login.forgot_password": "비밀번호를 잊으셨나요?", - "ui.userfront.login.link.action_label": "ui.userfront.login.link.action_label", + "ui.userfront.login.link.action_label": + "ui.userfront.login.link.action_label", "ui.userfront.login.link.code_only": "코드만 받기({{time}})", "ui.userfront.login.link.page_title": "ui.userfront.login.link.page_title", "ui.userfront.login.link.resend_with_time": "재발송 ({{time}})", @@ -1529,102 +1844,145 @@ const Map enStrings = { "domain.tenant_type.company_group": "Company Group", "domain.tenant_type.personal": "Personal", "domain.tenant_type.user_group": "User Group", - "err.backend.authorization_pending": "Authentication approval is still pending.", + "err.backend.authorization_pending": + "Authentication approval is still pending.", "err.backend.bad_request": "Please check your request.", "err.backend.conflict": "The request conflicts with the current state.", "err.backend.expired_token": "The token has expired.", "err.backend.forbidden": "This request is not allowed.", - "err.backend.internal_error": "An internal error occurred while processing the request.", + "err.backend.internal_error": + "An internal error occurred while processing the request.", "err.backend.invalid_code": "The verification code is invalid.", - "err.backend.invalid_or_expired_code": "The verification code is invalid or expired.", + "err.backend.invalid_or_expired_code": + "The verification code is invalid or expired.", "err.backend.invalid_session": "The session is invalid.", "err.backend.invalid_session_reference": "The session reference is invalid.", "err.backend.not_found": "The requested authentication flow was not found.", "err.backend.not_supported": "This login method is not supported.", "err.backend.password_or_email_mismatch": "Email or password does not match.", "err.backend.rate_limited": "Too many requests. Please try again later.", - "err.backend.service_unavailable": "The authentication service is currently unavailable.", - "err.backend.slow_down": "Requests are too frequent. Please try again shortly.", + "err.backend.service_unavailable": + "The authentication service is currently unavailable.", + "err.backend.slow_down": + "Requests are too frequent. Please try again shortly.", "err.common.unknown": "An unknown error occurred.", - "err.userfront.auth_proxy.consent_accept": "Failed to accept the consent request.", + "err.userfront.auth_proxy.consent_accept": + "Failed to accept the consent request.", "err.userfront.auth_proxy.consent_fetch": "Failed to load consent details.", - "err.userfront.auth_proxy.consent_reject": "Failed to reject the consent request.", - "err.userfront.auth_proxy.linked_app_revoke": "Failed to revoke the linked application.", + "err.userfront.auth_proxy.consent_reject": + "Failed to reject the consent request.", + "err.userfront.auth_proxy.linked_app_revoke": + "Failed to revoke the linked application.", "err.userfront.auth_proxy.login_failed": "Login failed.", "err.userfront.auth_proxy.oidc_accept": "OIDC Accept", - "err.userfront.auth_proxy.password_reset_complete": "Failed to complete the password reset.", - "err.userfront.auth_proxy.password_reset_init": "Failed to start the password reset.", + "err.userfront.auth_proxy.password_reset_complete": + "Failed to complete the password reset.", + "err.userfront.auth_proxy.password_reset_init": + "Failed to start the password reset.", "err.userfront.profile.load_failed": "Failed to load the profile.", "err.userfront.profile.password_change_failed": "Password Change Failed", - "err.userfront.profile.send_code_failed": "Failed to send the verification code.", + "err.userfront.profile.send_code_failed": + "Failed to send the verification code.", "err.userfront.profile.update_failed": "Failed to update the profile.", "err.userfront.profile.verify_code_failed": "Verification failed.", "err.userfront.session.missing": "No active session was found.", "msg.admin.api_keys.create.error": "Failed to create the API key.", "msg.admin.api_keys.create.name_required": "Name is required.", "msg.admin.api_keys.create.scope_required": "Select at least one scope.", - "msg.admin.api_keys.create.scopes_count": "{{count}} scopes will be assigned.", - "msg.admin.api_keys.create.scopes_hint": "Choose the scopes to grant to this API key.", - "msg.admin.api_keys.create.subtitle": "Create and issue an API key for machine-to-machine communication.", - "msg.admin.api_keys.create.success.copy_hint": "Copy the secret now. It will not be shown again.", - "msg.admin.api_keys.create.success.notice": "The generated secret is displayed only once.", - "msg.admin.api_keys.create.success.notice_emphasis": "Store it in a secure location.", - "msg.admin.api_keys.create.success.notice_suffix": "Rotate the key immediately if you think it has been exposed.", - "msg.admin.api_keys.list.delete_confirm": "Are you sure you want to delete this API key?", + "msg.admin.api_keys.create.scopes_count": + "{{count}} scopes will be assigned.", + "msg.admin.api_keys.create.scopes_hint": + "Choose the scopes to grant to this API key.", + "msg.admin.api_keys.create.subtitle": + "Create and issue an API key for machine-to-machine communication.", + "msg.admin.api_keys.create.success.copy_hint": + "Copy the secret now. It will not be shown again.", + "msg.admin.api_keys.create.success.notice": + "The generated secret is displayed only once.", + "msg.admin.api_keys.create.success.notice_emphasis": + "Store it in a secure location.", + "msg.admin.api_keys.create.success.notice_suffix": + "Rotate the key immediately if you think it has been exposed.", + "msg.admin.api_keys.list.delete_confirm": + "Are you sure you want to delete this API key?", "msg.admin.api_keys.list.empty": "No API keys have been issued yet.", "msg.admin.api_keys.list.fetch_error": "Failed to load the API key list.", "msg.admin.api_keys.list.registry.count": "{{count}} API keys loaded.", - "msg.admin.api_keys.list.subtitle": "View and manage the API keys issued for server-to-server communication.", + "msg.admin.api_keys.list.subtitle": + "View and manage the API keys issued for server-to-server communication.", "msg.admin.audit.empty": "No audit logs have been collected yet.", "msg.admin.audit.end": "End of audit feed", "msg.admin.audit.filters.empty": "No filters applied.", "msg.admin.audit.load_error": "Error loading logs: {{error}}", "msg.admin.audit.loading": "Loading audit logs...", "msg.admin.audit.registry.count": "{{count}} logs loaded.", - "msg.admin.audit.subtitle": "Review command-driven ClickHouse audit logs from the admin workspace.", - "msg.admin.common.forbidden": "You do not have permission to perform this action.", - "msg.admin.groups.create.description": "Adds a new organization unit such as a department or team.", + "msg.admin.audit.subtitle": + "Review command-driven ClickHouse audit logs from the admin workspace.", + "msg.admin.common.forbidden": + "You do not have permission to perform this action.", + "msg.admin.groups.create.description": + "Adds a new organization unit such as a department or team.", "msg.admin.groups.create.title": "Create New Organization Unit", - "msg.admin.groups.list.create_error": "Failed to create the organization unit.", - "msg.admin.groups.list.create_success": "Organization unit created successfully.", - "msg.admin.groups.list.delete_confirm": "Are you sure you want to delete this organization unit?", - "msg.admin.groups.list.delete_error": "Failed to delete the organization unit.", - "msg.admin.groups.list.delete_success": "Organization unit deleted successfully.", - "msg.admin.groups.list.empty": "No organization units have been registered yet.", + "msg.admin.groups.list.create_error": + "Failed to create the organization unit.", + "msg.admin.groups.list.create_success": + "Organization unit created successfully.", + "msg.admin.groups.list.delete_confirm": + "Are you sure you want to delete this organization unit?", + "msg.admin.groups.list.delete_error": + "Failed to delete the organization unit.", + "msg.admin.groups.list.delete_success": + "Organization unit deleted successfully.", + "msg.admin.groups.list.empty": + "No organization units have been registered yet.", "msg.admin.groups.list.import_error": "Import Error", "msg.admin.groups.list.import_success": "Import Success", "msg.admin.groups.list.loading": "Loading...", - "msg.admin.groups.list.subtitle": "Manage departments and teams under the current tenant.", + "msg.admin.groups.list.subtitle": + "Manage departments and teams under the current tenant.", "msg.admin.groups.members.add_success": "Member added successfully.", "msg.admin.groups.members.count": "{{count}} members loaded.", - "msg.admin.groups.members.empty": "No members are assigned to this organization unit.", - "msg.admin.groups.members.remove_confirm": "Are you sure you want to remove this member?", + "msg.admin.groups.members.empty": + "No members are assigned to this organization unit.", + "msg.admin.groups.members.remove_confirm": + "Are you sure you want to remove this member?", "msg.admin.groups.members.remove_success": "Member removed successfully.", "msg.admin.groups.members.title": "Member Management", "msg.admin.groups.prompt.user_id": "Enter the user's UUID to add:", "msg.admin.groups.roles.assign_success": "Assign Success", - "msg.admin.groups.roles.description": "Assign or revoke roles for members of this organization unit.", + "msg.admin.groups.roles.description": + "Assign or revoke roles for members of this organization unit.", "msg.admin.groups.roles.empty": "No roles have been assigned yet.", - "msg.admin.groups.roles.remove_confirm": "Are you sure you want to revoke this role?", + "msg.admin.groups.roles.remove_confirm": + "Are you sure you want to revoke this role?", "msg.admin.groups.roles.remove_success": "Role revoked successfully.", "msg.admin.header.subtitle": "Tenant isolation & least privilege by default", "msg.admin.idp_env_prod": "IDP env: prod", "msg.admin.logout_confirm": "Are you sure you want to log out?", - "msg.admin.notice.idp_policy": "IDP management keys are only used through server-side wrapper APIs with audit logging and rate limits enabled.", - "msg.admin.notice.scope": "Administrative features are exposed only within the /admin namespace.", + "msg.admin.notice.idp_policy": + "IDP management keys are only used through server-side wrapper APIs with audit logging and rate limits enabled.", + "msg.admin.notice.scope": + "Administrative features are exposed only within the /admin namespace.", "msg.admin.org.hover_member_info": "Hover to see member details.", - "msg.admin.org.import_description": "Upload a CSV file to bulk register the organization chart.", - "msg.admin.org.import_error": "An error occurred during organization chart import.", + "msg.admin.org.import_description": + "Upload a CSV file to bulk register the organization chart.", + "msg.admin.org.import_error": + "An error occurred during organization chart import.", "msg.admin.org.import_success": "Organization chart imported successfully.", - "msg.admin.overview.description": "Review shared metrics and policy status across all tenants in one place.", + "msg.admin.overview.description": + "Review shared metrics and policy status across all tenants in one place.", "msg.admin.overview.idp_fallback": "Fallback: Descope", "msg.admin.overview.idp_primary": "IDP: Ory primary", - "msg.admin.overview.playbook.description": "Operational guardrails and architecture decisions for the admin control plane.", - "msg.admin.overview.playbook.idp_body": "All IDP calls are routed through the backend only. Hydra and Kratos admin ports are never exposed publicly.", + "msg.admin.overview.playbook.description": + "Operational guardrails and architecture decisions for the admin control plane.", + "msg.admin.overview.playbook.idp_body": + "All IDP calls are routed through the backend only. Hydra and Kratos admin ports are never exposed publicly.", "msg.admin.overview.playbook.idp_title": "Backend-only IDP access", - "msg.admin.overview.playbook.tenant_body": "Tenant headers and audit logging are enabled by default and can later be extended with Keto policies.", + "msg.admin.overview.playbook.tenant_body": + "Tenant headers and audit logging are enabled by default and can later be extended with Keto policies.", "msg.admin.overview.playbook.tenant_title": "Tenant isolation", - "msg.admin.overview.quick_links.description": "Jump to the most frequently used administrative workflows.", + "msg.admin.overview.quick_links.description": + "Jump to the most frequently used administrative workflows.", "msg.admin.overview.summary.audit_events_24h": "24h Audit Events", "msg.admin.overview.summary.oidc_clients": "OIDC Clients", "msg.admin.overview.summary.policy_gate": "Policy Gate Status", @@ -1634,61 +1992,86 @@ const Map enStrings = { "msg.admin.tenant_headers": "Tenant-aware headers", "msg.admin.tenants.admins.add_success": "Tenant admin added successfully.", "msg.admin.tenants.admins.empty": "No tenant admins are assigned yet.", - "msg.admin.tenants.admins.remove_confirm": "Are you sure you want to remove this tenant admin?", + "msg.admin.tenants.admins.remove_confirm": + "Are you sure you want to remove this tenant admin?", "msg.admin.tenants.admins.remove_last": "Cannot remove the last admin.", "msg.admin.tenants.admins.remove_self": "Cannot remove yourself.", - "msg.admin.tenants.admins.remove_success": "Tenant admin removed successfully.", - "msg.admin.tenants.admins.subtitle": "Manage the administrators assigned to this tenant.", + "msg.admin.tenants.admins.remove_success": + "Tenant admin removed successfully.", + "msg.admin.tenants.admins.subtitle": + "Manage the administrators assigned to this tenant.", "msg.admin.tenants.approve_confirm": "Do you want to approve this tenant?", "msg.admin.tenants.approve_success": "Tenant approved successfully.", - "msg.admin.tenants.create.form.domains_help": "Users with these email domains will be automatically assigned to this tenant.", - "msg.admin.tenants.create.memo.body": "Leave operational notes or policy reminders for this tenant.", - "msg.admin.tenants.create.memo.subtitle": "Capture internal policy notes for administrators.", - "msg.admin.tenants.create.profile.subtitle": "Set the basic tenant profile information.", - "msg.admin.tenants.create.subtitle": "Enter the minimum required information to create a tenant.", + "msg.admin.tenants.create.form.domains_help": + "Users with these email domains will be automatically assigned to this tenant.", + "msg.admin.tenants.create.memo.body": + "Leave operational notes or policy reminders for this tenant.", + "msg.admin.tenants.create.memo.subtitle": + "Capture internal policy notes for administrators.", + "msg.admin.tenants.create.profile.subtitle": + "Set the basic tenant profile information.", + "msg.admin.tenants.create.subtitle": + "Enter the minimum required information to create a tenant.", "msg.admin.tenants.delete_confirm": "Delete Tenant \\\\\\\"{{name}}\\\\\\\"?", "msg.admin.tenants.delete_success": "Tenant deleted.", "msg.admin.tenants.empty": "No tenants have been registered yet.", "msg.admin.tenants.fetch_error": "Failed to load the tenant list.", - "msg.admin.tenants.members.desc": "View the list of users belonging to this organization.", + "msg.admin.tenants.members.desc": + "View the list of users belonging to this organization.", "msg.admin.tenants.members.empty": "No members found.", - "msg.admin.tenants.members.limit_notice": "Showing members from the first 10 descendant organizations due to size limits.", + "msg.admin.tenants.members.limit_notice": + "Showing members from the first 10 descendant organizations due to size limits.", "msg.admin.tenants.missing_id": "No Tenant ID.", "msg.admin.tenants.not_found": "Tenant not found.", "msg.admin.tenants.owners.add_success": "Owner added successfully.", "msg.admin.tenants.owners.empty": "No owners registered.", - "msg.admin.tenants.owners.remove_confirm": "Are you sure you want to remove this owner?", + "msg.admin.tenants.owners.remove_confirm": + "Are you sure you want to remove this owner?", "msg.admin.tenants.owners.remove_last": "Cannot remove the last owner.", "msg.admin.tenants.owners.remove_self": "Cannot remove yourself.", "msg.admin.tenants.owners.remove_success": "Owner permission revoked.", - "msg.admin.tenants.owners.subtitle": "List of owners with top-level permissions for this tenant.", + "msg.admin.tenants.owners.subtitle": + "List of owners with top-level permissions for this tenant.", "msg.admin.tenants.registry.count": "{{count}} tenants loaded.", - "msg.admin.tenants.remove_sub_confirm": "Remove tenant \\\"{{name}}\\\" from sub-tenants?", - "msg.admin.tenants.schema.empty": "No custom fields defined. Click \\\\\\\"Add Field\\\\\\\" to begin.", - "msg.admin.tenants.schema.forbidden_desc": "Only administrators can access user schema settings.", + "msg.admin.tenants.remove_sub_confirm": + "Remove tenant \\\"{{name}}\\\" from sub-tenants?", + "msg.admin.tenants.schema.empty": + "No custom fields defined. Click \\\\\\\"Add Field\\\\\\\" to begin.", + "msg.admin.tenants.schema.forbidden_desc": + "Only administrators can access user schema settings.", "msg.admin.tenants.schema.missing_id": "Tenant ID missing", - "msg.admin.tenants.schema.subtitle": "Define custom attributes for users in this tenant.", + "msg.admin.tenants.schema.subtitle": + "Define custom attributes for users in this tenant.", "msg.admin.tenants.schema.update_error": "Failed to update schema", "msg.admin.tenants.schema.update_success": "Schema updated successfully", "msg.admin.tenants.sub.empty": "No child tenants are connected.", - "msg.admin.tenants.sub.subtitle": "Review and manage child tenants linked under this tenant.", - "msg.admin.tenants.subtitle": "Review registered tenants and manage their current status.", - "msg.admin.users.bulk.delete_confirm": "Are you sure you want to delete the selected {{count}} users?", + "msg.admin.tenants.sub.subtitle": + "Review and manage child tenants linked under this tenant.", + "msg.admin.tenants.subtitle": + "Review registered tenants and manage their current status.", + "msg.admin.users.bulk.delete_confirm": + "Are you sure you want to delete the selected {{count}} users?", "msg.admin.users.bulk.delete_success": "{{count}} users have been deleted.", - "msg.admin.users.bulk.description": "Bulk register or manage users via CSV file.", - "msg.admin.users.bulk.move_description": "Bulk move selected users to another tenant.", + "msg.admin.users.bulk.description": + "Bulk register or manage users via CSV file.", + "msg.admin.users.bulk.move_description": + "Bulk move selected users to another tenant.", "msg.admin.users.bulk.move_error": "Error moving users.", "msg.admin.users.bulk.move_success": "{{count}} users moved successfully.", "msg.admin.users.bulk.parsed_count": "Parsed {{count}} rows.", - "msg.admin.users.bulk.schema_incompatible": "Fields not in target schema may be lost:", - "msg.admin.users.bulk.schema_missing": "Missing required fields for target tenant:", + "msg.admin.users.bulk.schema_incompatible": + "Fields not in target schema may be lost:", + "msg.admin.users.bulk.schema_missing": + "Missing required fields for target tenant:", "msg.admin.users.bulk.update_success": "User info updated successfully.", - "msg.admin.users.create.account.subtitle": "Fill in the account details required to create the user.", + "msg.admin.users.create.account.subtitle": + "Fill in the account details required to create the user.", "msg.admin.users.create.error": "Failed to User Create.", "msg.admin.users.create.form.email_required": "Email Required", "msg.admin.users.create.form.field_invalid": "Invalid {{label}} format.", "msg.admin.users.create.form.field_required": "{{label}} is required.", - "msg.admin.users.create.form.login_id_help": "msg.admin.users.create.form.login_id_help", + "msg.admin.users.create.form.login_id_help": + "msg.admin.users.create.form.login_id_help", "msg.admin.users.create.form.name_required": "Name is required.", "msg.admin.users.create.form.password_auto_help": "Password Auto Help", "msg.admin.users.create.form.password_manual_help": "Password Manual Help", @@ -1697,7 +2080,8 @@ const Map enStrings = { "msg.admin.users.create.password_generated.with_email": "With Email", "msg.admin.users.create.password_required": "Password Required", "msg.admin.users.create.success": "User created successfully.", - "msg.admin.users.detail.delete_confirm": "Are you sure you want to delete this user?", + "msg.admin.users.detail.delete_confirm": + "Are you sure you want to delete this user?", "msg.admin.users.detail.delete_error": "msg.admin.users.detail.delete_error", "msg.admin.users.detail.delete_success": "User deleted.", "msg.admin.users.detail.edit_subtitle": "Edit Subtitle", @@ -1708,26 +2092,35 @@ const Map enStrings = { "msg.admin.users.detail.no_history": "No History", "msg.admin.users.detail.no_tenants": "No Tenants", "msg.admin.users.detail.not_found": "Not Found", - "msg.admin.users.detail.password_generated": "A secure password has been generated.", - "msg.admin.users.detail.password_generated_help": "Generate a temporary password that meets the security policy and apply it immediately.", + "msg.admin.users.detail.password_generated": + "A secure password has been generated.", + "msg.admin.users.detail.password_generated_help": + "Generate a temporary password that meets the security policy and apply it immediately.", "msg.admin.users.detail.password_manual_required": "Please enter a password.", "msg.admin.users.detail.reset_auto_desc": "Reset Auto Desc", - "msg.admin.users.detail.reset_password_confirm": "msg.admin.users.detail.reset_password_confirm", - "msg.admin.users.detail.reset_password_help": "Force-reset the user's password and apply either an auto-generated password or a manually entered one.", + "msg.admin.users.detail.reset_password_confirm": + "msg.admin.users.detail.reset_password_confirm", + "msg.admin.users.detail.reset_password_help": + "Force-reset the user's password and apply either an auto-generated password or a manually entered one.", "msg.admin.users.detail.security.password_hint": "Password Hint", "msg.admin.users.detail.security_desc": "Security Desc", - "msg.admin.users.detail.self_password_reset_blocked": "Please change your own password from the UserFront settings page.", + "msg.admin.users.detail.self_password_reset_blocked": + "Please change your own password from the UserFront settings page.", "msg.admin.users.detail.tenant_slug_help": "Tenant Slug Help", "msg.admin.users.detail.tenants_desc": "Tenants Desc", "msg.admin.users.detail.update_error": "Failed to User Edit.", "msg.admin.users.detail.update_success": "Update Success", - "msg.admin.users.list.columns.description": "Select columns to display in the table.", - "msg.admin.users.list.columns.no_custom": "No custom fields defined for this tenant.", - "msg.admin.users.list.delete_confirm": "Are you sure you want to delete the selected user?", + "msg.admin.users.list.columns.description": + "Select columns to display in the table.", + "msg.admin.users.list.columns.no_custom": + "No custom fields defined for this tenant.", + "msg.admin.users.list.delete_confirm": + "Are you sure you want to delete the selected user?", "msg.admin.users.list.empty": "No users match the current filters.", "msg.admin.users.list.fetch_error": "Failed to load the user list.", "msg.admin.users.list.registry.count": "{{count}} users loaded.", - "msg.admin.users.list.subtitle": "Search and manage users registered in the current tenant.", + "msg.admin.users.list.subtitle": + "Search and manage users registered in the current tenant.", "msg.common.copied": "Copied", "msg.common.copied_to_clipboard": "Copied to clipboard.", "msg.common.error": "An error occurred.", @@ -1739,20 +2132,27 @@ const Map enStrings = { "msg.common.saving": "Saving...", "msg.common.unknown_error": "unknown error", "msg.dev.audit.empty": "No audit logs found.", - "msg.dev.audit.forbidden": "You do not have permission to view audit logs. Please request access from an administrator.", + "msg.dev.audit.forbidden": + "You do not have permission to view audit logs. Please request access from an administrator.", "msg.dev.audit.load_error": "Error loading audit logs: {{error}}", "msg.dev.audit.loaded_count": "Loaded {{count}} rows", "msg.dev.audit.loading": "Loading audit logs...", - "msg.dev.audit.subtitle": "Shows DevFront activity history within current tenant/app scope.", - "msg.dev.auth.access_denied_description": "DevFront is for administrators only. Request access from your administrator.", + "msg.dev.audit.subtitle": + "Shows DevFront activity history within current tenant/app scope.", + "msg.dev.auth.access_denied_description": + "DevFront is for administrators only. Request access from your administrator.", "msg.dev.auth.access_denied_title": "Access denied.", "msg.dev.clients.consents.empty": "No consents found.", "msg.dev.clients.consents.load_error": "Error loading consents: {{error}}", "msg.dev.clients.consents.loading": "Loading consents...", - "msg.dev.clients.consents.revoke_confirm": "Are you sure you want to revoke this user's permissions? After revocation, the user must consent again on next login.", - "msg.dev.clients.consents.showing": "Showing {{from}} to {{to}} of {{total}} users", - "msg.dev.clients.consents.subtitle": "Review consent grants and users who have approved this application.", - "msg.dev.clients.delete_confirm": "Are you sure you want to delete this app? This action cannot be undone.", + "msg.dev.clients.consents.revoke_confirm": + "Are you sure you want to revoke this user's permissions? After revocation, the user must consent again on next login.", + "msg.dev.clients.consents.showing": + "Showing {{from}} to {{to}} of {{total}} users", + "msg.dev.clients.consents.subtitle": + "Review consent grants and users who have approved this application.", + "msg.dev.clients.delete_confirm": + "Are you sure you want to delete this app? This action cannot be undone.", "msg.dev.clients.delete_error": "Failed to delete: {{error}}", "msg.dev.clients.deleted": "App deleted.", "msg.dev.clients.details.copy_client_id": "Client ID copied.", @@ -1761,94 +2161,153 @@ const Map enStrings = { "msg.dev.clients.details.load_error": "Error loading client: {{error}}", "msg.dev.clients.details.loading": "Loading client...", "msg.dev.clients.details.missing_id": "Client ID is required.", - "msg.dev.clients.details.redirect.description": "List the allowed URLs that users can be redirected to after authentication. Separate multiple values with commas.", + "msg.dev.clients.details.redirect.description": + "List the allowed URLs that users can be redirected to after authentication. Separate multiple values with commas.", "msg.dev.clients.details.redirect_saved": "Redirect URIs saved.", "msg.dev.clients.details.rotate_confirm": "Rotate Confirm", "msg.dev.clients.details.rotate_error": "Rotate Error", "msg.dev.clients.details.save_error": "Save Error", "msg.dev.clients.details.secret_rotated": "Secret Rotated", - "msg.dev.clients.details.secret_unavailable": "The client secret is not available.", - "msg.dev.clients.details.security.footer": "When rotating a secret, confirm the admin session TTL, rate limits, and notification flow.", - "msg.dev.clients.details.security.note": "Keep endpoints read-only and link secret copy or rotation actions to audit logs.", - "msg.dev.clients.details.subtitle": "Inspect this application's credentials, endpoints, and security settings.", - "msg.dev.clients.federation.add_subtitle": "Connect an external OIDC provider.", + "msg.dev.clients.details.secret_unavailable": + "The client secret is not available.", + "msg.dev.clients.details.security.footer": + "When rotating a secret, confirm the admin session TTL, rate limits, and notification flow.", + "msg.dev.clients.details.security.note": + "Keep endpoints read-only and link secret copy or rotation actions to audit logs.", + "msg.dev.clients.details.subtitle": + "Inspect this application's credentials, endpoints, and security settings.", + "msg.dev.clients.federation.add_subtitle": + "Connect an external OIDC provider.", "msg.dev.clients.federation.empty": "No IdP configurations found.", - "msg.dev.clients.federation.subtitle": "Manage external identity providers for this application.", + "msg.dev.clients.federation.subtitle": + "Manage external identity providers for this application.", "msg.dev.clients.general.identity.logo_help": "Logo Help", - "msg.dev.clients.general.identity.subtitle": "Manage the OIDC identity, branding, and basic metadata for this application.", + "msg.dev.clients.general.identity.subtitle": + "Manage the OIDC identity, branding, and basic metadata for this application.", "msg.dev.clients.general.load_error": "Error loading client: {{error}}", "msg.dev.clients.general.loading": "Loading client...", - "msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "Allowed Algorithms Tooltip", - "msg.dev.clients.general.public_key.auth_method_client_secret_basic_help": "Standard authentication method for server-side applications.", - "msg.dev.clients.general.public_key.auth_method_none_help": "Use this for PKCE-based public clients.", - "msg.dev.clients.general.public_key.auth_method_private_key_jwt_help": "Signed key-based client authentication recommended for trusted RP bootstrap and JAR verification.", - "msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "Missing Algorithm Badge", - "msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "Missing Algorithm Reason", - "msg.dev.clients.general.public_key.cache.missing_algorithms_help": "Missing Algorithms Help", - "msg.dev.clients.general.public_key.cache.missing_algorithms_title": "Missing Algorithms Title", - "msg.dev.clients.general.public_key.cache.parsed_keys_empty": "No parsed JWKS keys are available yet.", - "msg.dev.clients.general.public_key.cache.parsed_keys_help": "Raw JWKS stays hidden. Only parsed key metadata is shown here.", - "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "Unsupported Algorithm Reason", - "msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "Unsupported Algorithms Help", - "msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "Unsupported Algorithms Title", + "msg.dev.clients.general.public_key.allowed_algorithms_tooltip": + "Allowed Algorithms Tooltip", + "msg.dev.clients.general.public_key.auth_method_client_secret_basic_help": + "Standard authentication method for server-side applications.", + "msg.dev.clients.general.public_key.auth_method_none_help": + "Use this for PKCE-based public clients.", + "msg.dev.clients.general.public_key.auth_method_private_key_jwt_help": + "Signed key-based client authentication recommended for trusted RP bootstrap and JAR verification.", + "msg.dev.clients.general.public_key.cache.missing_algorithm_badge": + "Missing Algorithm Badge", + "msg.dev.clients.general.public_key.cache.missing_algorithm_reason": + "Missing Algorithm Reason", + "msg.dev.clients.general.public_key.cache.missing_algorithms_help": + "Missing Algorithms Help", + "msg.dev.clients.general.public_key.cache.missing_algorithms_title": + "Missing Algorithms Title", + "msg.dev.clients.general.public_key.cache.parsed_keys_empty": + "No parsed JWKS keys are available yet.", + "msg.dev.clients.general.public_key.cache.parsed_keys_help": + "Raw JWKS stays hidden. Only parsed key metadata is shown here.", + "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": + "Unsupported Algorithm Reason", + "msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": + "Unsupported Algorithms Help", + "msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": + "Unsupported Algorithms Title", "msg.dev.clients.general.public_key.cache_empty": "Cache Empty", "msg.dev.clients.general.public_key.cache_help": "Cache Help", - "msg.dev.clients.general.public_key.cache_refresh_failed": "Cache Refresh Failed", + "msg.dev.clients.general.public_key.cache_refresh_failed": + "Cache Refresh Failed", "msg.dev.clients.general.public_key.cache_refreshed": "Cache Refreshed", - "msg.dev.clients.general.public_key.cache_revoke_confirm": "Cache Revoke Confirm", - "msg.dev.clients.general.public_key.cache_revoke_failed": "Cache Revoke Failed", + "msg.dev.clients.general.public_key.cache_revoke_confirm": + "Cache Revoke Confirm", + "msg.dev.clients.general.public_key.cache_revoke_failed": + "Cache Revoke Failed", "msg.dev.clients.general.public_key.cache_revoked": "Cache Revoked", - "msg.dev.clients.general.public_key.guide_example": "Recommended example: https://rp.example.com/.well-known/jwks.json", - "msg.dev.clients.general.public_key.guide_intro": "A JWKS URI is not created by Baron. It is the URL where the RP backend exposes its public key.", - "msg.dev.clients.general.public_key.guide_step_1": "Generate a key pair on the RP server and keep the private key only in the RP backend.", - "msg.dev.clients.general.public_key.guide_step_2": "Expose the public key from the RP backend through a JWKS (JSON Web Key Set) endpoint.", - "msg.dev.clients.general.public_key.guide_step_3": "Enter a URL such as https://rp.example.com/.well-known/jwks.json in DevFront.", - "msg.dev.clients.general.public_key.headless_help": "You can design your own login UI within the application. While the UI is yours, the actual identity verification and security checks are handled in the background via Baron's API.", - "msg.dev.clients.general.public_key.jwks_inline_help": "Prefer the SSH-RSA public key format first. If you paste an 'ssh-rsa AAA...' key, Baron converts it to OIDC-standard JWKS (JSON) before saving.", - "msg.dev.clients.general.public_key.jwks_uri_help": "Enter the public key endpoint URL exposed by the RP backend. Example: https://rp.example.com/.well-known/jwks.json", - "msg.dev.clients.general.public_key.request_object_alg_help": "Specify the JAR (Request Object) signing algorithm used for headless login.", - "msg.dev.clients.general.public_key.source_help": "Register the JWKS URI served by the RP so Baron can verify the public key.", - "msg.dev.clients.general.public_key.subtitle": "Manage the public key and headless login settings required for trusted RP evaluation.", - "msg.dev.clients.general.public_key.validation.headless_requires_alg": "Headless login requires a Request Object Signing Algorithm.", - "msg.dev.clients.general.public_key.validation.headless_requires_private_key_jwt": "Headless login requires token endpoint auth method to be private_key_jwt.", - "msg.dev.clients.general.public_key.validation.headless_requires_public_key": "Headless login requires a JWKS URI.", - "msg.dev.clients.general.public_key.validation.invalid_jwks_inline": "The input must be valid JSON (JWKS). For SSH-RSA input, it must start with 'ssh-rsa'.", - "msg.dev.clients.general.public_key.validation.invalid_jwks_uri": "JWKS URI format is invalid.", - "msg.dev.clients.general.public_key.validation.missing_jwks_inline": "Enter a public key in SSH-RSA or JWKS format.", - "msg.dev.clients.general.public_key.validation.missing_jwks_uri": "JWKS URI is required.", - "msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "Missing Parsed Algorithms", - "msg.dev.clients.general.public_key.validation.private_key_jwt_requires_public_key": "Signed key-based authentication requires a JWKS URI.", - "msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "Unsupported Parsed Algorithms", - "msg.dev.clients.general.redirect.help": "Enter the redirect URIs. You can modify them in the Federation tab after creation.", + "msg.dev.clients.general.public_key.guide_example": + "Recommended example: https://rp.example.com/.well-known/jwks.json", + "msg.dev.clients.general.public_key.guide_intro": + "A JWKS URI is not created by Baron. It is the URL where the RP backend exposes its public key.", + "msg.dev.clients.general.public_key.guide_step_1": + "Generate a key pair on the RP server and keep the private key only in the RP backend.", + "msg.dev.clients.general.public_key.guide_step_2": + "Expose the public key from the RP backend through a JWKS (JSON Web Key Set) endpoint.", + "msg.dev.clients.general.public_key.guide_step_3": + "Enter a URL such as https://rp.example.com/.well-known/jwks.json in DevFront.", + "msg.dev.clients.general.public_key.headless_help": + "You can design your own login UI within the application. While the UI is yours, the actual identity verification and security checks are handled in the background via Baron's API.", + "msg.dev.clients.general.public_key.jwks_inline_help": + "Prefer the SSH-RSA public key format first. If you paste an 'ssh-rsa AAA...' key, Baron converts it to OIDC-standard JWKS (JSON) before saving.", + "msg.dev.clients.general.public_key.jwks_uri_help": + "Enter the public key endpoint URL exposed by the RP backend. Example: https://rp.example.com/.well-known/jwks.json", + "msg.dev.clients.general.public_key.request_object_alg_help": + "Specify the JAR (Request Object) signing algorithm used for headless login.", + "msg.dev.clients.general.public_key.source_help": + "Register the JWKS URI served by the RP so Baron can verify the public key.", + "msg.dev.clients.general.public_key.subtitle": + "Manage the public key and headless login settings required for trusted RP evaluation.", + "msg.dev.clients.general.public_key.validation.headless_requires_alg": + "Headless login requires a Request Object Signing Algorithm.", + "msg.dev.clients.general.public_key.validation.headless_requires_private_key_jwt": + "Headless login requires token endpoint auth method to be private_key_jwt.", + "msg.dev.clients.general.public_key.validation.headless_requires_public_key": + "Headless login requires a JWKS URI.", + "msg.dev.clients.general.public_key.validation.invalid_jwks_inline": + "The input must be valid JSON (JWKS). For SSH-RSA input, it must start with 'ssh-rsa'.", + "msg.dev.clients.general.public_key.validation.invalid_jwks_uri": + "JWKS URI format is invalid.", + "msg.dev.clients.general.public_key.validation.missing_jwks_inline": + "Enter a public key in SSH-RSA or JWKS format.", + "msg.dev.clients.general.public_key.validation.missing_jwks_uri": + "JWKS URI is required.", + "msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": + "Missing Parsed Algorithms", + "msg.dev.clients.general.public_key.validation.private_key_jwt_requires_public_key": + "Signed key-based authentication requires a JWKS URI.", + "msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": + "Unsupported Parsed Algorithms", + "msg.dev.clients.general.redirect.help": + "Enter the redirect URIs. You can modify them in the Federation tab after creation.", "msg.dev.clients.general.save_error": "Failed to save: {{error}}", "msg.dev.clients.general.saved": "Saved", - "msg.dev.clients.general.scopes.empty": "No custom scopes have been added yet.", - "msg.dev.clients.general.scopes.subtitle": "Define the scopes this application can request.", - "msg.dev.clients.general.security.pkce_help": "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory.", - "msg.dev.clients.general.security.private_help": "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers.", - "msg.dev.clients.general.security.subtitle": "Select application type. Security level determines authentication method.", + "msg.dev.clients.general.scopes.empty": + "No custom scopes have been added yet.", + "msg.dev.clients.general.scopes.subtitle": + "Define the scopes this application can request.", + "msg.dev.clients.general.security.pkce_help": + "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory.", + "msg.dev.clients.general.security.private_help": + "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers.", + "msg.dev.clients.general.security.subtitle": + "Select application type. Security level determines authentication method.", "msg.dev.clients.general.status_changed": "Status changed to {{status}}.", - "msg.dev.clients.help.docs_body": "Includes PKCE, client_secret_basic, redirect URI validation tips.", - "msg.dev.clients.help.subtitle": "Developer guides for Confidential/Public clients, redirect URIs, and auth methods.", + "msg.dev.clients.help.docs_body": + "Includes PKCE, client_secret_basic, redirect URI validation tips.", + "msg.dev.clients.help.subtitle": + "Developer guides for Confidential/Public clients, redirect URIs, and auth methods.", "msg.dev.clients.load_error": "Error loading clients: {{error}}", "msg.dev.clients.loading": "Loading apps...", - "msg.dev.clients.registry.description": "Manage OIDC applications, authentication methods, redirect URIs, and secret key reissue with audit logs.", + "msg.dev.clients.registry.description": + "Manage OIDC applications, authentication methods, redirect URIs, and secret key reissue with audit logs.", "msg.dev.clients.scopes.email": "Email", "msg.dev.clients.scopes.openid": "Openid", "msg.dev.clients.scopes.profile": "Profile", "msg.dev.clients.showing": "Showing {{shown}} of {{total}} apps", - "msg.dev.dashboard.hero.body": "Monitor RP readiness, consent activity, and operational status for the current developer workspace.", + "msg.dev.dashboard.hero.body": + "Monitor RP readiness, consent activity, and operational status for the current developer workspace.", "msg.dev.dashboard.hero.title_emphasis": "Title Emphasis", "msg.dev.dashboard.hero.title_prefix": "Title Prefix", "msg.dev.dashboard.hero.title_suffix": "Title Suffix", "msg.dev.dashboard.notice.consent_audit": "Consent Audit", "msg.dev.dashboard.notice.dev_scope": "Dev Scope", "msg.dev.dashboard.notice.hydra_health": "Hydra Health", - "msg.dev.forbidden.default": "You do not have permission to access this resource. Please contact an administrator.", - "msg.dev.forbidden.rp_admin": "RP administrators can only access resources for the apps they manage.", - "msg.dev.forbidden.tenant_admin": "Tenant administrator permissions are not configured correctly or have expired.", + "msg.dev.forbidden.default": + "You do not have permission to access this resource. Please contact an administrator.", + "msg.dev.forbidden.rp_admin": + "RP administrators can only access resources for the apps they manage.", + "msg.dev.forbidden.tenant_admin": + "Tenant administrator permissions are not configured correctly or have expired.", "msg.dev.forbidden.title": "Access Denied: {{resource}}", - "msg.dev.forbidden.user": "Regular users cannot access the developer console.", + "msg.dev.forbidden.user": + "Regular users cannot access the developer console.", "msg.dev.logout_confirm": "Are you sure you want to log out?", "msg.dev.sidebar.notice": "Developer Console", "msg.dev.sidebar.notice_detail": "Register and manage client applications.", @@ -1862,27 +2321,41 @@ const Map enStrings = { "msg.userfront.audit.session_id": "Session ID: {{value}}", "msg.userfront.audit.status": "Status: pending", "msg.userfront.consent.accept_error": "Failed to process consent: {{error}}", - "msg.userfront.consent.cancel.confirm": "If you cancel consent, you will not be able to use this service. Do you want to cancel?", - "msg.userfront.consent.cancel.error": "An error occurred while cancelling consent: {{error}}", + "msg.userfront.consent.cancel.confirm": + "If you cancel consent, you will not be able to use this service. Do you want to cancel?", + "msg.userfront.consent.cancel.error": + "An error occurred while cancelling consent: {{error}}", "msg.userfront.consent.client_id": "Client ID: {{id}}", "msg.userfront.consent.client_unknown": "Unknown application", - "msg.userfront.consent.description": "The service below is requesting access to your account information.\\\\nPlease choose whether to continue.", - "msg.userfront.consent.load_error": "Failed to load consent information: {{error}}", - "msg.userfront.consent.missing_redirect": "Consent was processed, but the redirect URL was missing.", - "msg.userfront.consent.redirect_notice": "After consent, you will be redirected automatically.", - "msg.userfront.consent.scope.email": "Email address (account identification and notifications)", - "msg.userfront.consent.scope.offline_access": "Offline access (keep signed in)", - "msg.userfront.consent.scope.openid": "OpenID authentication information (signin session check)", - "msg.userfront.consent.scope.phone": "Phone number (identity verification and notifications)", - "msg.userfront.consent.scope.profile": "Basic profile information (name, user identifier)", + "msg.userfront.consent.description": + "The service below is requesting access to your account information.\\\\nPlease choose whether to continue.", + "msg.userfront.consent.load_error": + "Failed to load consent information: {{error}}", + "msg.userfront.consent.missing_redirect": + "Consent was processed, but the redirect URL was missing.", + "msg.userfront.consent.redirect_notice": + "After consent, you will be redirected automatically.", + "msg.userfront.consent.scope.email": + "Email address (account identification and notifications)", + "msg.userfront.consent.scope.offline_access": + "Offline access (keep signed in)", + "msg.userfront.consent.scope.openid": + "OpenID authentication information (signin session check)", + "msg.userfront.consent.scope.phone": + "Phone number (identity verification and notifications)", + "msg.userfront.consent.scope.profile": + "Basic profile information (name, user identifier)", "msg.userfront.consent.scope_count": "Total {{count}}", "msg.userfront.dashboard.activities.empty": "No linked apps yet.", - "msg.userfront.dashboard.activities.empty_detail": "Linked apps and their latest activity will appear here.", + "msg.userfront.dashboard.activities.empty_detail": + "Linked apps and their latest activity will appear here.", "msg.userfront.dashboard.activities.error": "Could not load linked apps.", "msg.userfront.dashboard.approved_device": "Approved device: {{device}}", "msg.userfront.dashboard.approved_ip": "Approved IP: {{ip}}", - "msg.userfront.dashboard.approved_session.copy_click": "{{label}}: {{id}}\\\\\\\\\\\\\\\\nClick to copy.", - "msg.userfront.dashboard.approved_session.copy_tap": "{{label}}: {{id}}\\\\\\\\\\\\\\\\nTap to copy.", + "msg.userfront.dashboard.approved_session.copy_click": + "{{label}}: {{id}}\\\\\\\\\\\\\\\\nClick to copy.", + "msg.userfront.dashboard.approved_session.copy_tap": + "{{label}}: {{id}}\\\\\\\\\\\\\\\\nTap to copy.", "msg.userfront.dashboard.approved_session.none": "No {{label}}", "msg.userfront.dashboard.audit_empty": "No recent sign-in activity.", "msg.userfront.dashboard.audit_load_error": "Could not load sign-in history.", @@ -1891,92 +2364,140 @@ const Map enStrings = { "msg.userfront.dashboard.client_id_missing": "No client ID available.", "msg.userfront.dashboard.current_status": "Current status: {{status}}", "msg.userfront.dashboard.last_auth": "Last signed in: {{value}}", - "msg.userfront.dashboard.link_missing": "This app does not have a launch URL configured.", + "msg.userfront.dashboard.link_missing": + "This app does not have a launch URL configured.", "msg.userfront.dashboard.link_open_error": "Could not open the app link.", "msg.userfront.dashboard.render_error": "Dashboard render error: {{error}}", - "msg.userfront.dashboard.revoke.confirm": "Disconnect {{app}}?\\\\\\\\\\\\\\\\nYou will need to grant access again the next time you sign in.", - "msg.userfront.dashboard.revoke.error": "Could not disconnect the app: {{error}}", + "msg.userfront.dashboard.revoke.confirm": + "Disconnect {{app}}?\\\\\\\\\\\\\\\\nYou will need to grant access again the next time you sign in.", + "msg.userfront.dashboard.revoke.error": + "Could not disconnect the app: {{error}}", "msg.userfront.dashboard.revoke.success": "{{app}} has been disconnected.", "msg.userfront.dashboard.scopes.empty": "No scopes were requested.", "msg.userfront.dashboard.session_id_copied": "Session ID copied.", "msg.userfront.dashboard.sessions.browser": "Browser: {{value}}", "msg.userfront.dashboard.sessions.empty": "No active sessions.", - "msg.userfront.dashboard.sessions.empty_detail": "Devices signed in with this account will appear here.", + "msg.userfront.dashboard.sessions.empty_detail": + "Devices signed in with this account will appear here.", "msg.userfront.dashboard.sessions.error": "Could not load sessions.", "msg.userfront.dashboard.sessions.os": "OS: {{value}}", "msg.userfront.dashboard.sessions.recent_app": "Recent app: {{app}}", - "msg.userfront.dashboard.sessions.revoke.confirm": "End the session for {{target}}?\nThat device will need to sign in again.", - "msg.userfront.dashboard.sessions.revoke.error": "Could not end the session: {{error}}", - "msg.userfront.dashboard.sessions.revoke.success": "The session has been ended.", + "msg.userfront.dashboard.sessions.revoke.confirm": + "End the session for {{target}}?\nThat device will need to sign in again.", + "msg.userfront.dashboard.sessions.revoke.error": + "Could not end the session: {{error}}", + "msg.userfront.dashboard.sessions.revoke.success": + "The session has been ended.", "msg.userfront.dashboard.sessions.session_id": "Session ID: {{id}}", - "msg.userfront.dashboard.timeline.load_error": "Could not load sign-in history.", - "msg.userfront.error.detail_contact": "If the problem continues, please contact your administrator.", + "msg.userfront.dashboard.timeline.load_error": + "Could not load sign-in history.", + "msg.userfront.error.detail_contact": + "If the problem continues, please contact your administrator.", "msg.userfront.error.detail_generic": "Something went wrong.", - "msg.userfront.error.detail_request": "We had trouble processing your request.", + "msg.userfront.error.detail_request": + "We had trouble processing your request.", "msg.userfront.error.id": "Error ID: {{id}}", "msg.userfront.error.ory.\"\$normalizedCode\"": "{{error}}", - "msg.userfront.error.ory.access_denied": "The user denied the consent request.", - "msg.userfront.error.ory.consent_required": "Consent is required to continue.", - "msg.userfront.error.ory.interaction_required": "Additional interaction is required. Please try again.", + "msg.userfront.error.ory.access_denied": + "The user denied the consent request.", + "msg.userfront.error.ory.consent_required": + "Consent is required to continue.", + "msg.userfront.error.ory.interaction_required": + "Additional interaction is required. Please try again.", "msg.userfront.error.ory.invalid_client": "Client authentication failed.", - "msg.userfront.error.ory.invalid_grant": "The authorization grant is invalid or expired.", + "msg.userfront.error.ory.invalid_grant": + "The authorization grant is invalid or expired.", "msg.userfront.error.ory.invalid_request": "The request is invalid.", "msg.userfront.error.ory.invalid_scope": "The requested scope is invalid.", "msg.userfront.error.ory.login_required": "Login is required.", "msg.userfront.error.ory.request_forbidden": "The request was forbidden.", - "msg.userfront.error.ory.server_error": "An authentication server error occurred.", - "msg.userfront.error.ory.temporarily_unavailable": "The authentication server is temporarily unavailable.", - "msg.userfront.error.ory.unauthorized_client": "The client is not authorized for this request.", - "msg.userfront.error.ory.unsupported_response_type": "The response type is not supported.", + "msg.userfront.error.ory.server_error": + "An authentication server error occurred.", + "msg.userfront.error.ory.temporarily_unavailable": + "The authentication server is temporarily unavailable.", + "msg.userfront.error.ory.unauthorized_client": + "The client is not authorized for this request.", + "msg.userfront.error.ory.unsupported_response_type": + "The response type is not supported.", "msg.userfront.error.title": "An error occurred during authentication.", "msg.userfront.error.title_generic": "An error occurred.", "msg.userfront.error.title_with_code": "Error: {{code}}", "msg.userfront.error.type": "Error type: {{type}}", "msg.userfront.error.whitelist.\"\$normalizedCode\"": "{{error}}", "msg.userfront.error.whitelist.bad_request": "Please check your input.", - "msg.userfront.error.whitelist.invalid_session": "Your session has expired. Please sign in again.", - "msg.userfront.error.whitelist.not_found": "The requested page could not be found.", - "msg.userfront.error.whitelist.password_or_email_mismatch": "Email or password does not match.", - "msg.userfront.error.whitelist.rate_limited": "Too many requests. Please try again later.", - "msg.userfront.error.whitelist.recovery_expired": "The recovery link has expired. Please request a new one.", - "msg.userfront.error.whitelist.recovery_invalid": "The recovery link is invalid.", - "msg.userfront.error.whitelist.settings_disabled": "Account settings are currently unavailable.", - "msg.userfront.error.whitelist.verification_required": "Additional verification is required. Please follow the instructions.", - "msg.userfront.forgot.description": "Enter the email address or phone number linked to your account and we will send you a password reset link.", - "msg.userfront.forgot.dry_send": "Dry-send mode: no email or SMS was actually sent.", + "msg.userfront.error.whitelist.invalid_session": + "Your session has expired. Please sign in again.", + "msg.userfront.error.whitelist.not_found": + "The requested page could not be found.", + "msg.userfront.error.whitelist.password_or_email_mismatch": + "Email or password does not match.", + "msg.userfront.error.whitelist.rate_limited": + "Too many requests. Please try again later.", + "msg.userfront.error.whitelist.recovery_expired": + "The recovery link has expired. Please request a new one.", + "msg.userfront.error.whitelist.recovery_invalid": + "The recovery link is invalid.", + "msg.userfront.error.whitelist.settings_disabled": + "Account settings are currently unavailable.", + "msg.userfront.error.whitelist.verification_required": + "Additional verification is required. Please follow the instructions.", + "msg.userfront.forgot.description": + "Enter the email address or phone number linked to your account and we will send you a password reset link.", + "msg.userfront.forgot.dry_send": + "Dry-send mode: no email or SMS was actually sent.", "msg.userfront.forgot.error": "Failed to send the reset link: {{error}}", - "msg.userfront.forgot.input_required": "Enter your email address or phone number.", - "msg.userfront.forgot.sent": "A password reset link has been sent. Check your email or SMS.", + "msg.userfront.forgot.input_required": + "Enter your email address or phone number.", + "msg.userfront.forgot.sent": + "A password reset link has been sent. Check your email or SMS.", "msg.userfront.greeting": "Hello, {{name}}.", - "msg.userfront.login.cookie_check_failed": "Could not verify your sign-in state: {{error}}", - "msg.userfront.login.dry_send": "Dry-send mode: no email or SMS was actually sent.", - "msg.userfront.login.link.approved": "Sign-in approved. You will be redirected to the sign-in page shortly.", - "msg.userfront.login.link.helper": "We will send a sign-in link using the information you enter.", - "msg.userfront.login.link.missing_login_id": "Enter your email address or phone number.", + "msg.userfront.login.cookie_check_failed": + "Could not verify your sign-in state: {{error}}", + "msg.userfront.login.dry_send": + "Dry-send mode: no email or SMS was actually sent.", + "msg.userfront.login.link.approved": + "Sign-in approved. You will be redirected to the sign-in page shortly.", + "msg.userfront.login.link.helper": + "We will send a sign-in link using the information you enter.", + "msg.userfront.login.link.missing_login_id": + "Enter your email address or phone number.", "msg.userfront.login.link.missing_phone": "Enter your phone number.", "msg.userfront.login.link.resend_wait": "You can resend in {{time}}.", - "msg.userfront.login.link.short_code_help": "You can also sign in with the last 2 letters and 6 digits from the link you received.", - "msg.userfront.login.link_failed": "Could not complete link sign-in: {{error}}", - "msg.userfront.login.link_send_failed": "Failed to send the sign-in link: {{error}}", - "msg.userfront.login.link_sent_email": "We sent a sign-in link to your email address.", - "msg.userfront.login.link_sent_phone": "We sent a sign-in link to your phone number.", + "msg.userfront.login.link.short_code_help": + "You can also sign in with the last 2 letters and 6 digits from the link you received.", + "msg.userfront.login.link_failed": + "Could not complete link sign-in: {{error}}", + "msg.userfront.login.link_send_failed": + "Failed to send the sign-in link: {{error}}", + "msg.userfront.login.link_sent_email": + "We sent a sign-in link to your email address.", + "msg.userfront.login.link_sent_phone": + "We sent a sign-in link to your phone number.", "msg.userfront.login.link_timeout": "Time expired.", "msg.userfront.login.no_account": "New to Baron?", "msg.userfront.login.oidc_failed": "OIDC sign-in failed. Please try again.", "msg.userfront.login.password.failed": "Sign-in failed: {{error}}", - "msg.userfront.login.password.missing_credentials": "Enter both your email or phone number and your password.", + "msg.userfront.login.password.missing_credentials": + "Enter both your email or phone number and your password.", "msg.userfront.login.qr.load_failed": "Could not load the QR code.", "msg.userfront.login.qr.scan_hint": "Scan it with the mobile app.", "msg.userfront.login.qr_expired": "Time expired.", - "msg.userfront.login.qr_init_failed": "Failed to initialize QR sign-in: {{error}}", - "msg.userfront.login.qr_login_required": "You need to be signed in to approve a QR sign-in.", - "msg.userfront.login.short_code.invalid": "Enter the 2 letters and 6 digits from your code.", + "msg.userfront.login.qr_init_failed": + "Failed to initialize QR sign-in: {{error}}", + "msg.userfront.login.qr_login_required": + "You need to be signed in to approve a QR sign-in.", + "msg.userfront.login.short_code.invalid": + "Enter the 2 letters and 6 digits from your code.", "msg.userfront.login.token_missing": "Could not find the sign-in token.", - "msg.userfront.login.unregistered.body": "We could not find an account for that information.\\\\\\\\\\\\\\\\nPlease sign up before continuing.", - "msg.userfront.login.verification.approved": "Approved. Complete sign-in in the original window.", - "msg.userfront.login.verification.approved_local": "Approved. This device is already signed in, and the remote window will be signed in shortly.", + "msg.userfront.login.unregistered.body": + "We could not find an account for that information.\\\\\\\\\\\\\\\\nPlease sign up before continuing.", + "msg.userfront.login.verification.approved": + "Approved. Complete sign-in in the original window.", + "msg.userfront.login.verification.approved_local": + "Approved. This device is already signed in, and the remote window will be signed in shortly.", "msg.userfront.login.verification.success": "Sign-in approval completed.", - "msg.userfront.login.verification_failed": "Failed to approve the sign-in request: {{error}}", + "msg.userfront.login.verification_failed": + "Failed to approve the sign-in request: {{error}}", "msg.userfront.login_success.subtitle": "You have signed in successfully.", "msg.userfront.profile.department_missing": "No department information", "msg.userfront.profile.department_required": "Enter your department.", @@ -1985,40 +2506,57 @@ const Map enStrings = { "msg.userfront.profile.load_failed": "Could not load your profile.", "msg.userfront.profile.name_missing": "No name provided", "msg.userfront.profile.name_required": "Enter your name.", - "msg.userfront.profile.password.change_failed": "Failed to change your password: {{error}}", + "msg.userfront.profile.password.change_failed": + "Failed to change your password: {{error}}", "msg.userfront.profile.password.changed": "Your password has been changed.", - "msg.userfront.profile.password.current_required": "Enter your current password.", + "msg.userfront.profile.password.current_required": + "Enter your current password.", "msg.userfront.profile.password.mismatch": "The new passwords do not match.", "msg.userfront.profile.password.new_required": "Enter a new password.", - "msg.userfront.profile.password.subtitle": "Verify your current password before setting a new one.", + "msg.userfront.profile.password.subtitle": + "Verify your current password before setting a new one.", "msg.userfront.profile.phone.code_sent": "A verification code has been sent.", - "msg.userfront.profile.phone.send_failed": "Failed to send the code: {{error}}", + "msg.userfront.profile.phone.send_failed": + "Failed to send the code: {{error}}", "msg.userfront.profile.phone.verified": "Phone number verified.", "msg.userfront.profile.phone.verify_failed": "Verification failed: {{error}}", - "msg.userfront.profile.phone.verify_notice": "SMS verification is required to change your phone number.", + "msg.userfront.profile.phone.verify_notice": + "SMS verification is required to change your phone number.", "msg.userfront.profile.phone_required": "Enter your phone number.", - "msg.userfront.profile.phone_verify_required": "Phone verification is required.", - "msg.userfront.profile.section.basic": "Manage your basic account information.", - "msg.userfront.profile.section.organization": "Your organization and affiliation details.", + "msg.userfront.profile.phone_verify_required": + "Phone verification is required.", + "msg.userfront.profile.section.basic": + "Manage your basic account information.", + "msg.userfront.profile.section.organization": + "Your organization and affiliation details.", "msg.userfront.profile.section.security": "Keep your password secure.", - "msg.userfront.profile.update_failed": "Failed to update your profile: {{error}}", + "msg.userfront.profile.update_failed": + "Failed to update your profile: {{error}}", "msg.userfront.profile.update_success": "Your profile has been updated.", "msg.userfront.qr.approve_error": "QR approval failed: {{error}}", - "msg.userfront.qr.approve_success": "QR approval complete. Continue on your desktop.", + "msg.userfront.qr.approve_success": + "QR approval complete. Continue on your desktop.", "msg.userfront.qr.camera_error": "Camera error: {{error}}", - "msg.userfront.qr.permission_error": "Could not request camera access. Check your browser or OS settings.", + "msg.userfront.qr.permission_error": + "Could not request camera access. Check your browser or OS settings.", "msg.userfront.qr.permission_required": "Camera access is required.", "msg.userfront.reset.error.empty_password": "Please enter Password.", - "msg.userfront.reset.error.generic": "Failed to change your password: {{error}}", - "msg.userfront.reset.error.lowercase": "Include at least one lowercase letter.", + "msg.userfront.reset.error.generic": + "Failed to change your password: {{error}}", + "msg.userfront.reset.error.lowercase": + "Include at least one lowercase letter.", "msg.userfront.reset.error.min_length": "Use at least {{count}} characters.", - "msg.userfront.reset.error.min_types": "Use at least {{count}} character types: uppercase, lowercase, number, or symbol.", + "msg.userfront.reset.error.min_types": + "Use at least {{count}} character types: uppercase, lowercase, number, or symbol.", "msg.userfront.reset.error.mismatch": "The passwords do not match.", "msg.userfront.reset.error.number": "Include at least one number.", "msg.userfront.reset.error.symbol": "Include at least one symbol.", - "msg.userfront.reset.error.uppercase": "Include at least one uppercase letter.", - "msg.userfront.reset.invalid_body": "This password reset link is invalid or has expired. Please request a new one.", - "msg.userfront.reset.invalid_link": "This reset link is invalid. Missing loginId or token.", + "msg.userfront.reset.error.uppercase": + "Include at least one uppercase letter.", + "msg.userfront.reset.invalid_body": + "This password reset link is invalid or has expired. Please request a new one.", + "msg.userfront.reset.invalid_link": + "This reset link is invalid. Missing loginId or token.", "msg.userfront.reset.invalid_title": "Invalid reset link", "msg.userfront.reset.policy.lowercase": "At least one lowercase letter", "msg.userfront.reset.policy.min_length": "At least {{count}} characters", @@ -2027,43 +2565,69 @@ const Map enStrings = { "msg.userfront.reset.policy.symbol": "At least one symbol", "msg.userfront.reset.policy.uppercase": "At least one uppercase letter", "msg.userfront.reset.policy_loading": "Loading the password policy...", - "msg.userfront.reset.success": "Your password has been changed successfully. Please sign in again.", - "msg.userfront.sections.apps_subtitle": "Your linked apps and their latest sign-in status.", - "msg.userfront.sections.audit_subtitle": "Recent access history for Baron sign-in.", - "msg.userfront.sections.sessions_subtitle": "Your currently signed-in devices and browser sessions.", - "msg.userfront.settings.disabled": "Account settings are currently unavailable.", - "msg.userfront.signup.agreement.all_hint": "Agree to both required documents to continue to the next step.", - "msg.userfront.signup.agreement.description": "Review the service terms and privacy collection notice, then agree to continue.", - "msg.userfront.signup.agreement.privacy_summary": "Review what personal data is collected, why it is used, and how it is retained.", - "msg.userfront.signup.agreement.progress": "{{count}} of {{total}} required agreements completed", - "msg.userfront.signup.agreement.title": "Please review and agree to the terms to continue.", - "msg.userfront.signup.agreement.tos_summary": "Review the service terms, usage conditions, and responsibilities.", - "msg.userfront.signup.auth.affiliate_notice": "If you are an affiliate employee, use your official company email address.", + "msg.userfront.reset.success": + "Your password has been changed successfully. Please sign in again.", + "msg.userfront.sections.apps_subtitle": + "Your linked apps and their latest sign-in status.", + "msg.userfront.sections.audit_subtitle": + "Recent access history for Baron sign-in.", + "msg.userfront.sections.sessions_subtitle": + "Your currently signed-in devices and browser sessions.", + "msg.userfront.settings.disabled": + "Account settings are currently unavailable.", + "msg.userfront.signup.agreement.all_hint": + "Agree to both required documents to continue to the next step.", + "msg.userfront.signup.agreement.description": + "Review the service terms and privacy collection notice, then agree to continue.", + "msg.userfront.signup.agreement.privacy_summary": + "Review what personal data is collected, why it is used, and how it is retained.", + "msg.userfront.signup.agreement.progress": + "{{count}} of {{total}} required agreements completed", + "msg.userfront.signup.agreement.title": + "Please review and agree to the terms to continue.", + "msg.userfront.signup.agreement.tos_summary": + "Review the service terms, usage conditions, and responsibilities.", + "msg.userfront.signup.auth.affiliate_notice": + "If you are an affiliate employee, use your official company email address.", "msg.userfront.signup.auth.title": "Verify your email and phone number.", - "msg.userfront.signup.email.code_mismatch": "The verification code does not match.", - "msg.userfront.signup.email.duplicate": "This email address is already registered.", + "msg.userfront.signup.email.code_mismatch": + "The verification code does not match.", + "msg.userfront.signup.email.duplicate": + "This email address is already registered.", "msg.userfront.signup.email.invalid": "Enter a valid email address.", - "msg.userfront.signup.email.send_failed": "Failed to send the email: {{error}}", + "msg.userfront.signup.email.send_failed": + "Failed to send the email: {{error}}", "msg.userfront.signup.email.verified": "Email verified.", - "msg.userfront.signup.email.verify_failed": "Email verification failed: {{error}}", + "msg.userfront.signup.email.verify_failed": + "Email verification failed: {{error}}", "msg.userfront.signup.failed": "Failed", - "msg.userfront.signup.password.length_required": "Your password must be at least 12 characters long.", - "msg.userfront.signup.password.lowercase_required": "Include at least one lowercase letter.", + "msg.userfront.signup.password.length_required": + "Your password must be at least 12 characters long.", + "msg.userfront.signup.password.lowercase_required": + "Include at least one lowercase letter.", "msg.userfront.signup.password.mismatch": "The passwords do not match.", - "msg.userfront.signup.password.number_required": "Include at least one number.", + "msg.userfront.signup.password.number_required": + "Include at least one number.", "msg.userfront.signup.password.rule.lowercase": "Lowercase letter", - "msg.userfront.signup.password.rule.min_length": "At least {{count}} characters", - "msg.userfront.signup.password.rule.min_types": "At least {{count}} character types", + "msg.userfront.signup.password.rule.min_length": + "At least {{count}} characters", + "msg.userfront.signup.password.rule.min_types": + "At least {{count}} character types", "msg.userfront.signup.password.rule.number": "Number", "msg.userfront.signup.password.rule.symbol": "Symbol", "msg.userfront.signup.password.rule.uppercase": "Uppercase letter", - "msg.userfront.signup.password.symbol_required": "Include at least one symbol.", - "msg.userfront.signup.password.title": "Create a secure password to finish signing up.", - "msg.userfront.signup.password.uppercase_required": "Include at least one uppercase letter.", - "msg.userfront.signup.phone.code_mismatch": "The verification code does not match.", + "msg.userfront.signup.password.symbol_required": + "Include at least one symbol.", + "msg.userfront.signup.password.title": + "Create a secure password to finish signing up.", + "msg.userfront.signup.password.uppercase_required": + "Include at least one uppercase letter.", + "msg.userfront.signup.phone.code_mismatch": + "The verification code does not match.", "msg.userfront.signup.phone.send_failed": "Failed to send the SMS: {{error}}", "msg.userfront.signup.phone.verified": "Phone number verified.", - "msg.userfront.signup.phone.verify_failed": "Phone verification failed: {{error}}", + "msg.userfront.signup.phone.verify_failed": + "Phone verification failed: {{error}}", "msg.userfront.signup.policy.loading": "Loading the password policy...", "msg.userfront.signup.policy.lowercase": "Lowercase letter", "msg.userfront.signup.policy.min_length": "At least {{count}} characters", @@ -2072,12 +2636,16 @@ const Map enStrings = { "msg.userfront.signup.policy.summary": "Security policy: {{rules}}", "msg.userfront.signup.policy.symbol": "Symbol", "msg.userfront.signup.policy.uppercase": "Uppercase letter", - "msg.userfront.signup.privacy_full": "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 수집 및 이용 동의\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론서비스 개인정보처리방침\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론컨설턴트(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"회사\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")는 바론서비스(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")를 이용하는 고객(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"이용자\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")의 개인정보를 보호하고, 「개인정보 보호법」에 따라 책임과 의무를 다하기 위해 본 개인정보처리방침을 마련했습니다. 본 방침은 이용자가 제공한 개인정보가 어떻게 수집, 이용, 보관, 보호되는지를 설명합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (개인정보의 처리목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 다음의 목적을 위해 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 본인확인: 회원가입 및 관리를 위한 본인 확인, 전화 또는 이메일을 통한 연락\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 제공: 각종 통보 및 서비스 제공을 위한 업무 처리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제품소개서 다운로드: 설명자료 전달\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 상담 및 데모 신청: 상담 제공 및 데모 제공, 계약 처리자 정보 수집\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 행사 참가 신청: 참석 안내 및 세미나/설명회/교육 제공\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 보안가이드 제공: 안내자료 전달\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 기술지원 문의: 서비스 사용 지원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 개선 의견 접수: 서비스 품질 개선\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 마케팅 활동: 동의한 고객에 한해 뉴스레터 및 매거진 발송\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (개인정보의 처리 및 보유 기간)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 법령에 따른 개인정보 보유 및 이용기간 또는 정보주체로부터 개인정보를 수집 시 동의받은 개인정보 보유 및 이용기간 내에서 개인정보를 처리 및 보유합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 회원정보: 회원가입일부터 회원탈퇴 후 1년까지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 홍보, 상담, 계약용 개인정보: 2년\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (개인정보의 제3자 제공)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 정보주체의 개인정보를 제2조에서 명시한 범위 내에서만 처리하며, 정보 주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 회사는 다음과 같이 개인정보를 제3자에게 제공하고 있습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제공받는 자: 수사기관 및 유관기관, 피신고업체\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이용 목적: 개인정보 침해 민원 처리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제공하는 개인정보 항목: 성명, 연락처, 이메일\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 보유 및 이용기간: 법령에서 정한 보존기간 및 제공목적 달성 시 파기\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (개인정보 처리 위탁)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 처리업무를 외부 업체에 위탁하지 않으며, 자체적으로 처리하고 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 회사가 특정 업무(예: 채용 업무)를 외부 업체에 위탁할 경우, 개인정보 처리방침 시행 전 회사 홈페이지에서 공지한 후 정보주체의 동의를 받은 후 위탁합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (정보주체의 권리·의무 및 행사 방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 정보주체는 회사에 대해 언제든지 개인정보 열람, 정정, 삭제, 처리정지 요구 등의 권리를 행사할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 권리 행사는 다음과 같은 방법으로 할 수 있습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서면: 회사 주소로 서면 제출\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 전자우편: 회사 이메일로 요청\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 모사전송(FAX): 회사 FAX로 요청\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n③ 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자를 통해 대리로도 가능합니다. 이 경우 “개인정보 처리 방법에 관한 고시” 별지 제11호 서식에 따른 위임장을 제출해야 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 따라 제한될 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에 따라 수집된 개인정보인 경우 제한될 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n⑥ 회사는 권리 행사를 요청한 자가 본인 또는 정당한 대리인인지를 확인합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (처리하는 개인정보의 항목)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 다음의 개인정보 항목을 처리합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 수집 항목:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 필수 항목: 성명, 휴대전화번호, 이메일\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 선택 항목: 회사전화번호, 문의사항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 수집 방법:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 홈페이지, 전화, 이메일을 통해 수집\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (개인정보의 파기)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 보유 기간의 경과, 처리 목적 달성 등 개인정보가 불필요하게 되었을 때 지체 없이 해당 개인정보를 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 정보주체로부터 동의받은 개인정보 보유 기간이 경과하거나 처리 목적이 달성된 경우에도 다른 법령에 따라 개인정보를 계속 보존해야 할 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관 장소를 달리하여 보존합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n③ 개인정보 파기의 절차 및 방법은 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 파기 절차: 회사는 파기 사유가 발생한 개인정보를 선정하고, 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 파기 방법: 전자적 파일 형태로 기록된 개인정보는 복구할 수 없도록 기술적 방법을 사용해 삭제하며, 종이 문서에 기록된 개인정보는 분쇄기로 분쇄하거나 소각하여 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9조 (개인정보의 안전성 확보 조치)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 관리적 조치: 내부관리계획 수립·시행, 정기적 직원 교육\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 기술적 조치: 개인정보처리시스템 접근 권한 관리, 접근통제시스템 설치, 고유식별정보 암호화, 보안 프로그램 설치\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 물리적 조치: 전산실 및 자료보관실 접근 통제\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10조 (개인정보 자동 수집 장치의 설치·운영 및 거부에 관한 사항)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 쿠키(Cookie)를 사용하지 않습니다. 쿠키는 이용자의 이용 정보를 저장하고 수시로 불러오는 작은 파일로, 바론서비스에서는 쿠키를 사용하지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11조 (개인정보 보호책임자)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보 처리에 관한 업무를 총괄하여 책임지고, 개인정보 처리와 관련된 정보주체의 불만처리 및 피해구제를 위해 개인정보 보호책임자를 지정하고 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 보호책임자:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 성명: 염승호\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 직책: 수석연구원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 연락처: 02-2141-7448\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 팩스번호: 02-2141-7599\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일: b23008@baroncs.co.kr\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제12조 (개인정보 열람청구)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n정보주체는 「개인정보 보호법」 제35조에 따른 개인정보 열람 청구를 아래 부서에 할 수 있습니다. 회사는 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 열람청구 접수·처리 부서:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 부서명: 총괄기획실\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 담당자: 권혁진\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 연락처: 02-2141-7465\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 팩스번호: 02-2141-7599\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일: baroncs@baroncs.co.kr\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제13조 (권익침해 구제방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n정보주체는 개인정보 침해로 인한 구제를 위해 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보해신고센터 등에 분쟁 해결이나 상담을 신청할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 개인정보분쟁조정위원회: (국번없이) 1833-6972 (www.kopico.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 개인정보침해신고센터: (국번없이) 118 (privacy.kisa.or.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 대검찰청: (국번없이) 1301 (www.spo.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 경찰청: (국번없이) 182 (www.police.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제14조 (개인정보 처리방침의 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 개인정보처리방침은 법령, 정책 또는 보안 기술의 변경에 따라 내용의 추가, 삭제 및 수정이 있을 시, 개정 최소 7일 전에 홈페이지를 통해 사전 공지합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n부칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (시행일자)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이 개인정보처리방침은 2024년 10월 1일부터 시행됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (개정 및 고지의 의무)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보처리방침을 변경하는 경우, 변경사항을 시행일자 7일 전부터 서비스 내 공지사항 페이지를 통해 고지할 것입니다. 다만, 이용자의 권리나 의무에 중대한 변경이 발생하는 경우에는 시행일자 30일 전부터 고지합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (유효성)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 개인정보처리방침의 일부 조항이 법적 또는 기타 사유로 인해 무효화되거나 시행할 수 없는 경우, 나머지 조항들은 계속해서 유효합니다. 무효화된 조항은 관련 법령에 부합하는 방식으로 수정되어 효력을 지속합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (변경 통지의 방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보처리방침의 변경 시, 다음의 방법으로 이용자에게 고지합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 초기화면 또는 팝업 공지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일 발송\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 회사 홈페이지 공지사항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (비회원의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 비회원의 개인정보도 회원과 동일한 수준으로 보호합니다. 비회원이 개인정보 제공을 거부할 경우 일부 서비스 이용에 제한이 있을 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (14세 미만 아동의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 14세 미만 아동의 개인정보를 수집하지 않습니다. 만일 14세 미만 아동의 개인정보가 수집된 경우, 법정 대리인의 동의를 받아야 하며, 법정 대리인의 동의 없이 수집된 경우 이를 지체 없이 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (개인정보의 국외 이전)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (기타)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n", - "msg.userfront.signup.profile.affiliate_hint": "This will be selected automatically when you use an affiliate email.", + "msg.userfront.signup.privacy_full": + "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 수집 및 이용 동의\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론서비스 개인정보처리방침\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론컨설턴트(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"회사\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")는 바론서비스(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")를 이용하는 고객(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"이용자\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\")의 개인정보를 보호하고, 「개인정보 보호법」에 따라 책임과 의무를 다하기 위해 본 개인정보처리방침을 마련했습니다. 본 방침은 이용자가 제공한 개인정보가 어떻게 수집, 이용, 보관, 보호되는지를 설명합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (개인정보의 처리목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 다음의 목적을 위해 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 본인확인: 회원가입 및 관리를 위한 본인 확인, 전화 또는 이메일을 통한 연락\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 제공: 각종 통보 및 서비스 제공을 위한 업무 처리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제품소개서 다운로드: 설명자료 전달\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 상담 및 데모 신청: 상담 제공 및 데모 제공, 계약 처리자 정보 수집\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 행사 참가 신청: 참석 안내 및 세미나/설명회/교육 제공\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 보안가이드 제공: 안내자료 전달\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 기술지원 문의: 서비스 사용 지원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 개선 의견 접수: 서비스 품질 개선\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 마케팅 활동: 동의한 고객에 한해 뉴스레터 및 매거진 발송\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (개인정보의 처리 및 보유 기간)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 법령에 따른 개인정보 보유 및 이용기간 또는 정보주체로부터 개인정보를 수집 시 동의받은 개인정보 보유 및 이용기간 내에서 개인정보를 처리 및 보유합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 회원정보: 회원가입일부터 회원탈퇴 후 1년까지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 홍보, 상담, 계약용 개인정보: 2년\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (개인정보의 제3자 제공)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 정보주체의 개인정보를 제2조에서 명시한 범위 내에서만 처리하며, 정보 주체의 동의, 법률의 특별한 규정 등 「개인정보 보호법」 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 회사는 다음과 같이 개인정보를 제3자에게 제공하고 있습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제공받는 자: 수사기관 및 유관기관, 피신고업체\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이용 목적: 개인정보 침해 민원 처리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 제공하는 개인정보 항목: 성명, 연락처, 이메일\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 보유 및 이용기간: 법령에서 정한 보존기간 및 제공목적 달성 시 파기\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (개인정보 처리 위탁)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 처리업무를 외부 업체에 위탁하지 않으며, 자체적으로 처리하고 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 회사가 특정 업무(예: 채용 업무)를 외부 업체에 위탁할 경우, 개인정보 처리방침 시행 전 회사 홈페이지에서 공지한 후 정보주체의 동의를 받은 후 위탁합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (정보주체의 권리·의무 및 행사 방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 정보주체는 회사에 대해 언제든지 개인정보 열람, 정정, 삭제, 처리정지 요구 등의 권리를 행사할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 권리 행사는 다음과 같은 방법으로 할 수 있습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서면: 회사 주소로 서면 제출\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 전자우편: 회사 이메일로 요청\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 모사전송(FAX): 회사 FAX로 요청\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n③ 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자를 통해 대리로도 가능합니다. 이 경우 “개인정보 처리 방법에 관한 고시” 별지 제11호 서식에 따른 위임장을 제출해야 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 따라 제한될 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에 따라 수집된 개인정보인 경우 제한될 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n⑥ 회사는 권리 행사를 요청한 자가 본인 또는 정당한 대리인인지를 확인합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (처리하는 개인정보의 항목)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 다음의 개인정보 항목을 처리합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 수집 항목:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 필수 항목: 성명, 휴대전화번호, 이메일\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 선택 항목: 회사전화번호, 문의사항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 수집 방법:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 홈페이지, 전화, 이메일을 통해 수집\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (개인정보의 파기)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 보유 기간의 경과, 처리 목적 달성 등 개인정보가 불필요하게 되었을 때 지체 없이 해당 개인정보를 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n② 정보주체로부터 동의받은 개인정보 보유 기간이 경과하거나 처리 목적이 달성된 경우에도 다른 법령에 따라 개인정보를 계속 보존해야 할 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관 장소를 달리하여 보존합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n③ 개인정보 파기의 절차 및 방법은 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 파기 절차: 회사는 파기 사유가 발생한 개인정보를 선정하고, 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 파기 방법: 전자적 파일 형태로 기록된 개인정보는 복구할 수 없도록 기술적 방법을 사용해 삭제하며, 종이 문서에 기록된 개인정보는 분쇄기로 분쇄하거나 소각하여 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9조 (개인정보의 안전성 확보 조치)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 관리적 조치: 내부관리계획 수립·시행, 정기적 직원 교육\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 기술적 조치: 개인정보처리시스템 접근 권한 관리, 접근통제시스템 설치, 고유식별정보 암호화, 보안 프로그램 설치\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 물리적 조치: 전산실 및 자료보관실 접근 통제\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10조 (개인정보 자동 수집 장치의 설치·운영 및 거부에 관한 사항)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 쿠키(Cookie)를 사용하지 않습니다. 쿠키는 이용자의 이용 정보를 저장하고 수시로 불러오는 작은 파일로, 바론서비스에서는 쿠키를 사용하지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11조 (개인정보 보호책임자)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보 처리에 관한 업무를 총괄하여 책임지고, 개인정보 처리와 관련된 정보주체의 불만처리 및 피해구제를 위해 개인정보 보호책임자를 지정하고 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 보호책임자:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 성명: 염승호\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 직책: 수석연구원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 연락처: 02-2141-7448\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 팩스번호: 02-2141-7599\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일: b23008@baroncs.co.kr\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제12조 (개인정보 열람청구)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n정보주체는 「개인정보 보호법」 제35조에 따른 개인정보 열람 청구를 아래 부서에 할 수 있습니다. 회사는 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n개인정보 열람청구 접수·처리 부서:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 부서명: 총괄기획실\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 담당자: 권혁진\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 연락처: 02-2141-7465\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 팩스번호: 02-2141-7599\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일: baroncs@baroncs.co.kr\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제13조 (권익침해 구제방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n정보주체는 개인정보 침해로 인한 구제를 위해 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보해신고센터 등에 분쟁 해결이나 상담을 신청할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 개인정보분쟁조정위원회: (국번없이) 1833-6972 (www.kopico.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 개인정보침해신고센터: (국번없이) 118 (privacy.kisa.or.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 대검찰청: (국번없이) 1301 (www.spo.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 경찰청: (국번없이) 182 (www.police.go.kr)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제14조 (개인정보 처리방침의 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 개인정보처리방침은 법령, 정책 또는 보안 기술의 변경에 따라 내용의 추가, 삭제 및 수정이 있을 시, 개정 최소 7일 전에 홈페이지를 통해 사전 공지합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n부칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (시행일자)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이 개인정보처리방침은 2024년 10월 1일부터 시행됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (개정 및 고지의 의무)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보처리방침을 변경하는 경우, 변경사항을 시행일자 7일 전부터 서비스 내 공지사항 페이지를 통해 고지할 것입니다. 다만, 이용자의 권리나 의무에 중대한 변경이 발생하는 경우에는 시행일자 30일 전부터 고지합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (유효성)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 개인정보처리방침의 일부 조항이 법적 또는 기타 사유로 인해 무효화되거나 시행할 수 없는 경우, 나머지 조항들은 계속해서 유효합니다. 무효화된 조항은 관련 법령에 부합하는 방식으로 수정되어 효력을 지속합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (변경 통지의 방법)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 개인정보처리방침의 변경 시, 다음의 방법으로 이용자에게 고지합니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 서비스 초기화면 또는 팝업 공지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 이메일 발송\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- 회사 홈페이지 공지사항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (비회원의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 비회원의 개인정보도 회원과 동일한 수준으로 보호합니다. 비회원이 개인정보 제공을 거부할 경우 일부 서비스 이용에 제한이 있을 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (14세 미만 아동의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 14세 미만 아동의 개인정보를 수집하지 않습니다. 만일 14세 미만 아동의 개인정보가 수집된 경우, 법정 대리인의 동의를 받아야 하며, 법정 대리인의 동의 없이 수집된 경우 이를 지체 없이 파기합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (개인정보의 국외 이전)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 이용자의 개인정보를 국외로 이전하지 않으며, 향후 필요한 경우, 사전에 이용자의 동의를 받습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (기타)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 방침에 명시되지 않은 사항은 회사의 내부 방침과 관련 법령에 따릅니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n", + "msg.userfront.signup.profile.affiliate_hint": + "This will be selected automatically when you use an affiliate email.", "msg.userfront.signup.profile.title": "Tell us about your affiliation.", - "msg.userfront.signup.success.body": "Your account has been created successfully.", + "msg.userfront.signup.success.body": + "Your account has been created successfully.", "msg.userfront.signup.success.title": "Sign-up complete", - "msg.userfront.signup.tos_full": "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론 소프트웨어 이용약관\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1장 총칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이 약관은 바론컨설턴트(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"회사\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"라 합니다)가 제공하는 바론소프트웨어(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"라 합니다)를 이용함에 있어 회사와 이용자 간의 권리, 의무 및 책임사항과 기타 필요한 사항을 정하는 것을 목적으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (용어의 정의)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 본 약관에서 사용하는 용어의 정의는 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “서비스”란 회사가 제공하는 소프트웨어 및 관련 제반 서비스를 의미합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “이용자”란 회사의 서비스에 접속하여 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “회원”이란 본 약관에 동의하고 회사와 이용계약을 체결한 자를 의미합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “비회원”이란 회원가입을 하지 않고 회사가 제공하는 일부 서비스를 이용하는 자를 말합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (약관의 효력 및 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 본 약관은 이용자가 본 약관에 동의하고, 회사가 이에 대한 승낙을 완료함으로써 효력이 발생합니다. ② 회사는 필요한 경우 본 약관을 변경할 수 있으며, 변경된 약관은 서비스 화면에 공지된 후 효력이 발생합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (약관 외 준칙)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관에 명시되지 않은 사항에 대해서는 대한민국의 관련 법령과 상관습에 따릅니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2장 서비스 이용계약\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (이용계약의 성립)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이용계약은 이용자가 약관의 내용에 동의하고, 회사가 제공하는 소정의 회원가입 신청서를 작성하여 가입을 완료한 후, 회사가 이를 승인함으로써 성립합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (이용계약의 유보와 거절)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 다음 각 호에 해당하는 경우 이용계약의 성립을 유보하거나 거절할 수 있습니다: - 신청서의 내용이 허위로 판명된 경우 - 서비스 제공이 기술적으로 어려운 경우\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (계약사항의 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 개인정보 관리 메뉴를 통해 언제든지 자신의 정보를 열람하고 수정할 수 있습니다. 회원의 정보가 변경된 경우 즉시 수정해야 하며, 수정하지 않아 발생하는 문제의 책임은 회원에게 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3장 개인정보 보호\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (개인정보 보호의 원칙)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원의 개인정보는 관련 법령에 따라 보호됩니다. ② 회사는 개인정보 보호와 관련된 세부 사항을 별도로 마련한 개인정보처리방침에 따라 관리하며, 이용자는 언제든지 해당 방침을 통해 개인정보 관리에 대한 자세한 내용을 확인할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9조 (개인정보처리방침 준수)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 보호와 관련된 구체적인 사항을 개인정보처리방침에 따라 관리합니다. ② 개인정보의 수집, 이용, 제공, 보관, 보호 등에 관한 사항은 회사의 개인정보처리방침을 따르며, 이용자는 회사 웹사이트에서 이를 확인할 수 있습니다. ③ 회사는 개인정보 보호를 위해 최선을 다하며, 관련 법령에 따라 이용자의 개인정보를 안전하게 관리합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10조 (14세 미만 아동의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 14세 미만 아동의 개인정보를 수집할 경우, 반드시 법정대리인의 동의를 받아야 합니다. ② 법정대리인은 아동의 개인정보 열람, 수정, 삭제를 요청할 수 있으며, 회사는 이를 신속하게 처리합니다. ③ 14세 미만 아동의 개인정보 보호와 관련된 구체적인 사항은 개인정보처리방침에 명시되어 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4장 서비스 제공 및 이용\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11조 (서비스 제공)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원의 이용 신청을 승인한 때부터 서비스를 개시합니다. 서비스 이용은 연중무휴 24시간을 원칙으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제12조 (서비스의 변경 및 중단)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 서비스 제공이 어려운 경우 사전 고지 후 서비스를 변경하거나 중단할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5장 정보 제공 및 광고\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제13조 (정보 제공 및 광고)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 서비스 이용 중 필요하다고 인정되는 정보 및 광고를 제공할 수 있습니다. ② 회원은 원치 않는 정보를 수신 거부할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6장 게시물 관리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제14조 (게시물의 관리)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원이 게시한 내용이 불법적이거나 약관에 위배될 경우 이를 삭제할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제15조 (게시물의 저작권)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n게시물의 저작권은 회원에게 있으며, 회사는 이를 서비스 홍보 및 개선 목적으로 사용할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7장 계약 해지 및 이용 제한\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제16조 (계약 해지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 언제든지 계약 해지를 요청할 수 있으며, 회사는 신속하게 처리합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제17조 (이용 제한)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원이 약관을 위반할 경우 서비스 이용을 제한할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8장 손해 배상 및 면책 조항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제18조 (손해 배상)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 무료로 제공되는 서비스와 관련하여 회원에게 발생한 손해에 대해 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제19조 (면책 조항)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 천재지변 등 불가항력적인 사유로 인해 서비스를 제공하지 못하는 경우 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9장 유료 서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n20조 (유료 서비스의 이용)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 회원에게 특정 서비스에 대해 유료로 제공할 수 있습니다. ② 유료 서비스의 이용 요금, 결제 방식, 환불 절차 등에 대한 상세 내용은 서비스 안내 페이지와 결제 화면에 명시합니다. ③ 유료 서비스 이용 요금은 회사가 정한 결제 방식에 따라 결제됩니다. 회원은 신용카드, 계좌이체, 휴대전화 결제 등 회사가 제공하는 다양한 결제 방식을 통해 요금을 납부할 수 있습니다. ④ 유료 서비스의 이용 요금은 선불 결제를 원칙으로 하며, 이용 기간 중 서비스 중지 및 해지 시 남은 이용 기간에 대한 환불은 회사의 환불 정책에 따라 처리됩니다. ⑤ 회사는 회원의 유료 서비스 이용과 관련하여 발생한 문제에 대해 최선을 다해 해결하도록 노력합니다. 다만, 회사의 고의 또는 중대한 과실이 없는 한 회원이 유료 서비스 이용 중 입은 손해에 대해서는 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제21조(환불 정책)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원은 결제 후 7일 이내에 서비스 이용을 시작하지 않은 경우, 요금 전액을 환불받을 수 있습니다. ② 유료 서비스 이용 중 부득이한 사유로 서비스가 중지된 경우, 회사는 이용하지 않은 부분에 대해 환불 절차를 밟습니다. ③ 회원의 귀책사유로 인해 서비스 이용이 중지된 경우, 환불이 불가능합니다. ④ 환불은 회원이 지정한 계좌로 환불 절차를 거치며, 환불 요청 후 7일 이내에 처리됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제22조 (유료 서비스의 중지 및 해지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원이 유료 서비스를 해지하고자 하는 경우, 회사의 고객 지원 센터에 해지 신청을 해야 합니다. ② 회사는 회원이 약관을 위반하거나 부정한 방법으로 유료 서비스를 이용한 경우, 유료 서비스 이용을 즉시 중지하고 계약을 해지할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10장 양도 금지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제23조 (양도 금지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 서비스 이용권한, 기타 이용계약상의 지위를 제3자에게 양도, 증여할 수 없으며, 이를 담보로 제공할 수 없습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11장 관할 법원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제24조 (분쟁 해결)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n서비스 이용과 관련하여 분쟁이 발생한 경우, 회사와 회원은 성실히 협의하여 해결합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제25조 (관할 법원)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n부칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관은 2024년 10월 1일부터 시행됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n", + "msg.userfront.signup.tos_full": + "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n바론 소프트웨어 이용약관\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1장 총칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제1조 (목적)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이 약관은 바론컨설턴트(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"회사\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"라 합니다)가 제공하는 바론소프트웨어(이하 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"라 합니다)를 이용함에 있어 회사와 이용자 간의 권리, 의무 및 책임사항과 기타 필요한 사항을 정하는 것을 목적으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2조 (용어의 정의)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 본 약관에서 사용하는 용어의 정의는 다음과 같습니다:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “서비스”란 회사가 제공하는 소프트웨어 및 관련 제반 서비스를 의미합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “이용자”란 회사의 서비스에 접속하여 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “회원”이란 본 약관에 동의하고 회사와 이용계약을 체결한 자를 의미합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n- “비회원”이란 회원가입을 하지 않고 회사가 제공하는 일부 서비스를 이용하는 자를 말합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3조 (약관의 효력 및 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 본 약관은 이용자가 본 약관에 동의하고, 회사가 이에 대한 승낙을 완료함으로써 효력이 발생합니다. ② 회사는 필요한 경우 본 약관을 변경할 수 있으며, 변경된 약관은 서비스 화면에 공지된 후 효력이 발생합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4조 (약관 외 준칙)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관에 명시되지 않은 사항에 대해서는 대한민국의 관련 법령과 상관습에 따릅니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제2장 서비스 이용계약\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5조 (이용계약의 성립)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n이용계약은 이용자가 약관의 내용에 동의하고, 회사가 제공하는 소정의 회원가입 신청서를 작성하여 가입을 완료한 후, 회사가 이를 승인함으로써 성립합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6조 (이용계약의 유보와 거절)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 다음 각 호에 해당하는 경우 이용계약의 성립을 유보하거나 거절할 수 있습니다: - 신청서의 내용이 허위로 판명된 경우 - 서비스 제공이 기술적으로 어려운 경우\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7조 (계약사항의 변경)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 개인정보 관리 메뉴를 통해 언제든지 자신의 정보를 열람하고 수정할 수 있습니다. 회원의 정보가 변경된 경우 즉시 수정해야 하며, 수정하지 않아 발생하는 문제의 책임은 회원에게 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제3장 개인정보 보호\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8조 (개인정보 보호의 원칙)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원의 개인정보는 관련 법령에 따라 보호됩니다. ② 회사는 개인정보 보호와 관련된 세부 사항을 별도로 마련한 개인정보처리방침에 따라 관리하며, 이용자는 언제든지 해당 방침을 통해 개인정보 관리에 대한 자세한 내용을 확인할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9조 (개인정보처리방침 준수)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 개인정보 보호와 관련된 구체적인 사항을 개인정보처리방침에 따라 관리합니다. ② 개인정보의 수집, 이용, 제공, 보관, 보호 등에 관한 사항은 회사의 개인정보처리방침을 따르며, 이용자는 회사 웹사이트에서 이를 확인할 수 있습니다. ③ 회사는 개인정보 보호를 위해 최선을 다하며, 관련 법령에 따라 이용자의 개인정보를 안전하게 관리합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10조 (14세 미만 아동의 개인정보 보호)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 14세 미만 아동의 개인정보를 수집할 경우, 반드시 법정대리인의 동의를 받아야 합니다. ② 법정대리인은 아동의 개인정보 열람, 수정, 삭제를 요청할 수 있으며, 회사는 이를 신속하게 처리합니다. ③ 14세 미만 아동의 개인정보 보호와 관련된 구체적인 사항은 개인정보처리방침에 명시되어 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제4장 서비스 제공 및 이용\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11조 (서비스 제공)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원의 이용 신청을 승인한 때부터 서비스를 개시합니다. 서비스 이용은 연중무휴 24시간을 원칙으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제12조 (서비스의 변경 및 중단)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 서비스 제공이 어려운 경우 사전 고지 후 서비스를 변경하거나 중단할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제5장 정보 제공 및 광고\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제13조 (정보 제공 및 광고)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 서비스 이용 중 필요하다고 인정되는 정보 및 광고를 제공할 수 있습니다. ② 회원은 원치 않는 정보를 수신 거부할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제6장 게시물 관리\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제14조 (게시물의 관리)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원이 게시한 내용이 불법적이거나 약관에 위배될 경우 이를 삭제할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제15조 (게시물의 저작권)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n게시물의 저작권은 회원에게 있으며, 회사는 이를 서비스 홍보 및 개선 목적으로 사용할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제7장 계약 해지 및 이용 제한\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제16조 (계약 해지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 언제든지 계약 해지를 요청할 수 있으며, 회사는 신속하게 처리합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제17조 (이용 제한)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 회원이 약관을 위반할 경우 서비스 이용을 제한할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제8장 손해 배상 및 면책 조항\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제18조 (손해 배상)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 무료로 제공되는 서비스와 관련하여 회원에게 발생한 손해에 대해 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제19조 (면책 조항)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회사는 천재지변 등 불가항력적인 사유로 인해 서비스를 제공하지 못하는 경우 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제9장 유료 서비스\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n20조 (유료 서비스의 이용)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회사는 회원에게 특정 서비스에 대해 유료로 제공할 수 있습니다. ② 유료 서비스의 이용 요금, 결제 방식, 환불 절차 등에 대한 상세 내용은 서비스 안내 페이지와 결제 화면에 명시합니다. ③ 유료 서비스 이용 요금은 회사가 정한 결제 방식에 따라 결제됩니다. 회원은 신용카드, 계좌이체, 휴대전화 결제 등 회사가 제공하는 다양한 결제 방식을 통해 요금을 납부할 수 있습니다. ④ 유료 서비스의 이용 요금은 선불 결제를 원칙으로 하며, 이용 기간 중 서비스 중지 및 해지 시 남은 이용 기간에 대한 환불은 회사의 환불 정책에 따라 처리됩니다. ⑤ 회사는 회원의 유료 서비스 이용과 관련하여 발생한 문제에 대해 최선을 다해 해결하도록 노력합니다. 다만, 회사의 고의 또는 중대한 과실이 없는 한 회원이 유료 서비스 이용 중 입은 손해에 대해서는 책임을 지지 않습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제21조(환불 정책)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원은 결제 후 7일 이내에 서비스 이용을 시작하지 않은 경우, 요금 전액을 환불받을 수 있습니다. ② 유료 서비스 이용 중 부득이한 사유로 서비스가 중지된 경우, 회사는 이용하지 않은 부분에 대해 환불 절차를 밟습니다. ③ 회원의 귀책사유로 인해 서비스 이용이 중지된 경우, 환불이 불가능합니다. ④ 환불은 회원이 지정한 계좌로 환불 절차를 거치며, 환불 요청 후 7일 이내에 처리됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제22조 (유료 서비스의 중지 및 해지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n① 회원이 유료 서비스를 해지하고자 하는 경우, 회사의 고객 지원 센터에 해지 신청을 해야 합니다. ② 회사는 회원이 약관을 위반하거나 부정한 방법으로 유료 서비스를 이용한 경우, 유료 서비스 이용을 즉시 중지하고 계약을 해지할 수 있습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제10장 양도 금지\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제23조 (양도 금지)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원은 서비스 이용권한, 기타 이용계약상의 지위를 제3자에게 양도, 증여할 수 없으며, 이를 담보로 제공할 수 없습니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제11장 관할 법원\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제24조 (분쟁 해결)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n서비스 이용과 관련하여 분쟁이 발생한 경우, 회사와 회원은 성실히 협의하여 해결합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n제25조 (관할 법원)\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관에 따른 분쟁은 서울중앙지방법원을 관할 법원으로 합니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n부칙\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n본 약관은 2024년 10월 1일부터 시행됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n", "non.existent.key": "Non-existent key", "test.key": "Test", "ui.admin.api_keys.create.name_label": "Name Label", @@ -2133,7 +2701,8 @@ const Map enStrings = { "ui.admin.brand": "Brand", "ui.admin.dev_role_switcher": "🛠 DEV Role Switcher", "ui.admin.dev_role_switcher_real": "Use real role", - "ui.admin.groups.create.description": "Adds a new organization unit such as a department or team.", + "ui.admin.groups.create.description": + "Adds a new organization unit such as a department or team.", "ui.admin.groups.create.title": "Create Organization Unit", "ui.admin.groups.detail.breadcrumb_org": "Breadcrumb Org", "ui.admin.groups.detail.breadcrumb_tenant": "Tenant Details", @@ -2197,7 +2766,8 @@ const Map enStrings = { "ui.admin.tenants.admins.dialog_description": "Dialog Description", "ui.admin.tenants.admins.dialog_no_results": "Dialog No Results", "ui.admin.tenants.admins.dialog_search_hint": "Dialog Search Hint", - "ui.admin.tenants.admins.dialog_search_placeholder": "Dialog Search Placeholder", + "ui.admin.tenants.admins.dialog_search_placeholder": + "Dialog Search Placeholder", "ui.admin.tenants.admins.dialog_title": "Dialog Title", "ui.admin.tenants.admins.remove_title": "Remove Title", "ui.admin.tenants.admins.table_actions": "Table Actions", @@ -2209,7 +2779,8 @@ const Map enStrings = { "ui.admin.tenants.create.breadcrumb.action": "Create", "ui.admin.tenants.create.breadcrumb.section": "Tenants", "ui.admin.tenants.create.form.description": "Tenant Description", - "ui.admin.tenants.create.form.domains_label": "Allowed Domains (Comma separated)", + "ui.admin.tenants.create.form.domains_label": + "Allowed Domains (Comma separated)", "ui.admin.tenants.create.form.domains_placeholder": "example.com, example.kr", "ui.admin.tenants.create.form.name": "Tenant name", "ui.admin.tenants.create.form.name_placeholder": "Enter tenant name", @@ -2244,7 +2815,8 @@ const Map enStrings = { "ui.admin.tenants.members.total_label": "Total", "ui.admin.tenants.owners.add_button": "Add Owner", "ui.admin.tenants.owners.already_owner": "Already Owner", - "ui.admin.tenants.owners.dialog_description": "Search users by name or email.", + "ui.admin.tenants.owners.dialog_description": + "Search users by name or email.", "ui.admin.tenants.owners.dialog_title": "Add New Owner", "ui.admin.tenants.owners.remove_title": "Revoke Owner Permission", "ui.admin.tenants.owners.table_actions": "Actions", @@ -2252,15 +2824,19 @@ const Map enStrings = { "ui.admin.tenants.owners.table_name": "Name", "ui.admin.tenants.owners.title": "Tenant Owners", "ui.admin.tenants.profile.allowed_domains": "Allowed Domains", - "ui.admin.tenants.profile.allowed_domains_help": "Users with these email domains will be automatically assigned to this tenant.", + "ui.admin.tenants.profile.allowed_domains_help": + "Users with these email domains will be automatically assigned to this tenant.", "ui.admin.tenants.profile.approve_button": "Approve Tenant", - "ui.admin.tenants.profile.description": "Review and edit the tenant's basic profile information.", + "ui.admin.tenants.profile.description": + "Review and edit the tenant's basic profile information.", "ui.admin.tenants.profile.form.parent": "Parent Tenant (Optional)", - "ui.admin.tenants.profile.form.parent_help": "Select a parent tenant if this is a subsidiary or sub-organization.", + "ui.admin.tenants.profile.form.parent_help": + "Select a parent tenant if this is a subsidiary or sub-organization.", "ui.admin.tenants.profile.name": "Tenant Name", "ui.admin.tenants.profile.slug": "Slug", "ui.admin.tenants.profile.status": "Status", - "ui.admin.tenants.profile.subtitle": "Slug and status changes are applied immediately.", + "ui.admin.tenants.profile.subtitle": + "Slug and status changes are applied immediately.", "ui.admin.tenants.profile.title": "Tenant Profile", "ui.admin.tenants.profile.type": "Type", "ui.admin.tenants.registry.title": "Tenant registry", @@ -2280,11 +2856,13 @@ const Map enStrings = { "ui.admin.tenants.schema.field.type_number": "Number", "ui.admin.tenants.schema.field.type_text": "Text Value", "ui.admin.tenants.schema.field.unsigned": "Unsigned", - "ui.admin.tenants.schema.field.validation_placeholder": "Regex Pattern (Optional)", + "ui.admin.tenants.schema.field.validation_placeholder": + "Regex Pattern (Optional)", "ui.admin.tenants.schema.save": "Save Schema", "ui.admin.tenants.schema.title": "User Schema Extension", "ui.admin.tenants.sub.add": "Add", - "ui.admin.tenants.sub.add_dialog_desc": "Select a tenant to add as a sub-tenant.", + "ui.admin.tenants.sub.add_dialog_desc": + "Select a tenant to add as a sub-tenant.", "ui.admin.tenants.sub.add_dialog_title": "Add Sub-tenant", "ui.admin.tenants.sub.add_existing": "Add Existing Tenant", "ui.admin.tenants.sub.manage": "Manage", @@ -2305,7 +2883,8 @@ const Map enStrings = { "ui.admin.tenants.table.updated": "UPDATED", "ui.admin.tenants.title": "Tenant Registry", "ui.admin.title": "Admin Control", - "ui.admin.users.bulk.acknowledge_warning": "I acknowledge the warning and will proceed.", + "ui.admin.users.bulk.acknowledge_warning": + "I acknowledge the warning and will proceed.", "ui.admin.users.bulk.do_move": "Execute Move", "ui.admin.users.bulk.download_template": "Download Template", "ui.admin.users.bulk.move_group": "Bulk Tenant Move", @@ -2343,126 +2922,217 @@ const Map enStrings = { "ui.admin.users.create.form.tenant": "Tenant", "ui.admin.users.create.form.tenant_global": "Tenant Global", "ui.admin.users.create.go_list": "Go List", - "ui.admin.users.create.password_generated.title": "Initial Password Generated", + "ui.admin.users.create.password_generated.title": + "Initial Password Generated", "ui.admin.users.create.submit": "User Create", "ui.admin.users.create.title": "User Add", "ui.admin.users.detail.back": "Back", "ui.admin.users.detail.breadcrumb.section": "Users", "ui.admin.users.detail.contact_title": "ui.admin.users.detail.contact_title", "ui.admin.users.detail.created_at": "Created At", - "ui.admin.users.detail.custom_fields.multi_title": "Per-tenant Profile Management", + "ui.admin.users.detail.custom_fields.multi_title": + "Per-tenant Profile Management", "ui.admin.users.detail.delete": "User Delete", "ui.admin.users.detail.edit_title": "Edit Title", - "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": + "ui.admin.users.detail.form.- ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", "ui.admin.users.detail.form.department": "Department", "ui.admin.users.detail.form.department_placeholder": "Department Placeholder", "ui.admin.users.detail.form.email": "Email", "ui.admin.users.detail.form.is_login_id": "Sign in ID", "ui.admin.users.detail.form.job_title": "Job Title", - "ui.admin.users.detail.form.job_title_placeholder": "ui.admin.users.detail.form.job_title_placeholder", + "ui.admin.users.detail.form.job_title_placeholder": + "ui.admin.users.detail.form.job_title_placeholder", "ui.admin.users.detail.form.login_id": "Sign in ID", "ui.admin.users.detail.form.login_id_placeholder": "Login Id Placeholder", - "ui.admin.users.detail.form.msg.admin.users.detail.history_desc": "ui.admin.users.detail.form.msg.admin.users.detail.history_desc", - "ui.admin.users.detail.form.msg.admin.users.detail.no_history": "ui.admin.users.detail.form.msg.admin.users.detail.no_history", - "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants": "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants", - "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc": "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc", - "ui.admin.users.detail.form.msg.admin.users.detail.security_desc": "ui.admin.users.detail.form.msg.admin.users.detail.security_desc", - "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help": "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help", - "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc": "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc", - "ui.admin.users.detail.form.msg.common.copied": "ui.admin.users.detail.form.msg.common.copied", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", - "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", + "ui.admin.users.detail.form.msg.admin.users.detail.history_desc": + "ui.admin.users.detail.form.msg.admin.users.detail.history_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.no_history": + "ui.admin.users.detail.form.msg.admin.users.detail.no_history", + "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants": + "ui.admin.users.detail.form.msg.admin.users.detail.no_tenants", + "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc": + "ui.admin.users.detail.form.msg.admin.users.detail.reset_auto_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.security_desc": + "ui.admin.users.detail.form.msg.admin.users.detail.security_desc", + "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help": + "ui.admin.users.detail.form.msg.admin.users.detail.tenant_slug_help", + "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc": + "ui.admin.users.detail.form.msg.admin.users.detail.tenants_desc", + "ui.admin.users.detail.form.msg.common.copied": + "ui.admin.users.detail.form.msg.common.copied", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.missing_algorithms_title", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_empty", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.parsed_keys_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_empty", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_help", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refresh_failed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_refreshed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_confirm", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoke_failed", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.cache_revoked", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": + "ui.admin.users.detail.form.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", "ui.admin.users.detail.form.name": "Name", "ui.admin.users.detail.form.name_placeholder": "Name Placeholder", "ui.admin.users.detail.form.phone": "Phone number", "ui.admin.users.detail.form.phone_placeholder": "010-1234-5678", "ui.admin.users.detail.form.position": "Position", - "ui.admin.users.detail.form.position_placeholder": "ui.admin.users.detail.form.position_placeholder", + "ui.admin.users.detail.form.position_placeholder": + "ui.admin.users.detail.form.position_placeholder", "ui.admin.users.detail.form.role": "Role", "ui.admin.users.detail.form.role_rp_admin": "Role Rp Admin", "ui.admin.users.detail.form.role_super_admin": "Role Super Admin", "ui.admin.users.detail.form.role_tenant_admin": "Role Tenant Admin", "ui.admin.users.detail.form.role_user": "User", "ui.admin.users.detail.form.status": "Status", - "ui.admin.users.detail.form.status_active": "ui.admin.users.detail.form.status_active", - "ui.admin.users.detail.form.status_inactive": "ui.admin.users.detail.form.status_inactive", + "ui.admin.users.detail.form.status_active": + "ui.admin.users.detail.form.status_active", + "ui.admin.users.detail.form.status_inactive": + "ui.admin.users.detail.form.status_inactive", "ui.admin.users.detail.form.tenant": "Representative Affiliated Tenant", "ui.admin.users.detail.form.tenant_global": "Tenant Global", "ui.admin.users.detail.form.tenant_slug": "Tenant Slug", - "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id": "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id", - "ui.admin.users.detail.form.ui.admin.users.detail.form.email": "ui.admin.users.detail.form.ui.admin.users.detail.form.email", - "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id": "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id", - "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin": "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin", - "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug": "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug", - "ui.admin.users.detail.form.ui.admin.users.detail.generate_button": "ui.admin.users.detail.form.ui.admin.users.detail.generate_button", - "ui.admin.users.detail.form.ui.admin.users.detail.history_title": "ui.admin.users.detail.form.ui.admin.users.detail.history_title", - "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm": "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm", - "ui.admin.users.detail.form.ui.admin.users.detail.manual_password": "ui.admin.users.detail.form.ui.admin.users.detail.manual_password", - "ui.admin.users.detail.form.ui.admin.users.detail.password_done": "ui.admin.users.detail.form.ui.admin.users.detail.password_done", - "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto": "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto", - "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute": "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute", - "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual": "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual", - "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants": "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants", - "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info", - "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security", - "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants": "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants", - "ui.admin.users.detail.form.ui.admin.users.detail.updated_at": "ui.admin.users.detail.form.ui.admin.users.detail.updated_at", - "ui.admin.users.detail.form.ui.common.generate": "ui.admin.users.detail.form.ui.common.generate", - "ui.admin.users.detail.form.ui.common.status.blocked": "ui.admin.users.detail.form.ui.common.status.blocked", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source", - "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri": "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri", - "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable": "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable", - "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help": "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help", - "ui.admin.users.detail.form.ui.dev.clients.help.docs_body": "ui.admin.users.detail.form.ui.dev.clients.help.docs_body", - "ui.admin.users.detail.form.ui.dev.clients.help.subtitle": "ui.admin.users.detail.form.ui.dev.clients.help.subtitle", - "ui.admin.users.detail.form.ui.dev.clients.registry.description": "ui.admin.users.detail.form.ui.dev.clients.registry.description", - "ui.admin.users.detail.form.ui.dev.clients.scopes.email": "ui.admin.users.detail.form.ui.dev.clients.scopes.email", - "ui.admin.users.detail.form.ui.dev.clients.scopes.openid": "ui.admin.users.detail.form.ui.dev.clients.scopes.openid", - "ui.admin.users.detail.form.ui.dev.clients.scopes.profile": "ui.admin.users.detail.form.ui.dev.clients.scopes.profile", - "ui.admin.users.detail.form.ui.dev.session.refresh": "ui.admin.users.detail.form.ui.dev.session.refresh", - "ui.admin.users.detail.form.ui.dev.session.refreshing": "ui.admin.users.detail.form.ui.dev.session.refreshing", + "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id": + "ui.admin.users.detail.form.ui.admin.users.create.form.is_login_id", + "ui.admin.users.detail.form.ui.admin.users.detail.form.email": + "ui.admin.users.detail.form.ui.admin.users.detail.form.email", + "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id": + "ui.admin.users.detail.form.ui.admin.users.detail.form.is_login_id", + "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin": + "ui.admin.users.detail.form.ui.admin.users.detail.form.role_rp_admin", + "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug": + "ui.admin.users.detail.form.ui.admin.users.detail.form.tenant_slug", + "ui.admin.users.detail.form.ui.admin.users.detail.generate_button": + "ui.admin.users.detail.form.ui.admin.users.detail.generate_button", + "ui.admin.users.detail.form.ui.admin.users.detail.history_title": + "ui.admin.users.detail.form.ui.admin.users.detail.history_title", + "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm": + "ui.admin.users.detail.form.ui.admin.users.detail.manual_confirm", + "ui.admin.users.detail.form.ui.admin.users.detail.manual_password": + "ui.admin.users.detail.form.ui.admin.users.detail.manual_password", + "ui.admin.users.detail.form.ui.admin.users.detail.password_done": + "ui.admin.users.detail.form.ui.admin.users.detail.password_done", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto": + "ui.admin.users.detail.form.ui.admin.users.detail.reset_auto", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute": + "ui.admin.users.detail.form.ui.admin.users.detail.reset_execute", + "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual": + "ui.admin.users.detail.form.ui.admin.users.detail.reset_manual", + "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants": + "ui.admin.users.detail.form.ui.admin.users.detail.save_tenants", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info": + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.info", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security": + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.security", + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants": + "ui.admin.users.detail.form.ui.admin.users.detail.tabs.tenants", + "ui.admin.users.detail.form.ui.admin.users.detail.updated_at": + "ui.admin.users.detail.form.ui.admin.users.detail.updated_at", + "ui.admin.users.detail.form.ui.common.generate": + "ui.admin.users.detail.form.ui.common.generate", + "ui.admin.users.detail.form.ui.common.status.blocked": + "ui.admin.users.detail.form.ui.common.status.blocked", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.allowed_algorithms_info", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_none", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.cached_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.error", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.expires_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.failures", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.kids", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_checked_at", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.last_success", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_key_n", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.parsed_keys", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.status", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.title", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.cache.uri", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.guide_toggle", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_disabled", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.headless_enabled", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source", + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri": + "ui.admin.users.detail.form.ui.dev.clients.general.public_key.source_uri", + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable": + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable", + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help": + "ui.admin.users.detail.form.ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.admin.users.detail.form.ui.dev.clients.help.docs_body": + "ui.admin.users.detail.form.ui.dev.clients.help.docs_body", + "ui.admin.users.detail.form.ui.dev.clients.help.subtitle": + "ui.admin.users.detail.form.ui.dev.clients.help.subtitle", + "ui.admin.users.detail.form.ui.dev.clients.registry.description": + "ui.admin.users.detail.form.ui.dev.clients.registry.description", + "ui.admin.users.detail.form.ui.dev.clients.scopes.email": + "ui.admin.users.detail.form.ui.dev.clients.scopes.email", + "ui.admin.users.detail.form.ui.dev.clients.scopes.openid": + "ui.admin.users.detail.form.ui.dev.clients.scopes.openid", + "ui.admin.users.detail.form.ui.dev.clients.scopes.profile": + "ui.admin.users.detail.form.ui.dev.clients.scopes.profile", + "ui.admin.users.detail.form.ui.dev.session.refresh": + "ui.admin.users.detail.form.ui.dev.session.refresh", + "ui.admin.users.detail.form.ui.dev.session.refreshing": + "ui.admin.users.detail.form.ui.dev.session.refreshing", "ui.admin.users.detail.generate_button": "Generate Button", "ui.admin.users.detail.generate_password": "Auto Generate", "ui.admin.users.detail.go_list": "Go List", @@ -2489,11 +3159,15 @@ const Map enStrings = { "ui.admin.users.detail.tabs.info": "Info", "ui.admin.users.detail.tabs.security": "Security", "ui.admin.users.detail.tabs.tenants": "Tenant Profile", - "ui.admin.users.detail.tenants_section.additional": "Additional Affiliated/Manageable Tenants", - "ui.admin.users.detail.tenants_section.primary": "Representative Affiliated Tenant", - "ui.admin.users.detail.tenants_section.title": "Affiliation & Organization Info", + "ui.admin.users.detail.tenants_section.additional": + "Additional Affiliated/Manageable Tenants", + "ui.admin.users.detail.tenants_section.primary": + "Representative Affiliated Tenant", + "ui.admin.users.detail.tenants_section.title": + "Affiliation & Organization Info", "ui.admin.users.detail.title": "User Details", - "ui.admin.users.detail.toggle_password_visibility": "Toggle password visibility", + "ui.admin.users.detail.toggle_password_visibility": + "Toggle password visibility", "ui.admin.users.detail.updated_at": "Updated At", "ui.admin.users.list.add": "User Add", "ui.admin.users.list.breadcrumb.list": "List", @@ -2510,94 +3184,178 @@ const Map enStrings = { "ui.admin.users.list.table.actions": "ACTIONS", "ui.admin.users.list.table.created": "CREATED", "ui.admin.users.list.table.login_id": "LOGIN ID", - "ui.admin.users.list.table.msg.admin.users.detail.history_desc": "ui.admin.users.list.table.msg.admin.users.detail.history_desc", - "ui.admin.users.list.table.msg.admin.users.detail.no_history": "ui.admin.users.list.table.msg.admin.users.detail.no_history", - "ui.admin.users.list.table.msg.admin.users.detail.no_tenants": "ui.admin.users.list.table.msg.admin.users.detail.no_tenants", - "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc": "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc", - "ui.admin.users.list.table.msg.admin.users.detail.security_desc": "ui.admin.users.list.table.msg.admin.users.detail.security_desc", - "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help": "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help", - "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc": "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc", - "ui.admin.users.list.table.msg.common.copied": "ui.admin.users.list.table.msg.common.copied", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked": "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", - "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", + "ui.admin.users.list.table.msg.admin.users.detail.history_desc": + "ui.admin.users.list.table.msg.admin.users.detail.history_desc", + "ui.admin.users.list.table.msg.admin.users.detail.no_history": + "ui.admin.users.list.table.msg.admin.users.detail.no_history", + "ui.admin.users.list.table.msg.admin.users.detail.no_tenants": + "ui.admin.users.list.table.msg.admin.users.detail.no_tenants", + "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc": + "ui.admin.users.list.table.msg.admin.users.detail.reset_auto_desc", + "ui.admin.users.list.table.msg.admin.users.detail.security_desc": + "ui.admin.users.list.table.msg.admin.users.detail.security_desc", + "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help": + "ui.admin.users.list.table.msg.admin.users.detail.tenant_slug_help", + "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc": + "ui.admin.users.list.table.msg.admin.users.detail.tenants_desc", + "ui.admin.users.list.table.msg.common.copied": + "ui.admin.users.list.table.msg.common.copied", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.allowed_algorithms_tooltip", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.missing_algorithms_title", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_empty", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.parsed_keys_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache.unsupported_algorithms_title", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_empty", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_help", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refresh_failed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_refreshed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_confirm", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoke_failed", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.cache_revoked", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.missing_parsed_algorithms", + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms": + "ui.admin.users.list.table.msg.dev.clients.general.public_key.validation.unsupported_parsed_algorithms", "ui.admin.users.list.table.name_email": "NAME / EMAIL", "ui.admin.users.list.table.role": "ROLE", "ui.admin.users.list.table.status": "STATUS", "ui.admin.users.list.table.tenant_dept": "TENANT / DEPT", - "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id": "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id", - "ui.admin.users.list.table.ui.admin.users.detail.form.email": "ui.admin.users.list.table.ui.admin.users.detail.form.email", - "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id": "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id", - "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin": "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin", - "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug": "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug", - "ui.admin.users.list.table.ui.admin.users.detail.generate_button": "ui.admin.users.list.table.ui.admin.users.detail.generate_button", - "ui.admin.users.list.table.ui.admin.users.detail.history_title": "ui.admin.users.list.table.ui.admin.users.detail.history_title", - "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm": "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm", - "ui.admin.users.list.table.ui.admin.users.detail.manual_password": "ui.admin.users.list.table.ui.admin.users.detail.manual_password", - "ui.admin.users.list.table.ui.admin.users.detail.password_done": "ui.admin.users.list.table.ui.admin.users.detail.password_done", - "ui.admin.users.list.table.ui.admin.users.detail.reset_auto": "ui.admin.users.list.table.ui.admin.users.detail.reset_auto", - "ui.admin.users.list.table.ui.admin.users.detail.reset_execute": "ui.admin.users.list.table.ui.admin.users.detail.reset_execute", - "ui.admin.users.list.table.ui.admin.users.detail.reset_manual": "ui.admin.users.list.table.ui.admin.users.detail.reset_manual", - "ui.admin.users.list.table.ui.admin.users.detail.save_tenants": "ui.admin.users.list.table.ui.admin.users.detail.save_tenants", - "ui.admin.users.list.table.ui.admin.users.detail.tabs.info": "ui.admin.users.list.table.ui.admin.users.detail.tabs.info", - "ui.admin.users.list.table.ui.admin.users.detail.tabs.security": "ui.admin.users.list.table.ui.admin.users.detail.tabs.security", - "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants": "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants", - "ui.admin.users.list.table.ui.admin.users.detail.updated_at": "ui.admin.users.list.table.ui.admin.users.detail.updated_at", - "ui.admin.users.list.table.ui.common.generate": "ui.admin.users.list.table.ui.common.generate", - "ui.admin.users.list.table.ui.common.status.blocked": "ui.admin.users.list.table.ui.common.status.blocked", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info": "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri": "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle": "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled": "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled": "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline": "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg": "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.source": "ui.admin.users.list.table.ui.dev.clients.general.public_key.source", - "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri": "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri", - "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable": "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable", - "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help": "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help", - "ui.admin.users.list.table.ui.dev.clients.help.docs_body": "ui.admin.users.list.table.ui.dev.clients.help.docs_body", - "ui.admin.users.list.table.ui.dev.clients.help.subtitle": "ui.admin.users.list.table.ui.dev.clients.help.subtitle", - "ui.admin.users.list.table.ui.dev.clients.registry.description": "ui.admin.users.list.table.ui.dev.clients.registry.description", - "ui.admin.users.list.table.ui.dev.clients.scopes.email": "ui.admin.users.list.table.ui.dev.clients.scopes.email", - "ui.admin.users.list.table.ui.dev.clients.scopes.openid": "ui.admin.users.list.table.ui.dev.clients.scopes.openid", - "ui.admin.users.list.table.ui.dev.clients.scopes.profile": "ui.admin.users.list.table.ui.dev.clients.scopes.profile", - "ui.admin.users.list.table.ui.dev.session.refresh": "ui.admin.users.list.table.ui.dev.session.refresh", - "ui.admin.users.list.table.ui.dev.session.refreshing": "ui.admin.users.list.table.ui.dev.session.refreshing", + "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id": + "ui.admin.users.list.table.ui.admin.users.create.form.is_login_id", + "ui.admin.users.list.table.ui.admin.users.detail.form.email": + "ui.admin.users.list.table.ui.admin.users.detail.form.email", + "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id": + "ui.admin.users.list.table.ui.admin.users.detail.form.is_login_id", + "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin": + "ui.admin.users.list.table.ui.admin.users.detail.form.role_rp_admin", + "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug": + "ui.admin.users.list.table.ui.admin.users.detail.form.tenant_slug", + "ui.admin.users.list.table.ui.admin.users.detail.generate_button": + "ui.admin.users.list.table.ui.admin.users.detail.generate_button", + "ui.admin.users.list.table.ui.admin.users.detail.history_title": + "ui.admin.users.list.table.ui.admin.users.detail.history_title", + "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm": + "ui.admin.users.list.table.ui.admin.users.detail.manual_confirm", + "ui.admin.users.list.table.ui.admin.users.detail.manual_password": + "ui.admin.users.list.table.ui.admin.users.detail.manual_password", + "ui.admin.users.list.table.ui.admin.users.detail.password_done": + "ui.admin.users.list.table.ui.admin.users.detail.password_done", + "ui.admin.users.list.table.ui.admin.users.detail.reset_auto": + "ui.admin.users.list.table.ui.admin.users.detail.reset_auto", + "ui.admin.users.list.table.ui.admin.users.detail.reset_execute": + "ui.admin.users.list.table.ui.admin.users.detail.reset_execute", + "ui.admin.users.list.table.ui.admin.users.detail.reset_manual": + "ui.admin.users.list.table.ui.admin.users.detail.reset_manual", + "ui.admin.users.list.table.ui.admin.users.detail.save_tenants": + "ui.admin.users.list.table.ui.admin.users.detail.save_tenants", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.info": + "ui.admin.users.list.table.ui.admin.users.detail.tabs.info", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.security": + "ui.admin.users.list.table.ui.admin.users.detail.tabs.security", + "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants": + "ui.admin.users.list.table.ui.admin.users.detail.tabs.tenants", + "ui.admin.users.list.table.ui.admin.users.detail.updated_at": + "ui.admin.users.list.table.ui.admin.users.detail.updated_at", + "ui.admin.users.list.table.ui.common.generate": + "ui.admin.users.list.table.ui.common.generate", + "ui.admin.users.list.table.ui.common.status.blocked": + "ui.admin.users.list.table.ui.common.status.blocked", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.allowed_algorithms_info", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_none", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.cached_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.error", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.expires_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.failures", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.kids", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_checked_at", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.last_success", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_key_n", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.parsed_keys", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.status", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.title", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.cache.uri", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.guide_toggle", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_disabled", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.headless_enabled", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.revoke_cache", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source", + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri": + "ui.admin.users.list.table.ui.dev.clients.general.public_key.source_uri", + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable": + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable", + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help": + "ui.admin.users.list.table.ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.admin.users.list.table.ui.dev.clients.help.docs_body": + "ui.admin.users.list.table.ui.dev.clients.help.docs_body", + "ui.admin.users.list.table.ui.dev.clients.help.subtitle": + "ui.admin.users.list.table.ui.dev.clients.help.subtitle", + "ui.admin.users.list.table.ui.dev.clients.registry.description": + "ui.admin.users.list.table.ui.dev.clients.registry.description", + "ui.admin.users.list.table.ui.dev.clients.scopes.email": + "ui.admin.users.list.table.ui.dev.clients.scopes.email", + "ui.admin.users.list.table.ui.dev.clients.scopes.openid": + "ui.admin.users.list.table.ui.dev.clients.scopes.openid", + "ui.admin.users.list.table.ui.dev.clients.scopes.profile": + "ui.admin.users.list.table.ui.dev.clients.scopes.profile", + "ui.admin.users.list.table.ui.dev.session.refresh": + "ui.admin.users.list.table.ui.dev.session.refresh", + "ui.admin.users.list.table.ui.dev.session.refreshing": + "ui.admin.users.list.table.ui.dev.session.refreshing", "ui.admin.users.list.title": "User Manage", "ui.admin.users.table.email": "Email", "ui.admin.users.table.name": "Name", @@ -2710,7 +3468,8 @@ const Map enStrings = { "ui.dev.clients.details.endpoints.title": "OIDC Endpoints", "ui.dev.clients.details.redirect.callback_label": "Callback Label", "ui.dev.clients.details.redirect.label": "Redirect URIs", - "ui.dev.clients.details.redirect.placeholder": "https://your-app.com/callback, http://localhost:3000/auth/callback", + "ui.dev.clients.details.redirect.placeholder": + "https://your-app.com/callback, http://localhost:3000/auth/callback", "ui.dev.clients.details.redirect.save": "Save", "ui.dev.clients.details.redirect.title": "Redirection Settings", "ui.dev.clients.details.secret.hide": "Hide", @@ -2729,76 +3488,113 @@ const Map enStrings = { "ui.dev.clients.general.create": "Create Application", "ui.dev.clients.general.display_new": "Add Connected Application", "ui.dev.clients.general.identity.description": "Application Description", - "ui.dev.clients.general.identity.description_placeholder": "Description Placeholder", + "ui.dev.clients.general.identity.description_placeholder": + "Description Placeholder", "ui.dev.clients.general.identity.logo": "App Logo URL", - "ui.dev.clients.general.identity.logo_placeholder": "https://example.com/logo.png", + "ui.dev.clients.general.identity.logo_placeholder": + "https://example.com/logo.png", "ui.dev.clients.general.identity.logo_preview": "Logo Preview", "ui.dev.clients.general.identity.name": "Name", "ui.dev.clients.general.identity.name_placeholder": "My Awesome Application", "ui.dev.clients.general.identity.title": "Application Identity", - "ui.dev.clients.general.public_key.allowed_algorithms_info": "Allowed Algorithms Info", - "ui.dev.clients.general.public_key.auth_method": "ui.dev.clients.general.public_key.auth_method", - "ui.dev.clients.general.public_key.auth_method_client_secret_basic": "ui.dev.clients.general.public_key.auth_method_client_secret_basic", - "ui.dev.clients.general.public_key.auth_method_none": "ui.dev.clients.general.public_key.auth_method_none", - "ui.dev.clients.general.public_key.auth_method_private_key_jwt": "ui.dev.clients.general.public_key.auth_method_private_key_jwt", + "ui.dev.clients.general.public_key.allowed_algorithms_info": + "Allowed Algorithms Info", + "ui.dev.clients.general.public_key.auth_method": + "ui.dev.clients.general.public_key.auth_method", + "ui.dev.clients.general.public_key.auth_method_client_secret_basic": + "ui.dev.clients.general.public_key.auth_method_client_secret_basic", + "ui.dev.clients.general.public_key.auth_method_none": + "ui.dev.clients.general.public_key.auth_method_none", + "ui.dev.clients.general.public_key.auth_method_private_key_jwt": + "ui.dev.clients.general.public_key.auth_method_private_key_jwt", "ui.dev.clients.general.public_key.cache.cached_at": "Cached At", "ui.dev.clients.general.public_key.cache.error": "Last Error", "ui.dev.clients.general.public_key.cache.expires_at": "Expires At", "ui.dev.clients.general.public_key.cache.failures": "Consecutive Failures", "ui.dev.clients.general.public_key.cache.kids": "Cached KIDs", "ui.dev.clients.general.public_key.cache.last_checked_at": "Last Checked", - "ui.dev.clients.general.public_key.cache.last_success": "Last Successful Verification", + "ui.dev.clients.general.public_key.cache.last_success": + "Last Successful Verification", "ui.dev.clients.general.public_key.cache.parsed_key_n": "N", "ui.dev.clients.general.public_key.cache.parsed_keys": "Parsed Keys", "ui.dev.clients.general.public_key.cache.status": "Status", "ui.dev.clients.general.public_key.cache.title": "JWKS Cache", "ui.dev.clients.general.public_key.cache.uri": "JWKS URI", - "ui.dev.clients.general.public_key.guide_toggle": "ui.dev.clients.general.public_key.guide_toggle", - "ui.dev.clients.general.public_key.headless_disabled": "ui.dev.clients.general.public_key.headless_disabled", - "ui.dev.clients.general.public_key.headless_enabled": "ui.dev.clients.general.public_key.headless_enabled", + "ui.dev.clients.general.public_key.guide_toggle": + "ui.dev.clients.general.public_key.guide_toggle", + "ui.dev.clients.general.public_key.headless_disabled": + "ui.dev.clients.general.public_key.headless_disabled", + "ui.dev.clients.general.public_key.headless_enabled": + "ui.dev.clients.general.public_key.headless_enabled", "ui.dev.clients.general.public_key.headless_toggle": "Headless Login", - "ui.dev.clients.general.public_key.jwks_inline": "ui.dev.clients.general.public_key.jwks_inline", - "ui.dev.clients.general.public_key.jwks_inline_placeholder": "ui.dev.clients.general.public_key.jwks_inline_placeholder", + "ui.dev.clients.general.public_key.jwks_inline": + "ui.dev.clients.general.public_key.jwks_inline", + "ui.dev.clients.general.public_key.jwks_inline_placeholder": + "ui.dev.clients.general.public_key.jwks_inline_placeholder", "ui.dev.clients.general.public_key.jwks_uri": "JWKS URI", - "ui.dev.clients.general.public_key.jwks_uri_placeholder": "https://rp.example.com/.well-known/jwks.json", - "ui.dev.clients.general.public_key.request_object_alg": "ui.dev.clients.general.public_key.request_object_alg", - "ui.dev.clients.general.public_key.request_object_alg_placeholder": "ui.dev.clients.general.public_key.request_object_alg_placeholder", + "ui.dev.clients.general.public_key.jwks_uri_placeholder": + "https://rp.example.com/.well-known/jwks.json", + "ui.dev.clients.general.public_key.request_object_alg": + "ui.dev.clients.general.public_key.request_object_alg", + "ui.dev.clients.general.public_key.request_object_alg_placeholder": + "ui.dev.clients.general.public_key.request_object_alg_placeholder", "ui.dev.clients.general.public_key.revoke_cache": "Revoke Cache", - "ui.dev.clients.general.public_key.source": "ui.dev.clients.general.public_key.source", - "ui.dev.clients.general.public_key.source_uri": "ui.dev.clients.general.public_key.source_uri", + "ui.dev.clients.general.public_key.source": + "ui.dev.clients.general.public_key.source", + "ui.dev.clients.general.public_key.source_uri": + "ui.dev.clients.general.public_key.source_uri", "ui.dev.clients.general.public_key.title": "Public Key Registration", "ui.dev.clients.general.public_key.validation_title": "Check before saving", "ui.dev.clients.general.redirect.label": "Redirect URIs", "ui.dev.clients.general.redirect.placeholder": "Placeholder", "ui.dev.clients.general.scopes.add": "Scope Add", - "ui.dev.clients.general.scopes.description_placeholder": "Description Placeholder", + "ui.dev.clients.general.scopes.description_placeholder": + "Description Placeholder", "ui.dev.clients.general.scopes.name_placeholder": "e.g. profile", - "ui.dev.clients.general.scopes.table.contact_title": "ui.dev.clients.general.scopes.table.contact_title", + "ui.dev.clients.general.scopes.table.contact_title": + "ui.dev.clients.general.scopes.table.contact_title", "ui.dev.clients.general.scopes.table.delete": "Delete", "ui.dev.clients.general.scopes.table.description": "Scope Description", - "ui.dev.clients.general.scopes.table.invalid_format": "ui.dev.clients.general.scopes.table.invalid_format", - "ui.dev.clients.general.scopes.table.login_id_help": "ui.dev.clients.general.scopes.table.login_id_help", + "ui.dev.clients.general.scopes.table.invalid_format": + "ui.dev.clients.general.scopes.table.invalid_format", + "ui.dev.clients.general.scopes.table.login_id_help": + "ui.dev.clients.general.scopes.table.login_id_help", "ui.dev.clients.general.scopes.table.mandatory": "Mandatory", "ui.dev.clients.general.scopes.table.name": "Scope Name", - "ui.dev.clients.general.scopes.table.password_title": "ui.dev.clients.general.scopes.table.password_title", - "ui.dev.clients.general.scopes.table.reset_password": "ui.dev.clients.general.scopes.table.reset_password", - "ui.dev.clients.general.scopes.table.reset_password_confirm": "ui.dev.clients.general.scopes.table.reset_password_confirm", - "ui.dev.clients.general.scopes.table.reset_password_label": "ui.dev.clients.general.scopes.table.reset_password_label", - "ui.dev.clients.general.scopes.table.role_super_admin": "ui.dev.clients.general.scopes.table.role_super_admin", - "ui.dev.clients.general.scopes.table.role_tenant_admin": "ui.dev.clients.general.scopes.table.role_tenant_admin", - "ui.dev.clients.general.scopes.table.role_user": "ui.dev.clients.general.scopes.table.role_user", - "ui.dev.clients.general.scopes.table.status_active": "ui.dev.clients.general.scopes.table.status_active", - "ui.dev.clients.general.scopes.table.status_inactive": "ui.dev.clients.general.scopes.table.status_inactive", - "ui.dev.clients.general.scopes.table.status_title": "ui.dev.clients.general.scopes.table.status_title", + "ui.dev.clients.general.scopes.table.password_title": + "ui.dev.clients.general.scopes.table.password_title", + "ui.dev.clients.general.scopes.table.reset_password": + "ui.dev.clients.general.scopes.table.reset_password", + "ui.dev.clients.general.scopes.table.reset_password_confirm": + "ui.dev.clients.general.scopes.table.reset_password_confirm", + "ui.dev.clients.general.scopes.table.reset_password_label": + "ui.dev.clients.general.scopes.table.reset_password_label", + "ui.dev.clients.general.scopes.table.role_super_admin": + "ui.dev.clients.general.scopes.table.role_super_admin", + "ui.dev.clients.general.scopes.table.role_tenant_admin": + "ui.dev.clients.general.scopes.table.role_tenant_admin", + "ui.dev.clients.general.scopes.table.role_user": + "ui.dev.clients.general.scopes.table.role_user", + "ui.dev.clients.general.scopes.table.status_active": + "ui.dev.clients.general.scopes.table.status_active", + "ui.dev.clients.general.scopes.table.status_inactive": + "ui.dev.clients.general.scopes.table.status_inactive", + "ui.dev.clients.general.scopes.table.status_title": + "ui.dev.clients.general.scopes.table.status_title", "ui.dev.clients.general.scopes.title": "Scopes", - "ui.dev.clients.general.security.headless_login_enable": "Headless Login (Custom Login UI)", - "ui.dev.clients.general.security.headless_login_enable_help": "Enable this if you want to implement your own login screen within the app instead of using the Baron SSO login page.", + "ui.dev.clients.general.security.headless_login_enable": + "Headless Login (Custom Login UI)", + "ui.dev.clients.general.security.headless_login_enable_help": + "Enable this if you want to implement your own login screen within the app instead of using the Baron SSO login page.", "ui.dev.clients.general.security.pkce": "PKCE", "ui.dev.clients.general.security.private": "Server Side App", "ui.dev.clients.general.security.title": "Security Settings", - "ui.dev.clients.general.security.trusted_rp_enable": "ui.dev.clients.general.security.trusted_rp_enable", - "ui.dev.clients.general.security.trusted_rp_enable_help": "ui.dev.clients.general.security.trusted_rp_enable_help", - "ui.dev.clients.general.subtitle": "Manage application settings and security configuration.", + "ui.dev.clients.general.security.trusted_rp_enable": + "ui.dev.clients.general.security.trusted_rp_enable", + "ui.dev.clients.general.security.trusted_rp_enable_help": + "ui.dev.clients.general.security.trusted_rp_enable_help", + "ui.dev.clients.general.subtitle": + "Manage application settings and security configuration.", "ui.dev.clients.general.title_create": "Create Client", "ui.dev.clients.general.title_edit": "Client Settings", "ui.dev.clients.help.docs_body": "ui.dev.clients.help.docs_body", @@ -2842,7 +3638,8 @@ const Map enStrings = { "ui.dev.dashboard.ops.card.consent_revoked": "Consent Revoked", "ui.dev.dashboard.ops.card.hydra_status": "Hydra Status", "ui.dev.dashboard.ops.card.rp_requests": "Rp Requests", - "ui.dev.dashboard.ops.subtitle": "Operational indicators for the current developer workspace.", + "ui.dev.dashboard.ops.subtitle": + "Operational indicators for the current developer workspace.", "ui.dev.dashboard.ops.tag.consent": "Consent grants", "ui.dev.dashboard.ops.tag.rp_status": "RP status", "ui.dev.dashboard.ops.title": "Ops board", @@ -2868,7 +3665,8 @@ const Map enStrings = { "ui.dev.profile.org.tenant": "Tenant", "ui.dev.profile.org.title": "Organization Info", "ui.dev.profile.role.current": "Current Role", - "ui.dev.profile.role.description": "The permission level granted to this account.", + "ui.dev.profile.role.description": + "The permission level granted to this account.", "ui.dev.profile.role.title": "System Role", "ui.dev.profile.subtitle": "View user details and assigned roles.", "ui.dev.profile.tab.basic": "Basic Info", @@ -2886,10 +3684,12 @@ const Map enStrings = { "ui.dev.session.refreshing": "ui.dev.session.refreshing", "ui.dev.session.remaining": "Expires in: {{minutes}}m {{seconds}}s", "ui.dev.session.unknown": "Unknown", - "ui.dev.tenant.single_notice": "You belong to a single tenant, so no switching is needed.", + "ui.dev.tenant.single_notice": + "You belong to a single tenant, so no switching is needed.", "ui.dev.tenant.switch_success": "Tenant switch completed", "ui.dev.tenant.workspace": "Workspace tenant (context)", - "ui.dev.tenant.workspace_desc": "Select and save the current working tenant to change API request context.", + "ui.dev.tenant.workspace_desc": + "Select and save the current working tenant to change API request context.", "ui.userfront.app_label.admin_console": "Admin Console", "ui.userfront.app_label.baron": "Baron", "ui.userfront.app_label.dev_console": "Dev Console", @@ -2912,7 +3712,8 @@ const Map enStrings = { "ui.userfront.consent.title": "Permission request", "ui.userfront.dashboard.activity.linked": "Linked", "ui.userfront.dashboard.approved_session.default": "Default", - "ui.userfront.dashboard.approved_session.userfront": "Approved UserFront session ID", + "ui.userfront.dashboard.approved_session.userfront": + "Approved UserFront session ID", "ui.userfront.dashboard.last_auth_label": "Last sign-in", "ui.userfront.dashboard.revoke.confirm_button": "Disconnect", "ui.userfront.dashboard.revoke.title": "Disconnect app", From d086b7ea3cf3312153d9b4a108a519b4140954ad Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 6 Apr 2026 17:48:07 +0900 Subject: [PATCH 14/39] =?UTF-8?q?userfront=20i18n=20placeholder=20?= =?UTF-8?q?=EC=B9=98=ED=99=98=EA=B3=BC=20=EB=B2=88=EC=97=AD=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/reset_password_screen.dart | 20 +++++++-- .../auth/presentation/signup_screen.dart | 24 +++++++--- .../presentation/dashboard_screen.dart | 44 +++++++++++++------ .../presentation/pages/profile_page.dart | 30 ++++++++++--- 4 files changed, 88 insertions(+), 30 deletions(-) diff --git a/userfront/lib/features/auth/presentation/reset_password_screen.dart b/userfront/lib/features/auth/presentation/reset_password_screen.dart index f2951e1c..53cbdd65 100644 --- a/userfront/lib/features/auth/presentation/reset_password_screen.dart +++ b/userfront/lib/features/auth/presentation/reset_password_screen.dart @@ -26,6 +26,18 @@ class _ResetPasswordScreenState extends State { Map? _policy; bool _isPolicyLoading = false; + String _renderTranslatedText( + String key, { + String? fallback, + Map values = const {}, + }) { + var text = tr(key, fallback: fallback); + values.forEach((name, value) { + text = text.replaceAll('{{$name}}', value).replaceAll('{$name}', value); + }); + return text; + } + @override void initState() { super.initState(); @@ -123,16 +135,16 @@ class _ResetPasswordScreenState extends State { final requiresSymbol = _policy?['nonAlphanumeric'] ?? true; final parts = [ - tr( + _renderTranslatedText( 'msg.userfront.reset.policy.min_length', - params: {'count': '$minLength'}, + values: {'count': '$minLength'}, ), ]; if (minTypes > 0) { parts.add( - tr( + _renderTranslatedText( 'msg.userfront.reset.policy.min_types', - params: {'count': '$minTypes'}, + values: {'count': '$minTypes'}, ), ); } diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index bde6317f..838e30c6 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -76,6 +76,18 @@ class _SignupScreenState extends State { 'baroncs.co.kr': 'BARON', }; + String _renderTranslatedText( + String key, { + String? fallback, + Map values = const {}, + }) { + var text = tr(key, fallback: fallback); + values.forEach((name, value) { + text = text.replaceAll('{{$name}}', value).replaceAll('{$name}', value); + }); + return text; + } + @override void initState() { super.initState(); @@ -1729,16 +1741,16 @@ class _SignupScreenState extends State { final requiresSymbol = _policy?['nonAlphanumeric'] ?? true; final parts = [ - tr( + _renderTranslatedText( 'msg.userfront.signup.policy.min_length', - params: {'count': minLength.toString()}, + values: {'count': minLength.toString()}, ), ]; if (minTypes > 0) { parts.add( - tr( + _renderTranslatedText( 'msg.userfront.signup.policy.min_types', - params: {'count': minTypes.toString()}, + values: {'count': minTypes.toString()}, ), ); } @@ -1755,9 +1767,9 @@ class _SignupScreenState extends State { parts.add(tr('msg.userfront.signup.policy.symbol')); } - return tr( + return _renderTranslatedText( 'msg.userfront.signup.policy.summary', - params: {'rules': parts.join(', ')}, + values: {'rules': parts.join(', ')}, ); } diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index cbbbc9fa..fc93c46f 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -55,6 +55,18 @@ class _DashboardScreenState extends ConsumerState { bool _showAllActivities = false; final Set _revokedClientIds = {}; + String _renderTranslatedText( + String key, { + String? fallback, + Map values = const {}, + }) { + var text = tr(key, fallback: fallback); + values.forEach((name, value) { + text = text.replaceAll('{{$name}}', value).replaceAll('{$name}', value); + }); + return text; + } + @override void initState() { super.initState(); @@ -700,10 +712,10 @@ class _DashboardScreenState extends ConsumerState { final clientId = log.clientId; final tooltip = clientId.isEmpty ? tr('msg.userfront.dashboard.client_id_missing') - : tr( + : _renderTranslatedText( 'msg.userfront.dashboard.client_id', fallback: 'Client ID: {{id}}', - params: {'id': clientId}, + values: {'id': clientId}, ); final baseStyle = style ?? const TextStyle(); final emphasisStyle = clientId.isEmpty @@ -891,7 +903,11 @@ class _DashboardScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - tr('msg.userfront.greeting', params: {'name': userName}), + _renderTranslatedText( + 'msg.userfront.greeting', + fallback: 'Hello, {{name}}.', + values: {'name': userName}, + ), style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, @@ -1122,18 +1138,18 @@ class _DashboardScreenState extends ConsumerState { const SizedBox(height: 12), if (browserLabel.isNotEmpty) Text( - tr( + _renderTranslatedText( 'msg.userfront.dashboard.sessions.browser', - params: {'value': browserLabel}, + values: {'value': browserLabel}, ), style: TextStyle(fontSize: 13, color: Colors.grey[700]), ), if (osLabel.isNotEmpty) ...[ const SizedBox(height: 4), Text( - tr( + _renderTranslatedText( 'msg.userfront.dashboard.sessions.os', - params: {'value': osLabel}, + values: {'value': osLabel}, ), style: TextStyle(fontSize: 13, color: Colors.grey[700]), ), @@ -1142,18 +1158,20 @@ class _DashboardScreenState extends ConsumerState { if (session.clientId.trim().isNotEmpty) ...[ const SizedBox(height: 6), Text( - tr( + _renderTranslatedText( 'msg.userfront.dashboard.client_id', - params: {'id': session.clientId}, + fallback: 'Client ID: {{id}}', + values: {'id': session.clientId}, ), style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], const SizedBox(height: 8), Text( - tr( + _renderTranslatedText( 'msg.userfront.dashboard.sessions.session_id', - params: {'id': _compactSessionId(session.sessionId)}, + fallback: 'Session ID: {{id}}', + values: {'id': _compactSessionId(session.sessionId)}, ), style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), @@ -1956,10 +1974,10 @@ class _DashboardScreenState extends ConsumerState { ), const SizedBox(height: 6), _selectableText( - tr( + _renderTranslatedText( 'msg.userfront.audit.session_id', fallback: 'Session ID: {{value}}', - params: { + values: { 'value': log.sessionId.isEmpty ? tr('ui.common.hyphen', fallback: '-') : log.sessionId, diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index 39987e19..9d25748a 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -57,6 +57,18 @@ class _ProfilePageState extends ConsumerState { Map? _passwordPolicy; bool _isPasswordPolicyLoading = false; + String _renderTranslatedText( + String key, { + String? fallback, + Map values = const {}, + }) { + var text = tr(key, fallback: fallback); + values.forEach((name, value) { + text = text.replaceAll('{{$name}}', value).replaceAll('{$name}', value); + }); + return text; + } + @override void initState() { super.initState(); @@ -98,16 +110,16 @@ class _ProfilePageState extends ConsumerState { final requiresSymbol = _passwordPolicy?['nonAlphanumeric'] ?? true; final parts = [ - tr( + _renderTranslatedText( 'msg.userfront.signup.policy.min_length', - params: {'count': '$minLength'}, + values: {'count': '$minLength'}, ), ]; if (minTypes > 0) { parts.add( - tr( + _renderTranslatedText( 'msg.userfront.signup.policy.min_types', - params: {'count': '$minTypes'}, + values: {'count': '$minTypes'}, ), ); } @@ -124,9 +136,9 @@ class _ProfilePageState extends ConsumerState { parts.add(tr('msg.userfront.signup.policy.symbol')); } - return tr( + return _renderTranslatedText( 'msg.userfront.signup.policy.summary', - params: {'rules': parts.join(", ")}, + values: {'rules': parts.join(", ")}, ); } @@ -688,7 +700,11 @@ class _ProfilePageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - tr('msg.userfront.profile.greeting', params: {'name': name}), + _renderTranslatedText( + 'msg.userfront.profile.greeting', + fallback: 'Hello, {{name}}.', + values: {'name': name}, + ), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, From 763c04398e507ab2ac34549d98fefd4af433bbfd Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 10:32:36 +0900 Subject: [PATCH 15/39] =?UTF-8?q?=EC=A0=91=EC=86=8D=EC=9D=B4=EB=A0=A5=20OI?= =?UTF-8?q?DC=20=EC=A0=91=EC=86=8D=20=EB=A1=9C=EA=B7=B8=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 102 ++++++++++++++++------- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 28259b32..f26e8683 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -4246,11 +4246,10 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { continue } consent, ok := consentMap[clientID] - if !ok { - continue - } - if !consent.ConsentAt.IsZero() && log.Timestamp.Before(consent.ConsentAt) { - continue + if ok { + if !consent.ConsentAt.IsZero() && log.Timestamp.Before(consent.ConsentAt) { + continue + } } oathkeeperLogs = append(oathkeeperLogs, log) if len(oathkeeperLogs) >= fetchLimit { @@ -4299,36 +4298,75 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { return info, true } + clientCache := make(map[string]loginClientInfo) + resolveClientByID := func(cid string) (loginClientInfo, bool) { + cid = strings.TrimSpace(cid) + if cid == "" || h.Hydra == nil { + return loginClientInfo{}, false + } + if cached, ok := clientCache[cid]; ok { + return cached, cached.ClientID != "" + } + client, err := h.Hydra.GetClient(c.Context(), cid) + if err != nil || client == nil { + clientCache[cid] = loginClientInfo{} + return loginClientInfo{}, false + } + name := strings.TrimSpace(client.ClientName) + if name == "" { + name = cid + } + info := loginClientInfo{ + ClientID: cid, + Name: name, + } + clientCache[cid] = info + return info, true + } + items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs)) for i := range authLogs { log := authLogs[i] appName := "Baron 로그인" clientID := "" path := strings.ToLower(extractAuditPath(log)) - if strings.Contains(path, "/api/v1/auth/oidc/login/accept") { - appName = "OIDC 로그인" - // 우선 audit details의 client 정보를 사용하고, 없으면 Hydra 조회로 보강 - if details, err := utils.ParseAuditDetails(log.Details); err == nil && details != nil { - if name, ok := details["client_name"].(string); ok && strings.TrimSpace(name) != "" { - appName = strings.TrimSpace(name) - } - if cid, ok := details["client_id"].(string); ok && strings.TrimSpace(cid) != "" { - clientID = strings.TrimSpace(cid) - if appName == "OIDC 로그인" { - appName = clientID - } - } + + isOidcAccept := strings.Contains(path, "/api/v1/auth/oidc/login/accept") + isPasswordLogin := strings.Contains(path, "/api/v1/auth/password/login") + + // 우선 audit details의 client 정보를 사용 + if details, err := utils.ParseAuditDetails(log.Details); err == nil && details != nil { + if cid, ok := details["client_id"].(string); ok && strings.TrimSpace(cid) != "" { + clientID = strings.TrimSpace(cid) } - if appName == "OIDC 로그인" { - loginChallenge := extractLoginChallengeFromAuditDetails(log.Details) - if loginChallenge != "" { - if info, ok := resolveLoginClient(loginChallenge); ok { - appName = info.Name - clientID = info.ClientID - } + if name, ok := details["client_name"].(string); ok && strings.TrimSpace(name) != "" { + appName = strings.TrimSpace(name) + } + } + + // 기본값이거나 클라이언트 ID인 경우 Hydra 조회로 보강 + if appName == "Baron 로그인" || appName == "" { + if isOidcAccept { + appName = "OIDC 로그인" + } + if clientID != "" { + appName = clientID + if info, ok := resolveClientByID(clientID); ok { + appName = info.Name } } } + + if (isOidcAccept || isPasswordLogin) && (appName == "OIDC 로그인" || appName == "Baron 로그인" || appName == clientID) { + loginChallenge := extractLoginChallengeFromAuditDetails(log.Details) + if loginChallenge != "" { + if info, ok := resolveLoginClient(loginChallenge); ok { + appName = info.Name + clientID = info.ClientID + } + } + } + item := authTimelineItem{ EventID: log.EventID, Timestamp: log.Timestamp, @@ -4353,11 +4391,17 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { if clientID == "" { continue } - consent := consentMap[clientID] - appName := consent.Name - if appName == "" { - appName = clientID + + appName := clientID + if consent, ok := consentMap[clientID]; ok { + appName = consent.Name } + if appName == "" || appName == clientID { + if info, ok := resolveClientByID(clientID); ok { + appName = info.Name + } + } + details := map[string]any{ "path": log.Path, "client_id": clientID, From 6843b96fe01e51dd24dab5c6053e430dd529b795 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 11:45:34 +0900 Subject: [PATCH 16/39] =?UTF-8?q?userfront=20=EC=A0=91=EC=86=8D=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=20UI=20=EC=84=B8=EC=85=98=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ko.toml | 2 +- userfront/assets/translations/en.toml | 11 +- userfront/assets/translations/ko.toml | 15 +- userfront/assets/translations/template.toml | 7 + .../presentation/dashboard_screen.dart | 561 +++++++++--------- userfront/lib/i18n_data.dart | 2 +- 6 files changed, 314 insertions(+), 284 deletions(-) diff --git a/locales/ko.toml b/locales/ko.toml index 72bf3eed..9a3aa1c8 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -2481,7 +2481,7 @@ linked = "연동됨" [ui.userfront.dashboard.sessions] active_badge = "활성화" -current_badge = "현재 접속중" +current_badge = "접속중" current_disabled = "현재 세션" unknown_device = "알 수 없는 기기" unknown_session = "세션 정보" diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 3a27641a..9791416c 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -47,11 +47,13 @@ greeting = "Hello, {name}." date = "Date: {value}" device = "Device: {value}" end = "No more items to show." +filtered_empty = "There are no active or current sessions to display." +filter.description = "Turn this on to show only active or current sessions." ip = "IP address: {value}" load_more_error = "Could not load more history." result = "Result: {value}" session_id = "Session ID: {value}" -status = "Status: pending" +status = "Status: {value}" [msg.userfront.consent] accept_error = "Failed to process consent: {error}" @@ -283,7 +285,7 @@ uppercase = "At least one uppercase letter" [msg.userfront.sections] apps_subtitle = "Your linked apps and their latest sign-in status." -audit_subtitle = "Recent access history for Baron sign-in." +audit_subtitle = "Review current session status and recent sign-in history together." sessions_subtitle = "Your currently signed-in devices and browser sessions." [msg.userfront.settings] @@ -434,7 +436,12 @@ dev_console = "Dev Console" [ui.userfront.audit] +[ui.userfront.audit.filter] +title = "Session filter" +toggle_label = "Active only" + [ui.userfront.audit.table] +action = "Action" app = "App" auth_method = "Auth Method" date = "Date" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 9fc24973..247f2bfd 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -44,11 +44,13 @@ missing = "활성 세션이 없습니다." date = "접속일자: {value}" device = "접속환경: {value}" end = "더 이상 항목이 없습니다." +filtered_empty = "활성화 또는 접속중인 세션이 없습니다." +filter.description = "토글을 켜면 활성화 또는 접속중인 세션만 표시됩니다." ip = "접속 IP: {value}" load_more_error = "더 불러오지 못했습니다." result = "인증결과: {value}" session_id = "Session ID: {value}" -status = "현황: (준비중)" +status = "현황: {value}" [msg.userfront.dashboard] approved_device = "승인 기기: {device}" @@ -138,7 +140,7 @@ success = "비밀번호가 성공적으로 변경되었습니다. 다시 로그 [msg.userfront.sections] apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다." -audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다." +audit_subtitle = "현재 세션 현황과 최근 접근 기록을 함께 확인할 수 있습니다." sessions_subtitle = "현재 로그인된 기기와 브라우저 세션입니다." [msg.userfront.settings] @@ -487,7 +489,7 @@ uppercase = "대문자 1개 이상" [msg.userfront.sections] apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다." -audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다." +audit_subtitle = "현재 세션 현황과 최근 접근 기록을 함께 확인할 수 있습니다." [msg.userfront.settings] disabled = "현재 계정 설정 화면은 준비 중입니다." @@ -637,7 +639,12 @@ dev_console = "Dev Console" [ui.userfront.audit] +[ui.userfront.audit.filter] +title = "세션 필터" +toggle_label = "활성 세션만" + [ui.userfront.audit.table] +action = "관리" app = "애플리케이션" auth_method = "인증수단" date = "접속일자" @@ -670,7 +677,7 @@ linked = "연동됨" [ui.userfront.dashboard.sessions] active_badge = "활성화" -current_badge = "현재 접속중" +current_badge = "접속중" current_disabled = "현재 세션" unknown_device = "알 수 없는 기기" unknown_session = "세션 정보" diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index c902ac09..d51aa5c6 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -226,6 +226,8 @@ greeting = "" date = "" device = "" end = "" +filtered_empty = "" +filter.description = "" ip = "" load_more_error = "" result = "" @@ -612,7 +614,12 @@ dev_console = "" [ui.userfront.audit] +[ui.userfront.audit.filter] +title = "" +toggle_label = "" + [ui.userfront.audit.table] +action = "" app = "" auth_method = "" date = "" diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index fc93c46f..8b575324 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -53,6 +53,7 @@ class _DashboardScreenState extends ConsumerState { bool _authBootstrapInProgress = false; bool _showAllActivities = false; + bool _showActiveSessionsOnly = false; final Set _revokedClientIds = {}; String _renderTranslatedText( @@ -836,13 +837,6 @@ class _DashboardScreenState extends ConsumerState { ), const SizedBox(height: 28), ], - _buildSectionTitle( - tr('ui.userfront.sections.sessions'), - tr('msg.userfront.sections.sessions_subtitle'), - ), - const SizedBox(height: 12), - _buildSessionSection(isMobile), - const SizedBox(height: 28), _buildSectionTitle( tr('ui.userfront.sections.apps'), tr('msg.userfront.sections.apps_subtitle'), @@ -972,245 +966,6 @@ class _DashboardScreenState extends ConsumerState { ); } - Widget _buildSessionSection(bool isMobile) { - final sessionsState = ref.watch(userSessionsProvider); - return sessionsState.when( - data: (sessions) { - if (sessions.isEmpty) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('msg.userfront.dashboard.sessions.empty'), - style: TextStyle( - fontSize: 14, - color: Colors.grey[700], - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 6), - Text( - tr('msg.userfront.dashboard.sessions.empty_detail'), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ], - ); - } - return _buildSessionGrid(sessions, isMobile); - }, - loading: () => const SizedBox( - height: 100, - child: Center(child: CircularProgressIndicator()), - ), - error: (error, stack) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr('msg.userfront.dashboard.sessions.error'), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - const SizedBox(height: 8), - TextButton( - onPressed: () => ref.read(userSessionsProvider.notifier).refresh(), - child: Text(tr('ui.common.retry')), - ), - ], - ), - ); - } - - Widget _buildSessionGrid(List sessions, bool isMobile) { - return LayoutBuilder( - builder: (context, constraints) { - final crossAxisCount = _dashboardCardColumnCount(constraints.maxWidth); - final cardWidth = _dashboardCardWidth( - constraints.maxWidth, - crossAxisCount, - ); - - return Wrap( - spacing: _dashboardCardSpacing, - runSpacing: _dashboardCardSpacing, - children: sessions.map((session) { - return SizedBox( - width: cardWidth, - child: _buildSessionCard(session, cardWidth: cardWidth), - ); - }).toList(), - ); - }, - ); - } - - Widget _buildSessionCard(UserSessionSummary session, {double? cardWidth}) { - final isCurrent = session.isCurrent; - final statusColor = session.isActive ? Colors.green : Colors.grey; - final primaryTime = - session.lastSeenAt ?? - session.authenticatedAt ?? - session.issuedAt ?? - session.expiresAt; - final primaryTimeLabel = primaryTime != null - ? _formatDateTime(primaryTime) - : tr('ui.userfront.session.unknown'); - final sessionLabel = _sessionPrimaryLabel(session); - final clientLabel = _sessionClientLabel(session); - final browserLabel = _sessionBrowserLabel(session.userAgent); - final osLabel = _sessionOsLabel(session.userAgent); - final canRevoke = !isCurrent && _revokingSessionId == null; - - return Container( - width: cardWidth ?? 320, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: _surface, - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: isCurrent ? Colors.blueGrey : _border, - width: isCurrent ? 1.5 : 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 8), - blurRadius: 12, - offset: const Offset(0, 6), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - sessionLabel, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: _ink, - ), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: isCurrent ? Colors.blueGrey : statusColor, - borderRadius: BorderRadius.circular(999), - ), - child: Text( - isCurrent - ? tr('ui.userfront.dashboard.sessions.current_badge') - : session.isActive - ? tr('ui.userfront.dashboard.sessions.active_badge') - : tr('ui.common.status.inactive'), - style: const TextStyle( - fontSize: 11, - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - if (clientLabel.isNotEmpty) ...[ - Text( - clientLabel, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: _ink, - ), - ), - const SizedBox(height: 8), - ], - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _buildInfoChip(Icons.access_time, primaryTimeLabel), - if (session.ipAddress.isNotEmpty) - _buildInfoChip(Icons.public, session.ipAddress), - ], - ), - if (browserLabel.isNotEmpty || osLabel.isNotEmpty) ...[ - const SizedBox(height: 12), - if (browserLabel.isNotEmpty) - Text( - _renderTranslatedText( - 'msg.userfront.dashboard.sessions.browser', - values: {'value': browserLabel}, - ), - style: TextStyle(fontSize: 13, color: Colors.grey[700]), - ), - if (osLabel.isNotEmpty) ...[ - const SizedBox(height: 4), - Text( - _renderTranslatedText( - 'msg.userfront.dashboard.sessions.os', - values: {'value': osLabel}, - ), - style: TextStyle(fontSize: 13, color: Colors.grey[700]), - ), - ], - ], - if (session.clientId.trim().isNotEmpty) ...[ - const SizedBox(height: 6), - Text( - _renderTranslatedText( - 'msg.userfront.dashboard.client_id', - fallback: 'Client ID: {{id}}', - values: {'id': session.clientId}, - ), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - ], - const SizedBox(height: 8), - Text( - _renderTranslatedText( - 'msg.userfront.dashboard.sessions.session_id', - fallback: 'Session ID: {{id}}', - values: {'id': _compactSessionId(session.sessionId)}, - ), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: OutlinedButton( - onPressed: canRevoke ? () => _onRevokeSession(session) : null, - style: OutlinedButton.styleFrom( - foregroundColor: canRevoke ? Colors.redAccent : Colors.grey, - side: BorderSide( - color: canRevoke ? Colors.redAccent : Colors.grey, - width: 0.6, - ), - padding: const EdgeInsets.symmetric(vertical: 10), - ), - child: _revokingSessionId == session.sessionId - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.redAccent, - ), - ) - : Text( - isCurrent - ? tr( - 'ui.userfront.dashboard.sessions.current_disabled', - ) - : tr('ui.userfront.dashboard.sessions.revoke.action'), - ), - ), - ), - ], - ), - ); - } - String _sessionDisplayLabel(UserSessionSummary session) { if (session.userAgent.trim().isNotEmpty) { return _sessionUserAgentLabel(session.userAgent); @@ -1709,46 +1464,161 @@ class _DashboardScreenState extends ConsumerState { } Widget _buildAccessHistory(AuthTimelineState state, bool isWide) { + final sessionsState = ref.watch(userSessionsProvider); if (state.isLoading && state.items.isEmpty) { return _buildHistoryContainer( - child: const Center(child: CircularProgressIndicator()), + child: const SizedBox( + height: 120, + child: Center(child: CircularProgressIndicator()), + ), ); } if (state.error != null && state.items.isEmpty) { return _buildHistoryContainer( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(tr('msg.userfront.dashboard.audit_load_error')), - const SizedBox(height: 8), - TextButton( - onPressed: () => - ref.read(authTimelineProvider.notifier).refresh(), - child: Text(tr('ui.common.retry')), - ), - ], + child: SizedBox( + height: 120, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(tr('msg.userfront.dashboard.audit_load_error')), + const SizedBox(height: 8), + TextButton( + onPressed: () => + ref.read(authTimelineProvider.notifier).refresh(), + child: Text(tr('ui.common.retry')), + ), + ], + ), ), ), ); } - if (state.items.isEmpty) { + if (sessionsState.isLoading && !sessionsState.hasValue) { return _buildHistoryContainer( - child: Center( - child: Text( - tr('msg.userfront.dashboard.audit_empty'), - style: TextStyle(color: Colors.grey[600]), + child: const SizedBox( + height: 120, + child: Center(child: CircularProgressIndicator()), + ), + ); + } + + if (sessionsState.hasError && !sessionsState.hasValue) { + return _buildHistoryContainer( + child: SizedBox( + height: 120, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(tr('msg.userfront.dashboard.sessions.error')), + const SizedBox(height: 8), + TextButton( + onPressed: () => + ref.read(userSessionsProvider.notifier).refresh(), + child: Text(tr('ui.common.retry')), + ), + ], + ), ), ), ); } + final sessions = sessionsState is AsyncData> + ? sessionsState.value + : const []; + final Map sessionById = { + for (final session in sessions) session.sessionId.trim(): session, + }; + final filteredItems = state.items.where((log) { + if (!_showActiveSessionsOnly) { + return true; + } + final status = _historySessionStatusForLog(log, sessionById); + return status != _HistorySessionStatus.inactive; + }).toList(); + + if (filteredItems.isEmpty) { + return _buildHistoryContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHistoryHeader(), + const SizedBox(height: 20), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + _showActiveSessionsOnly + ? tr('msg.userfront.audit.filtered_empty') + : tr('msg.userfront.dashboard.audit_empty'), + style: TextStyle(color: Colors.grey[600]), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ); + } + if (isWide) { - return _buildHistoryTable(state); + return _buildHistoryTable(state, filteredItems, sessionById); } - return _buildHistoryList(state); + return _buildHistoryList(state, filteredItems, sessionById); + } + + Widget _buildHistoryHeader() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tr('ui.userfront.audit.filter.title'), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: _ink, + ), + ), + const SizedBox(height: 4), + Text( + tr('msg.userfront.audit.filter.description'), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ), + ), + const SizedBox(width: 16), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + tr('ui.userfront.audit.filter.toggle_label'), + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: _ink, + ), + ), + Switch( + value: _showActiveSessionsOnly, + onChanged: (value) { + setState(() { + _showActiveSessionsOnly = value; + }); + }, + ), + ], + ), + ], + ); } Widget _buildHistoryContainer({required Widget child}) { @@ -1764,6 +1634,106 @@ class _DashboardScreenState extends ConsumerState { ); } + _HistorySessionStatus _historySessionStatusForLog( + AuditLogEntry log, + Map sessionById, + ) { + final sessionId = log.sessionId.trim(); + if (sessionId.isEmpty) { + return _HistorySessionStatus.inactive; + } + final session = sessionById[sessionId]; + if (session == null) { + return _HistorySessionStatus.inactive; + } + if (session.isCurrent) { + return _HistorySessionStatus.current; + } + if (session.isActive) { + return _HistorySessionStatus.active; + } + return _HistorySessionStatus.inactive; + } + + String _historySessionStatusLabel(_HistorySessionStatus status) { + switch (status) { + case _HistorySessionStatus.current: + return tr('ui.userfront.dashboard.sessions.current_badge'); + case _HistorySessionStatus.active: + return tr('ui.userfront.dashboard.sessions.active_badge'); + case _HistorySessionStatus.inactive: + return tr('ui.common.status.inactive'); + } + } + + Color _historySessionStatusColor(_HistorySessionStatus status) { + switch (status) { + case _HistorySessionStatus.current: + return Colors.blueGrey; + case _HistorySessionStatus.active: + return Colors.green; + case _HistorySessionStatus.inactive: + return Colors.grey; + } + } + + Widget _buildHistoryStatusBadge(_HistorySessionStatus status) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _historySessionStatusColor(status), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + _historySessionStatusLabel(status), + style: const TextStyle( + fontSize: 11, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + Widget _buildHistorySessionActionCell(UserSessionSummary? session) { + if (session == null) { + return _selectableText(tr('ui.common.hyphen', fallback: '-')); + } + final isCurrent = session.isCurrent; + final canRevoke = + !isCurrent && _revokingSessionId == null && session.isActive; + return SizedBox( + width: 108, + child: OutlinedButton( + onPressed: canRevoke ? () => _onRevokeSession(session) : null, + style: OutlinedButton.styleFrom( + foregroundColor: canRevoke ? Colors.redAccent : Colors.grey, + side: BorderSide( + color: canRevoke ? Colors.redAccent : Colors.grey, + width: 0.6, + ), + padding: const EdgeInsets.symmetric(vertical: 10), + ), + child: _revokingSessionId == session.sessionId + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.redAccent, + ), + ) + : Text( + isCurrent + ? tr('ui.userfront.dashboard.sessions.current_disabled') + : session.isActive + ? tr('ui.userfront.dashboard.sessions.revoke.action') + : tr('ui.common.hyphen', fallback: '-'), + ), + ), + ); + } + int _dashboardCardColumnCount(double maxWidth) { if (maxWidth > 1200) { return 4; @@ -1779,10 +1749,16 @@ class _DashboardScreenState extends ConsumerState { crossAxisCount; } - Widget _buildHistoryTable(AuthTimelineState state) { + Widget _buildHistoryTable( + AuthTimelineState state, + List items, + Map sessionById, + ) { return _buildHistoryContainer( child: Column( children: [ + _buildHistoryHeader(), + const SizedBox(height: 16), LayoutBuilder( builder: (context, constraints) { final sessionColumnWidth = _historySessionColumnWidth( @@ -1830,8 +1806,16 @@ class _DashboardScreenState extends ConsumerState { DataColumn( label: Text(tr('ui.userfront.audit.table.status')), ), + DataColumn( + label: Text(tr('ui.userfront.audit.table.action')), + ), ], - rows: state.items.map((log) { + rows: items.map((log) { + final matchedSession = sessionById[log.sessionId.trim()]; + final sessionStatus = _historySessionStatusForLog( + log, + sessionById, + ); final statusLabel = log.status == 'success' ? tr('ui.common.status.success') : tr('ui.common.status.failure'); @@ -1879,11 +1863,9 @@ class _DashboardScreenState extends ConsumerState { ), ), ), + DataCell(_buildHistoryStatusBadge(sessionStatus)), DataCell( - _selectableText( - tr('ui.userfront.audit.table.pending'), - style: const TextStyle(color: Colors.grey), - ), + _buildHistorySessionActionCell(matchedSession), ), ], ); @@ -1932,11 +1914,17 @@ class _DashboardScreenState extends ConsumerState { return Tooltip(message: sessionId, child: textWidget); } - Widget _buildHistoryList(AuthTimelineState state) { + Widget _buildHistoryList( + AuthTimelineState state, + List items, + Map sessionById, + ) { return _buildHistoryContainer( child: Column( children: [ - for (final log in state.items) + _buildHistoryHeader(), + const SizedBox(height: 16), + for (final log in items) Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(12), @@ -1948,6 +1936,15 @@ class _DashboardScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + children: [ + _buildHistoryStatusBadge( + _historySessionStatusForLog(log, sessionById), + ), + const Spacer(), + ], + ), + const SizedBox(height: 8), Row( children: [ Expanded( @@ -2025,8 +2022,18 @@ class _DashboardScreenState extends ConsumerState { ), ), _selectableText( - tr('msg.userfront.audit.status'), - style: TextStyle(color: Colors.grey[600]), + tr( + 'msg.userfront.audit.status', + params: { + 'value': _historySessionStatusLabel( + _historySessionStatusForLog(log, sessionById), + ), + }, + ), + ), + const SizedBox(height: 12), + _buildHistorySessionActionCell( + sessionById[log.sessionId.trim()], ), ], ), @@ -2143,6 +2150,8 @@ class _DashboardScreenState extends ConsumerState { } } +enum _HistorySessionStatus { current, active, inactive } + class _ActivityItem { final String clientId; final String appName; diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index 08155de5..db6b951a 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -1715,7 +1715,7 @@ const Map koStrings = { "ui.userfront.dashboard.revoke.title": "연동 해지", "ui.userfront.dashboard.scopes.title": "권한 (Scopes)", "ui.userfront.dashboard.sessions.active_badge": "활성화", - "ui.userfront.dashboard.sessions.current_badge": "현재 접속중", + "ui.userfront.dashboard.sessions.current_badge": "접속중", "ui.userfront.dashboard.sessions.current_disabled": "현재 세션", "ui.userfront.dashboard.sessions.revoke.action": "세션 종료", "ui.userfront.dashboard.sessions.revoke.title": "세션 종료", From e6ade9ce770b03caa33d2283b3c9d18747169dcb Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 13:12:11 +0900 Subject: [PATCH 17/39] =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A0=95=EB=A6=AC?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EC=A0=80=EC=9E=A5=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trouble-shooting/issue_489_completion_summary.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename issue_489_completion_summary.md => docs/trouble-shooting/issue_489_completion_summary.md (100%) diff --git a/issue_489_completion_summary.md b/docs/trouble-shooting/issue_489_completion_summary.md similarity index 100% rename from issue_489_completion_summary.md rename to docs/trouble-shooting/issue_489_completion_summary.md From 7b2004e05c8a78a672029ba7ba43b07b333fac79 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 13:12:44 +0900 Subject: [PATCH 18/39] =?UTF-8?q?=EC=A0=91=EC=86=8D=EC=9D=B4=EB=A0=A5=20?= =?UTF-8?q?=EB=B0=8F=20=ED=99=9C=EC=84=B1=20=EC=84=B8=EC=85=98=20UI=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=EB=B0=8F=20i18n=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.toml | 9 +++++++++ locales/ko.toml | 9 +++++++++ locales/template.toml | 9 +++++++++ userfront/assets/translations/en.toml | 4 ++++ userfront/assets/translations/ko.toml | 4 ++++ userfront/assets/translations/template.toml | 4 ++++ userfront/lib/i18n_data.dart | 9 +++++++++ 7 files changed, 48 insertions(+) diff --git a/locales/en.toml b/locales/en.toml index 9f07f641..aa9be933 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -2055,6 +2055,7 @@ dev_console = "Dev Console" [ui.userfront.audit] [ui.userfront.audit.table] +action = "Action" app = "App" auth_method = "Auth Method" date = "Date" @@ -2278,3 +2279,11 @@ verify = "Verification" [ui.userfront.signup.success] action = "Go to sign-in" + + +[ui.userfront.audit.filter] +title = "Manage My Activity" +toggle_label = "Show active sessions only" + +[msg.userfront.audit.filter] +description = "Toggle to view only active sessions." \ No newline at end of file diff --git a/locales/ko.toml b/locales/ko.toml index 9a3aa1c8..0c4c27a1 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -2449,6 +2449,7 @@ dev_console = "Dev Console" [ui.userfront.audit] [ui.userfront.audit.table] +action = "관리" app = "애플리케이션" auth_method = "인증수단" date = "접속일자" @@ -2671,3 +2672,11 @@ verify = "본인인증" [ui.userfront.signup.success] action = "로그인하기" + + +[ui.userfront.audit.filter] +title = "내 활동 관리" +toggle_label = "활성 세션만 보기" + +[msg.userfront.audit.filter] +description = "활성화된 세션만 보려면 토글을 켜주세요." \ No newline at end of file diff --git a/locales/template.toml b/locales/template.toml index ff8d60a1..d34c647e 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -2327,6 +2327,7 @@ dev_console = "" [ui.userfront.audit] [ui.userfront.audit.table] +action = "" app = "" auth_method = "" date = "" @@ -2549,3 +2550,11 @@ verify = "" [ui.userfront.signup.success] action = "" + + +[ui.userfront.audit.filter] +title = "" +toggle_label = "" + +[msg.userfront.audit.filter] +description = "" \ No newline at end of file diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 9791416c..33df87a6 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -665,3 +665,7 @@ verify = "Verification" [ui.userfront.signup.success] action = "Go to sign-in" + + +[msg.userfront.audit.filter] +description = "Toggle to view only active sessions." \ No newline at end of file diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 247f2bfd..da621486 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -867,3 +867,7 @@ verify = "본인인증" [ui.userfront.signup.success] action = "로그인하기" + + +[msg.userfront.audit.filter] +description = "활성화된 세션만 보려면 토글을 켜주세요." \ No newline at end of file diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index d51aa5c6..d5c036db 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -842,3 +842,7 @@ verify = "" [ui.userfront.signup.success] action = "" + + +[msg.userfront.audit.filter] +description = "" \ No newline at end of file diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index db6b951a..b1a416c3 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -416,6 +416,7 @@ const Map koStrings = { "msg.userfront.audit.date": "접속일자: {{value}}", "msg.userfront.audit.device": "접속환경: {{value}}", "msg.userfront.audit.end": "더 이상 항목이 없습니다.", + "msg.userfront.audit.filter.description": "활성화된 세션만 보려면 토글을 켜주세요.", "msg.userfront.audit.ip": "접속 IP: {{value}}", "msg.userfront.audit.load_more_error": "더 불러오지 못했습니다.", "msg.userfront.audit.result": "인증결과: {{value}}", @@ -1691,6 +1692,9 @@ const Map koStrings = { "ui.userfront.app_label.baron": "Baron 로그인", "ui.userfront.app_label.dev_console": "Dev Console", "ui.userfront.app_title": "Baron SW 포탈", + "ui.userfront.audit.filter.title": "내 활동 관리", + "ui.userfront.audit.filter.toggle_label": "활성 세션만 보기", + "ui.userfront.audit.table.action": "관리", "ui.userfront.audit.table.app": "애플리케이션", "ui.userfront.audit.table.auth_method": "인증수단", "ui.userfront.audit.table.date": "접속일자", @@ -2315,6 +2319,8 @@ const Map enStrings = { "msg.userfront.audit.date": "Date: {{value}}", "msg.userfront.audit.device": "Device: {{value}}", "msg.userfront.audit.end": "No more items to show.", + "msg.userfront.audit.filter.description": + "Toggle to view only active sessions.", "msg.userfront.audit.ip": "IP address: {{value}}", "msg.userfront.audit.load_more_error": "Could not load more history.", "msg.userfront.audit.result": "Result: {{value}}", @@ -3694,6 +3700,9 @@ const Map enStrings = { "ui.userfront.app_label.baron": "Baron", "ui.userfront.app_label.dev_console": "Dev Console", "ui.userfront.app_title": "Baron SW Portal", + "ui.userfront.audit.filter.title": "Manage My Activity", + "ui.userfront.audit.filter.toggle_label": "Show active sessions only", + "ui.userfront.audit.table.action": "Action", "ui.userfront.audit.table.app": "App", "ui.userfront.audit.table.auth_method": "Auth Method", "ui.userfront.audit.table.date": "Date", From c95105f01878cb43bbcc5be6399ac062f9cb1a32 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 13:38:13 +0900 Subject: [PATCH 19/39] =?UTF-8?q?=EC=A0=91=EC=86=8D=EC=9D=B4=EB=A0=A5=20?= =?UTF-8?q?=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.toml | 4 +- locales/ko.toml | 4 +- userfront/assets/translations/en.toml | 4 +- userfront/assets/translations/ko.toml | 4 +- userfront/assets/translations/template.toml | 4 +- .../presentation/dashboard_screen.dart | 68 +++++++++++++++---- userfront/lib/i18n_data.dart | 4 ++ 7 files changed, 72 insertions(+), 20 deletions(-) diff --git a/locales/en.toml b/locales/en.toml index aa9be933..cbc9ab29 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -509,6 +509,7 @@ saved_success = "Saved successfully." greeting = "Hello, {{name}}." [msg.userfront.audit] +browser = "Browser: {{value}}" date = "Date: {{value}}" device = "Device: {{value}}" end = "No more items to show." @@ -2058,6 +2059,7 @@ dev_console = "Dev Console" action = "Action" app = "App" auth_method = "Auth Method" +browser = "Browser" date = "Date" device = "Device" ip = "IP" @@ -2286,4 +2288,4 @@ title = "Manage My Activity" toggle_label = "Show active sessions only" [msg.userfront.audit.filter] -description = "Toggle to view only active sessions." \ No newline at end of file +description = "Toggle to view only active sessions." diff --git a/locales/ko.toml b/locales/ko.toml index 0c4c27a1..6243c0af 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -171,6 +171,7 @@ missing_jwks_uri = "JWKS URI를 입력해야 합니다." private_key_jwt_requires_public_key = "서명 키 기반 인증을 사용하려면 JWKS URI가 필요합니다." [msg.userfront.audit] +browser = "브라우저: {{value}}" date = "접속일자: {{value}}" device = "접속환경: {{value}}" end = "더 이상 항목이 없습니다." @@ -2452,6 +2453,7 @@ dev_console = "Dev Console" action = "관리" app = "애플리케이션" auth_method = "인증수단" +browser = "브라우저" date = "접속일자" device = "접속환경" ip = "IP" @@ -2679,4 +2681,4 @@ title = "내 활동 관리" toggle_label = "활성 세션만 보기" [msg.userfront.audit.filter] -description = "활성화된 세션만 보려면 토글을 켜주세요." \ No newline at end of file +description = "활성화된 세션만 보려면 토글을 켜주세요." diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 33df87a6..5f5a3298 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -44,6 +44,7 @@ missing = "No active session was found." greeting = "Hello, {name}." [msg.userfront.audit] +browser = "Browser: {value}" date = "Date: {value}" device = "Device: {value}" end = "No more items to show." @@ -444,6 +445,7 @@ toggle_label = "Active only" action = "Action" app = "App" auth_method = "Auth Method" +browser = "Browser" date = "Date" device = "Device" ip = "IP" @@ -668,4 +670,4 @@ action = "Go to sign-in" [msg.userfront.audit.filter] -description = "Toggle to view only active sessions." \ No newline at end of file +description = "Toggle to view only active sessions." diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index da621486..eee97bbe 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -41,6 +41,7 @@ verify_code_failed = "인증 실패: {error}" missing = "활성 세션이 없습니다." [msg.userfront.audit] +browser = "브라우저: {value}" date = "접속일자: {value}" device = "접속환경: {value}" end = "더 이상 항목이 없습니다." @@ -647,6 +648,7 @@ toggle_label = "활성 세션만" action = "관리" app = "애플리케이션" auth_method = "인증수단" +browser = "브라우저" date = "접속일자" device = "접속환경" ip = "IP" @@ -870,4 +872,4 @@ action = "로그인하기" [msg.userfront.audit.filter] -description = "활성화된 세션만 보려면 토글을 켜주세요." \ No newline at end of file +description = "활성화된 세션만 보려면 토글을 켜주세요." diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index d5c036db..f3d544ba 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -223,6 +223,7 @@ title = "" greeting = "" [msg.userfront.audit] +browser = "" date = "" device = "" end = "" @@ -622,6 +623,7 @@ toggle_label = "" action = "" app = "" auth_method = "" +browser = "" date = "" device = "" ip = "" @@ -845,4 +847,4 @@ action = "" [msg.userfront.audit.filter] -description = "" \ No newline at end of file +description = "" diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 8b575324..a7ba6f24 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -40,6 +40,8 @@ class _DashboardScreenState extends ConsumerState { static const double _historySessionMinWidth = 92; static const double _historyOtherColumnsBaselineWidth = 780; static const int _historySessionMinVisibleChars = 8; + static const double _historyStatusColumnWidth = 92; + static const double _historyActionColumnWidth = 108; final ScrollController _pageScrollController = ScrollController(); final ScrollController _rpScrollController = ScrollController(); @@ -1678,18 +1680,23 @@ class _DashboardScreenState extends ConsumerState { } Widget _buildHistoryStatusBadge(_HistorySessionStatus status) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: _historySessionStatusColor(status), - borderRadius: BorderRadius.circular(999), - ), - child: Text( - _historySessionStatusLabel(status), - style: const TextStyle( - fontSize: 11, - color: Colors.white, - fontWeight: FontWeight.w600, + return SizedBox( + width: _historyStatusColumnWidth, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _historySessionStatusColor(status), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + _historySessionStatusLabel(status), + style: const TextStyle( + fontSize: 11, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), ), ), ); @@ -1703,7 +1710,7 @@ class _DashboardScreenState extends ConsumerState { final canRevoke = !isCurrent && _revokingSessionId == null && session.isActive; return SizedBox( - width: 108, + width: _historyActionColumnWidth, child: OutlinedButton( onPressed: canRevoke ? () => _onRevokeSession(session) : null, style: OutlinedButton.styleFrom( @@ -1797,6 +1804,9 @@ class _DashboardScreenState extends ConsumerState { DataColumn( label: Text(tr('ui.userfront.audit.table.device')), ), + DataColumn( + label: Text(tr('ui.userfront.audit.table.browser')), + ), DataColumn( label: Text(tr('ui.userfront.audit.table.auth_method')), ), @@ -1804,10 +1814,20 @@ class _DashboardScreenState extends ConsumerState { label: Text(tr('ui.userfront.audit.table.result')), ), DataColumn( - label: Text(tr('ui.userfront.audit.table.status')), + label: SizedBox( + width: _historyStatusColumnWidth, + child: Center( + child: Text(tr('ui.userfront.audit.table.status')), + ), + ), ), DataColumn( - label: Text(tr('ui.userfront.audit.table.action')), + label: SizedBox( + width: _historyActionColumnWidth, + child: Center( + child: Text(tr('ui.userfront.audit.table.action')), + ), + ), ), ], rows: items.map((log) { @@ -1828,6 +1848,7 @@ class _DashboardScreenState extends ConsumerState { final deviceLabel = _deviceLabelFromUserAgent( log.userAgent, ); + final browserLabel = _sessionBrowserLabel(log.userAgent); return DataRow( cells: [ DataCell( @@ -1853,6 +1874,13 @@ class _DashboardScreenState extends ConsumerState { ), ), DataCell(_selectableText(deviceLabel)), + DataCell( + _selectableText( + browserLabel.isEmpty + ? tr('ui.common.hyphen', fallback: '-') + : browserLabel, + ), + ), DataCell(_buildAuthMethodCell(log, authMethod)), DataCell( _selectableText( @@ -2005,6 +2033,16 @@ class _DashboardScreenState extends ConsumerState { }, ), ), + _selectableText( + tr( + 'msg.userfront.audit.browser', + params: { + 'value': _sessionBrowserLabel(log.userAgent).isEmpty + ? tr('ui.common.hyphen', fallback: '-') + : _sessionBrowserLabel(log.userAgent), + }, + ), + ), _buildAuthMethodLine( log, log.authMethod.isNotEmpty diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index b1a416c3..7974493c 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -413,6 +413,7 @@ const Map koStrings = { "msg.dev.sidebar.notice": "개발자 전용 콘솔입니다.", "msg.dev.sidebar.notice_detail": "연동 앱 등록 및 관리를 수행할 수 있습니다.", "msg.info.saved_success": "저장이 완료되었습니다.", + "msg.userfront.audit.browser": "브라우저: {{value}}", "msg.userfront.audit.date": "접속일자: {{value}}", "msg.userfront.audit.device": "접속환경: {{value}}", "msg.userfront.audit.end": "더 이상 항목이 없습니다.", @@ -1697,6 +1698,7 @@ const Map koStrings = { "ui.userfront.audit.table.action": "관리", "ui.userfront.audit.table.app": "애플리케이션", "ui.userfront.audit.table.auth_method": "인증수단", + "ui.userfront.audit.table.browser": "브라우저", "ui.userfront.audit.table.date": "접속일자", "ui.userfront.audit.table.device": "접속환경", "ui.userfront.audit.table.ip": "IP", @@ -2316,6 +2318,7 @@ const Map enStrings = { "msg.dev.sidebar.notice": "Developer Console", "msg.dev.sidebar.notice_detail": "Register and manage client applications.", "msg.info.saved_success": "Saved successfully.", + "msg.userfront.audit.browser": "Browser: {{value}}", "msg.userfront.audit.date": "Date: {{value}}", "msg.userfront.audit.device": "Device: {{value}}", "msg.userfront.audit.end": "No more items to show.", @@ -3705,6 +3708,7 @@ const Map enStrings = { "ui.userfront.audit.table.action": "Action", "ui.userfront.audit.table.app": "App", "ui.userfront.audit.table.auth_method": "Auth Method", + "ui.userfront.audit.table.browser": "Browser", "ui.userfront.audit.table.date": "Date", "ui.userfront.audit.table.device": "Device", "ui.userfront.audit.table.ip": "IP", From 2fb7bae5f68de59387a9ad1e66b2610aee015a1c Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 14:05:35 +0900 Subject: [PATCH 20/39] =?UTF-8?q?=EC=A0=91=EC=86=8D=EC=9D=B4=EB=A0=A5=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=B0=B0=EC=B9=98=20=EC=9E=91?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/dashboard_screen.dart | 189 +++++++++++++----- 1 file changed, 139 insertions(+), 50 deletions(-) diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index a7ba6f24..2aca33e6 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -40,6 +40,13 @@ class _DashboardScreenState extends ConsumerState { static const double _historySessionMinWidth = 92; static const double _historyOtherColumnsBaselineWidth = 780; static const int _historySessionMinVisibleChars = 8; + static const double _historyDateColumnWidth = 132; + static const double _historyAppColumnWidth = 132; + static const double _historyIpColumnWidth = 118; + static const double _historyDeviceColumnWidth = 128; + static const double _historyBrowserColumnWidth = 112; + static const double _historyAuthMethodColumnWidth = 108; + static const double _historyResultColumnWidth = 88; static const double _historyStatusColumnWidth = 92; static const double _historyActionColumnWidth = 108; @@ -499,6 +506,17 @@ class _DashboardScreenState extends ConsumerState { return SelectableText(text, style: style); } + Widget _singleLineText(String text, {TextStyle? style}) { + return Text( + text, + style: style, + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ); + } + String _authMethodLabel() { if (AuthTokenStore.usesCookie()) { return tr('ui.userfront.auth_method.ory'); @@ -1704,7 +1722,12 @@ class _DashboardScreenState extends ConsumerState { Widget _buildHistorySessionActionCell(UserSessionSummary? session) { if (session == null) { - return _selectableText(tr('ui.common.hyphen', fallback: '-')); + return SizedBox( + width: _historyActionColumnWidth, + child: Center( + child: _selectableText(tr('ui.common.hyphen', fallback: '-')), + ), + ); } final isCurrent = session.isCurrent; final canRevoke = @@ -1756,6 +1779,20 @@ class _DashboardScreenState extends ConsumerState { crossAxisCount; } + Widget _buildCenteredHistoryHeader(String label, {double? width}) { + return SizedBox( + width: width, + child: Center(child: Text(label, textAlign: TextAlign.center)), + ); + } + + Widget _buildCenteredHistoryCell(Widget child, {double? width}) { + return SizedBox( + width: width, + child: Center(child: child), + ); + } + Widget _buildHistoryTable( AuthTimelineState state, List items, @@ -1780,53 +1817,66 @@ class _DashboardScreenState extends ConsumerState { horizontalMargin: 12, columns: [ DataColumn( - label: SizedBox( + label: _buildCenteredHistoryHeader( + tr( + 'ui.userfront.audit.table.session_id', + fallback: 'Session ID', + ), width: sessionColumnWidth, - child: Text( - tr( - 'ui.userfront.audit.table.session_id', - fallback: 'Session ID', - ), - ), ), ), DataColumn( - label: Text(tr('ui.userfront.audit.table.date')), + label: _buildCenteredHistoryHeader( + tr('ui.userfront.audit.table.date'), + width: _historyDateColumnWidth, + ), ), DataColumn( - label: Text(tr('ui.userfront.audit.table.app')), + label: _buildCenteredHistoryHeader( + tr('ui.userfront.audit.table.app'), + width: _historyAppColumnWidth, + ), ), DataColumn( - label: Text( + label: _buildCenteredHistoryHeader( tr('ui.userfront.audit.table.ip', fallback: 'IP'), + width: _historyIpColumnWidth, ), ), DataColumn( - label: Text(tr('ui.userfront.audit.table.device')), + label: _buildCenteredHistoryHeader( + tr('ui.userfront.audit.table.device'), + width: _historyDeviceColumnWidth, + ), ), DataColumn( - label: Text(tr('ui.userfront.audit.table.browser')), + label: _buildCenteredHistoryHeader( + tr('ui.userfront.audit.table.browser'), + width: _historyBrowserColumnWidth, + ), ), DataColumn( - label: Text(tr('ui.userfront.audit.table.auth_method')), + label: _buildCenteredHistoryHeader( + tr('ui.userfront.audit.table.auth_method'), + width: _historyAuthMethodColumnWidth, + ), ), DataColumn( - label: Text(tr('ui.userfront.audit.table.result')), + label: _buildCenteredHistoryHeader( + tr('ui.userfront.audit.table.result'), + width: _historyResultColumnWidth, + ), ), DataColumn( - label: SizedBox( + label: _buildCenteredHistoryHeader( + tr('ui.userfront.audit.table.status'), width: _historyStatusColumnWidth, - child: Center( - child: Text(tr('ui.userfront.audit.table.status')), - ), ), ), DataColumn( - label: SizedBox( + label: _buildCenteredHistoryHeader( + tr('ui.userfront.audit.table.action'), width: _historyActionColumnWidth, - child: Center( - child: Text(tr('ui.userfront.audit.table.action')), - ), ), ), ], @@ -1852,48 +1902,83 @@ class _DashboardScreenState extends ConsumerState { return DataRow( cells: [ DataCell( - SizedBox( - width: sessionColumnWidth, - child: _buildHistorySessionIdCell( + _buildCenteredHistoryCell( + _buildHistorySessionIdCell( log.sessionId.isEmpty ? tr('ui.common.hyphen', fallback: '-') : log.sessionId, sessionColumnWidth, ), + width: sessionColumnWidth, ), ), DataCell( - _selectableText(_formatDateTime(log.timestamp)), - ), - DataCell(_buildAppCell(log)), - DataCell( - _selectableText( - log.ipAddress.isEmpty - ? tr('ui.common.hyphen', fallback: '-') - : log.ipAddress, + _buildCenteredHistoryCell( + _selectableText(_formatDateTime(log.timestamp)), + width: _historyDateColumnWidth, ), ), - DataCell(_selectableText(deviceLabel)), DataCell( - _selectableText( - browserLabel.isEmpty - ? tr('ui.common.hyphen', fallback: '-') - : browserLabel, + _buildCenteredHistoryCell( + _buildAppCell(log), + width: _historyAppColumnWidth, ), ), - DataCell(_buildAuthMethodCell(log, authMethod)), DataCell( - _selectableText( - statusLabel, - style: TextStyle( - color: statusColor, - fontWeight: FontWeight.w600, + _buildCenteredHistoryCell( + _selectableText( + log.ipAddress.isEmpty + ? tr('ui.common.hyphen', fallback: '-') + : log.ipAddress, ), + width: _historyIpColumnWidth, ), ), - DataCell(_buildHistoryStatusBadge(sessionStatus)), DataCell( - _buildHistorySessionActionCell(matchedSession), + _buildCenteredHistoryCell( + _singleLineText(deviceLabel), + width: _historyDeviceColumnWidth, + ), + ), + DataCell( + _buildCenteredHistoryCell( + _selectableText( + browserLabel.isEmpty + ? tr('ui.common.hyphen', fallback: '-') + : browserLabel, + ), + width: _historyBrowserColumnWidth, + ), + ), + DataCell( + _buildCenteredHistoryCell( + _buildAuthMethodCell(log, authMethod), + width: _historyAuthMethodColumnWidth, + ), + ), + DataCell( + _buildCenteredHistoryCell( + _selectableText( + statusLabel, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + width: _historyResultColumnWidth, + ), + ), + DataCell( + _buildCenteredHistoryCell( + _buildHistoryStatusBadge(sessionStatus), + width: _historyStatusColumnWidth, + ), + ), + DataCell( + _buildCenteredHistoryCell( + _buildHistorySessionActionCell(matchedSession), + width: _historyActionColumnWidth, + ), ), ], ); @@ -1920,6 +2005,10 @@ class _DashboardScreenState extends ConsumerState { } String _compactSessionId(String sessionId) { + final parts = sessionId.split('-'); + if (parts.length >= 4) { + return '${parts.take(3).join('-')}-...'; + } if (sessionId.length <= _historySessionMinVisibleChars) { return sessionId; } @@ -1927,16 +2016,16 @@ class _DashboardScreenState extends ConsumerState { } Widget _buildHistorySessionIdCell(String sessionId, double columnWidth) { - final compactMode = columnWidth <= _historySessionMinWidth + 0.5; - final displayText = compactMode ? _compactSessionId(sessionId) : sessionId; + final displayText = _compactSessionId(sessionId); final textWidget = Text( displayText, maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, ); - if (displayText == sessionId) { + if (displayText == sessionId || sessionId.isEmpty) { return textWidget; } return Tooltip(message: sessionId, child: textWidget); From 6e312cc5fdc7aa36e99e8f6c53fdad33cb10f3aa Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 14:19:50 +0900 Subject: [PATCH 21/39] =?UTF-8?q?=EC=A0=91=EC=86=8D=EC=9D=B4=EB=A0=A5=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80/=EC=8A=A4=EC=9C=84=EC=B9=98=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/dashboard_screen.dart | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 2aca33e6..b201bdd8 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -1622,18 +1622,24 @@ class _DashboardScreenState extends ConsumerState { Text( tr('ui.userfront.audit.filter.toggle_label'), style: const TextStyle( - fontSize: 13, + fontSize: 14, fontWeight: FontWeight.w600, color: _ink, ), ), - Switch( - value: _showActiveSessionsOnly, - onChanged: (value) { - setState(() { - _showActiveSessionsOnly = value; - }); - }, + const SizedBox(width: 2), + Transform.scale( + scale: 0.84, + alignment: Alignment.centerRight, + child: Switch( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _showActiveSessionsOnly, + onChanged: (value) { + setState(() { + _showActiveSessionsOnly = value; + }); + }, + ), ), ], ), From 9e473ae8a866c20841506f6f9ac5db50dc203044 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 14:47:04 +0900 Subject: [PATCH 22/39] =?UTF-8?q?userfront=20=EC=A0=91=EC=86=8D=EC=9D=B4?= =?UTF-8?q?=EB=A0=A5=20=ED=83=80=EC=9E=84=EB=9D=BC=EC=9D=B8=20oathkeeper?= =?UTF-8?q?=20=EC=84=B8=EC=85=98=20ID=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 91 ++++++++++++++++--- .../handler/auth_handler_sessions_test.go | 67 ++++++++++++++ backend/internal/handler/common_test.go | 19 ++++ .../presentation/dashboard_screen.dart | 4 +- 4 files changed, 166 insertions(+), 15 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index f26e8683..d1195a30 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -4330,10 +4330,10 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { appName := "Baron 로그인" clientID := "" path := strings.ToLower(extractAuditPath(log)) - + isOidcAccept := strings.Contains(path, "/api/v1/auth/oidc/login/accept") isPasswordLogin := strings.Contains(path, "/api/v1/auth/password/login") - + // 우선 audit details의 client 정보를 사용 if details, err := utils.ParseAuditDetails(log.Details); err == nil && details != nil { if cid, ok := details["client_id"].(string); ok && strings.TrimSpace(cid) != "" { @@ -4343,7 +4343,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { appName = strings.TrimSpace(name) } } - + // 기본값이거나 클라이언트 ID인 경우 Hydra 조회로 보강 if appName == "Baron 로그인" || appName == "" { if isOidcAccept { @@ -4391,7 +4391,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { if clientID == "" { continue } - + appName := clientID if consent, ok := consentMap[clientID]; ok { appName = consent.Name @@ -4418,6 +4418,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { EventID: eventID, Timestamp: log.Timestamp, UserID: profile.ID, + SessionID: extractSessionIDFromOathkeeperLog(log), EventType: fmt.Sprintf("%s %s", log.Method, log.Path), Status: status, AuthMethod: "세션 위임", @@ -5741,6 +5742,16 @@ func extractClientIDFromOathkeeperLog(log domain.OathkeeperAccessLog) string { return parseClientIDFromRaw(log.Raw) } +func extractSessionIDFromOathkeeperLog(log domain.OathkeeperAccessLog) string { + if value := parseSessionIDFromURL(log.Target); value != "" { + return value + } + if value := parseSessionIDFromURL(log.Path); value != "" { + return value + } + return parseSessionIDFromRaw(log.Raw) +} + func parseClientIDFromURL(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { @@ -5759,6 +5770,23 @@ func parseClientIDFromURL(raw string) string { return "" } +func parseSessionIDFromURL(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + parsed, err := url.Parse(raw) + if err != nil { + return "" + } + for _, key := range []string{"session_id", "sid", "sessionId", "sessionID"} { + if id := strings.TrimSpace(parsed.Query().Get(key)); id != "" { + return id + } + } + return "" +} + func parseClientIDFromRaw(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { @@ -5810,15 +5838,7 @@ func extractSessionIDFromAuditDetails(details string) string { if err := json.Unmarshal([]byte(details), &payload); err != nil { return "" } - if raw, ok := payload["session_id"]; ok { - switch value := raw.(type) { - case string: - return value - default: - return fmt.Sprint(value) - } - } - return "" + return readSessionIDFromAny(payload) } func extractApprovedSessionIDFromAuditDetails(details string) string { @@ -5848,6 +5868,51 @@ func extractApprovedSessionIDFromAuditDetails(details string) string { return "" } +func parseSessionIDFromRaw(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + var payload any + if err := json.Unmarshal([]byte(raw), &payload); err != nil { + return "" + } + return readSessionIDFromAny(payload) +} + +func readSessionIDFromAny(payload any) string { + switch value := payload.(type) { + case map[string]any: + for _, key := range []string{"session_id", "sid", "sessionId", "sessionID"} { + if raw, ok := value[key]; ok { + switch sid := raw.(type) { + case string: + if strings.TrimSpace(sid) != "" { + return strings.TrimSpace(sid) + } + default: + rendered := strings.TrimSpace(fmt.Sprint(sid)) + if rendered != "" && rendered != "" { + return rendered + } + } + } + } + for _, nested := range value { + if sid := readSessionIDFromAny(nested); sid != "" { + return sid + } + } + case []any: + for _, nested := range value { + if sid := readSessionIDFromAny(nested); sid != "" { + return sid + } + } + } + return "" +} + func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) { id, _, _, err := h.getKratosIdentity(token) return id, err diff --git a/backend/internal/handler/auth_handler_sessions_test.go b/backend/internal/handler/auth_handler_sessions_test.go index 7dfc0129..817daf86 100644 --- a/backend/internal/handler/auth_handler_sessions_test.go +++ b/backend/internal/handler/auth_handler_sessions_test.go @@ -616,3 +616,70 @@ func TestGetHydraProfile_RejectsInactiveLinkedSession(t *testing.T) { assert.Contains(t, err.Error(), "inactive") mockKratos.AssertExpectations(t) } + +func TestGetAuthTimeline_FillsSessionIDFromOathkeeperRaw(t *testing.T) { + now := time.Date(2026, 4, 7, 4, 39, 0, 0, time.UTC) + setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) { + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]any{ + "id": "current-sid", + "authenticated_at": now.Format(time.RFC3339), + "identity": map[string]any{ + "id": "user-123", + "traits": map[string]any{ + "email": "user@example.com", + "name": "User", + "role": "user", + }, + }, + }), nil + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + })) + + h := &AuthHandler{ + AuditRepo: &mockAuditRepo{}, + OathkeeperRepo: &mockOathkeeperRepo{ + logs: []domain.OathkeeperAccessLog{ + { + Timestamp: now, + RequestID: "req-1", + Method: http.MethodGet, + Path: "/api/v1/dev/sessions", + Status: http.StatusOK, + Subject: "user-123", + ClientIP: "203.0.113.7", + UserAgent: "Mozilla/5.0", + Raw: `{"request":{"url":"https://devfront.example.com/callback?client_id=devfront"},"extra":{"session_id":"target-sid"}}`, + }, + }, + }, + } + + app := fiber.New() + app.Get("/api/v1/audit/auth/timeline", h.GetAuthTimeline) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/audit/auth/timeline", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var body struct { + Items []struct { + SessionID string `json:"session_id"` + ClientID string `json:"client_id"` + AppName string `json:"app_name"` + Source string `json:"source"` + } `json:"items"` + } + err = json.NewDecoder(resp.Body).Decode(&body) + assert.NoError(t, err) + if assert.Len(t, body.Items, 1) { + assert.Equal(t, "target-sid", body.Items[0].SessionID) + assert.Equal(t, "devfront", body.Items[0].ClientID) + assert.Equal(t, "devfront", body.Items[0].AppName) + assert.Equal(t, "oathkeeper", body.Items[0].Source) + } +} diff --git a/backend/internal/handler/common_test.go b/backend/internal/handler/common_test.go index 32bd1d21..c499eb39 100644 --- a/backend/internal/handler/common_test.go +++ b/backend/internal/handler/common_test.go @@ -115,6 +115,25 @@ func (m *mockAuditRepo) CountActiveSessionsSince(ctx context.Context, since time func (m *mockAuditRepo) Ping(ctx context.Context) error { return nil } +type mockOathkeeperRepo struct { + logs []domain.OathkeeperAccessLog +} + +func (m *mockOathkeeperRepo) FindPageBySubject(ctx context.Context, subject string, limit int, cursor *domain.AuditCursor) ([]domain.OathkeeperAccessLog, error) { + if subject == "" { + return m.logs, nil + } + results := make([]domain.OathkeeperAccessLog, 0, len(m.logs)) + for _, log := range m.logs { + if log.Subject == subject { + results = append(results, log) + } + } + return results, nil +} + +func (m *mockOathkeeperRepo) Ping(ctx context.Context) error { return nil } + // --- Mock Consent Repository --- type mockConsentRepo struct { diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index b201bdd8..d968c49d 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -161,9 +161,9 @@ class _DashboardScreenState extends ConsumerState { builder: (context) => AlertDialog( title: Text(tr('ui.userfront.dashboard.sessions.revoke.title')), content: Text( - tr( + _renderTranslatedText( 'msg.userfront.dashboard.sessions.revoke.confirm', - params: { + values: { 'target': session.isCurrent ? tr('ui.userfront.dashboard.sessions.current_badge') : _sessionDisplayLabel(session), From 3b56346c23eb98d8c5085ecef19fa3256343b1d1 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 7 Apr 2026 16:03:35 +0900 Subject: [PATCH 23/39] =?UTF-8?q?=EB=A1=9C=EC=BB=AC=20code-check=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientDetailsPage.tsx | 65 +++++++++++-------- locales/en.toml | 1 + locales/ko.toml | 3 + locales/template.toml | 5 +- userfront/assets/translations/en.toml | 15 ++--- userfront/assets/translations/ko.toml | 19 +++--- userfront/assets/translations/template.toml | 9 ++- 7 files changed, 67 insertions(+), 50 deletions(-) diff --git a/devfront/src/features/clients/ClientDetailsPage.tsx b/devfront/src/features/clients/ClientDetailsPage.tsx index 1fa793c6..f4a3550f 100644 --- a/devfront/src/features/clients/ClientDetailsPage.tsx +++ b/devfront/src/features/clients/ClientDetailsPage.tsx @@ -9,7 +9,7 @@ import { Save, Shield, } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; @@ -44,7 +44,7 @@ function ClientDetailsPage() { const queryClient = useQueryClient(); const clientId = params.id ?? ""; - const { data, isLoading, error } = useQuery({ + const { data, error } = useQuery({ queryKey: ["client", clientId], queryFn: () => fetchClient(clientId), enabled: clientId.length > 0, @@ -52,12 +52,18 @@ function ClientDetailsPage() { const [redirectUris, setRedirectUris] = useState(""); const [showSecret, setShowSecret] = useState(false); + const redirectUrisHydratedRef = useRef(false); useEffect(() => { - if (data?.client?.redirectUris) { + if ( + !redirectUrisHydratedRef.current && + data?.client?.redirectUris && + redirectUris === "" + ) { setRedirectUris(data.client.redirectUris.join(", ")); + redirectUrisHydratedRef.current = true; } - }, [data]); + }, [data, redirectUris]); const mutation = useMutation({ mutationFn: () => { @@ -129,15 +135,7 @@ function ClientDetailsPage() { ); } - if (isLoading) { - return ( -
- {t("msg.dev.clients.details.loading", "Loading app...")} -
- ); - } - - if (error || !data) { + if (error && !data) { const errMsg = (error as AxiosError<{ error?: string }>).response?.data?.error ?? (error as Error)?.message; @@ -152,37 +150,45 @@ function ClientDetailsPage() { ); } + const client = data?.client; + const endpointValues = data?.endpoints ?? { + discovery: "-", + issuer: "-", + authorization: "-", + token: "-", + userinfo: "-", + }; const endpoints = [ { labelKey: "ui.dev.clients.details.endpoint.discovery", labelFallback: "Discovery Endpoint", - value: data.endpoints.discovery, + value: endpointValues.discovery, }, { labelKey: "ui.dev.clients.details.endpoint.issuer", labelFallback: "Issuer URL", - value: data.endpoints.issuer, + value: endpointValues.issuer, }, { labelKey: "ui.dev.clients.details.endpoint.authorization", labelFallback: "Authorization Endpoint", - value: data.endpoints.authorization, + value: endpointValues.authorization, }, { labelKey: "ui.dev.clients.details.endpoint.token", labelFallback: "Token Endpoint", - value: data.endpoints.token, + value: endpointValues.token, }, { labelKey: "ui.dev.clients.details.endpoint.userinfo", labelFallback: "UserInfo Endpoint", - value: data.endpoints.userinfo, + value: endpointValues.userinfo, }, ]; // Client Secret from API const secretPlaceholder = "SECRET_NOT_AVAILABLE"; - const clientSecret = data.client.clientSecret || secretPlaceholder; + const clientSecret = client?.clientSecret || secretPlaceholder; const displaySecret = clientSecret === secretPlaceholder ? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE") @@ -200,7 +206,7 @@ function ClientDetailsPage() { {t("ui.dev.clients.consents.breadcrumb.clients", "Apps")} / - {data.client.name || clientId} + {client?.name || clientId} / {t("ui.dev.clients.details.tab.connection", "Federation")} @@ -215,7 +221,7 @@ function ClientDetailsPage() {

- {data.client.name || data.client.id} + {client?.name || client?.id || clientId}

{t( @@ -226,12 +232,14 @@ function ClientDetailsPage() {

- {data.client.status === "active" + {client?.status === "active" ? t("ui.common.status.active", "Active") - : t("ui.common.status.inactive", "Inactive")} + : client?.status === "inactive" + ? t("ui.common.status.inactive", "Inactive") + : t("msg.common.loading", "Loading...")}
@@ -276,10 +284,10 @@ function ClientDetailsPage() {

- {data.client.id} + {client?.id || clientId}

toast( t( @@ -461,7 +469,10 @@ function ClientDetailsPage() { )} rows={5} value={redirectUris} - onChange={(e) => setRedirectUris(e.target.value)} + onChange={(e) => { + redirectUrisHydratedRef.current = true; + setRedirectUris(e.target.value); + }} className="font-mono text-sm" />
diff --git a/locales/en.toml b/locales/en.toml index cbc9ab29..bc5d4d6a 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -513,6 +513,7 @@ browser = "Browser: {{value}}" date = "Date: {{value}}" device = "Device: {{value}}" end = "No more items to show." +filtered_empty = "No sign-in history matches the active session filter." ip = "IP address: {{value}}" load_more_error = "Could not load more history." result = "Result: {{value}}" diff --git a/locales/ko.toml b/locales/ko.toml index 6243c0af..680b34f8 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -175,6 +175,7 @@ browser = "브라우저: {{value}}" date = "접속일자: {{value}}" device = "접속환경: {{value}}" end = "더 이상 항목이 없습니다." +filtered_empty = "활성 세션으로 필터링된 접속 이력이 없습니다." ip = "접속 IP: {{value}}" load_more_error = "더 불러오지 못했습니다." result = "인증결과: {{value}}" @@ -905,9 +906,11 @@ saved_success = "저장이 완료되었습니다." greeting = "안녕하세요, {{name}}님" [msg.userfront.audit] +browser = "브라우저: {{value}}" date = "접속일자: {{value}}" device = "접속환경: {{value}}" end = "더 이상 항목이 없습니다." +filtered_empty = "활성 세션으로 필터링된 접속 이력이 없습니다." ip = "접속 IP: {{value}}" load_more_error = "더 불러오지 못했습니다." result = "인증결과: {{value}}" diff --git a/locales/template.toml b/locales/template.toml index d34c647e..2440c558 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -782,9 +782,11 @@ saved_success = "" greeting = "" [msg.userfront.audit] +browser = "" date = "" device = "" end = "" +filtered_empty = "" ip = "" load_more_error = "" result = "" @@ -2330,6 +2332,7 @@ dev_console = "" action = "" app = "" auth_method = "" +browser = "" date = "" device = "" ip = "" @@ -2557,4 +2560,4 @@ title = "" toggle_label = "" [msg.userfront.audit.filter] -description = "" \ No newline at end of file +description = "" diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 5f5a3298..f7eafeb2 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -48,13 +48,12 @@ browser = "Browser: {value}" date = "Date: {value}" device = "Device: {value}" end = "No more items to show." -filtered_empty = "There are no active or current sessions to display." -filter.description = "Turn this on to show only active or current sessions." +filtered_empty = "No sign-in history matches the active session filter." ip = "IP address: {value}" load_more_error = "Could not load more history." result = "Result: {value}" session_id = "Session ID: {value}" -status = "Status: {value}" +status = "Status: pending" [msg.userfront.consent] accept_error = "Failed to process consent: {error}" @@ -286,7 +285,7 @@ uppercase = "At least one uppercase letter" [msg.userfront.sections] apps_subtitle = "Your linked apps and their latest sign-in status." -audit_subtitle = "Review current session status and recent sign-in history together." +audit_subtitle = "Recent access history for Baron sign-in." sessions_subtitle = "Your currently signed-in devices and browser sessions." [msg.userfront.settings] @@ -437,10 +436,6 @@ dev_console = "Dev Console" [ui.userfront.audit] -[ui.userfront.audit.filter] -title = "Session filter" -toggle_label = "Active only" - [ui.userfront.audit.table] action = "Action" app = "App" @@ -669,5 +664,9 @@ verify = "Verification" action = "Go to sign-in" +[ui.userfront.audit.filter] +title = "Manage My Activity" +toggle_label = "Show active sessions only" + [msg.userfront.audit.filter] description = "Toggle to view only active sessions." diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index eee97bbe..244414a5 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -45,13 +45,12 @@ browser = "브라우저: {value}" date = "접속일자: {value}" device = "접속환경: {value}" end = "더 이상 항목이 없습니다." -filtered_empty = "활성화 또는 접속중인 세션이 없습니다." -filter.description = "토글을 켜면 활성화 또는 접속중인 세션만 표시됩니다." +filtered_empty = "활성 세션으로 필터링된 접속 이력이 없습니다." ip = "접속 IP: {value}" load_more_error = "더 불러오지 못했습니다." result = "인증결과: {value}" session_id = "Session ID: {value}" -status = "현황: {value}" +status = "현황: (준비중)" [msg.userfront.dashboard] approved_device = "승인 기기: {device}" @@ -141,7 +140,7 @@ success = "비밀번호가 성공적으로 변경되었습니다. 다시 로그 [msg.userfront.sections] apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다." -audit_subtitle = "현재 세션 현황과 최근 접근 기록을 함께 확인할 수 있습니다." +audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다." sessions_subtitle = "현재 로그인된 기기와 브라우저 세션입니다." [msg.userfront.settings] @@ -251,9 +250,11 @@ title = "회원가입" greeting = "안녕하세요, {name}님" [msg.userfront.audit] +browser = "브라우저: {value}" date = "접속일자: {value}" device = "접속환경: {value}" end = "더 이상 항목이 없습니다." +filtered_empty = "활성 세션으로 필터링된 접속 이력이 없습니다." ip = "접속 IP: {value}" load_more_error = "더 불러오지 못했습니다." result = "인증결과: {value}" @@ -490,7 +491,7 @@ uppercase = "대문자 1개 이상" [msg.userfront.sections] apps_subtitle = "현재 연결된 앱과 최근 인증 상태입니다." -audit_subtitle = "현재 세션 현황과 최근 접근 기록을 함께 확인할 수 있습니다." +audit_subtitle = "Baron 로그인 기준의 최근 접근 기록입니다." [msg.userfront.settings] disabled = "현재 계정 설정 화면은 준비 중입니다." @@ -640,10 +641,6 @@ dev_console = "Dev Console" [ui.userfront.audit] -[ui.userfront.audit.filter] -title = "세션 필터" -toggle_label = "활성 세션만" - [ui.userfront.audit.table] action = "관리" app = "애플리케이션" @@ -871,5 +868,9 @@ verify = "본인인증" action = "로그인하기" +[ui.userfront.audit.filter] +title = "내 활동 관리" +toggle_label = "활성 세션만 보기" + [msg.userfront.audit.filter] description = "활성화된 세션만 보려면 토글을 켜주세요." diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index f3d544ba..86653de2 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -228,7 +228,6 @@ date = "" device = "" end = "" filtered_empty = "" -filter.description = "" ip = "" load_more_error = "" result = "" @@ -615,10 +614,6 @@ dev_console = "" [ui.userfront.audit] -[ui.userfront.audit.filter] -title = "" -toggle_label = "" - [ui.userfront.audit.table] action = "" app = "" @@ -846,5 +841,9 @@ verify = "" action = "" +[ui.userfront.audit.filter] +title = "" +toggle_label = "" + [msg.userfront.audit.filter] description = "" From f5c4ffa92ff3ad5f500176fbc02a71ac25a178c5 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 10:47:57 +0900 Subject: [PATCH 24/39] =?UTF-8?q?linked=20RP=20=EC=9D=91=EB=8B=B5=EC=97=90?= =?UTF-8?q?=201st-party=20=EC=95=B1=20=EC=9E=90=EB=8F=99=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20init=5Furl=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 100 +++++++++++++++--- .../handler/auth_handler_linked_test.go | 41 +++++-- 2 files changed, 119 insertions(+), 22 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index d1195a30..40823eab 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -4483,7 +4483,8 @@ type linkedRpSummary struct { ID string `json:"id"` Name string `json:"name"` Logo string `json:"logo,omitempty"` - URL string `json:"url,omitempty"` // Added + URL string `json:"url,omitempty"` + InitURL string `json:"init_url,omitempty"` LastAuthenticatedAt string `json:"lastAuthenticatedAt,omitempty"` Status string `json:"status"` Scopes []string `json:"scopes,omitempty"` @@ -4564,17 +4565,19 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { if len(scopes) == 0 && strings.TrimSpace(client.Scope) != "" { scopes = strings.Fields(client.Scope) } + initURL := resolveLinkedRPInitURL(client.ClientID, scopes, client.RedirectURIs) existing := records[clientID] if existing == nil { records[clientID] = &linkedRpRecord{ linkedRpSummary: linkedRpSummary{ - ID: clientID, - Name: name, - Logo: extractHydraClientLogo(client.Metadata), - URL: clientURL, - Status: "active", // Hydra 세션이 있으면 활성 - Scopes: scopes, + ID: clientID, + Name: name, + Logo: extractHydraClientLogo(client.Metadata), + URL: clientURL, + InitURL: initURL, + Status: "active", // Hydra 세션이 있으면 활성 + Scopes: scopes, }, lastAuth: lastAuth, } @@ -4590,6 +4593,9 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { if existing.URL == "" { existing.URL = clientURL } + if existing.InitURL == "" { + existing.InitURL = initURL + } existing.Scopes = mergeScopes(existing.Scopes, scopes) if lastAuth.After(existing.lastAuth) { existing.lastAuth = lastAuth @@ -4644,15 +4650,21 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { client.ClientURI, client.RedirectURIs, ) + initURL := resolveLinkedRPInitURL( + client.ClientID, + dc.GrantedScopes, + client.RedirectURIs, + ) records[dc.ClientID] = &linkedRpRecord{ linkedRpSummary: linkedRpSummary{ - ID: dc.ClientID, - Name: name, - Logo: extractHydraClientLogo(client.Metadata), - URL: clientURL, - Status: status, - Scopes: dc.GrantedScopes, + ID: dc.ClientID, + Name: name, + Logo: extractHydraClientLogo(client.Metadata), + URL: clientURL, + InitURL: initURL, + Status: status, + Scopes: dc.GrantedScopes, }, lastAuth: dc.UpdatedAt, } @@ -4726,6 +4738,11 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { } } record.URL = clientURL + record.InitURL = resolveLinkedRPInitURL( + client.ClientID, + scopes, + client.RedirectURIs, + ) } else { // Hydra 정보 없음 (삭제됨 등) -> Audit 정보나 ID로 대체 if record.Name == "" { @@ -6778,6 +6795,63 @@ func resolveLinkedRPURL(clientID string, clientURI string, redirectURIs []string return "" } +func resolveLinkedRPInitURL(clientID string, scopes []string, redirectURIs []string) string { + clientID = strings.TrimSpace(clientID) + if clientID == "" { + return "" + } + + switch clientID { + case "adminfront": + if value := strings.TrimRight(strings.TrimSpace(os.Getenv("ADMINFRONT_URL")), "/"); value != "" { + return value + "/login?auto=1" + } + case "devfront": + if value := strings.TrimRight(strings.TrimSpace(os.Getenv("DEVFRONT_URL")), "/"); value != "" { + return value + "/login?auto=1&returnTo=%2Fclients" + } + } + + hydraPublicURL := strings.TrimRight(os.Getenv("HYDRA_PUBLIC_URL"), "/") + if hydraPublicURL == "" { + userfrontURL := strings.TrimRight(os.Getenv("USERFRONT_URL"), "/") + if userfrontURL == "" { + userfrontURL = "https://sso.hmac.kr" + } + hydraPublicURL = userfrontURL + "/oidc" + } + + redirectURI := "" + if len(redirectURIs) > 0 { + redirectURI = strings.TrimSpace(redirectURIs[0]) + } + + mergedScopes := make([]string, 0, len(scopes)+1) + seen := map[string]struct{}{} + for _, scope := range append([]string{"openid"}, scopes...) { + scope = strings.TrimSpace(scope) + if scope == "" { + continue + } + if _, ok := seen[scope]; ok { + continue + } + seen[scope] = struct{}{} + mergedScopes = append(mergedScopes, scope) + } + + params := url.Values{} + params.Set("client_id", clientID) + params.Set("response_type", "code") + params.Set("scope", strings.Join(mergedScopes, " ")) + params.Set("state", GenerateSecureAlnumToken(16)) + if redirectURI != "" { + params.Set("redirect_uri", redirectURI) + } + + return fmt.Sprintf("%s/oauth2/auth?%s", hydraPublicURL, params.Encode()) +} + func mergeScopes(current []string, next []string) []string { if len(next) == 0 { return current diff --git a/backend/internal/handler/auth_handler_linked_test.go b/backend/internal/handler/auth_handler_linked_test.go index b9618d77..0c7a9c07 100644 --- a/backend/internal/handler/auth_handler_linked_test.go +++ b/backend/internal/handler/auth_handler_linked_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "net/url" "testing" "time" @@ -45,11 +46,14 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) { return httpJSONAny(r, http.StatusOK, []map[string]interface{}{ { "client": map[string]interface{}{ - "client_id": "client-active", - "client_name": "Active App", + "client_id": "devfront", + "client_name": "DevFront", + "redirect_uris": []string{ + "https://active.example.com/callback", + }, }, - "granted_scope": []string{"openid"}, - "handled_at": time.Now().Format(time.RFC3339), + "grant_scope": []string{"openid", "profile"}, + "handled_at": time.Now().Format(time.RFC3339), }, }), nil } @@ -111,6 +115,8 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) { t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test") + t.Setenv("HYDRA_PUBLIC_URL", "https://sso.example.com/oidc") + t.Setenv("DEVFRONT_URL", "http://localhost:5174") app := newLinkedRpTestApp(h) @@ -123,10 +129,11 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) { var res struct { Items []struct { - ID string `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Scopes []string `json:"scopes"` + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Scopes []string `json:"scopes"` + InitURL string `json:"init_url"` } `json:"items"` } json.NewDecoder(resp.Body).Decode(&res) @@ -138,7 +145,23 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) { statusMap[item.ID] = item.Status } - assert.Equal(t, "active", statusMap["client-active"]) + assert.Equal(t, "active", statusMap["devfront"]) assert.Equal(t, "inactive", statusMap["client-consent"]) assert.Equal(t, "inactive", statusMap["client-audit"]) + + var activeInitURL string + for _, item := range res.Items { + if item.ID == "devfront" { + activeInitURL = item.InitURL + break + } + } + + parsedInitURL, err := url.Parse(activeInitURL) + assert.NoError(t, err) + assert.Equal(t, "http", parsedInitURL.Scheme) + assert.Equal(t, "localhost:5174", parsedInitURL.Host) + assert.Equal(t, "/login", parsedInitURL.Path) + assert.Equal(t, "1", parsedInitURL.Query().Get("auto")) + assert.Equal(t, "/clients", parsedInitURL.Query().Get("returnTo")) } From c3605cc86b259e08717c8e94f62484bfa3f7030d Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 10:48:39 +0900 Subject: [PATCH 25/39] =?UTF-8?q?App=20=ED=98=84=ED=99=A9=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20init=5Furl=20?= =?UTF-8?q?=EC=9A=B0=EC=84=A0=20=EC=A7=84=EC=9E=85=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/domain/linked_rp_launch.dart | 21 ++++++ .../lib/features/dashboard/domain/models.dart | 3 + .../domain/providers/linked_rps_provider.dart | 3 + .../presentation/dashboard_screen.dart | 6 +- userfront/test/linked_rp_launch_test.dart | 72 +++++++++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 userfront/lib/features/dashboard/domain/linked_rp_launch.dart create mode 100644 userfront/test/linked_rp_launch_test.dart diff --git a/userfront/lib/features/dashboard/domain/linked_rp_launch.dart b/userfront/lib/features/dashboard/domain/linked_rp_launch.dart new file mode 100644 index 00000000..cb7cb716 --- /dev/null +++ b/userfront/lib/features/dashboard/domain/linked_rp_launch.dart @@ -0,0 +1,21 @@ +import 'providers/linked_rps_provider.dart'; + +String? resolveLinkedRpLaunchUrl(LinkedRp rp) { + final normalizedStatus = rp.status.trim().toLowerCase(); + final isActive = normalizedStatus.isEmpty || normalizedStatus == 'active'; + if (!isActive) { + return null; + } + + final initUrl = rp.initUrl.trim(); + if (initUrl.isNotEmpty) { + return initUrl; + } + + final url = rp.url.trim(); + if (url.isNotEmpty) { + return url; + } + + return null; +} diff --git a/userfront/lib/features/dashboard/domain/models.dart b/userfront/lib/features/dashboard/domain/models.dart index 3f633490..0fa73d4e 100644 --- a/userfront/lib/features/dashboard/domain/models.dart +++ b/userfront/lib/features/dashboard/domain/models.dart @@ -96,6 +96,7 @@ class LinkedRp { final String name; final String logo; final String url; + final String initUrl; final String status; final List scopes; final DateTime? lastAuthenticatedAt; @@ -105,6 +106,7 @@ class LinkedRp { required this.name, required this.logo, required this.url, + required this.initUrl, required this.status, required this.scopes, this.lastAuthenticatedAt, @@ -126,6 +128,7 @@ class LinkedRp { name: json['name']?.toString() ?? '', logo: json['logo']?.toString() ?? '', url: json['url']?.toString() ?? '', + initUrl: json['init_url']?.toString() ?? '', status: json['status']?.toString() ?? '', scopes: (json['scopes'] as List?)?.whereType().toList() ?? [], lastAuthenticatedAt: parsedLastAuth, diff --git a/userfront/lib/features/dashboard/domain/providers/linked_rps_provider.dart b/userfront/lib/features/dashboard/domain/providers/linked_rps_provider.dart index b571351c..2c8ddbd3 100644 --- a/userfront/lib/features/dashboard/domain/providers/linked_rps_provider.dart +++ b/userfront/lib/features/dashboard/domain/providers/linked_rps_provider.dart @@ -10,6 +10,7 @@ class LinkedRp { final String name; final String logo; final String url; + final String initUrl; final String status; final List scopes; final DateTime? lastAuthenticatedAt; @@ -19,6 +20,7 @@ class LinkedRp { required this.name, required this.logo, required this.url, + required this.initUrl, required this.status, required this.scopes, required this.lastAuthenticatedAt, @@ -40,6 +42,7 @@ class LinkedRp { name: json['name']?.toString() ?? '', logo: json['logo']?.toString() ?? '', url: json['url']?.toString() ?? '', + initUrl: json['init_url']?.toString() ?? '', status: json['status']?.toString() ?? 'unknown', scopes: (json['scopes'] as List?)?.whereType().toList() ?? [], lastAuthenticatedAt: parsedLastAuth, diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index d968c49d..d06ef9c2 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../domain/linked_rp_launch.dart'; import '../domain/session_time_resolver.dart'; import '../domain/providers/linked_rps_provider.dart'; import '../domain/providers/user_sessions_provider.dart'; @@ -1216,6 +1217,7 @@ class _DashboardScreenState extends ConsumerState { isRevoked: isRevoked, onRevoke: isRevoked ? null : () => _onRevokeLink(rp.id, name), url: rp.url, + launchUrl: resolveLinkedRpLaunchUrl(rp), lastAuthDateTime: rp.lastAuthenticatedAt, ), ); @@ -1460,7 +1462,7 @@ class _DashboardScreenState extends ConsumerState { cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () async { - final itemUrl = item.url; + final itemUrl = item.launchUrl; if (itemUrl != null && itemUrl.isNotEmpty) { final uri = Uri.parse(itemUrl); final canOpen = await canLaunchUrl(uri); @@ -2291,6 +2293,7 @@ class _ActivityItem { final String lastAuthAt; final String status; final String? url; + final String? launchUrl; final List scopes; final bool isRevoked; final VoidCallback? onRevoke; @@ -2303,6 +2306,7 @@ class _ActivityItem { required this.status, required this.scopes, this.url, + this.launchUrl, this.isRevoked = false, this.onRevoke, this.lastAuthDateTime, diff --git a/userfront/test/linked_rp_launch_test.dart b/userfront/test/linked_rp_launch_test.dart new file mode 100644 index 00000000..3e06e01c --- /dev/null +++ b/userfront/test/linked_rp_launch_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:userfront/features/dashboard/domain/linked_rp_launch.dart'; +import 'package:userfront/features/dashboard/domain/providers/linked_rps_provider.dart'; + +LinkedRp _linkedRp({ + required String status, + String url = '', + String initUrl = '', +}) { + return LinkedRp( + id: 'client-1', + name: 'Example App', + logo: '', + url: url, + initUrl: initUrl, + status: status, + scopes: const ['openid', 'profile'], + lastAuthenticatedAt: null, + ); +} + +void main() { + test('LinkedRp.fromJson은 init_url을 읽는다', () { + final rp = LinkedRp.fromJson({ + 'id': 'client-1', + 'name': 'Example App', + 'status': 'active', + 'url': 'https://example.com', + 'init_url': 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1', + }); + + expect( + rp.initUrl, + 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1', + ); + }); + + test('활성 앱은 initUrl을 우선 진입 URL로 사용한다', () { + final launchUrl = resolveLinkedRpLaunchUrl( + _linkedRp( + status: 'active', + url: 'https://example.com', + initUrl: 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1', + ), + ); + + expect( + launchUrl, + 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1', + ); + }); + + test('활성 앱은 initUrl이 없으면 기존 url로 폴백한다', () { + final launchUrl = resolveLinkedRpLaunchUrl( + _linkedRp(status: 'active', url: 'https://example.com'), + ); + + expect(launchUrl, 'https://example.com'); + }); + + test('비활성 앱은 진입 URL을 만들지 않는다', () { + final launchUrl = resolveLinkedRpLaunchUrl( + _linkedRp( + status: 'inactive', + url: 'https://example.com', + initUrl: 'https://sso.example.com/oidc/oauth2/auth?client_id=client-1', + ), + ); + + expect(launchUrl, isNull); + }); +} From c7b213bf1775ea9c9e88f1532962fd1f046687c6 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 10:49:08 +0900 Subject: [PATCH 26/39] =?UTF-8?q?devfront=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20auto=20redirect=20SSO=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/auth/AuthCallbackPage.tsx | 11 ++++- devfront/src/features/auth/LoginPage.tsx | 41 ++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/devfront/src/features/auth/AuthCallbackPage.tsx b/devfront/src/features/auth/AuthCallbackPage.tsx index 929bc7b4..1cf9be59 100644 --- a/devfront/src/features/auth/AuthCallbackPage.tsx +++ b/devfront/src/features/auth/AuthCallbackPage.tsx @@ -17,12 +17,19 @@ export default function AuthCallbackPage() { } if (auth.isAuthenticated) { - navigate("/", { replace: true }); + const returnTo = + typeof auth.user?.state === "object" && + auth.user?.state !== null && + "returnTo" in auth.user.state && + typeof auth.user.state.returnTo === "string" + ? auth.user.state.returnTo + : "/clients"; + navigate(returnTo, { replace: true }); } else if (auth.error) { console.error("Auth Error:", auth.error); navigate("/login", { replace: true }); } - }, [auth.isAuthenticated, auth.error, navigate]); + }, [auth.isAuthenticated, auth.error, navigate, auth.user?.state]); return
Loading Auth...
; } diff --git a/devfront/src/features/auth/LoginPage.tsx b/devfront/src/features/auth/LoginPage.tsx index 3c87a65e..cd9f8ca1 100644 --- a/devfront/src/features/auth/LoginPage.tsx +++ b/devfront/src/features/auth/LoginPage.tsx @@ -1,7 +1,8 @@ import { ExternalLink, LogIn, ShieldHalf } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useAuth } from "react-oidc-context"; import { useNavigate } from "react-router-dom"; +import { useSearchParams } from "react-router-dom"; import { Button } from "../../components/ui/button"; import { Card, @@ -14,18 +15,48 @@ import { function LoginPage() { const auth = useAuth(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const autoStartedRef = useRef(false); + const returnTo = searchParams.get("returnTo") || "/clients"; + const shouldAutoLogin = searchParams.get("auto") === "1"; useEffect(() => { if (auth.isAuthenticated) { - navigate("/clients", { replace: true }); + navigate(returnTo, { replace: true }); } - }, [auth.isAuthenticated, navigate]); + }, [auth.isAuthenticated, navigate, returnTo]); + + useEffect(() => { + if (!shouldAutoLogin) { + return; + } + if (autoStartedRef.current || auth.isLoading || auth.activeNavigator) { + return; + } + + autoStartedRef.current = true; + void auth.signinRedirect({ + state: { + returnTo, + }, + }); + }, [ + auth, + auth.activeNavigator, + auth.isLoading, + returnTo, + shouldAutoLogin, + ]); const handleSSOLogin = async () => { try { - await auth.signinPopup(); + await auth.signinRedirect({ + state: { + returnTo: "/clients", + }, + }); } catch (error) { - console.error("Popup login failed", error); + console.error("Redirect login failed", error); } }; From 24f477a28e74ce12c76b4cd43ca48d7350cb1eaf Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 10:49:39 +0900 Subject: [PATCH 27/39] =?UTF-8?q?adminfront=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20auto=20redirect=20SSO?= =?UTF-8?q?=20=EC=A7=84=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/auth/AuthCallbackPage.tsx | 9 +++- adminfront/src/features/auth/LoginPage.tsx | 42 ++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/adminfront/src/features/auth/AuthCallbackPage.tsx b/adminfront/src/features/auth/AuthCallbackPage.tsx index ed8889d8..ed1e0630 100644 --- a/adminfront/src/features/auth/AuthCallbackPage.tsx +++ b/adminfront/src/features/auth/AuthCallbackPage.tsx @@ -14,7 +14,14 @@ function AuthCallbackPage() { if (user?.access_token) { window.localStorage.setItem("admin_session", user.access_token); } - navigate("/", { replace: true }); + const returnTo = + typeof auth.user?.state === "object" && + auth.user?.state !== null && + "returnTo" in auth.user.state && + typeof auth.user.state.returnTo === "string" + ? auth.user.state.returnTo + : "/"; + navigate(returnTo, { replace: true }); } else if (auth.error) { console.error("Auth Error:", auth.error); navigate("/login", { replace: true }); diff --git a/adminfront/src/features/auth/LoginPage.tsx b/adminfront/src/features/auth/LoginPage.tsx index 07da4600..a9edebb5 100644 --- a/adminfront/src/features/auth/LoginPage.tsx +++ b/adminfront/src/features/auth/LoginPage.tsx @@ -1,5 +1,7 @@ import { ExternalLink, LogIn, ShieldHalf } from "lucide-react"; +import { useEffect, useRef } from "react"; import { useAuth } from "react-oidc-context"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "../../components/ui/button"; import { Card, @@ -11,10 +13,46 @@ import { function LoginPage() { const auth = useAuth(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const autoStartedRef = useRef(false); + const returnTo = searchParams.get("returnTo") || "/"; + const shouldAutoLogin = searchParams.get("auto") === "1"; + + useEffect(() => { + if (auth.isAuthenticated) { + navigate(returnTo, { replace: true }); + } + }, [auth.isAuthenticated, navigate, returnTo]); + + useEffect(() => { + if (!shouldAutoLogin) { + return; + } + if (autoStartedRef.current || auth.isLoading || auth.activeNavigator) { + return; + } + + autoStartedRef.current = true; + void auth.signinRedirect({ + state: { + returnTo, + }, + }); + }, [ + auth, + auth.activeNavigator, + auth.isLoading, + returnTo, + shouldAutoLogin, + ]); const handleSSOLogin = () => { - // OIDC client-side authentication flow started here - auth.signinRedirect(); + void auth.signinRedirect({ + state: { + returnTo: "/", + }, + }); }; return ( From f4b1c449b157051f4c21e0eaf66be03517907324 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 14:19:35 +0900 Subject: [PATCH 28/39] =?UTF-8?q?App=20=EC=B9=B4=EB=93=9C=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=20=EC=A1=B0=EC=A0=95=20=EB=B0=8F=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=EB=B3=B4=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.toml | 6 +- locales/ko.toml | 6 +- userfront/assets/translations/en.toml | 6 +- userfront/assets/translations/ko.toml | 6 +- .../presentation/dashboard_screen.dart | 301 +++++++++++++----- userfront/lib/i18n_data.dart | 13 +- 6 files changed, 251 insertions(+), 87 deletions(-) diff --git a/locales/en.toml b/locales/en.toml index bc5d4d6a..35d8e8c0 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -551,6 +551,7 @@ client_id = "Client ID: {{id}}" client_id_missing = "No client ID available." current_status = "Current status: {{status}}" last_auth = "Last signed in: {{value}}" +link_status = "Link status: {{status}}" link_missing = "This app does not have a launch URL configured." link_open_error = "Could not open the app link." render_error = "Dashboard render error: {{error}}" @@ -2084,7 +2085,8 @@ title = "Cancel consent" [ui.userfront.dashboard] last_auth_label = "Last sign-in" -status_history = "Activity history" +link_status_label = "Link status" +status_history = "Link details" [ui.userfront.dashboard.activity] linked = "Linked" @@ -2109,7 +2111,7 @@ confirm_button = "Disconnect" title = "Disconnect app" [ui.userfront.dashboard.scopes] -title = "Permission (Scopes)" +title = "Consent scopes" [ui.userfront.dashboard.status] revoked = "Revoked" diff --git a/locales/ko.toml b/locales/ko.toml index 680b34f8..a89cea92 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -192,6 +192,7 @@ client_id = "Client ID: {{id}}" client_id_missing = "Client ID 없음" current_status = "현재 상태: {{status}}" last_auth = "최근 인증: {{value}}" +link_status = "연동 상태: {{status}}" link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다." link_open_error = "해당 링크를 열 수 없습니다." render_error = "대시보드 렌더링 오류: {{error}}" @@ -402,7 +403,8 @@ session = "세션" [ui.userfront.dashboard] last_auth_label = "최근 인증" -status_history = "상태 이력" +link_status_label = "연동 상태" +status_history = "연동 정보" [ui.userfront.device] android = "Mobile(Android)" @@ -2505,7 +2507,7 @@ confirm_button = "해지하기" title = "연동 해지" [ui.userfront.dashboard.scopes] -title = "권한 (Scopes)" +title = "동의 범위" [ui.userfront.dashboard.status] revoked = "해지됨" diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index f7eafeb2..acc22ea8 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -86,6 +86,7 @@ client_id = "Client ID: {id}" client_id_missing = "No client ID available." current_status = "Current status: {status}" last_auth = "Last signed in: {value}" +link_status = "Link status: {status}" link_missing = "This app does not have a launch URL configured." link_open_error = "Could not open the app link." render_error = "Dashboard render error: {error}" @@ -464,7 +465,8 @@ title = "Cancel consent" [ui.userfront.dashboard] last_auth_label = "Last sign-in" -status_history = "Activity history" +link_status_label = "Link status" +status_history = "Link details" [ui.userfront.dashboard.activity] linked = "Linked" @@ -489,7 +491,7 @@ confirm_button = "Disconnect" title = "Disconnect app" [ui.userfront.dashboard.scopes] -title = "Permission (Scopes)" +title = "Consent scopes" [ui.userfront.dashboard.status] revoked = "Revoked" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 244414a5..24f7ce3b 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -62,6 +62,7 @@ client_id = "Client ID: {id}" client_id_missing = "Client ID 없음" current_status = "현재 상태: {status}" last_auth = "최근 인증: {value}" +link_status = "연동 상태: {status}" link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다." link_open_error = "해당 링크를 열 수 없습니다." render_error = "대시보드 렌더링 오류: {error}" @@ -176,7 +177,8 @@ session = "세션" [ui.userfront.dashboard] last_auth_label = "최근 인증" -status_history = "상태 이력" +link_status_label = "연동 상태" +status_history = "연동 정보" [ui.userfront.device] android = "Mobile(Android)" @@ -694,7 +696,7 @@ confirm_button = "해지하기" title = "연동 해지" [ui.userfront.dashboard.scopes] -title = "권한 (Scopes)" +title = "동의 범위" [ui.userfront.dashboard.status] revoked = "해지됨" diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index d06ef9c2..5d46255c 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -38,6 +38,8 @@ class _DashboardScreenState extends ConsumerState { static const _border = Color(0xFFE5E7EB); static const _subtle = Color(0xFFF7F8FA); static const double _dashboardCardSpacing = 12; + static const double _dashboardCardMaxWidth = 228; + static const double _activityDialogMaxWidth = 360; static const double _historySessionMinWidth = 92; static const double _historyOtherColumnsBaselineWidth = 780; static const int _historySessionMinVisibleChars = 8; @@ -235,85 +237,158 @@ class _DashboardScreenState extends ConsumerState { context: context, builder: (context) => Consumer( builder: (context, ref, _) { + final dialogWidth = math.min( + MediaQuery.sizeOf(context).width - 48, + _activityDialogMaxWidth, + ); + final statusLabel = item.status == 'active' + ? tr('ui.userfront.dashboard.activity.linked') + : tr('ui.userfront.dashboard.status.revoked'); + final statusColor = _activityStatusColor(item.status); + return AlertDialog( - title: Text(item.appName), + backgroundColor: _surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 24, + ), + contentPadding: const EdgeInsets.fromLTRB(20, 20, 20, 8), content: SizedBox( - width: double.maxFinite, + width: dialogWidth, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - tr('ui.userfront.dashboard.scopes.title'), - style: const TextStyle(fontWeight: FontWeight.bold), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _subtle, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: _border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.appName, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: _ink, + ), + ), + const SizedBox(height: 4), + Text( + tr('ui.common.details'), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + ], + ), ), - const SizedBox(height: 8), - if (item.scopes.isEmpty) - Text( - tr('msg.userfront.dashboard.scopes.empty'), - style: const TextStyle(color: Colors.grey), - ) - else - Wrap( - spacing: 8, - runSpacing: 4, - children: item.scopes - .map( - (s) => Chip( - label: Text( - s, - style: const TextStyle(fontSize: 12), - ), - visualDensity: VisualDensity.compact, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, + const SizedBox(height: 16), + _buildActivityDetailSection( + title: tr('ui.userfront.dashboard.status_history'), + child: Row( + children: [ + Expanded( + child: _buildActivityDetailField( + label: tr( + 'ui.userfront.dashboard.link_status_label', + ), + value: statusLabel, + valueColor: statusColor, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _buildActivityDetailField( + label: tr('ui.userfront.dashboard.last_auth_label'), + value: item.lastAuthAt, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + _buildActivityDetailSection( + title: tr('ui.userfront.dashboard.scopes.title'), + child: item.scopes.isEmpty + ? Text( + tr('msg.userfront.dashboard.scopes.empty'), + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], ), ) - .toList(), - ), - const SizedBox(height: 24), - Text( - tr('ui.userfront.dashboard.status_history'), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - tr( - 'msg.userfront.dashboard.last_auth', - params: {'value': item.lastAuthAt}, - ), - ), - const SizedBox(height: 4), - Builder( - builder: (context) { - final statusLabel = item.status == 'active' - ? tr('ui.common.status.active') - : tr('ui.userfront.dashboard.status.revoked'); - return Text( - tr( - 'msg.userfront.dashboard.current_status', - params: {'status': statusLabel}, - ), - style: TextStyle( - color: item.status == 'active' - ? Colors.green - : Colors.grey, - ), - ); - }, - ), - ], + : Wrap( + spacing: 8, + runSpacing: 8, + children: item.scopes + .map( + (scope) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + decoration: BoxDecoration( + color: _subtle, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: _border), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.shield_outlined, + size: 14, + color: _ink, + ), + const SizedBox(width: 6), + Text( + scope, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _ink, + ), + ), + ], + ), + ), + ) + .toList(), + ), ), ], ), ), + actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(tr('ui.common.close')), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + foregroundColor: _ink, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + backgroundColor: _subtle, + ), + child: Text( + tr('ui.common.close'), + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), ), ], ); @@ -322,6 +397,73 @@ class _DashboardScreenState extends ConsumerState { ); } + Widget _buildActivityDetailSection({ + required String title, + required Widget child, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: _surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: _border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: _ink, + ), + ), + const SizedBox(height: 10), + child, + ], + ), + ); + } + + Widget _buildActivityDetailField({ + required String label, + required String value, + Color? valueColor, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _subtle, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 6), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: valueColor ?? _ink, + ), + ), + ], + ), + ); + } + Widget _buildSideMenu(BuildContext context, {required bool closeOnTap}) { return SafeArea( child: Column( @@ -1319,7 +1461,7 @@ class _DashboardScreenState extends ConsumerState { Widget _buildActivityCard(_ActivityItem item, {double? cardWidth}) { final isActive = item.status == 'active'; - final statusColor = isActive ? Colors.green : Colors.grey; + final statusColor = _activityStatusColor(item.status); final borderColor = isActive ? Colors.green.withValues(alpha: 128) : _border; @@ -1331,10 +1473,10 @@ class _DashboardScreenState extends ConsumerState { // 카드 컨텐츠 final cardContent = Container( width: cardWidth ?? 260, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: _surface, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: borderWidth), boxShadow: isActive ? [ @@ -1355,7 +1497,7 @@ class _DashboardScreenState extends ConsumerState { child: Text( item.appName, style: const TextStyle( - fontSize: 16, + fontSize: 15, fontWeight: FontWeight.w600, color: _ink, ), @@ -1380,7 +1522,7 @@ class _DashboardScreenState extends ConsumerState { ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 10), Text( tr('ui.userfront.dashboard.last_auth_label'), style: TextStyle(fontSize: 12, color: Colors.grey[600]), @@ -1389,12 +1531,12 @@ class _DashboardScreenState extends ConsumerState { Text( item.lastAuthAt, style: const TextStyle( - fontSize: 14, + fontSize: 13, fontWeight: FontWeight.w600, color: _ink, ), ), - const SizedBox(height: 16), + const SizedBox(height: 14), Row( children: [ Expanded( @@ -1403,7 +1545,7 @@ class _DashboardScreenState extends ConsumerState { style: OutlinedButton.styleFrom( foregroundColor: _ink, side: const BorderSide(color: _border), - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 7), ), child: Text( tr('ui.common.details'), @@ -1425,7 +1567,7 @@ class _DashboardScreenState extends ConsumerState { color: item.isRevoked ? Colors.grey : Colors.redAccent, width: 0.5, ), - padding: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 7), ), child: _isRevoking && !item.isRevoked ? const SizedBox( @@ -1783,8 +1925,15 @@ class _DashboardScreenState extends ConsumerState { } double _dashboardCardWidth(double maxWidth, int crossAxisCount) { - return (maxWidth - (_dashboardCardSpacing * (crossAxisCount - 1))) / - crossAxisCount; + return math.min( + (maxWidth - (_dashboardCardSpacing * (crossAxisCount - 1))) / + crossAxisCount, + _dashboardCardMaxWidth, + ); + } + + Color _activityStatusColor(String status) { + return status == 'active' ? Colors.green : Colors.grey; } Widget _buildCenteredHistoryHeader(String label, {double? width}) { diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index 7974493c..46f24bf9 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -418,6 +418,7 @@ const Map koStrings = { "msg.userfront.audit.device": "접속환경: {{value}}", "msg.userfront.audit.end": "더 이상 항목이 없습니다.", "msg.userfront.audit.filter.description": "활성화된 세션만 보려면 토글을 켜주세요.", + "msg.userfront.audit.filtered_empty": "활성 세션으로 필터링된 접속 이력이 없습니다.", "msg.userfront.audit.ip": "접속 IP: {{value}}", "msg.userfront.audit.load_more_error": "더 불러오지 못했습니다.", "msg.userfront.audit.result": "인증결과: {{value}}", @@ -460,6 +461,7 @@ const Map koStrings = { "msg.userfront.dashboard.last_auth": "최근 인증: {{value}}", "msg.userfront.dashboard.link_missing": "이동할 페이지 주소(Client URI)가 설정되지 않았습니다.", "msg.userfront.dashboard.link_open_error": "해당 링크를 열 수 없습니다.", + "msg.userfront.dashboard.link_status": "연동 상태: {{status}}", "msg.userfront.dashboard.render_error": "대시보드 렌더링 오류: {{error}}", "msg.userfront.dashboard.revoke.confirm": "{{app}} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.", @@ -1717,9 +1719,10 @@ const Map koStrings = { "ui.userfront.dashboard.approved_session.default": "승인한 세션 ID", "ui.userfront.dashboard.approved_session.userfront": "승인한 Userfront 세션 ID", "ui.userfront.dashboard.last_auth_label": "최근 인증", + "ui.userfront.dashboard.link_status_label": "연동 상태", "ui.userfront.dashboard.revoke.confirm_button": "해지하기", "ui.userfront.dashboard.revoke.title": "연동 해지", - "ui.userfront.dashboard.scopes.title": "권한 (Scopes)", + "ui.userfront.dashboard.scopes.title": "동의 범위", "ui.userfront.dashboard.sessions.active_badge": "활성화", "ui.userfront.dashboard.sessions.current_badge": "접속중", "ui.userfront.dashboard.sessions.current_disabled": "현재 세션", @@ -2324,6 +2327,8 @@ const Map enStrings = { "msg.userfront.audit.end": "No more items to show.", "msg.userfront.audit.filter.description": "Toggle to view only active sessions.", + "msg.userfront.audit.filtered_empty": + "No sign-in history matches the active session filter.", "msg.userfront.audit.ip": "IP address: {{value}}", "msg.userfront.audit.load_more_error": "Could not load more history.", "msg.userfront.audit.result": "Result: {{value}}", @@ -2376,6 +2381,7 @@ const Map enStrings = { "msg.userfront.dashboard.link_missing": "This app does not have a launch URL configured.", "msg.userfront.dashboard.link_open_error": "Could not open the app link.", + "msg.userfront.dashboard.link_status": "Link status: {{status}}", "msg.userfront.dashboard.render_error": "Dashboard render error: {{error}}", "msg.userfront.dashboard.revoke.confirm": "Disconnect {{app}}?\\\\\\\\\\\\\\\\nYou will need to grant access again the next time you sign in.", @@ -3728,9 +3734,10 @@ const Map enStrings = { "ui.userfront.dashboard.approved_session.userfront": "Approved UserFront session ID", "ui.userfront.dashboard.last_auth_label": "Last sign-in", + "ui.userfront.dashboard.link_status_label": "Link status", "ui.userfront.dashboard.revoke.confirm_button": "Disconnect", "ui.userfront.dashboard.revoke.title": "Disconnect app", - "ui.userfront.dashboard.scopes.title": "Permission (Scopes)", + "ui.userfront.dashboard.scopes.title": "Consent scopes", "ui.userfront.dashboard.sessions.active_badge": "Active", "ui.userfront.dashboard.sessions.current_badge": "Current", "ui.userfront.dashboard.sessions.current_disabled": "Current session", @@ -3739,7 +3746,7 @@ const Map enStrings = { "ui.userfront.dashboard.sessions.unknown_device": "Unknown device", "ui.userfront.dashboard.sessions.unknown_session": "Session", "ui.userfront.dashboard.status.revoked": "Revoked", - "ui.userfront.dashboard.status_history": "Activity history", + "ui.userfront.dashboard.status_history": "Link details", "ui.userfront.device.android": "Mobile(Android)", "ui.userfront.device.ios": "Mobile(iOS)", "ui.userfront.device.linux": "Desktop(Linux)", From 3d7d4767bff7e413ffd307fbe4e223ccebc66560 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 16:10:46 +0900 Subject: [PATCH 29/39] =?UTF-8?q?=ED=85=8C=EB=A7=88=20=ED=86=A0=EA=B8=80?= =?UTF-8?q?=20=EB=9D=BC=EB=B2=A8=20=EB=B2=88=EC=97=AD=EA=B3=BC=20=EC=98=81?= =?UTF-8?q?=EB=AC=B8=20=EB=AC=B8=EA=B5=AC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- userfront/assets/translations/en.toml | 2 +- userfront/assets/translations/ko.toml | 4 ++-- userfront/lib/i18n_data.dart | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index acc22ea8..9083bdb2 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -488,7 +488,7 @@ userfront = "Approved UserFront session ID" [ui.userfront.dashboard.revoke] confirm_button = "Disconnect" -title = "Disconnect app" +title = "Disconnect" [ui.userfront.dashboard.scopes] title = "Consent scopes" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 24f7ce3b..c39dd77c 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -613,8 +613,8 @@ select_file = "파일 선택" select_placeholder = "선택하세요" show_more = "+ 더보기" success = "성공" -theme_dark = "Dark" -theme_light = "Light" +theme_dark = "다크" +theme_light = "라이트" theme_toggle = "테마 전환" unknown = "Unknown" view = "보기" diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index 46f24bf9..f28934f8 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -1424,8 +1424,8 @@ const Map koStrings = { "ui.common.status.pending": "준비 중", "ui.common.status.success": "성공", "ui.common.success": "성공", - "ui.common.theme_dark": "Dark", - "ui.common.theme_light": "Light", + "ui.common.theme_dark": "다크", + "ui.common.theme_light": "라이트", "ui.common.theme_toggle": "테마 전환", "ui.common.unknown": "Unknown", "ui.common.view": "보기", @@ -3736,7 +3736,7 @@ const Map enStrings = { "ui.userfront.dashboard.last_auth_label": "Last sign-in", "ui.userfront.dashboard.link_status_label": "Link status", "ui.userfront.dashboard.revoke.confirm_button": "Disconnect", - "ui.userfront.dashboard.revoke.title": "Disconnect app", + "ui.userfront.dashboard.revoke.title": "Disconnect", "ui.userfront.dashboard.scopes.title": "Consent scopes", "ui.userfront.dashboard.sessions.active_badge": "Active", "ui.userfront.dashboard.sessions.current_badge": "Current", From dce418d0b9df1d3d095b2cb840e8048a1afc5029 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 16:11:40 +0900 Subject: [PATCH 30/39] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EB=8B=A4=ED=81=AC=20=EB=AA=A8=EB=93=9C/=ED=85=8C=EB=A7=88=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/error_screen.dart | 39 +- .../auth/presentation/login_screen.dart | 1007 +++++++++-------- .../presentation/dashboard_screen.dart | 75 +- .../presentation/pages/profile_page.dart | 65 +- 4 files changed, 649 insertions(+), 537 deletions(-) diff --git a/userfront/lib/features/auth/presentation/error_screen.dart b/userfront/lib/features/auth/presentation/error_screen.dart index b2ebc876..7977c0b6 100644 --- a/userfront/lib/features/auth/presentation/error_screen.dart +++ b/userfront/lib/features/auth/presentation/error_screen.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import '../../../core/constants/error_whitelist.dart'; import '../../../core/i18n/locale_utils.dart'; import '../../../core/services/auth_proxy_service.dart'; +import '../../../core/widgets/theme_toggle_button.dart'; import 'package:userfront/i18n.dart'; class ErrorScreen extends StatelessWidget { @@ -22,6 +23,7 @@ class ErrorScreen extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final colorScheme = theme.colorScheme; final isProd = isProdOverride ?? AuthProxyService.isProdEnv; final normalizedCode = (errorCode ?? '').trim(); final hasCode = normalizedCode.isNotEmpty; @@ -62,7 +64,7 @@ class ErrorScreen extends StatelessWidget { : tr('msg.userfront.error.detail_request'))); return Scaffold( - backgroundColor: const Color(0xFFF7F8FA), + backgroundColor: colorScheme.surfaceContainerLowest, body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), @@ -71,7 +73,7 @@ class ErrorScreen extends StatelessWidget { elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: const BorderSide(color: Color(0xFFE5E7EB)), + side: BorderSide(color: colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.fromLTRB(28, 28, 28, 24), @@ -79,18 +81,25 @@ class ErrorScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w700, - color: const Color(0xFF111827), - ), + Row( + children: [ + Expanded( + child: Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + ), + const ThemeToggleButton(compact: true), + ], ), const SizedBox(height: 12), Text( detail, style: theme.textTheme.bodyMedium?.copyWith( - color: const Color(0xFF4B5563), + color: colorScheme.onSurfaceVariant, height: 1.5, ), ), @@ -98,7 +107,7 @@ class ErrorScreen extends StatelessWidget { Text( tr('msg.userfront.error.type', params: {'type': errorType}), style: theme.textTheme.bodySmall?.copyWith( - color: const Color(0xFF6B7280), + color: colorScheme.onSurfaceVariant, ), ), if (errorId != null && errorId!.isNotEmpty) ...[ @@ -106,7 +115,7 @@ class ErrorScreen extends StatelessWidget { Text( tr('msg.userfront.error.id', params: {'id': errorId!}), style: theme.textTheme.bodySmall?.copyWith( - color: const Color(0xFF6B7280), + color: colorScheme.onSurfaceVariant, ), ), ], @@ -118,8 +127,8 @@ class ErrorScreen extends StatelessWidget { ElevatedButton( onPressed: () => context.go('/login'), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF111827), - foregroundColor: Colors.white, + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, @@ -134,12 +143,12 @@ class ErrorScreen extends StatelessWidget { onPressed: () => context.go(buildLocalizedHomePath(Uri.base)), style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF111827), + foregroundColor: colorScheme.onSurface, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), - side: const BorderSide(color: Color(0xFFCBD5F5)), + side: BorderSide(color: colorScheme.outline), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index 460e56db..8e02a8a8 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:userfront/i18n.dart'; import '../../../core/widgets/language_selector.dart'; +import '../../../core/widgets/theme_toggle_button.dart'; import '../../../core/services/web_auth_integration.dart'; import '../../../core/services/auth_proxy_service.dart'; import '../../../core/services/auth_token_store.dart'; @@ -1385,6 +1386,10 @@ class _LoginScreenState extends ConsumerState @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final mutedColor = colorScheme.onSurfaceVariant; + if (_verificationOnly && _verificationApproved) { return Scaffold( appBar: AppBar( @@ -1393,12 +1398,14 @@ class _LoginScreenState extends ConsumerState icon: const Icon(Icons.arrow_back), onPressed: () => context.go(buildLocalizedHomePath(Uri.base)), ), + actions: const [ThemeToggleButton(compact: true)], ), body: _buildVerificationResultView(), ); } return Scaffold( + backgroundColor: colorScheme.surfaceContainerLowest, body: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( @@ -1408,543 +1415,571 @@ class _LoginScreenState extends ConsumerState child: Container( constraints: const BoxConstraints(maxWidth: 400), padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - tr('ui.userfront.app_title'), - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - if (_drySendEnabled) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, + child: Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + tr('ui.userfront.app_title'), + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, ), - decoration: BoxDecoration( - color: const Color(0xFFFFF3CD), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFFFC107)), - ), - child: Row( - children: [ - const Icon( - Icons.warning_amber_rounded, - color: Color(0xFF8A6D3B), + if (_drySendEnabled) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, ), - const SizedBox(width: 8), - Expanded( - child: Text( - tr('msg.userfront.login.dry_send'), - style: const TextStyle( - color: Color(0xFF8A6D3B), - fontSize: 12, - ), + decoration: BoxDecoration( + color: const Color(0xFFFFF3CD), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFFFC107), ), ), - ], - ), - ), - ], - const SizedBox(height: 40), - - TabBar( - controller: _tabController, - tabs: [ - Tab(text: tr('ui.userfront.login.tabs.password')), - Tab(text: tr('ui.userfront.login.tabs.link')), - Tab(text: tr('ui.userfront.login.tabs.qr')), - ], - ), - const SizedBox(height: 24), - - SizedBox( - height: 350, - child: TabBarView( - controller: _tabController, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Column( + child: Row( children: [ - TextField( - key: const ValueKey( - 'password_login_id_input', - ), - controller: _passwordLoginIdController, - decoration: InputDecoration( - labelText: - _loginIdLabel ?? - tr( - 'ui.userfront.login.field.login_id', - ), - border: const OutlineInputBorder(), - prefixIcon: const Icon( - Icons.person_outline, - ), - ), - onSubmitted: (_) => _handlePasswordLogin(), + const Icon( + Icons.warning_amber_rounded, + color: Color(0xFF8A6D3B), ), - const SizedBox(height: 16), - TextField( - key: const ValueKey( - 'password_login_password_input', - ), - focusNode: _passwordFocusNode, - controller: _passwordController, - obscureText: true, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.field.password', - ), - border: const OutlineInputBorder(), - prefixIcon: const Icon( - Icons.lock_outline, - ), - ), - onSubmitted: (_) => _handlePasswordLogin(), - ), - if (_isPasswordCapsLockOn) ...[ - const SizedBox(height: 8), - Row( - children: [ - const Icon( - Icons.keyboard_capslock_rounded, - size: 18, - color: Colors.orange, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _capsLockWarningText(context), - style: const TextStyle( - color: Colors.orange, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ], - const SizedBox(height: 24), - FilledButton( - key: const ValueKey( - 'password_login_submit_button', - ), - onPressed: _handlePasswordLogin, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight(50), - ), + const SizedBox(width: 8), + Expanded( child: Text( - tr('ui.userfront.login.action.submit'), + tr('msg.userfront.login.dry_send'), + style: const TextStyle( + color: Color(0xFF8A6D3B), + fontSize: 12, + ), ), ), ], ), ), + ], + const SizedBox(height: 40), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Column( - children: [ - if (_linkPendingRef == null) ...[ - TextField( - controller: _linkIdController, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.field.login_id', + TabBar( + controller: _tabController, + tabs: [ + Tab(text: tr('ui.userfront.login.tabs.password')), + Tab(text: tr('ui.userfront.login.tabs.link')), + Tab(text: tr('ui.userfront.login.tabs.qr')), + ], + ), + const SizedBox(height: 24), + + SizedBox( + height: 350, + child: TabBarView( + controller: _tabController, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Column( + children: [ + TextField( + key: const ValueKey( + 'password_login_id_input', ), - hintText: '', - border: const OutlineInputBorder(), - prefixIcon: const Icon( - Icons.person_outline, + controller: _passwordLoginIdController, + decoration: InputDecoration( + labelText: + _loginIdLabel ?? + tr( + 'ui.userfront.login.field.login_id', + ), + prefixIcon: const Icon( + Icons.person_outline, + ), ), + onSubmitted: (_) => + _handlePasswordLogin(), ), - onSubmitted: (_) => _handleLinkLogin(), - ), - const SizedBox(height: 24), - FilledButton( - onPressed: _handleLinkLogin, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight(50), - ), - child: Text( - tr('ui.userfront.login.link.send'), - ), - ), - const SizedBox(height: 24), - Text( - tr('msg.userfront.login.link.helper'), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - if (_linkPendingRef != null) ...[ - if (_linkExpired) ...[ - Text( - tr('msg.userfront.login.link_timeout'), - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.grey, - fontSize: 12, + const SizedBox(height: 16), + TextField( + key: const ValueKey( + 'password_login_password_input', ), + focusNode: _passwordFocusNode, + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.field.password', + ), + prefixIcon: const Icon( + Icons.lock_outline, + ), + ), + onSubmitted: (_) => + _handlePasswordLogin(), ), - const SizedBox(height: 12), + if (_isPasswordCapsLockOn) ...[ + const SizedBox(height: 8), + Row( + children: [ + const Icon( + Icons.keyboard_capslock_rounded, + size: 18, + color: Colors.orange, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _capsLockWarningText(context), + style: const TextStyle( + color: Colors.orange, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + const SizedBox(height: 24), FilledButton( - onPressed: () { - setState(_resetLinkLoginState); - }, + key: const ValueKey( + 'password_login_submit_button', + ), + onPressed: _handlePasswordLogin, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight( - 45, + 50, ), ), - child: Text(tr('ui.common.refresh')), - ), - ] else ...[ - Text( - tr( - 'msg.userfront.login.link.short_code_help', + child: Text( + tr( + 'ui.userfront.login.action.submit', + ), ), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - textAlign: TextAlign.center, ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - flex: 2, - child: TextField( - controller: - _shortCodePrefixController, - textCapitalization: - TextCapitalization.characters, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.short_code.prefix', - ), - border: - const OutlineInputBorder(), - hintText: 'AB', - hintStyle: const TextStyle( - color: Colors.grey, - ), - ), - maxLength: 2, + ], + ), + ), + + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Column( + children: [ + if (_linkPendingRef == null) ...[ + TextField( + controller: _linkIdController, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.field.login_id', + ), + hintText: '', + prefixIcon: const Icon( + Icons.person_outline, ), ), - const SizedBox(width: 8), - Expanded( - flex: 4, - child: TextField( - controller: - _shortCodeDigitsController, - keyboardType: - TextInputType.number, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.short_code.digits', + onSubmitted: (_) => + _handleLinkLogin(), + ), + const SizedBox(height: 24), + FilledButton( + onPressed: _handleLinkLogin, + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight( + 50, + ), + ), + child: Text( + tr('ui.userfront.login.link.send'), + ), + ), + const SizedBox(height: 24), + Text( + tr('msg.userfront.login.link.helper'), + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ], + if (_linkPendingRef != null) ...[ + if (_linkExpired) ...[ + Text( + tr( + 'msg.userfront.login.link_timeout', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + ), + const SizedBox(height: 12), + FilledButton( + onPressed: () { + setState(_resetLinkLoginState); + }, + style: FilledButton.styleFrom( + minimumSize: + const Size.fromHeight(45), + ), + child: Text( + tr('ui.common.refresh'), + ), + ), + ] else ...[ + Text( + tr( + 'msg.userfront.login.link.short_code_help', + ), + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + flex: 2, + child: TextField( + controller: + _shortCodePrefixController, + textCapitalization: + TextCapitalization + .characters, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.short_code.prefix', + ), + hintText: 'AB', + hintStyle: TextStyle( + color: mutedColor, + ), + ), + maxLength: 2, ), - border: - const OutlineInputBorder(), - hintText: '345678', - hintStyle: const TextStyle( - color: Colors.grey, - ), - suffixText: - _linkExpireSeconds > 0 - ? tr( - 'ui.userfront.login.short_code.expire_time', - params: { - 'time': _formatTime( - _linkExpireSeconds, - ), - }, - ) - : null, ), - maxLength: 6, + const SizedBox(width: 8), + Expanded( + flex: 4, + child: TextField( + controller: + _shortCodeDigitsController, + keyboardType: + TextInputType.number, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.short_code.digits', + ), + hintText: '345678', + hintStyle: TextStyle( + color: mutedColor, + ), + suffixText: + _linkExpireSeconds > 0 + ? tr( + 'ui.userfront.login.short_code.expire_time', + params: { + 'time': _formatTime( + _linkExpireSeconds, + ), + }, + ) + : null, + ), + maxLength: 6, + ), + ), + ], + ), + const SizedBox(height: 12), + FilledButton( + onPressed: () { + final prefix = + _shortCodePrefixController + .text + .trim() + .toUpperCase(); + final digits = + _shortCodeDigitsController + .text + .trim(); + if (prefix.length != 2 || + digits.length != 6) { + _showError( + tr( + 'msg.userfront.login.short_code.invalid', + ), + ); + return; + } + _verifyShortCode(prefix + digits); + }, + style: FilledButton.styleFrom( + minimumSize: + const Size.fromHeight(45), + ), + child: Text( + tr( + 'ui.userfront.login.short_code.submit', + ), + ), + ), + const SizedBox(height: 12), + TextButton( + onPressed: () { + if (_linkResendSeconds > 0) { + _showInfo( + tr( + 'msg.userfront.login.link.resend_wait', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ), + ); + return; + } + final loginId = + _lastLinkLoginId ?? + _linkIdController.text.trim(); + if (loginId.isEmpty) { + _showError( + tr( + 'msg.userfront.login.link.missing_login_id', + ), + ); + return; + } + _startEnchantedFlow( + loginId, + isEmail: + _lastLinkIsEmail || + loginId.contains('@'), + codeOnly: false, + ); + }, + child: Text( + _linkResendSeconds > 0 + ? tr( + 'ui.userfront.login.link.resend_with_time', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ) + : tr('ui.common.resend'), + ), + ), + if (!_lastLinkIsEmail) ...[ + const SizedBox(height: 4), + TextButton( + onPressed: () { + if (_linkResendSeconds > 0) { + _showInfo( + tr( + 'msg.userfront.login.link.resend_wait', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ), + ); + return; + } + final loginId = + _lastLinkLoginId ?? + _linkIdController.text + .trim(); + if (loginId.isEmpty) { + _showError( + tr( + 'msg.userfront.login.link.missing_phone', + ), + ); + return; + } + _startEnchantedFlow( + loginId, + isEmail: false, + codeOnly: true, + ); + }, + child: Text( + tr( + 'ui.userfront.login.link.code_only', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ), + ), + ), + ], + ], + ], + ], + ), + ), + + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_isQrLoading) + const CircularProgressIndicator() + else if (_qrExpired) + Column( + children: [ + Text( + tr( + 'msg.userfront.login.qr_expired', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + ), + const SizedBox(height: 12), + FilledButton( + onPressed: _startQrFlow, + style: FilledButton.styleFrom( + minimumSize: + const Size.fromHeight(45), + ), + child: Text( + tr('ui.common.refresh'), ), ), ], - ), - const SizedBox(height: 12), - FilledButton( - onPressed: () { - final prefix = - _shortCodePrefixController.text - .trim() - .toUpperCase(); - final digits = - _shortCodeDigitsController.text - .trim(); - if (prefix.length != 2 || - digits.length != 6) { - _showError( - tr( - 'msg.userfront.login.short_code.invalid', + ) + else if (_qrImageBase64 != null) + Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all( + color: colorScheme.outline, ), - ); - return; - } - _verifyShortCode(prefix + digits); - }, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight( - 45, - ), - ), - child: Text( - tr( - 'ui.userfront.login.short_code.submit', - ), - ), - ), - const SizedBox(height: 12), - TextButton( - onPressed: () { - if (_linkResendSeconds > 0) { - _showInfo( - tr( - 'msg.userfront.login.link.resend_wait', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ), - ); - return; - } - final loginId = - _lastLinkLoginId ?? - _linkIdController.text.trim(); - if (loginId.isEmpty) { - _showError( - tr( - 'msg.userfront.login.link.missing_login_id', - ), - ); - return; - } - _startEnchantedFlow( - loginId, - isEmail: - _lastLinkIsEmail || - loginId.contains('@'), - codeOnly: false, - ); - }, - child: Text( - _linkResendSeconds > 0 - ? tr( - 'ui.userfront.login.link.resend_with_time', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ) - : tr('ui.common.resend'), - ), - ), - if (!_lastLinkIsEmail) ...[ - const SizedBox(height: 4), - TextButton( - onPressed: () { - if (_linkResendSeconds > 0) { - _showInfo( - tr( - 'msg.userfront.login.link.resend_wait', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ), - ); - return; - } - final loginId = - _lastLinkLoginId ?? - _linkIdController.text.trim(); - if (loginId.isEmpty) { - _showError( - tr( - 'msg.userfront.login.link.missing_phone', - ), - ); - return; - } - _startEnchantedFlow( - loginId, - isEmail: false, - codeOnly: true, - ); - }, - child: Text( - tr( - 'ui.userfront.login.link.code_only', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, + borderRadius: + BorderRadius.circular(12), + ), + child: QrImageView( + data: _qrImageBase64!, + version: QrVersions.auto, + size: 200.0, + backgroundColor: Colors.white, ), ), - ), - ], - ], - ], - ], - ), - ), - - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (_isQrLoading) - const CircularProgressIndicator() - else if (_qrExpired) - Column( - children: [ - Text( - tr('msg.userfront.login.qr_expired'), - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - ), - const SizedBox(height: 12), - FilledButton( - onPressed: _startQrFlow, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight( - 45, - ), - ), - child: Text(tr('ui.common.refresh')), - ), - ], - ) - else if (_qrImageBase64 != null) - Column( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all( - color: Colors.grey.shade300, - ), - borderRadius: BorderRadius.circular( - 12, - ), - ), - child: QrImageView( - data: _qrImageBase64!, - version: QrVersions.auto, - size: 200.0, - ), - ), - const SizedBox(height: 12), - Text( - _qrRemainingSeconds > 0 - ? tr( - 'ui.userfront.login.qr.remaining', - params: { - 'time': _formatTime( - _qrRemainingSeconds, + const SizedBox(height: 12), + Text( + _qrRemainingSeconds > 0 + ? tr( + 'ui.userfront.login.qr.remaining', + params: { + 'time': _formatTime( + _qrRemainingSeconds, + ), + }, + ) + : tr( + 'ui.userfront.login.qr.expired', ), - }, - ) - : tr( - 'ui.userfront.login.qr.expired', + textAlign: TextAlign.center, + style: TextStyle( + color: _qrRemainingSeconds > 30 + ? Colors.blue + : Colors.red, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + tr( + 'msg.userfront.login.qr.scan_hint', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + ), + TextButton( + onPressed: _startQrFlow, + child: Text( + tr( + 'ui.userfront.login.qr.refresh', ), - textAlign: TextAlign.center, - style: TextStyle( - color: _qrRemainingSeconds > 30 - ? Colors.blue - : Colors.red, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), + ), + ), + ], + ) + else Text( - tr('msg.userfront.login.qr.scan_hint'), + tr( + 'msg.userfront.login.qr.load_failed', + ), textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), ), - TextButton( - onPressed: _startQrFlow, - child: Text( - tr('ui.userfront.login.qr.refresh'), - ), - ), - ], - ) - else - Text( - tr('msg.userfront.login.qr.load_failed'), - textAlign: TextAlign.center, - ), + ], + ), ], ), - ], - ), - ), - const SizedBox(height: 16), - Column( - children: [ - TextButton( - onPressed: () => context.push('/forgot-password'), - child: Text( - tr('ui.userfront.login.forgot_password'), - ), ), - Row( - mainAxisAlignment: MainAxisAlignment.center, + const SizedBox(height: 16), + Column( children: [ - Text( - tr('msg.userfront.login.no_account'), - style: const TextStyle( - color: Colors.grey, - fontSize: 14, + TextButton( + onPressed: () => + context.push('/forgot-password'), + child: Text( + tr('ui.userfront.login.forgot_password'), ), ), - TextButton( - onPressed: () => context.push('/signup'), - child: Text(tr('ui.userfront.login.signup')), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + tr('msg.userfront.login.no_account'), + style: TextStyle( + color: mutedColor, + fontSize: 14, + ), + ), + TextButton( + onPressed: () => context.push('/signup'), + child: Text( + tr('ui.userfront.login.signup'), + ), + ), + ], ), ], ), + const SizedBox(height: 6), + const Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: [ThemeToggleButton(), LanguageSelector()], + ), ], ), - const SizedBox(height: 6), - const Align( - alignment: Alignment.center, - child: LanguageSelector(), - ), - ], + ), ), ), ), diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 5d46255c..4dea1f97 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -18,6 +18,7 @@ import '../../../../core/services/auth_token_store.dart'; import '../../../../core/services/http_client.dart'; import '../../../../core/i18n/locale_utils.dart'; import '../../../../core/widgets/language_selector.dart'; +import '../../../../core/widgets/theme_toggle_button.dart'; import '../../../../core/ui/layout_breakpoints.dart'; import '../../../../core/ui/toast_service.dart'; import '../../profile/domain/notifiers/profile_notifier.dart'; @@ -33,10 +34,6 @@ class DashboardScreen extends ConsumerStatefulWidget { } class _DashboardScreenState extends ConsumerState { - static const _ink = Color(0xFF1A1F2C); - static const _surface = Colors.white; - static const _border = Color(0xFFE5E7EB); - static const _subtle = Color(0xFFF7F8FA); static const double _dashboardCardSpacing = 12; static const double _dashboardCardMaxWidth = 228; static const double _activityDialogMaxWidth = 360; @@ -66,8 +63,14 @@ class _DashboardScreenState extends ConsumerState { bool _showAllActivities = false; bool _showActiveSessionsOnly = false; + bool _isDesktopSideMenuOpen = true; final Set _revokedClientIds = {}; + Color get _ink => Theme.of(context).colorScheme.onSurface; + Color get _surface => Theme.of(context).colorScheme.surface; + Color get _border => Theme.of(context).colorScheme.outlineVariant; + Color get _subtle => Theme.of(context).colorScheme.surfaceContainerLowest; + String _renderTranslatedText( String key, { String? fallback, @@ -275,7 +278,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( item.appName, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _ink, @@ -346,7 +349,7 @@ class _DashboardScreenState extends ConsumerState { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon( + Icon( Icons.shield_outlined, size: 14, color: _ink, @@ -354,7 +357,7 @@ class _DashboardScreenState extends ConsumerState { const SizedBox(width: 6), Text( scope, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _ink, @@ -414,7 +417,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: _ink, @@ -519,7 +522,14 @@ class _DashboardScreenState extends ConsumerState { ), const Padding( padding: EdgeInsets.only(bottom: 16), - child: LanguageSelector(compact: true), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ThemeToggleButton(), + SizedBox(height: 8), + LanguageSelector(compact: true), + ], + ), ), ], ), @@ -942,14 +952,35 @@ class _DashboardScreenState extends ConsumerState { return Scaffold( backgroundColor: _subtle, appBar: AppBar( + leading: isWide + ? IconButton( + icon: Icon( + _isDesktopSideMenuOpen ? Icons.menu_open : Icons.menu, + ), + tooltip: _isDesktopSideMenuOpen + ? tr('ui.common.collapse') + : '펼치기', + onPressed: () { + setState(() { + _isDesktopSideMenuOpen = !_isDesktopSideMenuOpen; + }); + }, + ) + : Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.menu), + tooltip: MaterialLocalizations.of( + context, + ).openAppDrawerTooltip, + onPressed: () => Scaffold.of(context).openDrawer(), + ), + ), title: Text( tr('ui.userfront.app_title'), style: const TextStyle(fontWeight: FontWeight.bold), ), - elevation: 0, - backgroundColor: _surface, - foregroundColor: Colors.black, actions: [ + const ThemeToggleButton(compact: true), IconButton( icon: const Icon(Icons.person_outline), tooltip: tr('ui.userfront.nav.profile'), @@ -972,7 +1003,7 @@ class _DashboardScreenState extends ConsumerState { : Drawer(child: _buildSideMenu(context, closeOnTap: true)), body: Row( children: [ - if (isWide) + if (isWide && _isDesktopSideMenuOpen) SizedBox( width: 240, child: _buildSideMenu(context, closeOnTap: false), @@ -1065,7 +1096,7 @@ class _DashboardScreenState extends ConsumerState { fallback: 'Hello, {{name}}.', values: {'name': userName}, ), - style: const TextStyle( + style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: _ink, @@ -1117,7 +1148,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _ink, @@ -1271,7 +1302,7 @@ class _DashboardScreenState extends ConsumerState { const SizedBox(width: 6), Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 12, color: _ink, fontWeight: FontWeight.w600, @@ -1496,7 +1527,7 @@ class _DashboardScreenState extends ConsumerState { Expanded( child: Text( item.appName, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: _ink, @@ -1530,7 +1561,7 @@ class _DashboardScreenState extends ConsumerState { const SizedBox(height: 4), Text( item.lastAuthAt, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: _ink, @@ -1544,7 +1575,7 @@ class _DashboardScreenState extends ConsumerState { onPressed: () => _showRpDetails(item), style: OutlinedButton.styleFrom( foregroundColor: _ink, - side: const BorderSide(color: _border), + side: BorderSide(color: _border), padding: const EdgeInsets.symmetric(vertical: 7), ), child: Text( @@ -1745,7 +1776,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( tr('ui.userfront.audit.filter.title'), - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: _ink, @@ -1765,7 +1796,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( tr('ui.userfront.audit.filter.toggle_label'), - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _ink, @@ -2224,7 +2255,7 @@ class _DashboardScreenState extends ConsumerState { Expanded( child: _buildAppCell( log, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.w600, color: _ink, ), diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index 9d25748a..b1b2a04e 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -9,6 +9,7 @@ import '../../../../core/services/logout_service.dart'; import '../../../../core/ui/layout_breakpoints.dart'; import '../../../../core/ui/toast_service.dart'; import '../../../../core/widgets/language_selector.dart'; +import '../../../../core/widgets/theme_toggle_button.dart'; import '../../data/models/user_profile_model.dart'; import '../../domain/notifiers/profile_notifier.dart'; @@ -20,10 +21,6 @@ class ProfilePage extends ConsumerStatefulWidget { } class _ProfilePageState extends ConsumerState { - static const _ink = Color(0xFF1A1F2C); - static const _surface = Colors.white; - static const _border = Color(0xFFE5E7EB); - static const _subtle = Color(0xFFF7F8FA); static final _log = Logger('ProfilePage'); UserProfile? _cachedProfile; @@ -54,9 +51,15 @@ class _ProfilePageState extends ConsumerState { bool _showCurrentPassword = false; bool _showNewPassword = false; bool _showConfirmPassword = false; + bool _isDesktopSideMenuOpen = true; Map? _passwordPolicy; bool _isPasswordPolicyLoading = false; + Color get _ink => Theme.of(context).colorScheme.onSurface; + Color get _surface => Theme.of(context).colorScheme.surface; + Color get _border => Theme.of(context).colorScheme.outlineVariant; + Color get _subtle => Theme.of(context).colorScheme.surfaceContainerLowest; + String _renderTranslatedText( String key, { String? fallback, @@ -615,7 +618,14 @@ class _ProfilePageState extends ConsumerState { ), const Padding( padding: EdgeInsets.only(bottom: 16), - child: LanguageSelector(compact: true), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ThemeToggleButton(), + SizedBox(height: 8), + LanguageSelector(compact: true), + ], + ), ), ], ); @@ -627,7 +637,7 @@ class _ProfilePageState extends ConsumerState { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _ink, @@ -654,7 +664,7 @@ class _ProfilePageState extends ConsumerState { const SizedBox(width: 6), Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 12, color: _ink, fontWeight: FontWeight.w600, @@ -705,7 +715,7 @@ class _ProfilePageState extends ConsumerState { fallback: 'Hello, {{name}}.', values: {'name': name}, ), - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: _ink, @@ -996,12 +1006,17 @@ class _ProfilePageState extends ConsumerState { const SizedBox(height: 8), Text( tr('msg.userfront.profile.password.subtitle'), - style: const TextStyle(color: Color(0xFF6B7280)), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 8), Text( _buildPasswordPolicyDescription(), - style: const TextStyle(color: Color(0xFF6B7280), fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), ), const SizedBox(height: 16), TextField( @@ -1231,14 +1246,35 @@ class _ProfilePageState extends ConsumerState { return Scaffold( backgroundColor: _subtle, appBar: AppBar( + leading: isWide + ? IconButton( + icon: Icon( + _isDesktopSideMenuOpen ? Icons.menu_open : Icons.menu, + ), + tooltip: _isDesktopSideMenuOpen + ? tr('ui.common.collapse') + : '펼치기', + onPressed: () { + setState(() { + _isDesktopSideMenuOpen = !_isDesktopSideMenuOpen; + }); + }, + ) + : Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.menu), + tooltip: MaterialLocalizations.of( + context, + ).openAppDrawerTooltip, + onPressed: () => Scaffold.of(context).openDrawer(), + ), + ), title: Text( tr('ui.userfront.app_title'), style: const TextStyle(fontWeight: FontWeight.bold), ), - elevation: 0, - backgroundColor: _surface, - foregroundColor: Colors.black, actions: [ + const ThemeToggleButton(compact: true), IconButton( icon: const Icon(Icons.home_outlined), tooltip: tr('ui.userfront.nav.dashboard'), @@ -1259,7 +1295,8 @@ class _ProfilePageState extends ConsumerState { drawer: isWide ? null : Drawer(child: _buildSideMenu(context)), body: Row( children: [ - if (isWide) SizedBox(width: 240, child: _buildSideMenu(context)), + if (isWide && _isDesktopSideMenuOpen) + SizedBox(width: 240, child: _buildSideMenu(context)), Expanded(child: _buildContent(profile, isUpdating)), ], ), From 873d56e35f354f8275f45ad6f37915f754027cc8 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 16:12:31 +0900 Subject: [PATCH 31/39] =?UTF-8?q?=ED=85=8C=EB=A7=88=20=EC=98=81=EC=86=8D?= =?UTF-8?q?=ED=99=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- userfront/test/theme_controller_test.dart | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 userfront/test/theme_controller_test.dart diff --git a/userfront/test/theme_controller_test.dart b/userfront/test/theme_controller_test.dart new file mode 100644 index 00000000..1c829b93 --- /dev/null +++ b/userfront/test/theme_controller_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:userfront/core/theme/theme_controller.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + await ThemeController.instance.setThemeMode(ThemeMode.light); + }); + + test('저장된 dark 값을 복원한다', () async { + SharedPreferences.setMockInitialValues({ + ThemeController.storageKey: 'dark', + }); + + await ThemeController.instance.restore(); + + expect(ThemeController.instance.value, ThemeMode.dark); + }); + + test('toggle 결과를 저장한다', () async { + await ThemeController.instance.restore(); + await ThemeController.instance.toggle(); + + final prefs = await SharedPreferences.getInstance(); + expect(ThemeController.instance.value, ThemeMode.dark); + expect(prefs.getString(ThemeController.storageKey), 'dark'); + }); +} From 332b657add5bdfe9db5e69a99f215feb2a2656d2 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 16:14:50 +0900 Subject: [PATCH 32/39] =?UTF-8?q?=EB=8B=A4=ED=81=AC=20=EB=AA=A8=EB=93=9C?= =?UTF-8?q?=20=EC=A0=84=EC=97=AD=20=EC=83=81=ED=83=9C=EC=99=80=20=ED=85=8C?= =?UTF-8?q?=EB=A7=88=20=EA=B8=B0=EB=B0=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- userfront/lib/core/theme/app_theme.dart | 148 ++++++++++++++++++ .../lib/core/theme/theme_controller.dart | 32 ++++ .../lib/core/widgets/theme_toggle_button.dart | 44 ++++++ userfront/lib/main.dart | 62 +++----- userfront/pubspec.lock | 2 +- userfront/pubspec.yaml | 1 + 6 files changed, 246 insertions(+), 43 deletions(-) create mode 100644 userfront/lib/core/theme/app_theme.dart create mode 100644 userfront/lib/core/theme/theme_controller.dart create mode 100644 userfront/lib/core/widgets/theme_toggle_button.dart diff --git a/userfront/lib/core/theme/app_theme.dart b/userfront/lib/core/theme/app_theme.dart new file mode 100644 index 00000000..328a6b22 --- /dev/null +++ b/userfront/lib/core/theme/app_theme.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; + +ThemeData buildLightTheme() { + final scheme = + ColorScheme.fromSeed( + seedColor: const Color(0xFF1A1F2C), + brightness: Brightness.light, + ).copyWith( + surface: Colors.white, + surfaceContainerLowest: const Color(0xFFF7F8FA), + surfaceContainerLow: const Color(0xFFF3F4F6), + surfaceContainerHighest: const Color(0xFFE5E7EB), + outline: const Color(0xFFD1D5DB), + outlineVariant: const Color(0xFFE5E7EB), + primary: const Color(0xFF1A1F2C), + onPrimary: Colors.white, + onSurface: const Color(0xFF111827), + onSurfaceVariant: const Color(0xFF6B7280), + ); + return _buildTheme(scheme); +} + +ThemeData buildDarkTheme() { + final scheme = + ColorScheme.fromSeed( + seedColor: const Color(0xFF7DD3FC), + brightness: Brightness.dark, + ).copyWith( + surface: const Color(0xFF0F172A), + surfaceContainerLowest: const Color(0xFF020617), + surfaceContainerLow: const Color(0xFF111827), + surfaceContainerHighest: const Color(0xFF1F2937), + outline: const Color(0xFF334155), + outlineVariant: const Color(0xFF1E293B), + primary: const Color(0xFFBAE6FD), + onPrimary: const Color(0xFF082F49), + onSurface: const Color(0xFFF8FAFC), + onSurfaceVariant: const Color(0xFF94A3B8), + ); + return _buildTheme(scheme); +} + +ThemeData _buildTheme(ColorScheme colorScheme) { + final isDark = colorScheme.brightness == Brightness.dark; + final base = ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + fontFamily: 'NotoSansKR', + ); + + return base.copyWith( + scaffoldBackgroundColor: colorScheme.surfaceContainerLowest, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: NoTransitionsBuilder(), + TargetPlatform.iOS: NoTransitionsBuilder(), + TargetPlatform.linux: NoTransitionsBuilder(), + TargetPlatform.macOS: NoTransitionsBuilder(), + TargetPlatform.windows: NoTransitionsBuilder(), + TargetPlatform.fuchsia: NoTransitionsBuilder(), + }, + ), + appBarTheme: AppBarTheme( + elevation: 0, + centerTitle: false, + backgroundColor: colorScheme.surface, + foregroundColor: colorScheme.onSurface, + surfaceTintColor: Colors.transparent, + ), + cardTheme: CardThemeData( + color: colorScheme.surface, + elevation: 0, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: colorScheme.outlineVariant), + ), + ), + dividerTheme: DividerThemeData( + color: colorScheme.outlineVariant, + thickness: 1, + ), + drawerTheme: DrawerThemeData( + backgroundColor: colorScheme.surface, + surfaceTintColor: Colors.transparent, + ), + dialogTheme: DialogThemeData( + backgroundColor: colorScheme.surface, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: isDark ? colorScheme.surfaceContainerLow : colorScheme.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: colorScheme.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: colorScheme.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: colorScheme.primary, width: 1.4), + ), + labelStyle: TextStyle(color: colorScheme.onSurfaceVariant), + hintStyle: TextStyle(color: colorScheme.onSurfaceVariant), + prefixIconColor: colorScheme.onSurfaceVariant, + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(50), + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: colorScheme.onSurface, + side: BorderSide(color: colorScheme.outline), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + ), + tabBarTheme: TabBarThemeData( + dividerColor: colorScheme.outlineVariant, + labelColor: colorScheme.onSurface, + unselectedLabelColor: colorScheme.onSurfaceVariant, + indicatorColor: colorScheme.primary, + ), + ); +} + +class NoTransitionsBuilder extends PageTransitionsBuilder { + const NoTransitionsBuilder(); + + @override + Widget buildTransitions( + PageRoute route, + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return child; + } +} diff --git a/userfront/lib/core/theme/theme_controller.dart b/userfront/lib/core/theme/theme_controller.dart new file mode 100644 index 00000000..02034217 --- /dev/null +++ b/userfront/lib/core/theme/theme_controller.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeController extends ValueNotifier { + ThemeController._() : super(ThemeMode.light); + + static const storageKey = 'userfront_theme'; + static final ThemeController instance = ThemeController._(); + + bool get isDark => value == ThemeMode.dark; + + Future restore() async { + final prefs = await SharedPreferences.getInstance(); + final stored = prefs.getString(storageKey); + value = stored == 'dark' ? ThemeMode.dark : ThemeMode.light; + } + + Future setThemeMode(ThemeMode mode) async { + if (value != mode) { + value = mode; + } + final prefs = await SharedPreferences.getInstance(); + await prefs.setString( + storageKey, + mode == ThemeMode.dark ? 'dark' : 'light', + ); + } + + Future toggle() { + return setThemeMode(isDark ? ThemeMode.light : ThemeMode.dark); + } +} diff --git a/userfront/lib/core/widgets/theme_toggle_button.dart b/userfront/lib/core/widgets/theme_toggle_button.dart new file mode 100644 index 00000000..e0b11d67 --- /dev/null +++ b/userfront/lib/core/widgets/theme_toggle_button.dart @@ -0,0 +1,44 @@ +import 'package:easy_localization/easy_localization.dart' hide tr; +import 'package:flutter/material.dart'; +import 'package:userfront/i18n.dart'; + +import '../theme/theme_controller.dart'; + +class ThemeToggleButton extends StatelessWidget { + const ThemeToggleButton({super.key, this.compact = false}); + + final bool compact; + + @override + Widget build(BuildContext context) { + context.locale; + + return ValueListenableBuilder( + valueListenable: ThemeController.instance, + builder: (context, mode, _) { + final isLight = mode == ThemeMode.light; + final icon = isLight + ? Icons.light_mode_outlined + : Icons.dark_mode_outlined; + final label = isLight + ? tr('ui.common.theme_light', fallback: 'Light') + : tr('ui.common.theme_dark', fallback: 'Dark'); + final tooltip = tr('ui.common.theme_toggle', fallback: '테마 전환'); + + if (compact) { + return IconButton( + tooltip: tooltip, + onPressed: () => ThemeController.instance.toggle(), + icon: Icon(icon), + ); + } + + return OutlinedButton.icon( + onPressed: () => ThemeController.instance.toggle(), + icon: Icon(icon, size: 18), + label: Text(label), + ); + }, + ); + } +} diff --git a/userfront/lib/main.dart b/userfront/lib/main.dart index 8fbabee1..deb1a9e0 100644 --- a/userfront/lib/main.dart +++ b/userfront/lib/main.dart @@ -24,6 +24,8 @@ import 'core/services/logger_service.dart'; import 'core/services/null_check_recovery.dart'; import 'core/services/web_window.dart'; import 'core/notifiers/auth_notifier.dart'; +import 'core/theme/app_theme.dart'; +import 'core/theme/theme_controller.dart'; import 'core/i18n/locale_gate.dart'; import 'core/i18n/locale_registry.dart'; import 'core/i18n/locale_utils.dart'; @@ -106,6 +108,7 @@ void main() async { // 0. Initialize Logger LoggerService.init(); + await ThemeController.instance.restore(); // 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화 await _loadBundledFonts(); @@ -366,50 +369,25 @@ class BaronSSOApp extends StatelessWidget { final locale = localization?.currentLocale ?? Locale(resolvePreferredLocaleCode()); - return MaterialApp.router( - title: tr('ui.userfront.app_title'), - localizationsDelegates: delegates, - supportedLocales: supportedLocales, - locale: locale, - builder: (context, child) { - return Stack( - children: [if (child != null) child, const ToastViewport()], + return ValueListenableBuilder( + valueListenable: ThemeController.instance, + builder: (context, themeMode, _) { + return MaterialApp.router( + title: tr('ui.userfront.app_title'), + localizationsDelegates: delegates, + supportedLocales: supportedLocales, + locale: locale, + builder: (context, child) { + return Stack( + children: [if (child != null) child, const ToastViewport()], + ); + }, + theme: buildLightTheme(), + darkTheme: buildDarkTheme(), + themeMode: themeMode, + routerConfig: _router, ); }, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base - brightness: Brightness.light, - ), - useMaterial3: true, - fontFamily: 'NotoSansKR', - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: NoTransitionsBuilder(), - TargetPlatform.iOS: NoTransitionsBuilder(), - TargetPlatform.linux: NoTransitionsBuilder(), - TargetPlatform.macOS: NoTransitionsBuilder(), - TargetPlatform.windows: NoTransitionsBuilder(), - TargetPlatform.fuchsia: NoTransitionsBuilder(), - }, - ), - ), - routerConfig: _router, ); } } - -class NoTransitionsBuilder extends PageTransitionsBuilder { - const NoTransitionsBuilder(); - - @override - Widget buildTransitions( - PageRoute route, - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - return child; - } -} diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock index fecd33f1..da86790b 100644 --- a/userfront/pubspec.lock +++ b/userfront/pubspec.lock @@ -485,7 +485,7 @@ packages: source: hosted version: "3.2.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" diff --git a/userfront/pubspec.yaml b/userfront/pubspec.yaml index 270c2fb4..71552d8c 100644 --- a/userfront/pubspec.yaml +++ b/userfront/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: easy_localization: ^3.0.7 toml: ^0.15.0 web: ^1.1.0 + shared_preferences: ^2.5.4 dev_dependencies: flutter_test: From 1e53b66abb52227abc649e1d9090ec694f50ca44 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 8 Apr 2026 17:45:51 +0900 Subject: [PATCH 33/39] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=94=8C=EB=9E=AB=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/core/theme/theme_controller.dart | 11 +- userfront/lib/core/theme/theme_scope.dart | 44 + .../lib/core/widgets/theme_toggle_button.dart | 12 +- .../auth/presentation/login_screen.dart | 1036 +++++++++-------- userfront/lib/main.dart | 164 ++- userfront/test/theme_controller_test.dart | 16 +- 6 files changed, 730 insertions(+), 553 deletions(-) create mode 100644 userfront/lib/core/theme/theme_scope.dart diff --git a/userfront/lib/core/theme/theme_controller.dart b/userfront/lib/core/theme/theme_controller.dart index 02034217..5e00b4b9 100644 --- a/userfront/lib/core/theme/theme_controller.dart +++ b/userfront/lib/core/theme/theme_controller.dart @@ -2,10 +2,15 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ThemeController extends ValueNotifier { - ThemeController._() : super(ThemeMode.light); + ThemeController._(this.storageKey) : super(ThemeMode.light); - static const storageKey = 'userfront_theme'; - static final ThemeController instance = ThemeController._(); + static const appStorageKey = 'userfront_theme'; + static const authStorageKey = 'userfront_auth_theme'; + static final ThemeController app = ThemeController._(appStorageKey); + static final ThemeController auth = ThemeController._(authStorageKey); + static final ThemeController instance = app; + + final String storageKey; bool get isDark => value == ThemeMode.dark; diff --git a/userfront/lib/core/theme/theme_scope.dart b/userfront/lib/core/theme/theme_scope.dart new file mode 100644 index 00000000..2f912d5f --- /dev/null +++ b/userfront/lib/core/theme/theme_scope.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'app_theme.dart'; +import 'theme_controller.dart'; + +class ThemeScope extends InheritedWidget { + const ThemeScope({super.key, required this.controller, required Widget child}) + : super(child: child); + + final ThemeController controller; + + static ThemeController of(BuildContext context) { + final scope = context.dependOnInheritedWidgetOfExactType(); + return scope?.controller ?? ThemeController.app; + } + + @override + bool updateShouldNotify(ThemeScope oldWidget) { + return oldWidget.controller != controller; + } +} + +class ScopedTheme extends StatelessWidget { + const ScopedTheme({super.key, required this.controller, required this.child}); + + final ThemeController controller; + final Widget child; + + @override + Widget build(BuildContext context) { + return ThemeScope( + controller: controller, + child: ValueListenableBuilder( + valueListenable: controller, + builder: (context, mode, _) { + return Theme( + data: mode == ThemeMode.dark ? buildDarkTheme() : buildLightTheme(), + child: child, + ); + }, + ), + ); + } +} diff --git a/userfront/lib/core/widgets/theme_toggle_button.dart b/userfront/lib/core/widgets/theme_toggle_button.dart index e0b11d67..05737ad8 100644 --- a/userfront/lib/core/widgets/theme_toggle_button.dart +++ b/userfront/lib/core/widgets/theme_toggle_button.dart @@ -1,8 +1,7 @@ -import 'package:easy_localization/easy_localization.dart' hide tr; import 'package:flutter/material.dart'; import 'package:userfront/i18n.dart'; -import '../theme/theme_controller.dart'; +import '../theme/theme_scope.dart'; class ThemeToggleButton extends StatelessWidget { const ThemeToggleButton({super.key, this.compact = false}); @@ -11,10 +10,11 @@ class ThemeToggleButton extends StatelessWidget { @override Widget build(BuildContext context) { - context.locale; + Localizations.localeOf(context); + final controller = ThemeScope.of(context); return ValueListenableBuilder( - valueListenable: ThemeController.instance, + valueListenable: controller, builder: (context, mode, _) { final isLight = mode == ThemeMode.light; final icon = isLight @@ -28,13 +28,13 @@ class ThemeToggleButton extends StatelessWidget { if (compact) { return IconButton( tooltip: tooltip, - onPressed: () => ThemeController.instance.toggle(), + onPressed: () => controller.toggle(), icon: Icon(icon), ); } return OutlinedButton.icon( - onPressed: () => ThemeController.instance.toggle(), + onPressed: () => controller.toggle(), icon: Icon(icon, size: 18), label: Text(label), ); diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index 8e02a8a8..bcb02973 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -1389,6 +1389,73 @@ class _LoginScreenState extends ConsumerState final theme = Theme.of(context); final colorScheme = theme.colorScheme; final mutedColor = colorScheme.onSurfaceVariant; + final inputForegroundColor = colorScheme.brightness == Brightness.dark + ? const Color(0xFFE2E8F0) + : const Color(0xFF334155); + final primaryColor = colorScheme.brightness == Brightness.dark + ? const Color(0xFF93C5FD) + : const Color(0xFF1E3A8A); + final onPrimaryColor = colorScheme.brightness == Brightness.dark + ? const Color(0xFF0F172A) + : Colors.white; + final inputDecorationTheme = theme.inputDecorationTheme.copyWith( + filled: false, + fillColor: Colors.transparent, + contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 18), + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: colorScheme.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: primaryColor, width: 1.6), + ), + labelStyle: TextStyle(color: inputForegroundColor), + floatingLabelStyle: TextStyle(color: primaryColor), + hintStyle: TextStyle(color: inputForegroundColor), + prefixIconColor: inputForegroundColor, + ); + final localTheme = theme.copyWith( + inputDecorationTheme: inputDecorationTheme, + tabBarTheme: theme.tabBarTheme.copyWith( + dividerColor: colorScheme.outlineVariant, + indicatorColor: primaryColor, + labelColor: colorScheme.onSurface, + unselectedLabelColor: mutedColor, + labelStyle: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + unselectedLabelStyle: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(48), + backgroundColor: primaryColor, + foregroundColor: onPrimaryColor, + textStyle: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: primaryColor, + textStyle: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + ); if (_verificationOnly && _verificationApproved) { return Scaffold( @@ -1408,379 +1475,345 @@ class _LoginScreenState extends ConsumerState backgroundColor: colorScheme.surfaceContainerLowest, body: LayoutBuilder( builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 400), - padding: const EdgeInsets.all(24), - child: Card( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - tr('ui.userfront.app_title'), - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, + return Theme( + data: localTheme, + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 480), + padding: const EdgeInsets.symmetric( + horizontal: 28, + vertical: 40, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + tr('ui.userfront.app_title'), + style: theme.textTheme.headlineMedium?.copyWith( + fontSize: 34, + fontWeight: FontWeight.w800, + letterSpacing: -0.7, ), - if (_drySendEnabled) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, + textAlign: TextAlign.center, + ), + if (_drySendEnabled) ...[ + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + decoration: BoxDecoration( + color: const Color(0xFFFFF3CD), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFFFC107), ), - decoration: BoxDecoration( - color: const Color(0xFFFFF3CD), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: const Color(0xFFFFC107), + ), + child: Row( + children: [ + const Icon( + Icons.warning_amber_rounded, + color: Color(0xFF8A6D3B), ), - ), - child: Row( - children: [ - const Icon( - Icons.warning_amber_rounded, - color: Color(0xFF8A6D3B), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - tr('msg.userfront.login.dry_send'), - style: const TextStyle( - color: Color(0xFF8A6D3B), - fontSize: 12, - ), + const SizedBox(width: 10), + Expanded( + child: Text( + tr('msg.userfront.login.dry_send'), + style: const TextStyle( + color: Color(0xFF8A6D3B), + fontSize: 12, ), ), - ], - ), + ), + ], ), - ], - const SizedBox(height: 40), - - TabBar( + ), + ], + const SizedBox(height: 52), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 34), + child: TabBar( controller: _tabController, + indicatorSize: TabBarIndicatorSize.label, tabs: [ Tab(text: tr('ui.userfront.login.tabs.password')), Tab(text: tr('ui.userfront.login.tabs.link')), Tab(text: tr('ui.userfront.login.tabs.qr')), ], ), - const SizedBox(height: 24), - - SizedBox( - height: 350, - child: TabBarView( - controller: _tabController, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Column( - children: [ - TextField( - key: const ValueKey( - 'password_login_id_input', - ), - controller: _passwordLoginIdController, - decoration: InputDecoration( - labelText: - _loginIdLabel ?? - tr( - 'ui.userfront.login.field.login_id', - ), - prefixIcon: const Icon( - Icons.person_outline, - ), - ), - onSubmitted: (_) => - _handlePasswordLogin(), - ), - const SizedBox(height: 16), - TextField( - key: const ValueKey( - 'password_login_password_input', - ), - focusNode: _passwordFocusNode, - controller: _passwordController, - obscureText: true, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.field.password', - ), - prefixIcon: const Icon( - Icons.lock_outline, - ), - ), - onSubmitted: (_) => - _handlePasswordLogin(), - ), - if (_isPasswordCapsLockOn) ...[ - const SizedBox(height: 8), - Row( - children: [ - const Icon( - Icons.keyboard_capslock_rounded, - size: 18, - color: Colors.orange, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _capsLockWarningText(context), - style: const TextStyle( - color: Colors.orange, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ], - const SizedBox(height: 24), - FilledButton( - key: const ValueKey( - 'password_login_submit_button', - ), - onPressed: _handlePasswordLogin, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight( - 50, - ), - ), - child: Text( - tr( - 'ui.userfront.login.action.submit', - ), - ), - ), - ], - ), - ), - - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Column( - children: [ - if (_linkPendingRef == null) ...[ + ), + const SizedBox(height: 28), + SizedBox( + height: 360, + child: TabBarView( + controller: _tabController, + children: [ + Padding( + padding: const EdgeInsets.only(top: 20), + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 356, + ), + child: Column( + children: [ TextField( - controller: _linkIdController, + key: const ValueKey( + 'password_login_id_input', + ), + controller: + _passwordLoginIdController, decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.field.login_id', - ), - hintText: '', + labelText: + _loginIdLabel ?? + tr( + 'ui.userfront.login.field.login_id', + ), prefixIcon: const Icon( Icons.person_outline, + size: 22, ), ), onSubmitted: (_) => - _handleLinkLogin(), + _handlePasswordLogin(), ), - const SizedBox(height: 24), - FilledButton( - onPressed: _handleLinkLogin, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight( - 50, + const SizedBox(height: 18), + TextField( + key: const ValueKey( + 'password_login_password_input', + ), + focusNode: _passwordFocusNode, + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.field.password', + ), + prefixIcon: const Icon( + Icons.lock_outline, + size: 22, ), ), - child: Text( - tr('ui.userfront.login.link.send'), - ), + onSubmitted: (_) => + _handlePasswordLogin(), ), - const SizedBox(height: 24), - Text( - tr('msg.userfront.login.link.helper'), - style: TextStyle( - color: mutedColor, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - if (_linkPendingRef != null) ...[ - if (_linkExpired) ...[ - Text( - tr( - 'msg.userfront.login.link_timeout', - ), - textAlign: TextAlign.center, - style: TextStyle( - color: mutedColor, - fontSize: 12, - ), - ), - const SizedBox(height: 12), - FilledButton( - onPressed: () { - setState(_resetLinkLoginState); - }, - style: FilledButton.styleFrom( - minimumSize: - const Size.fromHeight(45), - ), - child: Text( - tr('ui.common.refresh'), - ), - ), - ] else ...[ - Text( - tr( - 'msg.userfront.login.link.short_code_help', - ), - style: TextStyle( - color: mutedColor, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 12), + if (_isPasswordCapsLockOn) ...[ + const SizedBox(height: 10), Row( children: [ - Expanded( - flex: 2, - child: TextField( - controller: - _shortCodePrefixController, - textCapitalization: - TextCapitalization - .characters, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.short_code.prefix', - ), - hintText: 'AB', - hintStyle: TextStyle( - color: mutedColor, - ), - ), - maxLength: 2, - ), + const Icon( + Icons.keyboard_capslock_rounded, + size: 18, + color: Colors.orange, ), const SizedBox(width: 8), Expanded( - flex: 4, - child: TextField( - controller: - _shortCodeDigitsController, - keyboardType: - TextInputType.number, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.short_code.digits', - ), - hintText: '345678', - hintStyle: TextStyle( - color: mutedColor, - ), - suffixText: - _linkExpireSeconds > 0 - ? tr( - 'ui.userfront.login.short_code.expire_time', - params: { - 'time': _formatTime( - _linkExpireSeconds, - ), - }, - ) - : null, + child: Text( + _capsLockWarningText(context), + style: const TextStyle( + color: Colors.orange, + fontSize: 12, + fontWeight: FontWeight.w600, ), - maxLength: 6, ), ), ], ), - const SizedBox(height: 12), - FilledButton( - onPressed: () { - final prefix = - _shortCodePrefixController - .text - .trim() - .toUpperCase(); - final digits = - _shortCodeDigitsController - .text - .trim(); - if (prefix.length != 2 || - digits.length != 6) { - _showError( - tr( - 'msg.userfront.login.short_code.invalid', - ), - ); - return; - } - _verifyShortCode(prefix + digits); - }, - style: FilledButton.styleFrom( - minimumSize: - const Size.fromHeight(45), + ], + const SizedBox(height: 28), + FilledButton( + key: const ValueKey( + 'password_login_submit_button', + ), + onPressed: _handlePasswordLogin, + child: Text( + tr( + 'ui.userfront.login.action.submit', ), + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 356, + ), + child: Column( + children: [ + if (_linkPendingRef == null) ...[ + TextField( + controller: _linkIdController, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.field.login_id', + ), + hintText: '', + prefixIcon: const Icon( + Icons.person_outline, + size: 22, + ), + ), + onSubmitted: (_) => + _handleLinkLogin(), + ), + const SizedBox(height: 28), + FilledButton( + onPressed: _handleLinkLogin, child: Text( tr( - 'ui.userfront.login.short_code.submit', + 'ui.userfront.login.link.send', ), ), ), - const SizedBox(height: 12), - TextButton( - onPressed: () { - if (_linkResendSeconds > 0) { - _showInfo( - tr( - 'msg.userfront.login.link.resend_wait', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ), - ); - return; - } - final loginId = - _lastLinkLoginId ?? - _linkIdController.text.trim(); - if (loginId.isEmpty) { - _showError( - tr( - 'msg.userfront.login.link.missing_login_id', - ), - ); - return; - } - _startEnchantedFlow( - loginId, - isEmail: - _lastLinkIsEmail || - loginId.contains('@'), - codeOnly: false, - ); - }, - child: Text( - _linkResendSeconds > 0 - ? tr( - 'ui.userfront.login.link.resend_with_time', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ) - : tr('ui.common.resend'), + const SizedBox(height: 24), + Text( + tr( + 'msg.userfront.login.link.helper', ), + style: TextStyle( + color: mutedColor, + fontSize: 12, + height: 1.5, + ), + textAlign: TextAlign.center, ), - if (!_lastLinkIsEmail) ...[ - const SizedBox(height: 4), + ], + if (_linkPendingRef != null) ...[ + if (_linkExpired) ...[ + Text( + tr( + 'msg.userfront.login.link_timeout', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + ), + const SizedBox(height: 14), + FilledButton( + onPressed: () { + setState(_resetLinkLoginState); + }, + child: Text( + tr('ui.common.refresh'), + ), + ), + ] else ...[ + Text( + tr( + 'msg.userfront.login.link.short_code_help', + ), + style: TextStyle( + color: mutedColor, + fontSize: 12, + height: 1.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 14), + Row( + children: [ + Expanded( + flex: 2, + child: TextField( + controller: + _shortCodePrefixController, + textCapitalization: + TextCapitalization + .characters, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.short_code.prefix', + ), + hintText: 'AB', + hintStyle: TextStyle( + color: mutedColor, + ), + counterText: '', + ), + maxLength: 2, + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 4, + child: TextField( + controller: + _shortCodeDigitsController, + keyboardType: + TextInputType.number, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.short_code.digits', + ), + hintText: '345678', + hintStyle: TextStyle( + color: mutedColor, + ), + counterText: '', + suffixText: + _linkExpireSeconds > 0 + ? tr( + 'ui.userfront.login.short_code.expire_time', + params: { + 'time': _formatTime( + _linkExpireSeconds, + ), + }, + ) + : null, + ), + maxLength: 6, + ), + ), + ], + ), + const SizedBox(height: 14), + FilledButton( + onPressed: () { + final prefix = + _shortCodePrefixController + .text + .trim() + .toUpperCase(); + final digits = + _shortCodeDigitsController + .text + .trim(); + if (prefix.length != 2 || + digits.length != 6) { + _showError( + tr( + 'msg.userfront.login.short_code.invalid', + ), + ); + return; + } + _verifyShortCode( + prefix + digits, + ); + }, + child: Text( + tr( + 'ui.userfront.login.short_code.submit', + ), + ), + ), + const SizedBox(height: 14), TextButton( onPressed: () { if (_linkResendSeconds > 0) { @@ -1803,182 +1836,219 @@ class _LoginScreenState extends ConsumerState if (loginId.isEmpty) { _showError( tr( - 'msg.userfront.login.link.missing_phone', + 'msg.userfront.login.link.missing_login_id', ), ); return; } _startEnchantedFlow( loginId, - isEmail: false, - codeOnly: true, + isEmail: + _lastLinkIsEmail || + loginId.contains('@'), + codeOnly: false, ); }, child: Text( - tr( - 'ui.userfront.login.link.code_only', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ), + _linkResendSeconds > 0 + ? tr( + 'ui.userfront.login.link.resend_with_time', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ) + : tr('ui.common.resend'), ), ), + if (!_lastLinkIsEmail) ...[ + const SizedBox(height: 4), + TextButton( + onPressed: () { + if (_linkResendSeconds > 0) { + _showInfo( + tr( + 'msg.userfront.login.link.resend_wait', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ), + ); + return; + } + final loginId = + _lastLinkLoginId ?? + _linkIdController.text + .trim(); + if (loginId.isEmpty) { + _showError( + tr( + 'msg.userfront.login.link.missing_phone', + ), + ); + return; + } + _startEnchantedFlow( + loginId, + isEmail: false, + codeOnly: true, + ); + }, + child: Text( + tr( + 'ui.userfront.login.link.code_only', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ), + ), + ), + ], ], ], ], - ], + ), ), ), - - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (_isQrLoading) - const CircularProgressIndicator() - else if (_qrExpired) - Column( - children: [ - Text( - tr( - 'msg.userfront.login.qr_expired', - ), - textAlign: TextAlign.center, - style: TextStyle( - color: mutedColor, - fontSize: 12, - ), - ), - const SizedBox(height: 12), - FilledButton( - onPressed: _startQrFlow, - style: FilledButton.styleFrom( - minimumSize: - const Size.fromHeight(45), - ), - child: Text( - tr('ui.common.refresh'), - ), - ), - ], - ) - else if (_qrImageBase64 != null) - Column( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all( - color: colorScheme.outline, - ), - borderRadius: - BorderRadius.circular(12), - ), - child: QrImageView( - data: _qrImageBase64!, - version: QrVersions.auto, - size: 200.0, - backgroundColor: Colors.white, - ), - ), - const SizedBox(height: 12), - Text( - _qrRemainingSeconds > 0 - ? tr( - 'ui.userfront.login.qr.remaining', - params: { - 'time': _formatTime( - _qrRemainingSeconds, - ), - }, - ) - : tr( - 'ui.userfront.login.qr.expired', - ), - textAlign: TextAlign.center, - style: TextStyle( - color: _qrRemainingSeconds > 30 - ? Colors.blue - : Colors.red, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - tr( - 'msg.userfront.login.qr.scan_hint', - ), - textAlign: TextAlign.center, - style: TextStyle( - color: mutedColor, - fontSize: 12, - ), - ), - TextButton( - onPressed: _startQrFlow, - child: Text( - tr( - 'ui.userfront.login.qr.refresh', - ), - ), - ), - ], - ) - else - Text( - tr( - 'msg.userfront.login.qr.load_failed', - ), - textAlign: TextAlign.center, - ), - ], - ), - ], - ), - ), - const SizedBox(height: 16), - Column( - children: [ - TextButton( - onPressed: () => - context.push('/forgot-password'), - child: Text( - tr('ui.userfront.login.forgot_password'), - ), ), - Row( + Column( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - tr('msg.userfront.login.no_account'), - style: TextStyle( - color: mutedColor, - fontSize: 14, + if (_isQrLoading) + const CircularProgressIndicator() + else if (_qrExpired) + Column( + children: [ + Text( + tr('msg.userfront.login.qr_expired'), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + ), + const SizedBox(height: 14), + FilledButton( + onPressed: _startQrFlow, + child: Text(tr('ui.common.refresh')), + ), + ], + ) + else if (_qrImageBase64 != null) + Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + border: Border.all( + color: colorScheme.outline, + ), + borderRadius: BorderRadius.circular( + 18, + ), + ), + child: QrImageView( + data: _qrImageBase64!, + version: QrVersions.auto, + size: 200.0, + backgroundColor: Colors.white, + ), + ), + const SizedBox(height: 14), + Text( + _qrRemainingSeconds > 0 + ? tr( + 'ui.userfront.login.qr.remaining', + params: { + 'time': _formatTime( + _qrRemainingSeconds, + ), + }, + ) + : tr( + 'ui.userfront.login.qr.expired', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: _qrRemainingSeconds > 30 + ? primaryColor + : Colors.red, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + tr( + 'msg.userfront.login.qr.scan_hint', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + height: 1.5, + ), + ), + TextButton( + onPressed: _startQrFlow, + child: Text( + tr('ui.userfront.login.qr.refresh'), + ), + ), + ], + ) + else + Text( + tr('msg.userfront.login.qr.load_failed'), + textAlign: TextAlign.center, ), - ), - TextButton( - onPressed: () => context.push('/signup'), - child: Text( - tr('ui.userfront.login.signup'), - ), - ), ], ), ], ), - const SizedBox(height: 6), - const Wrap( - alignment: WrapAlignment.center, - spacing: 10, - runSpacing: 10, - children: [ThemeToggleButton(), LanguageSelector()], - ), - ], - ), + ), + const SizedBox(height: 18), + Column( + children: [ + TextButton( + onPressed: () => context.push('/forgot-password'), + child: Text( + tr('ui.userfront.login.forgot_password'), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + tr('msg.userfront.login.no_account'), + style: TextStyle( + color: mutedColor, + fontSize: 14, + ), + ), + TextButton( + onPressed: () => context.push('/signup'), + child: Text(tr('ui.userfront.login.signup')), + ), + ], + ), + ], + ), + const SizedBox(height: 12), + const Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: [ThemeToggleButton(), LanguageSelector()], + ), + ], ), ), ), diff --git a/userfront/lib/main.dart b/userfront/lib/main.dart index deb1a9e0..774ecb66 100644 --- a/userfront/lib/main.dart +++ b/userfront/lib/main.dart @@ -26,6 +26,7 @@ import 'core/services/web_window.dart'; import 'core/notifiers/auth_notifier.dart'; import 'core/theme/app_theme.dart'; import 'core/theme/theme_controller.dart'; +import 'core/theme/theme_scope.dart'; import 'core/i18n/locale_gate.dart'; import 'core/i18n/locale_registry.dart'; import 'core/i18n/locale_utils.dart'; @@ -108,7 +109,8 @@ void main() async { // 0. Initialize Logger LoggerService.init(); - await ThemeController.instance.restore(); + await ThemeController.app.restore(); + await ThemeController.auth.restore(); // 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화 await _loadBundledFonts(); @@ -180,12 +182,18 @@ final _router = GoRouter( GoRoute( path: 'dashboard', builder: (context, state) { - return const DashboardScreen(); + return ScopedTheme( + controller: ThemeController.app, + child: const DashboardScreen(), + ); }, ), GoRoute( path: 'profile', - builder: (context, state) => const ProfilePage(), + builder: (context, state) => ScopedTheme( + controller: ThemeController.app, + child: const ProfilePage(), + ), ), GoRoute( path: 'signin', @@ -195,10 +203,13 @@ final _router = GoRouter( final redirectUrl = state.uri.queryParameters['redirect_uri'] ?? state.uri.queryParameters['redirect_url']; - return LoginScreen( - key: state.pageKey, - loginChallenge: loginChallenge, - redirectUrl: redirectUrl, + return ScopedTheme( + controller: ThemeController.auth, + child: LoginScreen( + key: state.pageKey, + loginChallenge: loginChallenge, + redirectUrl: redirectUrl, + ), ); }, ), @@ -211,10 +222,13 @@ final _router = GoRouter( final redirectUrl = state.uri.queryParameters['redirect_uri'] ?? state.uri.queryParameters['redirect_url']; - return LoginScreen( - key: state.pageKey, - loginChallenge: loginChallenge, - redirectUrl: redirectUrl, + return ScopedTheme( + controller: ThemeController.auth, + child: LoginScreen( + key: state.pageKey, + loginChallenge: loginChallenge, + redirectUrl: redirectUrl, + ), ); }, ), @@ -230,88 +244,137 @@ final _router = GoRouter( ), ); } - return ConsentScreen(consentChallenge: consentChallenge); + return ScopedTheme( + controller: ThemeController.auth, + child: ConsentScreen(consentChallenge: consentChallenge), + ); }, ), GoRoute( path: 'signup', - builder: (context, state) => const SignupScreen(), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: const SignupScreen(), + ), ), GoRoute( path: 'registration', - builder: (context, state) => const SignupScreen(), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: const SignupScreen(), + ), ), GoRoute( path: 'verify', - builder: (context, state) => LoginScreen(key: state.pageKey), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: LoginScreen(key: state.pageKey), + ), ), GoRoute( path: 'verify/:token', builder: (context, state) { final token = state.pathParameters['token']; - return LoginScreen( - key: state.pageKey, - verificationToken: token, + return ScopedTheme( + controller: ThemeController.auth, + child: LoginScreen( + key: state.pageKey, + verificationToken: token, + ), ); }, ), GoRoute( path: 'verification', - builder: (context, state) => LoginScreen(key: state.pageKey), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: LoginScreen(key: state.pageKey), + ), ), GoRoute( path: 'l/:shortCode', builder: (context, state) { - return LoginScreen(key: state.pageKey); + return ScopedTheme( + controller: ThemeController.auth, + child: LoginScreen(key: state.pageKey), + ); }, ), GoRoute( path: 'forgot-password', - builder: (context, state) => const ForgotPasswordScreen(), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: const ForgotPasswordScreen(), + ), ), GoRoute( path: 'recovery', - builder: (context, state) => const ForgotPasswordScreen(), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: const ForgotPasswordScreen(), + ), ), GoRoute( path: 'reset-password', - builder: (context, state) => const ResetPasswordScreen(), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: const ResetPasswordScreen(), + ), ), GoRoute( path: 'error', builder: (context, state) { final params = state.uri.queryParameters; - return ErrorScreen( - errorId: params['id'], - errorCode: params['error'], - description: params['error_description'] ?? params['message'], + return ScopedTheme( + controller: ThemeController.auth, + child: ErrorScreen( + errorId: params['id'], + errorCode: params['error'], + description: + params['error_description'] ?? params['message'], + ), ); }, ), GoRoute( path: 'settings', - builder: (context, state) => ErrorScreen( - errorCode: 'settings_disabled', - description: tr('msg.userfront.settings.disabled'), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: ErrorScreen( + errorCode: 'settings_disabled', + description: tr('msg.userfront.settings.disabled'), + ), ), ), GoRoute( path: 'approve', - builder: (context, state) => - ApproveQrScreen(pendingRef: state.uri.queryParameters['ref']), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: ApproveQrScreen( + pendingRef: state.uri.queryParameters['ref'], + ), + ), ), GoRoute( path: 'ql/:ref', - builder: (context, state) => - ApproveQrScreen(pendingRef: state.pathParameters['ref']), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: ApproveQrScreen(pendingRef: state.pathParameters['ref']), + ), ), GoRoute( path: 'scan', - builder: (context, state) => const QRScanScreen(), + builder: (context, state) => ScopedTheme( + controller: ThemeController.auth, + child: const QRScanScreen(), + ), ), GoRoute( path: 'admin/users', - builder: (context, state) => const UserManagementScreen(), + builder: (context, state) => ScopedTheme( + controller: ThemeController.app, + child: const UserManagementScreen(), + ), ), ], ), @@ -369,25 +432,20 @@ class BaronSSOApp extends StatelessWidget { final locale = localization?.currentLocale ?? Locale(resolvePreferredLocaleCode()); - return ValueListenableBuilder( - valueListenable: ThemeController.instance, - builder: (context, themeMode, _) { - return MaterialApp.router( - title: tr('ui.userfront.app_title'), - localizationsDelegates: delegates, - supportedLocales: supportedLocales, - locale: locale, - builder: (context, child) { - return Stack( - children: [if (child != null) child, const ToastViewport()], - ); - }, - theme: buildLightTheme(), - darkTheme: buildDarkTheme(), - themeMode: themeMode, - routerConfig: _router, + return MaterialApp.router( + title: tr('ui.userfront.app_title'), + localizationsDelegates: delegates, + supportedLocales: supportedLocales, + locale: locale, + builder: (context, child) { + return Stack( + children: [if (child != null) child, const ToastViewport()], ); }, + theme: buildLightTheme(), + darkTheme: buildDarkTheme(), + themeMode: ThemeMode.light, + routerConfig: _router, ); } } diff --git a/userfront/test/theme_controller_test.dart b/userfront/test/theme_controller_test.dart index 1c829b93..447255c9 100644 --- a/userfront/test/theme_controller_test.dart +++ b/userfront/test/theme_controller_test.dart @@ -8,25 +8,25 @@ void main() { setUp(() async { SharedPreferences.setMockInitialValues({}); - await ThemeController.instance.setThemeMode(ThemeMode.light); + await ThemeController.app.setThemeMode(ThemeMode.light); }); test('저장된 dark 값을 복원한다', () async { SharedPreferences.setMockInitialValues({ - ThemeController.storageKey: 'dark', + ThemeController.appStorageKey: 'dark', }); - await ThemeController.instance.restore(); + await ThemeController.app.restore(); - expect(ThemeController.instance.value, ThemeMode.dark); + expect(ThemeController.app.value, ThemeMode.dark); }); test('toggle 결과를 저장한다', () async { - await ThemeController.instance.restore(); - await ThemeController.instance.toggle(); + await ThemeController.app.restore(); + await ThemeController.app.toggle(); final prefs = await SharedPreferences.getInstance(); - expect(ThemeController.instance.value, ThemeMode.dark); - expect(prefs.getString(ThemeController.storageKey), 'dark'); + expect(ThemeController.app.value, ThemeMode.dark); + expect(prefs.getString(ThemeController.appStorageKey), 'dark'); }); } From df09694ed62d4d79caf2f1cbcdecc33deaf346f6 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 9 Apr 2026 09:46:40 +0900 Subject: [PATCH 34/39] =?UTF-8?q?=EC=95=B1=20=EB=A1=9C=EA=B3=A0=20URL=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=ED=83=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientGeneralPage.tsx | 121 +++++++++++++++++- devfront/src/locales/en.toml | 7 + devfront/src/locales/ko.toml | 7 + devfront/src/locales/template.toml | 7 + 4 files changed, 137 insertions(+), 5 deletions(-) diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 2446daf9..84c4d470 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { ArrowLeft, + ExternalLink, Info, Plus, Save, @@ -133,6 +134,9 @@ function ClientGeneralPage() { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [logoUrl, setLogoUrl] = useState(""); + const [logoPreviewStatus, setLogoPreviewStatus] = useState< + "idle" | "loading" | "loaded" | "error" + >("idle"); const [clientType, setClientType] = useState("private"); const [status, setStatus] = useState("active"); const [initialStatus, setInitialStatus] = useState("active"); @@ -240,6 +244,21 @@ function ClientGeneralPage() { const securityProfile: SecurityProfile = clientType === "pkce" ? "pkce" : "private"; + const trimmedLogoUrl = logoUrl.trim(); + const hasLogoUrl = trimmedLogoUrl.length > 0; + const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl); + + useEffect(() => { + if (!hasLogoUrl) { + setLogoPreviewStatus("idle"); + return; + } + if (!hasValidLogoUrl) { + setLogoPreviewStatus("error"); + return; + } + setLogoPreviewStatus("loading"); + }, [hasLogoUrl, hasValidLogoUrl, trimmedLogoUrl]); const handleSecurityProfileChange = (profile: SecurityProfile) => { setClientType(profile); @@ -438,6 +457,15 @@ function ClientGeneralPage() { const mutation = useMutation({ mutationFn: async () => { + if (hasLogoUrl && !hasValidLogoUrl) { + throw new Error( + t( + "msg.dev.clients.general.identity.logo_invalid", + "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.", + ), + ); + } + const scopeNames = scopes.map((scope) => scope.name).filter(Boolean); const effectiveTokenEndpointAuthMethod = @@ -457,7 +485,7 @@ function ClientGeneralPage() { : undefined, metadata: { description, - logo_url: logoUrl, + logo_url: trimmedLogoUrl, structured_scopes: scopes, token_endpoint_auth_method: effectiveTokenEndpointAuthMethod, headless_login_enabled: headlessLoginEnabled, @@ -722,6 +750,8 @@ function ClientGeneralPage() { setLogoUrl(e.target.value)} + aria-invalid={!hasValidLogoUrl} + className={!hasValidLogoUrl ? "border-destructive" : ""} placeholder={t( "ui.dev.clients.general.identity.logo_placeholder", "https://example.com/logo.png", @@ -733,19 +763,100 @@ function ClientGeneralPage() { "인증 화면에 표시될 PNG/SVG URL입니다.", )}

+ {!hasValidLogoUrl ? ( +

+ {t( + "msg.dev.clients.general.identity.logo_invalid", + "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.", + )} +

+ ) : null} + {hasLogoUrl && hasValidLogoUrl ? ( +
+ + {logoPreviewStatus === "loading" + ? t( + "msg.dev.clients.general.identity.logo_preview_loading", + "로고 미리보기를 불러오는 중입니다.", + ) + : logoPreviewStatus === "loaded" + ? t( + "msg.dev.clients.general.identity.logo_preview_ready", + "로고 미리보기를 확인했습니다.", + ) + : logoPreviewStatus === "error" + ? t( + "msg.dev.clients.general.identity.logo_preview_failed", + "로고 미리보기를 불러오지 못했습니다. URL 또는 이미지 접근 권한을 확인하세요.", + ) + : null} + + + + {t( + "ui.dev.clients.general.identity.logo_open", + "새 탭에서 열기", + )} + +
+ ) : null}
-
- {logoUrl ? ( +
+ {hasLogoUrl && hasValidLogoUrl ? ( {t( setLogoPreviewStatus("loaded")} + onError={() => setLogoPreviewStatus("error")} /> ) : ( - +
+ + {logoPreviewStatus === "error" ? ( + + {t( + "ui.dev.clients.general.identity.logo_preview_error_badge", + "미리보기 실패", + )} + + ) : ( + + {t( + "ui.dev.clients.general.identity.logo_preview_empty", + "미리보기", + )} + + )} +
)}
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index d3ae2a09..87256821 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -377,6 +377,10 @@ empty = "No IdP configurations found." [msg.dev.clients.general.identity] logo_help = "PNG or SVG URL shown on the consent and authentication screens." +logo_invalid = "The app logo URL format is invalid. Enter an http or https address." +logo_preview_loading = "Loading the logo preview." +logo_preview_ready = "Logo preview loaded." +logo_preview_failed = "Failed to load the logo preview. Check the URL or image access policy." subtitle = "Set the application name, description, and logo." [msg.dev.clients.general.redirect] @@ -1378,6 +1382,9 @@ description_placeholder = "Description Placeholder" logo = "App Logo URL" logo_placeholder = "https://example.com/logo.png" logo_preview = "Logo Preview" +logo_open = "Open in new tab" +logo_preview_error_badge = "Preview failed" +logo_preview_empty = "Preview" name = "Name" name_placeholder = "My Awesome Application" title = "Application Identity" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index b6abb943..2ac3c74a 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -377,6 +377,10 @@ subtitle = "이 애플리케이션의 외부 IdP 설정을 관리합니다." [msg.dev.clients.general.identity] logo_help = "인증 화면에 표시될 PNG/SVG URL입니다." +logo_invalid = "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요." +logo_preview_loading = "로고 미리보기를 불러오는 중입니다." +logo_preview_ready = "로고 미리보기를 확인했습니다." +logo_preview_failed = "로고 미리보기를 불러오지 못했습니다. URL 또는 이미지 접근 권한을 확인하세요." subtitle = "앱 이름과 설명, 로고를 설정합니다." [msg.dev.clients.general.redirect] @@ -1377,6 +1381,9 @@ description_placeholder = "앱에 대한 간단한 설명을 입력하세요." logo = "앱 로고 URL" logo_placeholder = "https://example.com/logo.png" logo_preview = "로고 미리보기" +logo_open = "새 탭에서 열기" +logo_preview_error_badge = "미리보기 실패" +logo_preview_empty = "미리보기" name = "앱 이름" name_placeholder = "예: 멋진 애플리케이션" title = "애플리케이션 정보" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index 87853adf..460baaec 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -377,6 +377,10 @@ empty = "" [msg.dev.clients.general.identity] logo_help = "" +logo_invalid = "" +logo_preview_loading = "" +logo_preview_ready = "" +logo_preview_failed = "" subtitle = "" [msg.dev.clients.general.redirect] @@ -1378,6 +1382,9 @@ description_placeholder = "" logo = "" logo_placeholder = "" logo_preview = "" +logo_open = "" +logo_preview_error_badge = "" +logo_preview_empty = "" name = "" name_placeholder = "" title = "" From 06a6875cdb828b9c4c5580c469ef2afbe3fed614 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 9 Apr 2026 11:27:46 +0900 Subject: [PATCH 35/39] =?UTF-8?q?App=20=EC=B9=B4=EB=93=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 42 +++++ .../handler/auth_handler_linked_test.go | 85 ++++++++++ .../presentation/dashboard_screen.dart | 145 ++++++++++++++---- userfront/pubspec.lock | 48 ++++++ userfront/pubspec.yaml | 1 + 5 files changed, 290 insertions(+), 31 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 40823eab..4d99e49a 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -4602,6 +4602,48 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { } } + // Consent session payload may omit metadata fields such as logo_url. + // Rehydrate missing display fields from the full Hydra client object. + for clientID, record := range records { + if record == nil { + continue + } + needsHydraLookup := record.Logo == "" || record.URL == "" || record.InitURL == "" + if !needsHydraLookup { + continue + } + + client, err := h.Hydra.GetClient(c.Context(), clientID) + if err != nil { + continue + } + + if record.Name == "" { + name := strings.TrimSpace(client.ClientName) + if name == "" { + name = client.ClientID + } + record.Name = name + } + if record.Logo == "" { + record.Logo = extractHydraClientLogo(client.Metadata) + } + if record.URL == "" { + record.URL = resolveLinkedRPURL( + client.ClientID, + client.ClientURI, + client.RedirectURIs, + ) + } + if record.InitURL == "" { + record.InitURL = resolveLinkedRPInitURL( + client.ClientID, + record.Scopes, + client.RedirectURIs, + ) + } + } + // [New] DB에서 과거 동의 내역 가져와 병합 (비활성 RP 포함) if h.ConsentRepo != nil { for _, subject := range subjects { diff --git a/backend/internal/handler/auth_handler_linked_test.go b/backend/internal/handler/auth_handler_linked_test.go index 0c7a9c07..f4ec811a 100644 --- a/backend/internal/handler/auth_handler_linked_test.go +++ b/backend/internal/handler/auth_handler_linked_test.go @@ -165,3 +165,88 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) { assert.Equal(t, "1", parsedInitURL.Query().Get("auto")) assert.Equal(t, "/clients", parsedInitURL.Query().Get("returnTo")) } + +func TestListLinkedRps_EnrichesLogoFromHydraClientWhenConsentSessionOmitsMetadata(t *testing.T) { + transport := roundTripFunc(func(r *http.Request) (*http.Response, error) { + switch r.URL.Host { + case "kratos.test": + if r.URL.Path == "/sessions/whoami" { + return httpJSONAny(r, http.StatusOK, map[string]interface{}{ + "identity": map[string]interface{}{ + "id": "user-123", + }, + }), nil + } + case "hydra.test": + if r.URL.Path == "/oauth2/auth/sessions/consent" { + return httpJSONAny(r, http.StatusOK, []map[string]interface{}{ + { + "client": map[string]interface{}{ + "client_id": "gitea-client", + "client_name": "Gitea", + "redirect_uris": []string{ + "https://gitea.example.com/callback", + }, + }, + "grant_scope": []string{"openid", "profile"}, + "handled_at": time.Now().Format(time.RFC3339), + }, + }), nil + } + if r.URL.Path == "/clients/gitea-client" { + return httpJSONAny(r, http.StatusOK, map[string]interface{}{ + "client_id": "gitea-client", + "client_name": "Gitea", + "redirect_uris": []string{ + "https://gitea.example.com/callback", + }, + "metadata": map[string]interface{}{ + "logo_url": "https://cdn.example.com/gitea.svg", + }, + }), nil + } + } + return httpResponse(r, http.StatusNotFound, "not found"), nil + }) + + client := &http.Client{Transport: transport} + + origDefault := http.DefaultClient + http.DefaultClient = client + defer func() { + http.DefaultClient = origDefault + }() + + h := &AuthHandler{ + Hydra: &service.HydraAdminService{ + AdminURL: "http://hydra.test", + HTTPClient: client, + }, + KratosAdmin: new(MockKratosAdminService), + } + + t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") + t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test") + t.Setenv("HYDRA_PUBLIC_URL", "https://sso.example.com/oidc") + + app := newLinkedRpTestApp(h) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/user/rp/linked", nil) + req.Header.Set("Cookie", "ory_kratos_session=valid") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var res struct { + Items []struct { + ID string `json:"id"` + Logo string `json:"logo"` + } `json:"items"` + } + json.NewDecoder(resp.Body).Decode(&res) + + assert.Len(t, res.Items, 1) + assert.Equal(t, "gitea-client", res.Items[0].ID) + assert.Equal(t, "https://cdn.example.com/gitea.svg", res.Items[0].Logo) +} diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 4dea1f97..09649860 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -1384,6 +1385,7 @@ class _DashboardScreenState extends ConsumerState { _ActivityItem( clientId: rp.id, appName: name, + logo: rp.logo.trim(), lastAuthAt: lastAuthLabel, status: statusCode, scopes: rp.scopes, @@ -1522,37 +1524,7 @@ class _DashboardScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - item.appName, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: _ink, - ), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: statusColor, - borderRadius: BorderRadius.circular(999), - ), - child: Text( - item.status == 'active' - ? tr('ui.userfront.dashboard.activity.linked') - : tr('ui.userfront.dashboard.status.revoked'), - style: const TextStyle( - fontSize: 11, - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), + _buildActivityCardHeader(item, statusColor), const SizedBox(height: 10), Text( tr('ui.userfront.dashboard.last_auth_label'), @@ -1658,6 +1630,115 @@ class _DashboardScreenState extends ConsumerState { return opaqueCard; } + Widget _buildActivityCardHeader(_ActivityItem item, Color statusColor) { + final statusBadge = Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + item.status == 'active' + ? tr('ui.userfront.dashboard.activity.linked') + : tr('ui.userfront.dashboard.status.revoked'), + style: const TextStyle( + fontSize: 11, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ); + + return SizedBox( + height: 40, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (item.logo.isNotEmpty) ...[ + _buildActivityLogo(item.logo), + const SizedBox(width: 10), + ], + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Text( + item.appName, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: _ink, + height: 1.25, + ), + ), + ), + ), + const SizedBox(width: 8), + statusBadge, + ], + ), + ); + } + + Widget _buildActivityLogo(String logoUrl) { + return SizedBox( + width: 40, + height: 40, + child: _buildActivityLogoImage(logoUrl), + ); + } + + Widget _buildActivityLogoImage(String logoUrl) { + final isSvg = _isSvgLogoUrl(logoUrl); + return isSvg + ? SvgPicture.network( + logoUrl, + fit: BoxFit.contain, + placeholderBuilder: (context) => _buildActivityLogoLoading(), + ) + : Image.network( + logoUrl, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return _buildActivityLogoFallback(); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + return _buildActivityLogoLoading(); + }, + ); + } + + bool _isSvgLogoUrl(String logoUrl) { + final normalized = logoUrl.trim().toLowerCase(); + if (normalized.isEmpty) { + return false; + } + final uri = Uri.tryParse(normalized); + final path = uri?.path.toLowerCase() ?? normalized; + return path.endsWith('.svg'); + } + + Widget _buildActivityLogoLoading() { + return Center( + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.grey[400], + ), + ), + ); + } + + Widget _buildActivityLogoFallback() { + return Icon(Icons.apps_rounded, size: 20, color: Colors.grey[500]); + } + Widget _buildAccessHistory(AuthTimelineState state, bool isWide) { final sessionsState = ref.watch(userSessionsProvider); if (state.isLoading && state.items.isEmpty) { @@ -2470,6 +2551,7 @@ enum _HistorySessionStatus { current, active, inactive } class _ActivityItem { final String clientId; final String appName; + final String logo; final String lastAuthAt; final String status; final String? url; @@ -2482,6 +2564,7 @@ class _ActivityItem { _ActivityItem({ required this.clientId, required this.appName, + required this.logo, required this.lastAuthAt, required this.status, required this.scopes, diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock index da86790b..238c821f 100644 --- a/userfront/pubspec.lock +++ b/userfront/pubspec.lock @@ -184,6 +184,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" + url: "https://pub.dev" + source: hosted + version: "2.2.4" flutter_test: dependency: "direct dev" description: flutter @@ -388,6 +396,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider_linux: dependency: transitive description: @@ -753,6 +769,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.5" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" + url: "https://pub.dev" + source: hosted + version: "1.1.21" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: @@ -825,6 +865,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/userfront/pubspec.yaml b/userfront/pubspec.yaml index 71552d8c..cc71655e 100644 --- a/userfront/pubspec.yaml +++ b/userfront/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: go_router: ^17.0.1 http: ^1.6.0 flutter_dotenv: ^6.0.0 + flutter_svg: ^2.2.1 url_launcher: ^6.3.2 logging: ^1.2.0 logger: ^2.0.0 From c6ddf7c485e18ba99199b4d75a6df856078fda41 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 9 Apr 2026 16:45:26 +0900 Subject: [PATCH 36/39] =?UTF-8?q?code=20check=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 13 ++++++++---- adminfront/package.json | 2 +- adminfront/scripts/runtime-mode.sh | 4 ++-- adminfront/src/features/auth/LoginPage.tsx | 8 +------ adminfront/src/locales/template.toml | 1 + adminfront/vite.config.ts | 4 ++-- devfront/package.json | 2 +- devfront/scripts/runtime-mode.sh | 4 ++-- devfront/src/features/auth/LoginPage.tsx | 8 +------ .../features/clients/ClientDetailsPage.tsx | 12 +++++++++++ .../features/clients/ClientGeneralPage.tsx | 7 +++++-- devfront/src/locales/template.toml | 1 + .../tests/devfront-role-switch-report.spec.ts | 1 + devfront/tests/helpers/devfront-fixtures.ts | 6 +++++- devfront/vite.config.ts | 4 ++-- locales/template.toml | 1 + scripts/run_adminfront_ci_tests.sh | 13 +++++++++++- tools/i18n-scanner/report.js | 21 ++++++++++++++++++- userfront/assets/translations/en.toml | 2 +- userfront/assets/translations/ko.toml | 4 ++-- userfront/assets/translations/template.toml | 1 + 21 files changed, 83 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index 855f72ef..f08f8d6a 100644 --- a/Makefile +++ b/Makefile @@ -107,12 +107,17 @@ logs-app: docker compose -f $(COMPOSE_APP) logs -f # --- 로컬 통합 코드 체크 --- +PLAYWRIGHT_BROWSERS_PATH := $(HOME)/.cache/ms-playwright +PLAYWRIGHT_CHROMIUM_COMPLETE := $(PLAYWRIGHT_BROWSERS_PATH)/chromium-1208/INSTALLATION_COMPLETE +PLAYWRIGHT_FIREFOX_COMPLETE := $(PLAYWRIGHT_BROWSERS_PATH)/firefox-1509/INSTALLATION_COMPLETE +PLAYWRIGHT_WEBKIT_COMPLETE := $(PLAYWRIGHT_BROWSERS_PATH)/webkit-2248/INSTALLATION_COMPLETE + ifeq ($(CI),) -PLAYWRIGHT_INSTALL_ALL := npx playwright install -PLAYWRIGHT_INSTALL_CHROMIUM := npx playwright install chromium +PLAYWRIGHT_INSTALL_ALL := sh -c 'if [ -f "$(PLAYWRIGHT_CHROMIUM_COMPLETE)" ] && [ -f "$(PLAYWRIGHT_FIREFOX_COMPLETE)" ] && [ -f "$(PLAYWRIGHT_WEBKIT_COMPLETE)" ]; then echo "Playwright browsers already installed"; else npx playwright install; fi' +PLAYWRIGHT_INSTALL_CHROMIUM := sh -c 'if [ -f "$(PLAYWRIGHT_CHROMIUM_COMPLETE)" ]; then echo "Playwright chromium already installed"; else npx playwright install chromium; fi' else -PLAYWRIGHT_INSTALL_ALL := npx playwright install --with-deps -PLAYWRIGHT_INSTALL_CHROMIUM := npx playwright install --with-deps chromium +PLAYWRIGHT_INSTALL_ALL := sh -c 'if [ -f "$(PLAYWRIGHT_CHROMIUM_COMPLETE)" ] && [ -f "$(PLAYWRIGHT_FIREFOX_COMPLETE)" ] && [ -f "$(PLAYWRIGHT_WEBKIT_COMPLETE)" ]; then echo "Playwright browsers already installed"; else npx playwright install --with-deps; fi' +PLAYWRIGHT_INSTALL_CHROMIUM := sh -c 'if [ -f "$(PLAYWRIGHT_CHROMIUM_COMPLETE)" ]; then echo "Playwright chromium already installed"; else npx playwright install --with-deps chromium; fi' endif .PHONY: code-check code-check-lint code-check-test-jobs code-check-i18n code-check-i18n-values code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint code-check-front-lint code-check-backend-tests code-check-userfront-tests code-check-adminfront-tests code-check-devfront-tests code-check-userfront-e2e-tests diff --git a/adminfront/package.json b/adminfront/package.json index fbcba5b6..80dd2bb4 100644 --- a/adminfront/package.json +++ b/adminfront/package.json @@ -7,7 +7,7 @@ "node": ">=24.0.0" }, "scripts": { - "dev": "vite --host 0.0.0.0", + "dev": "vite --host 127.0.0.1", "build": "tsc -b && vite build", "lint": "biome check .", "lint:fix": "biome check . --write", diff --git a/adminfront/scripts/runtime-mode.sh b/adminfront/scripts/runtime-mode.sh index aa41dce1..8191bf0d 100644 --- a/adminfront/scripts/runtime-mode.sh +++ b/adminfront/scripts/runtime-mode.sh @@ -19,8 +19,8 @@ fi if [ "$mode" = "production" ]; then echo "Running in production mode with Vite preview..." - exec sh -c "npm run build && npm run preview -- --host 0.0.0.0" + exec sh -c "npm run build && npm run preview -- --host 127.0.0.1" fi echo "Running in development mode..." -exec npm run dev -- --host 0.0.0.0 +exec npm run dev -- --host 127.0.0.1 diff --git a/adminfront/src/features/auth/LoginPage.tsx b/adminfront/src/features/auth/LoginPage.tsx index a9edebb5..bc6d780d 100644 --- a/adminfront/src/features/auth/LoginPage.tsx +++ b/adminfront/src/features/auth/LoginPage.tsx @@ -39,13 +39,7 @@ function LoginPage() { returnTo, }, }); - }, [ - auth, - auth.activeNavigator, - auth.isLoading, - returnTo, - shouldAutoLogin, - ]); + }, [auth, auth.activeNavigator, auth.isLoading, returnTo, shouldAutoLogin]); const handleSSOLogin = () => { void auth.signinRedirect({ diff --git a/adminfront/src/locales/template.toml b/adminfront/src/locales/template.toml index 868d4f8d..eaacc511 100644 --- a/adminfront/src/locales/template.toml +++ b/adminfront/src/locales/template.toml @@ -1501,6 +1501,7 @@ ory = "" session = "" [ui.userfront.dashboard] +link_status_label = "" last_auth_label = "" status_history = "" diff --git a/adminfront/vite.config.ts b/adminfront/vite.config.ts index 2a8338a8..8176399c 100644 --- a/adminfront/vite.config.ts +++ b/adminfront/vite.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ plugins: [react()], envPrefix: ["VITE_", "USERFRONT_"], server: { - host: "0.0.0.0", + host: "127.0.0.1", allowedHosts: ["sadmin.hmac.kr", "localhost", "172.16.10.176", "127.0.0.1"], proxy: { "/api": { @@ -15,7 +15,7 @@ export default defineConfig({ }, }, preview: { - host: "0.0.0.0", + host: "127.0.0.1", port: 5173, allowedHosts: ["sadmin.hmac.kr", "localhost", "172.16.10.176", "127.0.0.1"], proxy: { diff --git a/devfront/package.json b/devfront/package.json index 4dd04e0b..8809ce3a 100644 --- a/devfront/package.json +++ b/devfront/package.json @@ -7,7 +7,7 @@ "node": ">=24.0.0" }, "scripts": { - "dev": "vite --host 0.0.0.0", + "dev": "vite --host 127.0.0.1", "build": "tsc -b && vite build", "lint": "biome check .", "preview": "vite preview", diff --git a/devfront/scripts/runtime-mode.sh b/devfront/scripts/runtime-mode.sh index aa41dce1..8191bf0d 100644 --- a/devfront/scripts/runtime-mode.sh +++ b/devfront/scripts/runtime-mode.sh @@ -19,8 +19,8 @@ fi if [ "$mode" = "production" ]; then echo "Running in production mode with Vite preview..." - exec sh -c "npm run build && npm run preview -- --host 0.0.0.0" + exec sh -c "npm run build && npm run preview -- --host 127.0.0.1" fi echo "Running in development mode..." -exec npm run dev -- --host 0.0.0.0 +exec npm run dev -- --host 127.0.0.1 diff --git a/devfront/src/features/auth/LoginPage.tsx b/devfront/src/features/auth/LoginPage.tsx index cd9f8ca1..212eabf7 100644 --- a/devfront/src/features/auth/LoginPage.tsx +++ b/devfront/src/features/auth/LoginPage.tsx @@ -40,13 +40,7 @@ function LoginPage() { returnTo, }, }); - }, [ - auth, - auth.activeNavigator, - auth.isLoading, - returnTo, - shouldAutoLogin, - ]); + }, [auth, auth.activeNavigator, auth.isLoading, returnTo, shouldAutoLogin]); const handleSSOLogin = async () => { try { diff --git a/devfront/src/features/clients/ClientDetailsPage.tsx b/devfront/src/features/clients/ClientDetailsPage.tsx index f4a3550f..26346563 100644 --- a/devfront/src/features/clients/ClientDetailsPage.tsx +++ b/devfront/src/features/clients/ClientDetailsPage.tsx @@ -150,7 +150,18 @@ function ClientDetailsPage() { ); } + if (isLoading && !data) { + return ( +
+ {t("msg.dev.clients.details.loading", "Loading app details...")} +
+ ); + } + const client = data?.client; + if (!client) { + return null; + } const endpointValues = data?.endpoints ?? { discovery: "-", issuer: "-", @@ -469,6 +480,7 @@ function ClientDetailsPage() { )} rows={5} value={redirectUris} + onFocus={(e) => e.currentTarget.select()} onChange={(e) => { redirectUrisHydratedRef.current = true; setRedirectUris(e.target.value); diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 84c4d470..32928281 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -258,7 +258,7 @@ function ClientGeneralPage() { return; } setLogoPreviewStatus("loading"); - }, [hasLogoUrl, hasValidLogoUrl, trimmedLogoUrl]); + }, [hasLogoUrl, hasValidLogoUrl]); const handleSecurityProfileChange = (profile: SecurityProfile) => { setClientType(profile); @@ -814,7 +814,9 @@ function ClientGeneralPage() {