From 4ffe5110dd39892f129d81947edcff517f5f9c41 Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Tue, 24 Feb 2026 15:23:36 +0900 Subject: [PATCH 01/22] =?UTF-8?q?e2e=20=EA=B5=AC=EC=A1=B0=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/code_check.yml | 229 ++++++++++++ Makefile | 23 +- README.md | 13 + adminfront/src/locales/ko.toml | 2 +- backend/internal/handler/api_key_handler.go | 22 +- backend/internal/handler/audit_handler.go | 24 +- backend/internal/handler/auth_handler.go | 342 +++++++++--------- .../handler/auth_handler_link_test.go | 23 ++ .../handler/auth_handler_login_code_test.go | 206 +++++++++++ .../auth_handler_profile_cache_test.go | 109 ++++++ .../internal/handler/auth_handler_qr_test.go | 1 + backend/internal/handler/auth_handler_test.go | 40 ++ backend/internal/handler/common_test.go | 8 + backend/internal/handler/dev_handler.go | 94 +++-- backend/internal/handler/error_helper.go | 17 + .../internal/handler/federation_handler.go | 34 +- .../internal/handler/relying_party_handler.go | 24 +- backend/internal/handler/tenant_handler.go | 86 ++--- .../internal/handler/user_group_handler.go | 28 +- backend/internal/handler/user_handler.go | 58 +-- backend/internal/middleware/error_helper.go | 17 + backend/internal/middleware/rbac.go | 24 +- docs/test-plan.md | 5 +- .../userfront-wasm-e2e-expansion-plan.md | 38 +- .../userfront-wasm-e2e-route-inventory.md | 59 +++ ...e-303-login-link-shortcode-locale-route.md | 47 +++ locales/en.toml | 18 + locales/ko.toml | 18 + locales/template.toml | 18 + userfront-e2e/.gitignore | 3 + userfront-e2e/README.md | 29 ++ userfront-e2e/package-lock.json | 111 ++++++ userfront-e2e/package.json | 18 + userfront-e2e/playwright.config.ts | 39 ++ .../scripts/serve-userfront-build.mjs | 68 ++++ userfront-e2e/tests/auth-routing.spec.ts | 143 ++++++++ .../tests/password-and-reset.spec.ts | 257 +++++++++++++ .../tests/profile-department.spec.ts | 275 ++++++++++++++ userfront-e2e/tests/route-inventory.spec.ts | 320 ++++++++++++++++ userfront-e2e/tsconfig.json | 14 + userfront/assets/translations/ko.toml | 6 +- .../auth/presentation/login_screen.dart | 9 + .../presentation/reset_password_screen.dart | 3 + .../presentation/dashboard_screen.dart | 60 ++- .../presentation/pages/profile_page.dart | 34 +- .../profile_notifier_persistence_test.dart | 112 ++++++ 46 files changed, 2735 insertions(+), 393 deletions(-) create mode 100644 backend/internal/handler/auth_handler_login_code_test.go create mode 100644 backend/internal/handler/auth_handler_profile_cache_test.go create mode 100644 backend/internal/handler/error_helper.go create mode 100644 backend/internal/middleware/error_helper.go create mode 100644 docs/test-plan/userfront-wasm-e2e-route-inventory.md create mode 100644 docs/trouble-shooting/issue-303-login-link-shortcode-locale-route.md create mode 100644 userfront-e2e/.gitignore create mode 100644 userfront-e2e/README.md create mode 100644 userfront-e2e/package-lock.json create mode 100644 userfront-e2e/package.json create mode 100644 userfront-e2e/playwright.config.ts create mode 100644 userfront-e2e/scripts/serve-userfront-build.mjs create mode 100644 userfront-e2e/tests/auth-routing.spec.ts create mode 100644 userfront-e2e/tests/password-and-reset.spec.ts create mode 100644 userfront-e2e/tests/profile-department.spec.ts create mode 100644 userfront-e2e/tests/route-inventory.spec.ts create mode 100644 userfront-e2e/tsconfig.json create mode 100644 userfront/test/profile_notifier_persistence_test.dart diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml index 3e802d76..afcbff43 100644 --- a/.gitea/workflows/code_check.yml +++ b/.gitea/workflows/code_check.yml @@ -24,6 +24,11 @@ on: required: true type: boolean default: true + run_userfront_e2e_tests: + description: "Run userfront WASM Playwright E2E tests" + required: true + type: boolean + default: true run_adminfront_tests: description: "Run adminfront Playwright tests" required: true @@ -96,6 +101,10 @@ jobs: working-directory: backend args: --enable-only=gofmt,gofumpt + - name: Sync userfront locales + run: | + /bin/sh ./scripts/sync_userfront_locales.sh + - name: Format Flutter userfront run: | cd userfront @@ -196,6 +205,10 @@ jobs: channel: "stable" cache: true + - name: Sync userfront locales + run: | + /bin/sh ./scripts/sync_userfront_locales.sh + - name: Run userfront tests run: | cd userfront @@ -271,6 +284,222 @@ jobs: reports/userfront-test.log if-no-files-found: ignore + userfront-e2e-tests: + needs: lint + if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_userfront_e2e_tests == true) }} + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: userfront-e2e/package-lock.json + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + cache: true + + - name: Sync userfront locales + run: | + /bin/sh ./scripts/sync_userfront_locales.sh + + - name: Install userfront-e2e dependencies + run: | + mkdir -p reports + set +e + cd userfront-e2e + npm ci 2>&1 | tee ../reports/userfront-e2e-install.log + install_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$install_exit_code" -ne 0 ]; then + { + echo "# Userfront E2E Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`userfront-e2e-tests\`" + echo "- Reason: \`Dependency install failed\`" + echo "- Exit Code: \`$install_exit_code\`" + echo + echo "## Command" + echo "\`cd userfront-e2e && npm ci\`" + echo + echo "## Install Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/userfront-e2e-install.log + echo '```' + } > reports/userfront-e2e-test-failure-report.md + exit 1 + fi + + - name: Build userfront WASM + run: | + mkdir -p reports + set +e + cd userfront + flutter build web --wasm --release 2>&1 | tee ../reports/userfront-e2e-build.log + build_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$build_exit_code" -ne 0 ]; then + { + echo "# Userfront E2E Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`userfront-e2e-tests\`" + echo "- Reason: \`WASM build failed\`" + echo "- Exit Code: \`$build_exit_code\`" + echo + echo "## Command" + echo "\`cd userfront && flutter build web --wasm --release\`" + echo + echo "## Build Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/userfront-e2e-build.log + echo '```' + } > reports/userfront-e2e-test-failure-report.md + exit 1 + fi + + - name: Provision browsers for userfront-e2e tests + run: | + set +e + cd userfront-e2e + npx playwright install --with-deps chromium 2>&1 | tee ../reports/userfront-e2e-provision.log + provision_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$provision_exit_code" -ne 0 ]; then + { + echo "# Userfront E2E Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`userfront-e2e-tests\`" + echo "- Reason: \`Browser provisioning failed\`" + echo "- Exit Code: \`$provision_exit_code\`" + echo + echo "## Command" + echo "\`cd userfront-e2e && npx playwright install --with-deps chromium\`" + echo + echo "## Provision Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/userfront-e2e-provision.log + echo '```' + } > reports/userfront-e2e-test-failure-report.md + exit 1 + fi + + - name: Run userfront-e2e tests + run: | + mkdir -p reports + set +e + cd userfront-e2e + npm test 2>&1 | tee ../reports/userfront-e2e-test.log + test_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$test_exit_code" -ne 0 ]; then + { + echo "# Userfront E2E Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`userfront-e2e-tests\`" + echo "- Exit Code: \`$test_exit_code\`" + echo + echo "## Commands" + echo "1. \`cd userfront-e2e\`" + echo "2. \`npm ci\`" + echo "3. \`cd ../userfront && flutter build web --wasm --release\`" + echo "4. \`cd ../userfront-e2e && npx playwright install --with-deps chromium\`" + echo "5. \`npm test\`" + echo + echo "## Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/userfront-e2e-test.log + echo '```' + } > reports/userfront-e2e-test-failure-report.md + fi + + exit "$test_exit_code" + + - name: Ensure userfront-e2e failure report exists + if: ${{ failure() }} + run: | + mkdir -p reports + if [ -f reports/userfront-e2e-test-failure-report.md ]; then + exit 0 + fi + + { + echo "# Userfront E2E Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`userfront-e2e-tests\`" + echo "- Reason: \`Job failed before detailed report generation\`" + echo + if [ -f reports/userfront-e2e-install.log ]; then + echo "## Install Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/userfront-e2e-install.log + echo '```' + echo + fi + if [ -f reports/userfront-e2e-build.log ]; then + echo "## Build Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/userfront-e2e-build.log + echo '```' + echo + fi + if [ -f reports/userfront-e2e-provision.log ]; then + echo "## Provision Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/userfront-e2e-provision.log + echo '```' + echo + fi + if [ -f reports/userfront-e2e-test.log ]; then + echo "## Test Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/userfront-e2e-test.log + echo '```' + fi + } > reports/userfront-e2e-test-failure-report.md + + - name: Publish userfront-e2e failure summary + if: ${{ failure() }} + run: | + if [ -f reports/userfront-e2e-test-failure-report.md ]; then + cat reports/userfront-e2e-test-failure-report.md >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload userfront-e2e failure report artifact + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + continue-on-error: true + with: + name: userfront-e2e-test-failure-report + path: | + reports/userfront-e2e-test-failure-report.md + reports/userfront-e2e-install.log + reports/userfront-e2e-build.log + reports/userfront-e2e-provision.log + reports/userfront-e2e-test.log + userfront-e2e/playwright-report + userfront-e2e/test-results + if-no-files-found: ignore + adminfront-tests: needs: lint if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_adminfront_tests == true) }} diff --git a/Makefile b/Makefile index cccb840b..bd75229a 100644 --- a/Makefile +++ b/Makefile @@ -107,9 +107,9 @@ logs-app: docker compose -f $(COMPOSE_APP) logs -f # --- 로컬 통합 코드 체크 --- -.PHONY: code-check code-check-i18n code-check-go-lint code-check-userfront-lint code-check-front-lint code-check-backend-tests code-check-userfront-tests code-check-adminfront-tests code-check-devfront-tests +.PHONY: code-check code-check-i18n code-check-go-lint 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 -code-check: code-check-i18n code-check-go-lint 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: code-check-i18n code-check-go-lint 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 @echo "code-check complete." code-check-i18n: @@ -182,3 +182,22 @@ code-check-devfront-tests: [ -d devfront/playwright-report ] && cp -R devfront/playwright-report reports/devfront/ || true; \ [ -d devfront/test-results ] && cp -R devfront/test-results reports/devfront/ || true; \ exit $$status + +code-check-userfront-e2e-tests: + @echo "==> userfront wasm playwright e2e tests" + @mkdir -p reports/userfront-e2e + @rm -rf reports/userfront-e2e/playwright-report reports/userfront-e2e/test-results + @status=0; \ + (cd userfront && flutter build web --wasm --release) || status=$$?; \ + if [ $$status -eq 0 ]; then \ + (cd userfront-e2e && npm ci) || status=$$?; \ + fi; \ + if [ $$status -eq 0 ]; then \ + (cd userfront-e2e && npx playwright install --with-deps chromium) || status=$$?; \ + fi; \ + if [ $$status -eq 0 ]; then \ + (cd userfront-e2e && npm test) || status=$$?; \ + fi; \ + [ -d userfront-e2e/playwright-report ] && cp -R userfront-e2e/playwright-report reports/userfront-e2e/ || true; \ + [ -d userfront-e2e/test-results ] && cp -R userfront-e2e/test-results reports/userfront-e2e/ || true; \ + exit $$status diff --git a/README.md b/README.md index 7bac9d2c..cbf08d14 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,7 @@ KETO_WRITE_URL = "http://keto:4467" - `run_lint`: Go/Flutter lint 실행 여부 - `run_backend_tests`: backend 테스트 실행 여부 - `run_userfront_tests`: userfront 테스트 실행 여부 +- `run_userfront_e2e_tests`: userfront WASM Playwright E2E 실행 여부 - `run_adminfront_tests`: adminfront 테스트 실행 여부 - `run_devfront_tests`: devfront 테스트 실행 여부 @@ -327,6 +328,7 @@ KETO_WRITE_URL = "http://keto:4467" - `lint` - `backend-tests` - `userfront-tests` +- `userfront-e2e-tests` - `adminfront-tests` - `devfront-tests` @@ -353,6 +355,17 @@ KETO_WRITE_URL = "http://keto:4467" - 단일 파일만 확인하려면 다음 명령을 사용합니다. - `flutter test test/locale_storage_platform_test.dart` +### userfront WASM Playwright E2E +- 워크스페이스: `userfront-e2e/` +- 빌드+실행: + - `cd userfront-e2e && npm run test:wasm` +- 빌드 결과가 이미 있을 때: + - `cd userfront-e2e && npm test` +- Makefile 타깃: + - `make code-check-userfront-e2e-tests` +- 전수 인벤토리: + - `docs/test-plan/userfront-wasm-e2e-route-inventory.md` + ### 로컬 개발 (Manual) Docker 없이는 개발할 수 없지만 Backend 및 [user/admin/dev]Front 코드는 개발모드로 수정하며 개발가능. 백그라운드로 infra 및 ory stack이 구동중이라는 가정 diff --git a/adminfront/src/locales/ko.toml b/adminfront/src/locales/ko.toml index 73958bf0..517a6120 100644 --- a/adminfront/src/locales/ko.toml +++ b/adminfront/src/locales/ko.toml @@ -329,7 +329,7 @@ empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." error = "연동 정보를 불러오지 못했습니다." [msg.userfront.dashboard.approved_session] -copy_click = "{{label}}: {{id}}\\\\\\\\n클릭하면 복사됩니다." +copy_click = "{{label}}: {{id}} \\n클릭하면 복사됩니다." copy_tap = "{{label}}: {{id}}\\\\\\\\n탭하면 복사됩니다." none = "{{label}} 없음" diff --git a/backend/internal/handler/api_key_handler.go b/backend/internal/handler/api_key_handler.go index bb1d8afb..a8934159 100644 --- a/backend/internal/handler/api_key_handler.go +++ b/backend/internal/handler/api_key_handler.go @@ -35,7 +35,7 @@ type apiKeyListResponse struct { func (h *ApiKeyHandler) ListApiKeys(c *fiber.Ctx) error { if h.DB == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } limit := c.QueryInt("limit", 50) @@ -43,12 +43,12 @@ func (h *ApiKeyHandler) ListApiKeys(c *fiber.Ctx) error { var total int64 if err := h.DB.Model(&domain.ApiKey{}).Count(&total).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } var keys []domain.ApiKey if err := h.DB.Order("created_at desc").Limit(limit).Offset(offset).Find(&keys).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } items := make([]apiKeySummary, 0, len(keys)) @@ -73,7 +73,7 @@ func (h *ApiKeyHandler) ListApiKeys(c *fiber.Ctx) error { func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error { if h.DB == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } var req struct { @@ -81,11 +81,11 @@ func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error { Scopes []string `json:"scopes"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } if strings.TrimSpace(req.Name) == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"}) + return errorJSON(c, fiber.StatusBadRequest, "name is required") } // Generate Client ID (16 chars hex) @@ -96,7 +96,7 @@ func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error { hashedSecret, err := bcrypt.GenerateFromPassword([]byte(plainSecret), bcrypt.DefaultCost) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to hash secret"}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to hash secret") } apiKey := domain.ApiKey{ @@ -108,7 +108,7 @@ func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error { } if err := h.DB.Create(&apiKey).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // Return summary + PLAIN SECRET (only this time) @@ -129,16 +129,16 @@ func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error { func (h *ApiKeyHandler) DeleteApiKey(c *fiber.Ctx) error { if h.DB == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } id := c.Params("id") if id == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "id is required") } if err := h.DB.Delete(&domain.ApiKey{}, "id = ?", id).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) diff --git a/backend/internal/handler/audit_handler.go b/backend/internal/handler/audit_handler.go index d246b352..be968edd 100644 --- a/backend/internal/handler/audit_handler.go +++ b/backend/internal/handler/audit_handler.go @@ -23,9 +23,7 @@ func NewAuditHandler(repo domain.AuditRepository) *AuditHandler { func (h *AuditHandler) CreateLog(c *fiber.Ctx) error { var req domain.AuditLog if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Cannot parse JSON", - }) + return errorJSON(c, fiber.StatusBadRequest, "Cannot parse JSON") } // Auto-fill metadata if missing @@ -43,16 +41,12 @@ func (h *AuditHandler) CreateLog(c *fiber.Ctx) error { } if h.repo == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ - "error": "Audit service unavailable", - }) + return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable") } if err := h.repo.Create(&req); err != nil { // Log internal error but don't expose details - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to save audit log", - }) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to save audit log") } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ @@ -66,22 +60,16 @@ func (h *AuditHandler) ListLogs(c *fiber.Ctx) error { cursorRaw := c.Query("cursor") cursor, err := parseAuditCursor(cursorRaw) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid cursor", - }) + return errorJSON(c, fiber.StatusBadRequest, "Invalid cursor") } if h.repo == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ - "error": "Audit service unavailable", - }) + return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable") } logs, err := h.repo.FindPage(c.Context(), limit+1, cursor) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Failed to retrieve audit logs", - }) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to retrieve audit logs") } nextCursor := "" diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 11b5ee11..3d40d242 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -4,7 +4,6 @@ import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/logger" "baron-sso-backend/internal/repository" - "baron-sso-backend/internal/response" "baron-sso-backend/internal/service" "baron-sso-backend/internal/utils" "bytes" @@ -171,21 +170,21 @@ func NewAuthHandler(redisService domain.RedisRepository, idpProvider domain.Iden func (h *AuthHandler) CheckEmail(c *fiber.Ctx) error { var req domain.CheckEmailRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request") } // Email Format Validation if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid email format"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid email format") } if h.IdpProvider == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable") } exists, err := h.IdpProvider.UserExists(req.Email) if err != nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable") } if exists { return c.JSON(fiber.Map{"available": false, "message": "Email already registered"}) @@ -197,7 +196,7 @@ func (h *AuthHandler) CheckEmail(c *fiber.Ctx) error { func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error { var req domain.SendSignupCodeRequest if err := c.BodyParser(&req); err != nil || req.Target == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid email"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid email") } req.Type = "email" // Enforce type @@ -209,7 +208,7 @@ func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error { // Check if block expired // Simple block implementation: if FailCount > 5, user is blocked until TTL expires // Since we refresh TTL on each update, we rely on Redis TTL. - return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "Too many failed attempts. Try again later."}) + return errorJSON(c, fiber.StatusTooManyRequests, "Too many failed attempts. Try again later.") } // 2. Generate Code @@ -237,7 +236,7 @@ func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error { // 4. Send Email if h.EmailService == nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) + return errorJSON(c, fiber.StatusInternalServerError, "Email service not configured") } subject := "[Baron 로그인] 회원가입 인증코드" @@ -259,7 +258,7 @@ func (h *AuthHandler) SendSignupEmailCode(c *fiber.Ctx) error { func (h *AuthHandler) SendSignupSmsCode(c *fiber.Ctx) error { var req domain.SendSignupCodeRequest if err := c.BodyParser(&req); err != nil || req.Target == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid phone number"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid phone number") } req.Type = "phone" @@ -270,7 +269,7 @@ func (h *AuthHandler) SendSignupSmsCode(c *fiber.Ctx) error { // 1. Check existing state state, _ := h.getSignupState(key) if state != nil && state.FailCount > maxSignupFailures { - return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "Too many failed attempts. Try again later."}) + return errorJSON(c, fiber.StatusTooManyRequests, "Too many failed attempts. Try again later.") } // 2. Generate Code @@ -300,7 +299,7 @@ func (h *AuthHandler) SendSignupSmsCode(c *fiber.Ctx) error { func (h *AuthHandler) VerifySignupCode(c *fiber.Ctx) error { var req domain.VerifySignupCodeRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request") } var key string @@ -310,12 +309,12 @@ func (h *AuthHandler) VerifySignupCode(c *fiber.Ctx) error { phone := strings.ReplaceAll(req.Target, "-", "") key = prefixSignupPhone + phone } else { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid type"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid type") } state, err := h.getSignupState(key) if err != nil || state == nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Verification session expired or not found"}) + return errorJSON(c, fiber.StatusBadRequest, "Verification session expired or not found") } // Check Verified @@ -325,19 +324,23 @@ func (h *AuthHandler) VerifySignupCode(c *fiber.Ctx) error { // Check Attempts if state.FailCount > maxSignupFailures { - return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "Too many failed attempts"}) + return errorJSON(c, fiber.StatusTooManyRequests, "Too many failed attempts") } // Check Code match if state.Code != req.Code { state.FailCount++ h.saveSignupState(key, state, signupStateExpiration) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid code", "failCount": state.FailCount}) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Invalid code", + "code": "invalid_code", + "failCount": state.FailCount, + }) } // Check Expiry (Logic time vs stored time) if time.Now().Unix() > state.ExpiresAt { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Code expired"}) + return errorJSON(c, fiber.StatusBadRequest, "Code expired") } // Success @@ -351,24 +354,24 @@ func (h *AuthHandler) VerifySignupCode(c *fiber.Ctx) error { func (h *AuthHandler) Signup(c *fiber.Ctx) error { var req domain.SignupRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } // 1. Validate Fields (Simple validation) if req.Email == "" || req.Password == "" || req.Name == "" || req.Phone == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing required fields"}) + return errorJSON(c, fiber.StatusBadRequest, "Missing required fields") } if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid email format"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid email format") } if !req.TermsAccepted { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Terms must be accepted"}) + return errorJSON(c, fiber.StatusBadRequest, "Terms must be accepted") } // 비밀번호 정책 검증 policy := h.resolvePasswordPolicy() if err := validatePasswordWithPolicy(policy, req.Password); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusBadRequest, err.Error()) } // 2. Verify Auth Status (Redis) @@ -379,14 +382,14 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error { phoneState, _ := h.getSignupState(phoneKey) if emailState == nil || !emailState.Verified { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Email not verified"}) + return errorJSON(c, fiber.StatusUnauthorized, "Email not verified") } if phoneState == nil || !phoneState.Verified { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Phone not verified"}) + return errorJSON(c, fiber.StatusUnauthorized, "Phone not verified") } if h.IdpProvider == nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSON(c, fiber.StatusInternalServerError, "Identity provider unavailable") } // [Strict] Enforce Tenant Auto-Assignment @@ -404,7 +407,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error { tenantID = &tenant.ID } else { slog.Warn("[Signup] Attempted to join non-active tenant by domain", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status) - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Your organization's tenant is currently not active."}) + return errorJSON(c, fiber.StatusForbidden, "Your organization's tenant is currently not active.") } } } @@ -420,12 +423,12 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error { companyCode = tenant.Slug tenantID = &tenant.ID } else { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "The specified organization is not active."}) + return errorJSON(c, fiber.StatusForbidden, "The specified organization is not active.") } } else { // If companyCode provided but not found, we should probably reject if we want strictness, // or just treat as GENERAL user. Given the risk "존재하지 않는 테넌트도 저장됨", we should reject. - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid company code."}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid company code.") } } @@ -456,13 +459,13 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error { providerID, err := h.IdpProvider.CreateUser(brokerUser, req.Password) if err != nil { if errors.Is(err, domain.ErrNotSupported) { - return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Signup method not supported"}) + return errorJSON(c, fiber.StatusNotImplemented, "Signup method not supported") } slog.Error("[Signup] Failed to create user via IDP", "provider", h.IdpProvider.Name(), "error", err) if strings.Contains(err.Error(), "already exists") { - return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "User already exists"}) + return errorJSON(c, fiber.StatusConflict, "User already exists") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create user"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to create user") } // 4. Cleanup Redis @@ -829,7 +832,7 @@ func (h *AuthHandler) GetPasswordPolicy(c *fiber.Ctx) error { func (h *AuthHandler) SendSms(c *fiber.Ctx) error { var req domain.SmsRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } slog.Info("[SMS] Sending code", "phoneNumber", req.PhoneNumber) @@ -840,7 +843,7 @@ func (h *AuthHandler) SendSms(c *fiber.Ctx) error { h.RedisService.StoreVerificationCode(sanitizedPhone, code) if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to send SMS") } return c.JSON(fiber.Map{"message": "SMS sent successfully"}) @@ -850,32 +853,32 @@ func (h *AuthHandler) SendSms(c *fiber.Ctx) error { func (h *AuthHandler) VerifySms(c *fiber.Ctx) error { var req domain.SmsVerifyRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "") storedCode, _ := h.RedisService.GetVerificationCode(sanitizedPhone) if storedCode == "" || storedCode != req.Code { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid or expired code") } h.RedisService.DeleteVerificationCode(sanitizedPhone) if h.IdpProvider == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Authentication service not configured"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Authentication service not configured") } loginID := normalizePhoneForLoginID(req.PhoneNumber) authInfo, err := h.IdpProvider.IssueSession(loginID) if err != nil { if errors.Is(err, domain.ErrNotSupported) { - return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"}) + return errorJSON(c, fiber.StatusNotImplemented, "Login method not supported") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue session") } if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue session") } c.Locals("login_id", loginID) setSessionIDLocal(c, authInfo.SessionToken) @@ -891,7 +894,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { var req domain.EnchantedLinkInitRequest if err := c.BodyParser(&req); err != nil { slog.Error("[Enchanted] Body parse error", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } loginID := strings.ReplaceAll(req.LoginID, "-", "") @@ -903,16 +906,16 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { // 사용자 존재 여부 확인 if h.IdpProvider == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable") } exists, err := h.IdpProvider.UserExists(lookupLoginID) if err != nil { slog.Warn("[Enchanted] IDP user lookup failed", "loginID", loginID, "error", err) - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable") } if !exists { slog.Warn("[Enchanted] User not found", "loginID", loginID) - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not registered"}) + return errorJSON(c, fiber.StatusNotFound, "User not registered") } userfrontURL := os.Getenv("USERFRONT_URL") @@ -981,7 +984,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { }) } else if err != nil && !errors.Is(err, domain.ErrNotSupported) { slog.Error("[Enchanted] Link login init failed", "provider", h.IdpProvider.Name(), "error", err) - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable") } // [Changed] 토큰 길이를 사용자의 요청에 맞춰 6글자(3바이트)로, pendingRef를 8글자(4바이트)로 조정 @@ -1016,7 +1019,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { // Send Email if !drySend && h.EmailService == nil { slog.Error("[Enchanted] Email Service not configured") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) + return errorJSON(c, fiber.StatusInternalServerError, "Email service not configured") } subject := "[Baron 로그인] 링크" @@ -1039,7 +1042,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { slog.Info("[Enchanted] Sending Email via AWS SES", "loginID", loginID) if err := h.EmailService.SendEmail(loginID, subject, body); err != nil { slog.Error("[Enchanted] Email Failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send Email"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to send Email") } } } else { @@ -1051,7 +1054,7 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { slog.Info("[Enchanted] Sending SMS via Naver Cloud", "loginID", loginID) if err := h.SmsService.SendSms(loginID, content); err != nil { slog.Error("[Enchanted] SMS Failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to send SMS") } } } @@ -1071,20 +1074,24 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error { var req domain.EnchantedLinkPollRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } pollKey := prefixPollMeta + "enchanted:" + req.PendingRef if slowDown, interval := checkPollInterval(h.RedisService, pollKey, minPollInterval); slowDown { return c.JSON(fiber.Map{ "error": "slow_down", + "code": "slow_down", "interval": interval, }) } val, err := h.RedisService.Get(prefixSession + req.PendingRef) if err != nil || val == "" { - return c.JSON(fiber.Map{"error": "expired_token"}) + return c.JSON(fiber.Map{ + "error": "expired_token", + "code": "expired_token", + }) } var data map[string]string @@ -1105,10 +1112,10 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error { } if loginID == "" { slog.Warn("[Poll] Approved but missing loginId", "pendingRef", req.PendingRef) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid session reference") } if h.IdpProvider == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Identity provider unavailable") } loginStrategy := h.loadLoginStrategy(req.PendingRef) @@ -1123,32 +1130,32 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error { code = normalizeLoginCode(code) if code == "" { slog.Warn("[Poll] Missing login code for approved flow", "pendingRef", req.PendingRef) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login code expired"}) + return errorJSON(c, fiber.StatusBadRequest, "Login code expired") } flowID, _ := h.RedisService.Get(prefixLoginCode + loginID) if flowID == "" { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Login flow expired"}) + return errorJSON(c, fiber.StatusNotFound, "Login flow expired") } authInfo, err = h.IdpProvider.VerifyLoginCode(loginID, flowID, code) if err != nil { if errors.Is(err, domain.ErrNotSupported) { - return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"}) + return errorJSON(c, fiber.StatusNotImplemented, "Login method not supported") } slog.Error("[Poll] IDP code verify failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to verify login code"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to verify login code") } } else { authInfo, err = h.IdpProvider.IssueSession(loginID) if err != nil { if errors.Is(err, domain.ErrNotSupported) { - return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"}) + return errorJSON(c, fiber.StatusNotImplemented, "Login method not supported") } slog.Error("[Poll] IDP session issue failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue session") } } if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue session") } c.Locals("login_id", loginID) @@ -1190,6 +1197,7 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "error": "authorization_pending", + "code": "authorization_pending", "interval": int(minPollInterval.Seconds()), }) } @@ -1199,7 +1207,7 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error { var req domain.MagicLinkVerifyRequest if err := c.BodyParser(&req); err != nil { slog.Error("[Verify] Body parse error", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } slog.Info("[Verify] Attempting to verify token", "token", req.Token) @@ -1208,7 +1216,7 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error { val, err := h.RedisService.Get(tokenKey) if err != nil || val == "" { slog.Warn("[Verify] Token not found or expired in Redis", "token", req.Token) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired token"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid or expired token") } var tokenData map[string]string @@ -1222,7 +1230,7 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error { c.Locals("auth_timeline_skip", true) if pendingRef == "" || loginID == "" { slog.Warn("[Verify] Missing pendingRef/loginID for verify-only", "token", req.Token) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid session reference") } h.storeLoginApproverMeta(pendingRef, c, defaultExpiration) @@ -1243,21 +1251,21 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error { if h.IdpProvider == nil { slog.Error("[Verify] IDP Provider is nil") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"}) + return errorJSON(c, fiber.StatusInternalServerError, "Authentication service not configured") } authInfo, err := h.IdpProvider.IssueSession(loginID) if err != nil { if errors.Is(err, domain.ErrNotSupported) { slog.Warn("[Verify] IDP session issue not supported", "provider", h.IdpProvider.Name()) - return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"}) + return errorJSON(c, fiber.StatusNotImplemented, "Login method not supported") } slog.Error("[Verify] IDP session issue failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue session") } if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" { slog.Error("[Verify] IDP returned empty session") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue session") } sessionToken := authInfo.SessionToken.JWT c.Locals("login_id", loginID) @@ -1293,13 +1301,13 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error { } if err := c.BodyParser(&req); err != nil { slog.Error("[LoginCode] Body parse error", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSONCode(c, fiber.StatusBadRequest, "bad_request", "Invalid request body") } loginID := strings.TrimSpace(req.LoginID) loginID = strings.ReplaceAll(loginID, " ", "+") if loginID == "" || req.Code == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "loginId and code are required"}) + return errorJSONCode(c, fiber.StatusBadRequest, "bad_request", "loginId and code are required") } lookupLoginID := loginID @@ -1308,12 +1316,12 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error { } if h.IdpProvider == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSONCode(c, fiber.StatusServiceUnavailable, "service_unavailable", "Identity provider unavailable") } flowID, err := h.RedisService.Get(prefixLoginCode + lookupLoginID) if err != nil || flowID == "" { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Login flow expired"}) + return errorJSONCode(c, fiber.StatusNotFound, "not_found", "Login flow expired") } if req.VerifyOnly { @@ -1329,16 +1337,16 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error { if pendingRef == "" { pendingRef = storedRef } else if storedRef != "" && pendingRef != storedRef { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"}) + return errorJSONCode(c, fiber.StatusBadRequest, "invalid_session_reference", "Invalid session reference") } if pendingRef == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"}) + return errorJSONCode(c, fiber.StatusBadRequest, "invalid_session_reference", "Invalid session reference") } expectedCode, _ := h.RedisService.Get(prefixLoginCodeValue + pendingRef) expectedCode = normalizeLoginCode(expectedCode) inputCode := normalizeLoginCode(req.Code) if expectedCode == "" || inputCode == "" || inputCode != expectedCode { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid code"}) + return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_code", "Invalid code") } h.storeLoginApproverMeta(pendingRef, c, loginCodeExpiration) sessionData, _ := json.Marshal(map[string]string{ @@ -1356,18 +1364,18 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error { authInfo, err := h.IdpProvider.VerifyLoginCode(lookupLoginID, flowID, req.Code) if err != nil { if errors.Is(err, domain.ErrNotSupported) { - return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"}) + return errorJSONCode(c, fiber.StatusNotImplemented, "not_supported", "Login method not supported") } slog.Error("[LoginCode] Verify failed", "loginID", loginID, "error", err) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid code"}) + return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_code", "Invalid code") } if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"}) + return errorJSONCode(c, fiber.StatusInternalServerError, "internal_error", "Failed to issue session") } subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), lookupLoginID) if resolveErr != nil || subject == "" { slog.Error("[LoginCode] Failed to resolve kratos identity", "loginID", lookupLoginID, "error", resolveErr) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"}) + return errorJSONCode(c, fiber.StatusInternalServerError, "internal_error", "Failed to resolve user identity") } authInfo.Subject = subject c.Locals("login_id", lookupLoginID) @@ -1415,31 +1423,31 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error { } if err := c.BodyParser(&req); err != nil { slog.Error("[LoginShortCode] Body parse error", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSONCode(c, fiber.StatusBadRequest, "bad_request", "Invalid request body") } shortCode := strings.ToUpper(strings.TrimSpace(req.ShortCode)) if shortCode == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "shortCode is required"}) + return errorJSONCode(c, fiber.StatusBadRequest, "bad_request", "shortCode is required") } val, _ := h.RedisService.Get(prefixLoginCodeShort + shortCode) if val == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"}) + return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_or_expired_code", "Invalid or expired code") } var payload shortLoginCodePayload if err := json.Unmarshal([]byte(val), &payload); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Invalid code payload"}) + return errorJSONCode(c, fiber.StatusInternalServerError, "internal_error", "Invalid code payload") } if payload.LoginID == "" || payload.Code == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"}) + return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_or_expired_code", "Invalid or expired code") } if req.VerifyOnly { c.Locals("auth_timeline_skip", true) if payload.PendingRef == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"}) + return errorJSONCode(c, fiber.StatusBadRequest, "invalid_session_reference", "Invalid session reference") } normalizedCode := normalizeLoginCode(payload.Code) if normalizedCode != "" { @@ -1460,29 +1468,29 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error { } if h.IdpProvider == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"}) + return errorJSONCode(c, fiber.StatusServiceUnavailable, "service_unavailable", "Identity provider unavailable") } flowID, err := h.RedisService.Get(prefixLoginCode + payload.LoginID) if err != nil || flowID == "" { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Login flow expired"}) + return errorJSONCode(c, fiber.StatusNotFound, "not_found", "Login flow expired") } authInfo, err := h.IdpProvider.VerifyLoginCode(payload.LoginID, flowID, payload.Code) if err != nil { if errors.Is(err, domain.ErrNotSupported) { - return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"}) + return errorJSONCode(c, fiber.StatusNotImplemented, "not_supported", "Login method not supported") } slog.Error("[LoginShortCode] Verify failed", "loginID", payload.LoginID, "error", err) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid code"}) + return errorJSONCode(c, fiber.StatusUnauthorized, "invalid_code", "Invalid code") } if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"}) + return errorJSONCode(c, fiber.StatusInternalServerError, "internal_error", "Failed to issue session") } subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), payload.LoginID) if resolveErr != nil || subject == "" { slog.Error("[LoginShortCode] Failed to resolve kratos identity", "loginID", payload.LoginID, "error", resolveErr) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"}) + return errorJSONCode(c, fiber.StatusInternalServerError, "internal_error", "Failed to resolve user identity") } authInfo.Subject = subject c.Locals("login_id", payload.LoginID) @@ -1561,7 +1569,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Body parse error") - return response.Error(c, fiber.StatusBadRequest, "bad_request", "Invalid request body") + return errorJSONCode(c, fiber.StatusBadRequest, "bad_request", "Invalid request body") } loginID := strings.TrimSpace(req.LoginID) @@ -1575,22 +1583,22 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = "IDP Provider is nil" ale.Log(slog.LevelError, "IDP Provider is nil") - return response.Error(c, fiber.StatusInternalServerError, "service_unavailable", "Authentication service not configured") + return errorJSONCode(c, fiber.StatusInternalServerError, "service_unavailable", "Authentication service not configured") } authInfo, err := h.IdpProvider.SignIn(loginID, req.Password) if err != nil { if errors.Is(err, domain.ErrNotSupported) { - return response.Error(c, fiber.StatusNotImplemented, "not_supported", "Login method not supported") + return errorJSONCode(c, fiber.StatusNotImplemented, "not_supported", "Login method not supported") } ale.Status = fiber.StatusUnauthorized ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelWarn, "IDP sign-in failed", slog.String("provider", h.IdpProvider.Name())) if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "identity") { - return response.Error(c, fiber.StatusNotFound, "not_found", "User not registered") + return errorJSONCode(c, fiber.StatusNotFound, "not_found", "User not registered") } - return response.Error(c, fiber.StatusUnauthorized, "password_or_email_mismatch", "Invalid credentials") + return errorJSONCode(c, fiber.StatusUnauthorized, "password_or_email_mismatch", "Invalid credentials") } subject, resolveErr := h.resolveKratosIdentityIDFromLoginID(c.Context(), loginID) @@ -1659,7 +1667,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Body parse error") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } loginID := strings.TrimSpace(req.LoginID) @@ -1671,7 +1679,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = "Login ID is required" ale.Log(slog.LevelWarn, "Login ID missing") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID is required"}) + return errorJSON(c, fiber.StatusBadRequest, "Login ID is required") } if h.IdpProvider == nil { @@ -1679,7 +1687,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = "IDP Provider is not initialized" ale.Log(slog.LevelError, "IDP Provider is not initialized") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"}) + return errorJSON(c, fiber.StatusInternalServerError, "Authentication service not configured") } userfrontURL := os.Getenv("USERFRONT_URL") @@ -1688,7 +1696,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = "USERFRONT_URL is not set" ale.Log(slog.LevelError, "USERFRONT_URL is not set") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "USERFRONT_URL environment variable is not set"}) + return errorJSON(c, fiber.StatusInternalServerError, "USERFRONT_URL environment variable is not set") } // [Changed] Point to Backend API for verification (which then redirects to Frontend) redirectURL := fmt.Sprintf("%s/api/v1/auth/password/reset/verify", userfrontURL) @@ -1701,7 +1709,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = "Failed to generate reset token" ale.Log(slog.LevelError, "Failed to generate reset token") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate reset token"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to generate reset token") } if err := h.RedisService.Set(prefixPwdResetToken+resetToken, loginID, pwdResetExpiration); err != nil { @@ -1709,7 +1717,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Failed to store reset token in Redis") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to store reset token"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to store reset token") } resetLink := fmt.Sprintf("%s/reset-password?token=%s", userfrontURL, resetToken) @@ -1727,7 +1735,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = "Email service not configured" ale.Log(slog.LevelError, "Email service not configured") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) + return errorJSON(c, fiber.StatusInternalServerError, "Email service not configured") } subject := "[Baron 로그인] 비밀번호 재설정" body := fmt.Sprintf(` @@ -1748,7 +1756,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Failed to send reset email", slog.String("loginId", loginID)) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset email"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to send reset email") } } } else { @@ -1761,7 +1769,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Failed to send reset SMS", slog.String("loginId", loginID)) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset SMS"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to send reset SMS") } } } @@ -1899,7 +1907,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Body parse error") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } // loginID는 URL 쿼리 파라미터 또는 토큰 조회로 받습니다. @@ -1913,7 +1921,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { ale.ProviderError = "Invalid or expired reset token" ale.Token = resetToken ale.Log(slog.LevelWarn, "Reset token invalid or expired") - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired reset token"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid or expired reset token") } loginID = strings.TrimSpace(val) ale.Token = resetToken @@ -1939,7 +1947,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = "Login ID and new password are required" ale.Log(slog.LevelWarn, "Login ID or new password missing") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID and new password are required"}) + return errorJSON(c, fiber.StatusBadRequest, "Login ID and new password are required") } // 새 비밀번호 값은 기록하지 않고, 요청 수신 이벤트만 남깁니다. @@ -1951,7 +1959,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelWarn, "Validation failed: "+err.Error()) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusBadRequest, err.Error()) } ale.Log(slog.LevelInfo, "Attempting to update password via IDP", slog.String("idp", providerName)) @@ -1961,7 +1969,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = "IDP Provider is nil" ale.Log(slog.LevelError, "IDP Provider is nil") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"}) + return errorJSON(c, fiber.StatusInternalServerError, "Authentication service not configured") } if err := h.IdpProvider.UpdateUserPassword(loginID, req.NewPassword, nil); err != nil { @@ -1969,7 +1977,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error { ale.LatencyMs = time.Since(startTime) ale.ProviderError = err.Error() ale.Log(slog.LevelError, "Failed to update password via IDP") - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to update password") } ale.Status = fiber.StatusOK @@ -2017,20 +2025,21 @@ func (h *AuthHandler) PollQRLogin(c *fiber.Ctx) error { PendingRef string `json:"pendingRef"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid body") } pollKey := prefixPollMeta + "qr:" + req.PendingRef if slowDown, interval := checkPollInterval(h.RedisService, pollKey, minPollInterval); slowDown { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "slow_down", + "code": "slow_down", "interval": interval, }) } val, err := h.RedisService.Get(prefixSession + req.PendingRef) if err != nil || val == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "expired_token"}) + return errorJSON(c, fiber.StatusBadRequest, "expired_token") } var data map[string]string @@ -2046,6 +2055,7 @@ func (h *AuthHandler) PollQRLogin(c *fiber.Ctx) error { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "authorization_pending", + "code": "authorization_pending", "interval": int(minPollInterval.Seconds()), }) } @@ -2059,13 +2069,13 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error { } if err := c.BodyParser(&req); err != nil { slog.Error("[QR] Scan body parse error", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid body") } rawRef := strings.TrimSpace(req.PendingRef) pendingRef, err := h.resolveQrPendingRef(rawRef) if err != nil || pendingRef == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid pendingRef"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid pendingRef") } slog.Info("[QR] Scan & Approve", "pendingRef", pendingRef) @@ -2073,32 +2083,32 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error { // 1. Redis에서 세션 확인 val, err := h.RedisService.Get(prefixSession + pendingRef) if err != nil || val == "" { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Session expired or not found"}) + return errorJSON(c, fiber.StatusNotFound, "Session expired or not found") } if req.Token == "" { cookie := c.Get(fiber.HeaderCookie) if cookie == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing session token"}) + return errorJSON(c, fiber.StatusUnauthorized, "Missing session token") } _, traits, err := h.getKratosIdentityWithCookie(cookie) if err != nil { slog.Warn("[QR] Cookie session invalid", "error", err) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } if sessionID, err := h.getKratosSessionIDWithCookie(cookie); err == nil && sessionID != "" { h.storeQrApproverSessionID(pendingRef, sessionID) } loginID := pickLoginIDFromTraits(traits) if loginID == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } if !strings.Contains(loginID, "@") { loginID = normalizePhoneForLoginID(loginID) } if err := h.startQrCodeLoginForQr(loginID, pendingRef, rawRef); err != nil { slog.Error("[QR] Start code login failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue web session"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue web session") } return c.JSON(fiber.Map{"message": "QR Login Approved"}) } @@ -2117,11 +2127,11 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error { loginID, err := h.resolveKratosLoginID(req.Token) if err != nil { slog.Warn("[QR] Invalid token", "error", err) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } if err := h.startQrCodeLoginForQr(loginID, pendingRef, rawRef); err != nil { slog.Error("[QR] Start code login failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue web session"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to issue web session") } return c.JSON(fiber.Map{"message": "QR Login Approved"}) @@ -2140,12 +2150,12 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error { var req kratosCourierRequest if err := c.BodyParser(&req); err != nil { slog.Error("[Kratos Courier] Body parsing failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } if req.Recipient == "" { slog.Warn("[Kratos Courier] Missing recipient") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing recipient"}) + return errorJSON(c, fiber.StatusBadRequest, "Missing recipient") } loginID := req.Recipient @@ -2164,17 +2174,17 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error { code := normalizeLoginCode(extractFirstString(req.TemplateData, "login_code")) if code == "" { slog.Error("[QR] Missing login code in courier", "loginID", loginID) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Missing login code"}) + return errorJSON(c, fiber.StatusInternalServerError, "Missing login code") } flowID, _ := h.RedisService.Get(prefixLoginCode + loginID) if flowID == "" { slog.Error("[QR] Missing login flow for code verify", "loginID", loginID) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Login flow expired"}) + return errorJSON(c, fiber.StatusInternalServerError, "Login flow expired") } authInfo, err := h.IdpProvider.VerifyLoginCode(loginID, flowID, code) if err != nil || authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" { slog.Error("[QR] Code verify failed", "loginID", loginID, "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to verify login code"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to verify login code") } sessionData, _ := json.Marshal(map[string]string{ "status": statusSuccess, @@ -2192,17 +2202,17 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error { code := normalizeLoginCode(extractFirstString(req.TemplateData, "login_code")) if code == "" { slog.Error("[QR] Missing login code in courier", "loginID", loginID) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Missing login code"}) + return errorJSON(c, fiber.StatusInternalServerError, "Missing login code") } flowID, _ := h.RedisService.Get(prefixLoginCode + loginID) if flowID == "" { slog.Error("[QR] Missing login flow for code verify", "loginID", loginID) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Login flow expired"}) + return errorJSON(c, fiber.StatusInternalServerError, "Login flow expired") } authInfo, err := h.IdpProvider.VerifyLoginCode(loginID, flowID, code) if err != nil || authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" { slog.Error("[QR] Code verify failed", "loginID", loginID, "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to verify login code"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to verify login code") } sessionData, _ := json.Marshal(map[string]string{ "status": statusSuccess, @@ -2221,7 +2231,7 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error { subject, body := h.buildKratosCourierMessage(&req) if strings.TrimSpace(body) == "" { slog.Warn("[Kratos Courier] Empty body", "recipient", req.Recipient, "template", req.TemplateType) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Empty message"}) + return errorJSON(c, fiber.StatusBadRequest, "Empty message") } if strings.Contains(req.Recipient, "@") { @@ -2237,7 +2247,7 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error { } if err := h.SmsService.SendSms(phone, smsBody); err != nil { slog.Error("[Kratos Courier] SMS send failed", "to", phone, "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to send SMS") } slog.Info("[Kratos Courier] SMS sent (email relay)", "to", phone, "template", req.TemplateType) return c.JSON(fiber.Map{"status": "ok"}) @@ -2246,7 +2256,7 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error { if strings.Contains(req.Recipient, "@") { if !drySend && h.EmailService == nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"}) + return errorJSON(c, fiber.StatusInternalServerError, "Email service not configured") } if shortSubject, shortBody := h.buildKratosShortEmailBody(&req, req.Recipient); shortBody != "" { subject = shortSubject @@ -2258,14 +2268,14 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error { } if err := h.EmailService.SendEmail(req.Recipient, subject, body); err != nil { slog.Error("[Kratos Courier] Email send failed", "to", req.Recipient, "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send email"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to send email") } slog.Info("[Kratos Courier] Email sent", "to", req.Recipient, "template", req.TemplateType) return c.JSON(fiber.Map{"status": "ok"}) } if !drySend && h.SmsService == nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "SMS service not configured"}) + return errorJSON(c, fiber.StatusInternalServerError, "SMS service not configured") } phone := sanitizePhoneForSms(req.Recipient) smsLoginID := req.Recipient @@ -2287,7 +2297,7 @@ func (h *AuthHandler) HandleKratosCourierRelay(c *fiber.Ctx) error { } if err := h.SmsService.SendSms(phone, smsBody); err != nil { slog.Error("[Kratos Courier] SMS send failed", "to", phone, "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to send SMS") } slog.Info("[Kratos Courier] SMS sent", "to", phone, "template", req.TemplateType) return c.JSON(fiber.Map{"status": "ok"}) @@ -2544,7 +2554,7 @@ func (h *AuthHandler) formatPhoneForStorage(phone string) string { func (h *AuthHandler) GetMe(c *fiber.Ctx) error { profile, err := h.resolveCurrentProfile(c) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusUnauthorized, err.Error()) } return c.JSON(profile) } @@ -2918,7 +2928,7 @@ type loginClientInfo struct { func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { if h.AuditRepo == nil && h.OathkeeperRepo == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Audit service unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable") } limit := c.QueryInt("limit", 20) @@ -2935,13 +2945,13 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { var err error cursor, err = parseAuditCursor(cursorRaw) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid cursor"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid cursor") } } profile, err := h.resolveCurrentProfile(c) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } currentSessionID := "" if token := h.getBearerToken(c); token != "" { @@ -3022,7 +3032,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error { for batch := 0; batch < maxBatches && len(authLogs) < fetchLimit; batch++ { logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, currentCursor) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve audit logs"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to retrieve audit logs") } if len(logs) == 0 { break @@ -3298,12 +3308,12 @@ type linkedRpRecord struct { func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { if h.Hydra == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "hydra admin unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "hydra admin unavailable") } subjects, err := h.resolveConsentSubjects(c) if err != nil || len(subjects) == 0 { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } var sessions []domain.HydraConsentSession @@ -3324,7 +3334,7 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { sessions = append(sessions, linked...) } if !hasSuccess && lastErr != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": lastErr.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, lastErr.Error()) } records := make(map[string]*linkedRpRecord) @@ -5137,7 +5147,7 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error { token := h.getBearerToken(c) var req domain.UpdateUserRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } var ( @@ -5150,12 +5160,12 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error { } else { cookie := c.Get("Cookie") if cookie == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing authorization token"}) + return errorJSON(c, fiber.StatusUnauthorized, "Missing authorization token") } identityID, traits, err = h.getKratosIdentityWithCookie(cookie) } if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } currentPhone, _ := traits["phone_number"].(string) @@ -5168,7 +5178,7 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error { val, _ := h.RedisService.Get(verifyKey) if val != "verified" { slog.Warn("[UpdateMe] Phone verification missing (Kratos)", "key", verifyKey) - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "휴대폰 번호 변경을 위해 SMS 인증이 필요합니다."}) + return errorJSON(c, fiber.StatusForbidden, "휴대폰 번호 변경을 위해 SMS 인증이 필요합니다.") } traits["phone_number"] = newPhoneStorage h.RedisService.Delete(verifyKey) @@ -5183,7 +5193,13 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error { if err := h.updateKratosIdentity(identityID, traits); err != nil { slog.Error("Failed to update profile in Kratos", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "프로필 업데이트에 실패했습니다."}) + return errorJSON(c, fiber.StatusInternalServerError, "프로필 업데이트에 실패했습니다.") + } + + // Invalidate token-based profile cache so refreshed /user/me returns latest traits. + if h.RedisService != nil && token != "" { + cacheKey := "cache:profile:token:" + token + _ = h.RedisService.Delete(cacheKey) } slog.Info("[UpdateMe] Profile update completed successfully (Kratos)", "identityID", identityID) @@ -5197,18 +5213,18 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error { func (h *AuthHandler) ChangeMyPassword(c *fiber.Ctx) error { var req domain.PasswordChangeRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request body") } currentPassword := strings.TrimSpace(req.CurrentPassword) newPassword := strings.TrimSpace(req.NewPassword) if currentPassword == "" || newPassword == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Current password and new password are required"}) + return errorJSON(c, fiber.StatusBadRequest, "Current password and new password are required") } policy := h.resolvePasswordPolicy() if err := validatePasswordWithPolicy(policy, newPassword); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusBadRequest, err.Error()) } loginID := "" @@ -5222,15 +5238,15 @@ func (h *AuthHandler) ChangeMyPassword(c *fiber.Ctx) error { if loginID == "" { cookie := c.Get("Cookie") if cookie == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing authorization token"}) + return errorJSON(c, fiber.StatusUnauthorized, "Missing authorization token") } _, traits, err := h.getKratosIdentityWithCookie(cookie) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } loginID = pickLoginIDFromTraits(traits) if loginID == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Login ID not found"}) + return errorJSON(c, fiber.StatusUnauthorized, "Login ID not found") } if !strings.Contains(loginID, "@") { loginID = normalizePhoneForLoginID(loginID) @@ -5238,11 +5254,11 @@ func (h *AuthHandler) ChangeMyPassword(c *fiber.Ctx) error { } if _, err := h.IdpProvider.SignIn(loginID, currentPassword); err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Current password is invalid"}) + return errorJSON(c, fiber.StatusUnauthorized, "Current password is invalid") } if err := h.IdpProvider.UpdateUserPassword(loginID, newPassword, nil); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to update password") } return c.JSON(fiber.Map{"message": "Password updated"}) @@ -5260,19 +5276,19 @@ func (h *AuthHandler) SendUpdateCode(c *fiber.Ctx) error { } else { cookie := c.Get("Cookie") if cookie == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) + return errorJSON(c, fiber.StatusUnauthorized, "Unauthorized") } userID, _, err = h.getKratosIdentityWithCookie(cookie) } if err != nil || userID == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } var req struct { Phone string `json:"phone"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid phone"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid phone") } phone := h.formatPhoneForStorage(req.Phone) @@ -5301,12 +5317,12 @@ func (h *AuthHandler) VerifyUpdateCode(c *fiber.Ctx) error { } else { cookie := c.Get("Cookie") if cookie == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } userID, _, err = h.getKratosIdentityWithCookie(cookie) } if err != nil || userID == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } var req struct { @@ -5314,7 +5330,7 @@ func (h *AuthHandler) VerifyUpdateCode(c *fiber.Ctx) error { Code string `json:"code"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) + return errorJSON(c, fiber.StatusBadRequest, "Invalid request") } phone := h.formatPhoneForStorage(req.Phone) @@ -5322,7 +5338,7 @@ func (h *AuthHandler) VerifyUpdateCode(c *fiber.Ctx) error { storedCode, _ := h.RedisService.Get(key) if storedCode == "" || storedCode != req.Code { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "인증번호가 일치하지 않거나 만료되었습니다."}) + return errorJSON(c, fiber.StatusUnauthorized, "인증번호가 일치하지 않거나 만료되었습니다.") } // Mark as verified for 10 minutes @@ -5410,16 +5426,16 @@ type rpHistoryItem struct { func (h *AuthHandler) ListRpHistory(c *fiber.Ctx) error { subject, err := h.resolveConsentSubject(c) if err != nil || subject == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + return errorJSON(c, fiber.StatusUnauthorized, "Invalid session") } if h.AuditRepo == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Audit service unavailable"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable") } logs, err := h.AuditRepo.FindByUserAndEvents(c.Context(), subject, []string{"consent.granted", "consent.revoked"}, 100) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to fetch history"}) + return errorJSON(c, fiber.StatusInternalServerError, "Failed to fetch history") } historyMap := make(map[string]*rpHistoryItem) diff --git a/backend/internal/handler/auth_handler_link_test.go b/backend/internal/handler/auth_handler_link_test.go index 331ac4f6..4a51cc6e 100644 --- a/backend/internal/handler/auth_handler_link_test.go +++ b/backend/internal/handler/auth_handler_link_test.go @@ -121,3 +121,26 @@ func TestEnchantedLinkFlow_Sms_Success(t *testing.T) { json.NewDecoder(resp.Body).Decode(&initResp) assert.NotEmpty(t, initResp["userCode"]) } + +func TestPollEnchantedLink_ExpiredToken_ReturnsCode(t *testing.T) { + redis := &mockRedisRepo{data: make(map[string]string)} + h := &AuthHandler{ + RedisService: redis, + } + app := fiber.New() + app.Post("/api/v1/auth/enchanted-link/poll", h.PollEnchantedLink) + + body, _ := json.Marshal(map[string]string{ + "pendingRef": "missing-ref", + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/poll", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + resp, _ := app.Test(req, -1) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var got map[string]interface{} + json.NewDecoder(resp.Body).Decode(&got) + assert.Equal(t, "expired_token", got["error"]) + assert.Equal(t, "expired_token", got["code"]) +} diff --git a/backend/internal/handler/auth_handler_login_code_test.go b/backend/internal/handler/auth_handler_login_code_test.go new file mode 100644 index 00000000..2f3f7ea2 --- /dev/null +++ b/backend/internal/handler/auth_handler_login_code_test.go @@ -0,0 +1,206 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" +) + +func newVerifyLoginCodeTestApp(h *AuthHandler) *fiber.App { + app := fiber.New() + app.Post("/api/v1/auth/login/code/verify", h.VerifyLoginCode) + app.Post("/api/v1/auth/login/code/verify-short", h.VerifyLoginShortCode) + return app +} + +func decodeJSONBody(t *testing.T, resp *http.Response) map[string]any { + t.Helper() + + var got map[string]any + if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { + t.Fatalf("failed to decode response body: %v", err) + } + return got +} + +func TestVerifyLoginCode_InvalidBody_ReturnsExplicitCode(t *testing.T) { + h := &AuthHandler{} + app := newVerifyLoginCodeTestApp(h) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify", bytes.NewBufferString("{")) + 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.StatusBadRequest { + t.Fatalf("expected 400, got %d", resp.StatusCode) + } + + got := decodeJSONBody(t, resp) + if got["code"] != "bad_request" { + t.Fatalf("expected code=bad_request, got %v", got["code"]) + } +} + +func TestVerifyLoginCode_IdpUnavailable_ReturnsExplicitCode(t *testing.T) { + h := &AuthHandler{} + app := newVerifyLoginCodeTestApp(h) + + body, _ := json.Marshal(map[string]any{ + "loginId": "user@example.com", + "code": "AA-111111", + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify", 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.StatusServiceUnavailable { + t.Fatalf("expected 503, got %d", resp.StatusCode) + } + + got := decodeJSONBody(t, resp) + if got["code"] != "service_unavailable" { + t.Fatalf("expected code=service_unavailable, got %v", got["code"]) + } +} + +func TestVerifyLoginCode_VerifyOnlyInvalidCode_ReturnsExplicitCode(t *testing.T) { + redis := &mockRedisRepo{data: make(map[string]string)} + redis.data[prefixLoginCode+"user@example.com"] = "flow-1" + redis.data[prefixLoginCodePending+"user@example.com"] = "pending-1" + redis.data[prefixLoginCodeValue+"pending-1"] = "AB-123" + + h := &AuthHandler{ + RedisService: redis, + IdpProvider: &mockIdpProvider{}, + } + app := newVerifyLoginCodeTestApp(h) + + body, _ := json.Marshal(map[string]any{ + "loginId": "user@example.com", + "code": "ZZ-999", + "verifyOnly": true, + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify", 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.StatusUnauthorized { + t.Fatalf("expected 401, got %d", resp.StatusCode) + } + + got := decodeJSONBody(t, resp) + if got["code"] != "invalid_code" { + t.Fatalf("expected code=invalid_code, got %v", got["code"]) + } +} + +func TestVerifyLoginShortCode_MissingShortCode_ReturnsExplicitCode(t *testing.T) { + h := &AuthHandler{ + RedisService: &mockRedisRepo{data: make(map[string]string)}, + } + app := newVerifyLoginCodeTestApp(h) + + body, _ := json.Marshal(map[string]any{ + "shortCode": "", + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify-short", 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.StatusBadRequest { + t.Fatalf("expected 400, got %d", resp.StatusCode) + } + + got := decodeJSONBody(t, resp) + if got["code"] != "bad_request" { + t.Fatalf("expected code=bad_request, got %v", got["code"]) + } +} + +func TestVerifyLoginShortCode_InvalidOrExpired_ReturnsExplicitCode(t *testing.T) { + h := &AuthHandler{ + RedisService: &mockRedisRepo{data: make(map[string]string)}, + } + app := newVerifyLoginCodeTestApp(h) + + body, _ := json.Marshal(map[string]any{ + "shortCode": "AB-123456", + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify-short", 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.StatusUnauthorized { + t.Fatalf("expected 401, got %d", resp.StatusCode) + } + + got := decodeJSONBody(t, resp) + if got["code"] != "invalid_or_expired_code" { + t.Fatalf("expected code=invalid_or_expired_code, got %v", got["code"]) + } +} + +func TestVerifyLoginShortCode_VerifyOnlyMissingPendingRef_ReturnsExplicitCode(t *testing.T) { + redis := &mockRedisRepo{data: make(map[string]string)} + payload, _ := json.Marshal(shortLoginCodePayload{ + LoginID: "user@example.com", + Code: "AB-123", + }) + redis.data[prefixLoginCodeShort+"AB-123456"] = string(payload) + + h := &AuthHandler{ + RedisService: redis, + } + app := newVerifyLoginCodeTestApp(h) + + body, _ := json.Marshal(map[string]any{ + "shortCode": "AB-123456", + "verifyOnly": true, + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify-short", 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.StatusBadRequest { + t.Fatalf("expected 400, got %d", resp.StatusCode) + } + + got := decodeJSONBody(t, resp) + if got["code"] != "invalid_session_reference" { + t.Fatalf("expected code=invalid_session_reference, got %v", got["code"]) + } +} diff --git a/backend/internal/handler/auth_handler_profile_cache_test.go b/backend/internal/handler/auth_handler_profile_cache_test.go new file mode 100644 index 00000000..aa66ac14 --- /dev/null +++ b/backend/internal/handler/auth_handler_profile_cache_test.go @@ -0,0 +1,109 @@ +package handler + +import ( + "baron-sso-backend/internal/domain" + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/require" +) + +func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) { + token := "token-abc" + identityID := "user-1" + traits := map[string]interface{}{ + "email": "qa@example.com", + "name": "QA User", + "phone_number": "+821012345678", + "department": "Old Dept", + "affiliationType": "employee", + "companyCode": "", + "role": domain.RoleUser, + } + + transport := roundTripFunc(func(r *http.Request) (*http.Response, error) { + switch { + case r.URL.Host == "kratos.test" && + r.URL.Path == "/sessions/whoami" && + r.Method == http.MethodGet: + if r.Header.Get("X-Session-Token") != token { + return httpResponse(r, http.StatusUnauthorized, `{"error":"invalid token"}`), nil + } + return httpJSONAny(r, http.StatusOK, map[string]interface{}{ + "identity": map[string]interface{}{ + "id": identityID, + "traits": traits, + }, + }), nil + + case r.URL.Host == "kratos.test" && + r.URL.Path == "/admin/identities/"+identityID && + r.Method == http.MethodPut: + var payload struct { + Traits map[string]interface{} `json:"traits"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + return httpResponse(r, http.StatusBadRequest, `{"error":"invalid body"}`), nil + } + for k, v := range payload.Traits { + traits[k] = v + } + return httpResponse(r, http.StatusOK, `{"ok":true}`), nil + } + + return httpResponse(r, http.StatusNotFound, "not found"), nil + }) + setDefaultHTTPClientForTest(t, transport) + t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test") + t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test") + + redis := &mockRedisRepo{data: make(map[string]string)} + h := &AuthHandler{ + RedisService: redis, + } + app := fiber.New() + app.Get("/api/v1/user/me", h.GetMe) + app.Put("/api/v1/user/me", h.UpdateMe) + + // 1) 첫 조회로 Old Dept가 캐시에 저장됨 + getReq1 := httptest.NewRequest(http.MethodGet, "/api/v1/user/me", nil) + getReq1.Header.Set("Authorization", "Bearer "+token) + getResp1, err := app.Test(getReq1, -1) + require.NoError(t, err) + require.Equal(t, http.StatusOK, getResp1.StatusCode) + var profile1 map[string]interface{} + require.NoError(t, json.NewDecoder(getResp1.Body).Decode(&profile1)) + require.Equal(t, "Old Dept", profile1["department"]) + + // 2) 소속을 New Dept로 변경 + updateBody, _ := json.Marshal(map[string]string{ + "name": "QA User", + "phone": "01012345678", + "department": "New Dept", + }) + updateReq := httptest.NewRequest( + http.MethodPut, + "/api/v1/user/me", + bytes.NewReader(updateBody), + ) + updateReq.Header.Set("Content-Type", "application/json") + updateReq.Header.Set("Authorization", "Bearer "+token) + updateResp, err := app.Test(updateReq, -1) + require.NoError(t, err) + require.Equal(t, http.StatusOK, updateResp.StatusCode) + require.Equal(t, "New Dept", traits["department"]) + + // 3) 새로고침 재조회 시 New Dept가 보여야 함(캐시 무효화 회귀 방지) + getReq2 := httptest.NewRequest(http.MethodGet, "/api/v1/user/me", nil) + getReq2.Header.Set("Authorization", "Bearer "+token) + getResp2, err := app.Test(getReq2, -1) + require.NoError(t, err) + require.Equal(t, http.StatusOK, getResp2.StatusCode) + var profile2 map[string]interface{} + require.NoError(t, json.NewDecoder(getResp2.Body).Decode(&profile2)) + require.Equal(t, "New Dept", profile2["department"]) +} diff --git a/backend/internal/handler/auth_handler_qr_test.go b/backend/internal/handler/auth_handler_qr_test.go index 9089a948..7caf77b8 100644 --- a/backend/internal/handler/auth_handler_qr_test.go +++ b/backend/internal/handler/auth_handler_qr_test.go @@ -83,6 +83,7 @@ func TestQRLoginFlow_Success(t *testing.T) { var pollResp map[string]interface{} json.NewDecoder(resp.Body).Decode(&pollResp) assert.Equal(t, "authorization_pending", pollResp["error"]) + assert.Equal(t, "authorization_pending", pollResp["code"]) // 3. Mock Approval sessionData, _ := json.Marshal(map[string]string{ diff --git a/backend/internal/handler/auth_handler_test.go b/backend/internal/handler/auth_handler_test.go index e92f19fc..87a10b64 100644 --- a/backend/internal/handler/auth_handler_test.go +++ b/backend/internal/handler/auth_handler_test.go @@ -1,6 +1,7 @@ package handler import ( + "baron-sso-backend/internal/middleware" "bytes" "encoding/json" "fmt" @@ -26,6 +27,13 @@ func newResetFlowTestApp(h *AuthHandler) *fiber.App { return app } +func newResetInitAppWithErrorCodeEnricher(h *AuthHandler) *fiber.App { + app := fiber.New() + app.Use(middleware.ErrorCodeEnricher()) + app.Post("/api/v1/auth/password/reset/init", h.InitiatePasswordReset) + return app +} + type testRedisRepo struct { values map[string]string } @@ -286,3 +294,35 @@ func TestProcessPasswordResetToken_EncodesLoginIDInRedirect(t *testing.T) { t.Fatalf("expected encoded loginId round-trip=%s, got %s (location=%s)", loginID, gotLoginID, location) } } + +func TestPasswordResetInit_LegacyErrorResponseHasCodeViaMiddleware(t *testing.T) { + h := &AuthHandler{} + app := newResetInitAppWithErrorCodeEnricher(h) + + body, _ := json.Marshal(map[string]string{ + "loginId": "", + }) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/reset/init", 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.StatusBadRequest { + t.Fatalf("expected 400, got %d", resp.StatusCode) + } + + var got map[string]any + if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + if got["error"] != "Login ID is required" { + t.Fatalf("unexpected error message: %v", got["error"]) + } + if got["code"] != "bad_request" { + t.Fatalf("expected code=bad_request, got %v", got["code"]) + } +} diff --git a/backend/internal/handler/common_test.go b/backend/internal/handler/common_test.go index 1b862c84..15a6277e 100644 --- a/backend/internal/handler/common_test.go +++ b/backend/internal/handler/common_test.go @@ -163,6 +163,14 @@ func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } +func setDefaultHTTPClientForTest(t interface{ Cleanup(func()) }, transport http.RoundTripper) { + origDefault := http.DefaultClient + http.DefaultClient = &http.Client{Transport: transport} + t.Cleanup(func() { + http.DefaultClient = origDefault + }) +} + func httpResponse(r *http.Request, code int, body string) *http.Response { return &http.Response{ StatusCode: code, diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index e25ceeca..498752ad 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -275,15 +275,13 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error { clients, err := h.Hydra.ListClients(c.Context(), limit, offset) if err != nil { if errors.Is(err, service.ErrHydraNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "clients not found"}) + return errorJSON(c, fiber.StatusNotFound, "clients not found") } errMsg := err.Error() if strings.Contains(errMsg, "connection refused") || strings.Contains(errMsg, "dial tcp") { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ - "error": "Hydra service is unavailable. Please check if Ory Hydra is running.", - }) + return errorJSON(c, fiber.StatusServiceUnavailable, "Hydra service is unavailable. Please check if Ory Hydra is running.") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": errMsg}) + return errorJSON(c, fiber.StatusInternalServerError, errMsg) } items := make([]clientSummary, 0, len(clients)) @@ -306,15 +304,15 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error { func (h *DevHandler) GetClient(c *fiber.Ctx) error { clientID := c.Params("id") if clientID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "client id is required") } client, err := h.Hydra.GetClient(c.Context(), clientID) if err != nil { if errors.Is(err, service.ErrHydraNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"}) + return errorJSON(c, fiber.StatusNotFound, "client not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } summary := h.mapClientSummary(*client) @@ -323,10 +321,10 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error { if summary.Type == "private" { isAppManager, err := h.checkAppManagerPermission(c) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"}) + return errorJSON(c, fiber.StatusInternalServerError, "permission check error") } if !isAppManager { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client") } } @@ -345,19 +343,19 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error { func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error { clientID := c.Params("id") if clientID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "client id is required") } var req struct { Status string `json:"status"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } status := strings.ToLower(strings.TrimSpace(req.Status)) if status != "active" && status != "inactive" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"}) + return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive") } // [Security] Check permission before patching @@ -367,7 +365,7 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error { if summary.Type == "private" { isAppManager, _ := h.checkAppManagerPermission(c) if !isAppManager { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client") } } } @@ -375,9 +373,9 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error { updated, err := h.Hydra.PatchClientStatus(c.Context(), clientID, status) if err != nil { if errors.Is(err, service.ErrHydraNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"}) + return errorJSON(c, fiber.StatusNotFound, "client not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } summary := h.mapClientSummary(*updated) @@ -396,7 +394,7 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error { func (h *DevHandler) CreateClient(c *fiber.Ctx) error { var req clientUpsertRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } clientID := strings.TrimSpace(valueOr(req.ID, "")) @@ -411,7 +409,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error { redirectURIs := derefSlice(req.RedirectURIs, nil) if len(redirectURIs) == 0 { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "redirectUris is required"}) + return errorJSON(c, fiber.StatusBadRequest, "redirectUris is required") } scopes := derefSlice(req.Scopes, defaultClientScopes()) @@ -420,23 +418,23 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error { clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "private"))) if clientType != "pkce" && clientType != "private" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"}) + return errorJSON(c, fiber.StatusBadRequest, "type must be pkce or private") } // [Security] Check permission for private clients if clientType == "private" { isAppManager, err := h.checkAppManagerPermission(c) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"}) + return errorJSON(c, fiber.StatusInternalServerError, "permission check error") } if !isAppManager { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions to create private client"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions to create private client") } } status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active"))) if status != "active" && status != "inactive" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"}) + return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive") } metadata := mergeMetadata(nil, req.Metadata) @@ -468,7 +466,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error { created, err := h.Hydra.CreateClient(c.Context(), clientReq) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // Store secret in metadata for later retrieval @@ -500,27 +498,27 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error { func (h *DevHandler) UpdateClient(c *fiber.Ctx) error { clientID := strings.TrimSpace(c.Params("id")) if clientID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "client id is required") } var req clientUpsertRequest if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } current, err := h.Hydra.GetClient(c.Context(), clientID) if err != nil { if errors.Is(err, service.ErrHydraNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"}) + return errorJSON(c, fiber.StatusNotFound, "client not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } clientType := "" if req.Type != nil { clientType = strings.ToLower(strings.TrimSpace(*req.Type)) if clientType != "pkce" && clientType != "private" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"}) + return errorJSON(c, fiber.StatusBadRequest, "type must be pkce or private") } } @@ -529,10 +527,10 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error { if currentSummary.Type == "private" || clientType == "private" { isAppManager, err := h.checkAppManagerPermission(c) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"}) + return errorJSON(c, fiber.StatusInternalServerError, "permission check error") } if !isAppManager { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client") } } @@ -540,7 +538,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error { if req.Status != nil { status = strings.ToLower(strings.TrimSpace(*req.Status)) if status != "active" && status != "inactive" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"}) + return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive") } } @@ -554,7 +552,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error { } if req.RedirectURIs != nil && len(*req.RedirectURIs) == 0 { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "redirectUris cannot be empty"}) + return errorJSON(c, fiber.StatusBadRequest, "redirectUris cannot be empty") } metadata := mergeMetadata(current.Metadata, req.Metadata) @@ -579,9 +577,9 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error { updatedClient, err := h.Hydra.UpdateClient(c.Context(), clientID, updated) if err != nil { if errors.Is(err, service.ErrHydraNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"}) + return errorJSON(c, fiber.StatusNotFound, "client not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } summary := h.mapClientSummary(*updatedClient) @@ -600,7 +598,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error { func (h *DevHandler) DeleteClient(c *fiber.Ctx) error { clientID := strings.TrimSpace(c.Params("id")) if clientID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "client id is required") } // [Security] Check permission for private clients @@ -610,16 +608,16 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error { if summary.Type == "private" { isAppManager, _ := h.checkAppManagerPermission(c) if !isAppManager { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client") } } } if err := h.Hydra.DeleteClient(c.Context(), clientID); err != nil { if errors.Is(err, service.ErrHydraNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"}) + return errorJSON(c, fiber.StatusNotFound, "client not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // 1. Clean up PostgreSQL @@ -638,7 +636,7 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error { func (h *DevHandler) ListConsents(c *fiber.Ctx) error { clientID := strings.TrimSpace(c.Query("client_id")) if clientID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client_id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "client_id is required") } subject := strings.TrimSpace(c.Query("subject")) @@ -678,7 +676,7 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error { } if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } items := make([]consentSummary, 0, len(consents)) @@ -719,7 +717,7 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error { func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error { subject := strings.TrimSpace(c.Query("subject")) if subject == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subject is required"}) + return errorJSON(c, fiber.StatusBadRequest, "subject is required") } clientID := strings.TrimSpace(c.Query("client_id")) @@ -733,7 +731,7 @@ func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error { // 1. Revoke in Hydra if err := h.Hydra.RevokeConsentSessions(c.Context(), subject, clientID); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // 2. Sync to Local DB (Delete) @@ -747,7 +745,7 @@ func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error { func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error { clientID := strings.TrimSpace(c.Params("id")) if clientID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "client id is required") } // [Security] Check permission for private clients @@ -757,7 +755,7 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error { if summary.Type == "private" { isAppManager, _ := h.checkAppManagerPermission(c) if !isAppManager { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client") } } } @@ -765,22 +763,22 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error { // 1. Generate new secret newSecret, err := generateRandomSecret(20) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate secret"}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to generate secret") } // 2. Get current client to preserve other fields (already fetched above) if err != nil { if errors.Is(err, service.ErrHydraNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"}) + return errorJSON(c, fiber.StatusNotFound, "client not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // 3. Update Hydra current.ClientSecret = newSecret updated, err := h.Hydra.UpdateClient(c.Context(), clientID, *current) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // 4. Update Persistence (DB & Redis) diff --git a/backend/internal/handler/error_helper.go b/backend/internal/handler/error_helper.go new file mode 100644 index 00000000..e91fa4a8 --- /dev/null +++ b/backend/internal/handler/error_helper.go @@ -0,0 +1,17 @@ +package handler + +import ( + "baron-sso-backend/internal/response" + + "github.com/gofiber/fiber/v2" +) + +// errorJSON은 기존 error 필드를 유지하면서 기계 판독용 code를 명시적으로 추가합니다. +func errorJSON(c *fiber.Ctx, status int, message string) error { + return response.Error(c, status, response.StatusCode(status), message) +} + +// errorJSONCode는 상태코드 기반 매핑만으로 부족한 경우 명시 코드를 강제할 때 사용합니다. +func errorJSONCode(c *fiber.Ctx, status int, code, message string) error { + return response.Error(c, status, code, message) +} diff --git a/backend/internal/handler/federation_handler.go b/backend/internal/handler/federation_handler.go index 0c05123f..e8821de0 100644 --- a/backend/internal/handler/federation_handler.go +++ b/backend/internal/handler/federation_handler.go @@ -33,13 +33,13 @@ func (h *FederationHandler) InitiateOIDCLogin(c *fiber.Ctx) error { loginChallenge := c.Query("login_challenge") if providerID == "" || loginChallenge == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "provider_id and login_challenge are required"}) + return errorJSON(c, fiber.StatusBadRequest, "provider_id and login_challenge are required") } redirectURL, err := h.fedSvc.InitiateOIDCLogin(c.Context(), providerID, loginChallenge) if err != nil { // Log the error properly in a real application - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to initiate OIDC login"}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to initiate OIDC login") } return c.Redirect(redirectURL, fiber.StatusFound) @@ -51,12 +51,12 @@ func (h *FederationHandler) HandleOIDCCallback(c *fiber.Ctx) error { state := c.Query("state") if code == "" || state == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "code and state are required"}) + return errorJSON(c, fiber.StatusBadRequest, "code and state are required") } redirectURL, err := h.fedSvc.HandleOIDCCallback(c.Context(), code, state) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to handle OIDC callback"}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to handle OIDC callback") } return c.Redirect(redirectURL, fiber.StatusFound) @@ -68,12 +68,12 @@ func (h *FederationHandler) HandleOIDCCallback(c *fiber.Ctx) error { func (h *FederationHandler) ListIdpConfigsForClient(c *fiber.Ctx) error { clientID := c.Params("clientId") if clientID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "clientId is required"}) + return errorJSON(c, fiber.StatusBadRequest, "clientId is required") } var configs []domain.IdentityProviderConfig if err := h.db.Where("client_id = ?", clientID).Find(&configs).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(configs) @@ -83,12 +83,12 @@ func (h *FederationHandler) ListIdpConfigsForClient(c *fiber.Ctx) error { func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error { clientID := c.Params("clientId") if clientID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "clientId is required in path"}) + return errorJSON(c, fiber.StatusBadRequest, "clientId is required in path") } var req domain.IdentityProviderConfig if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } // Assign clientID from path parameter @@ -96,14 +96,14 @@ func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error { // Basic validation if req.DisplayName == "" || req.ProviderType == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "display_name and provider_type are required"}) + return errorJSON(c, fiber.StatusBadRequest, "display_name and provider_type are required") } // TODO: Optionally, validate if the clientID exists in Hydra // Create in DB if err := h.db.Create(&req).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusCreated).JSON(req) @@ -115,7 +115,7 @@ func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error { func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error { tenantID := c.Params("tenantId") if tenantID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenantId is required") } // This is a temporary solution. We should create a proper method in the repository. @@ -123,7 +123,7 @@ func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error { // Note: This now queries client_id, which is incorrect for tenants. // This method is deprecated. if err := h.db.Where("tenant_id = ?", tenantID).Find(&configs).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(configs) @@ -133,26 +133,26 @@ func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error { func (h *FederationHandler) CreateIdpConfig(c *fiber.Ctx) error { var req domain.IdentityProviderConfig if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } // Basic validation - This is the old validation logic if req.ClientID == "" || req.DisplayName == "" || req.ProviderType == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client_id, display_name, and provider_type are required"}) + return errorJSON(c, fiber.StatusBadRequest, "client_id, display_name, and provider_type are required") } // This check is now incorrect and deprecated. var tenant domain.Tenant if err := h.db.First(&tenant, "id = ?", req.ClientID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant not found"}) + return errorJSON(c, fiber.StatusBadRequest, "tenant not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // Create in DB if err := h.db.Create(&req).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusCreated).JSON(req) diff --git a/backend/internal/handler/relying_party_handler.go b/backend/internal/handler/relying_party_handler.go index 342d7f5d..74bddee0 100644 --- a/backend/internal/handler/relying_party_handler.go +++ b/backend/internal/handler/relying_party_handler.go @@ -20,17 +20,17 @@ func NewRelyingPartyHandler(s service.RelyingPartyService, kratos *service.Krato func (h *RelyingPartyHandler) Create(c *fiber.Ctx) error { tenantID := c.Params("tenantId") if tenantID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenantId is required") } var req domain.HydraClient if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } rp, err := h.Service.Create(c.Context(), tenantID, req) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusCreated).JSON(rp) @@ -39,7 +39,7 @@ func (h *RelyingPartyHandler) Create(c *fiber.Ctx) error { func (h *RelyingPartyHandler) ListAll(c *fiber.Ctx) error { profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse) if !ok { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized: user profile not found in context"}) + return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: user profile not found in context") } var rps []domain.RelyingParty @@ -51,11 +51,11 @@ func (h *RelyingPartyHandler) ListAll(c *fiber.Ctx) error { rps, err = h.Service.List(c.Context(), *profile.TenantID) } else { slog.Warn("Forbidden access to all applications", "userID", profile.ID, "role", profile.Role) - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient role to list all applications"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient role to list all applications") } if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(rps) @@ -64,12 +64,12 @@ func (h *RelyingPartyHandler) ListAll(c *fiber.Ctx) error { func (h *RelyingPartyHandler) List(c *fiber.Ctx) error { tenantID := c.Params("tenantId") if tenantID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenantId is required") } rps, err := h.Service.List(c.Context(), tenantID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(rps) @@ -79,7 +79,7 @@ func (h *RelyingPartyHandler) Get(c *fiber.Ctx) error { id := c.Params("id") rp, hydraClient, err := h.Service.Get(c.Context(), id) if err != nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "relying party not found"}) + return errorJSON(c, fiber.StatusNotFound, "relying party not found") } return c.JSON(fiber.Map{ @@ -92,12 +92,12 @@ func (h *RelyingPartyHandler) Update(c *fiber.Ctx) error { id := c.Params("id") var req domain.HydraClient if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } rp, err := h.Service.Update(c.Context(), id, req) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(rp) @@ -106,7 +106,7 @@ func (h *RelyingPartyHandler) Update(c *fiber.Ctx) error { func (h *RelyingPartyHandler) Delete(c *fiber.Ctx) error { id := c.Params("id") if err := h.Service.Delete(c.Context(), id); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go index 956e7696..62f5100e 100644 --- a/backend/internal/handler/tenant_handler.go +++ b/backend/internal/handler/tenant_handler.go @@ -56,17 +56,17 @@ func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error { AdminEmail string `json:"adminEmail"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } // Basic validation if req.Name == "" || req.Domain == "" || req.AdminEmail == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name, domain, and adminEmail are required"}) + return errorJSON(c, fiber.StatusBadRequest, "name, domain, and adminEmail are required") } tenant, err := h.Service.RequestRegistration(c.Context(), req.Name, req.Slug, req.Description, req.Domain, req.AdminEmail) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusBadRequest, err.Error()) } return c.Status(fiber.StatusAccepted).JSON(fiber.Map{ @@ -78,11 +78,11 @@ func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error { func (h *TenantHandler) ApproveTenant(c *fiber.Ctx) error { tenantID := c.Params("id") if tenantID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenant id is required") } if err := h.Service.ApproveTenant(c.Context(), tenantID); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(fiber.Map{"message": "Tenant approved successfully"}) @@ -90,7 +90,7 @@ func (h *TenantHandler) ApproveTenant(c *fiber.Ctx) error { func (h *TenantHandler) ListTenants(c *fiber.Ctx) error { if h.DB == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } limit := c.QueryInt("limit", 50) @@ -104,12 +104,12 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error { var total int64 if err := h.DB.Model(&domain.Tenant{}).Count(&total).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } var tenants []domain.Tenant if err := h.DB.Order("created_at desc").Limit(limit).Offset(offset).Preload("Domains").Find(&tenants).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } items := make([]tenantSummary, 0, len(tenants)) @@ -122,20 +122,20 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error { func (h *TenantHandler) GetTenant(c *fiber.Ctx) error { if h.DB == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } tenantID := strings.TrimSpace(c.Params("id")) if tenantID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenant id is required") } var tenant domain.Tenant if err := h.DB.Preload("Domains").First(&tenant, "id = ?", tenantID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant not found"}) + return errorJSON(c, fiber.StatusNotFound, "tenant not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(mapTenantSummary(tenant)) @@ -143,7 +143,7 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error { func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error { if h.DB == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } var req struct { @@ -155,12 +155,12 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error { Config map[string]any `json:"config"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } name := strings.TrimSpace(req.Name) if name == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"}) + return errorJSON(c, fiber.StatusBadRequest, "name is required") } slug := normalizeTenantSlug(req.Slug) @@ -168,7 +168,7 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error { slug = normalizeTenantSlug(name) } if slug == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "slug is required"}) + return errorJSON(c, fiber.StatusBadRequest, "slug is required") } status := normalizeTenantStatus(req.Status) @@ -180,9 +180,9 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error { tenant, err := h.Service.RegisterTenant(c.Context(), name, slug, req.Description, req.Domains) if err != nil { if strings.Contains(err.Error(), "already exists") { - return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusConflict, err.Error()) } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } if req.Config != nil { @@ -195,20 +195,20 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error { func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error { if h.DB == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } tenantID := strings.TrimSpace(c.Params("id")) if tenantID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenant id is required") } var tenant domain.Tenant if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant not found"}) + return errorJSON(c, fiber.StatusNotFound, "tenant not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } var req struct { @@ -220,27 +220,27 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error { Config map[string]any `json:"config"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } if req.Name != nil { name := strings.TrimSpace(*req.Name) if name == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name cannot be empty"}) + return errorJSON(c, fiber.StatusBadRequest, "name cannot be empty") } tenant.Name = name } if req.Slug != nil { slug := normalizeTenantSlug(*req.Slug) if slug == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "slug cannot be empty"}) + return errorJSON(c, fiber.StatusBadRequest, "slug cannot be empty") } if slug != tenant.Slug { var exists domain.Tenant if err := h.DB.Unscoped().Where("slug = ?", slug).First(&exists).Error; err == nil { - return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "slug already exists"}) + return errorJSON(c, fiber.StatusConflict, "slug already exists") } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } tenant.Slug = slug } @@ -251,7 +251,7 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error { if req.Status != nil { status := normalizeTenantStatus(*req.Status) if status == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"}) + return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive") } tenant.Status = status } @@ -260,14 +260,14 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error { } if err := h.DB.Save(&tenant).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // Update domains if provided if req.Domains != nil { // Simple approach: Delete existing and recreate if err := h.DB.Delete(&domain.TenantDomain{}, "tenant_id = ?", tenant.ID).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to clear old domains"}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to clear old domains") } for _, d := range req.Domains { if strings.TrimSpace(d) == "" { @@ -275,7 +275,7 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error { } // Use repository for consistency if err := repository.NewTenantRepository(h.DB).AddDomain(c.Context(), tenant.ID, strings.TrimSpace(d), true); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to add domain: " + d}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to add domain: "+d) } } } @@ -288,30 +288,30 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error { func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error { if h.DB == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "database not available") } tenantID := strings.TrimSpace(c.Params("id")) if tenantID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenant id is required") } var tenant domain.Tenant if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant not found"}) + return errorJSON(c, fiber.StatusNotFound, "tenant not found") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // Rename slug to release it for reuse before soft delete deletedSlug := tenant.Slug + "-deleted-" + time.Now().Format("20060102150405") if err := h.DB.Model(&tenant).Update("slug", deletedSlug).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to release slug"}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to release slug") } if err := h.DB.Delete(&tenant).Error; err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) @@ -320,13 +320,13 @@ func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error { func (h *TenantHandler) ListAdmins(c *fiber.Ctx) error { tenantID := c.Params("id") if tenantID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenant id is required") } // Fetch admins from Keto relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admin", "") if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } type adminInfo struct { @@ -372,11 +372,11 @@ func (h *TenantHandler) AddAdmin(c *fiber.Ctx) error { tenantID := c.Params("id") userID := c.Params("userId") if tenantID == "" || userID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId and userId are required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required") } if err := h.Keto.CreateRelation(c.Context(), "Tenant", tenantID, "admin", "User:"+userID); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusOK) @@ -386,11 +386,11 @@ func (h *TenantHandler) RemoveAdmin(c *fiber.Ctx) error { tenantID := c.Params("id") userID := c.Params("userId") if tenantID == "" || userID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId and userId are required"}) + return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required") } if err := h.Keto.DeleteRelation(c.Context(), "Tenant", tenantID, "admin", "User:"+userID); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) diff --git a/backend/internal/handler/user_group_handler.go b/backend/internal/handler/user_group_handler.go index 359754e9..9d9fa0cf 100644 --- a/backend/internal/handler/user_group_handler.go +++ b/backend/internal/handler/user_group_handler.go @@ -19,7 +19,7 @@ func (h *UserGroupHandler) List(c *fiber.Ctx) error { tenantID := c.Params("tenantId") groups, err := h.Service.List(c.Context(), tenantID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(groups) } @@ -28,12 +28,12 @@ func (h *UserGroupHandler) Create(c *fiber.Ctx) error { tenantID := c.Params("tenantId") var group domain.UserGroup if err := c.BodyParser(&group); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid body") } group.TenantID = tenantID if err := h.Service.Create(c.Context(), &group); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.Status(fiber.StatusCreated).JSON(group) } @@ -42,7 +42,7 @@ func (h *UserGroupHandler) Get(c *fiber.Ctx) error { id := c.Params("id") group, err := h.Service.Get(c.Context(), id) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to get group: " + err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to get group: "+err.Error()) } return c.JSON(group) } @@ -51,12 +51,12 @@ func (h *UserGroupHandler) Update(c *fiber.Ctx) error { id := c.Params("id") var group domain.UserGroup if err := c.BodyParser(&group); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid body") } group.ID = id if err := h.Service.Update(c.Context(), &group); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(group) } @@ -64,7 +64,7 @@ func (h *UserGroupHandler) Update(c *fiber.Ctx) error { func (h *UserGroupHandler) Delete(c *fiber.Ctx) error { id := c.Params("id") if err := h.Service.Delete(c.Context(), id); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) } @@ -75,11 +75,11 @@ func (h *UserGroupHandler) AddMember(c *fiber.Ctx) error { UserID string `json:"userId"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "userId is required"}) + return errorJSON(c, fiber.StatusBadRequest, "userId is required") } if err := h.Service.AddMember(c.Context(), groupID, req.UserID); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusOK) } @@ -89,7 +89,7 @@ func (h *UserGroupHandler) RemoveMember(c *fiber.Ctx) error { userID := c.Params("userId") if err := h.Service.RemoveMember(c.Context(), groupID, userID); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) } @@ -101,11 +101,11 @@ func (h *UserGroupHandler) AssignRole(c *fiber.Ctx) error { Relation string `json:"relation"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid body") } if err := h.Service.AssignRoleToTenant(c.Context(), groupID, req.TenantID, req.Relation); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusOK) } @@ -114,7 +114,7 @@ func (h *UserGroupHandler) ListRoles(c *fiber.Ctx) error { groupID := c.Params("id") roles, err := h.Service.ListRoles(c.Context(), groupID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.JSON(roles) } @@ -125,7 +125,7 @@ func (h *UserGroupHandler) RemoveRole(c *fiber.Ctx) error { relation := c.Params("relation") if err := h.Service.RemoveRoleFromTenant(c.Context(), groupID, tenantID, relation); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } return c.SendStatus(fiber.StatusNoContent) } diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 00a64ba7..b2775802 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -125,7 +125,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error { // Fetch from UserRepo users, total, err := h.UserRepo.List(c.Context(), offset, limit, search) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to fetch users from both kratos and local db"}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch users from both kratos and local db") } items := make([]userSummary, 0, len(users)) @@ -154,20 +154,20 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error { func (h *UserHandler) GetUser(c *fiber.Ctx) error { if h.KratosAdmin == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available") } userID := strings.TrimSpace(c.Params("id")) if userID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "user id is required") } identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } if identity == nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) + return errorJSON(c, fiber.StatusNotFound, "user not found") } // [New] Check access scope @@ -175,7 +175,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error { if requester != nil && requester.Role == domain.RoleTenantAdmin { compCode := extractTraitString(identity.Traits, "companyCode") if requester.CompanyCode == "" || compCode != requester.CompanyCode { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: access to user in another tenant denied"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: access to user in another tenant denied") } } @@ -184,7 +184,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error { func (h *UserHandler) CreateUser(c *fiber.Ctx) error { if h.OryProvider == nil || h.KratosAdmin == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available") } var req struct { @@ -198,19 +198,19 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error { Metadata map[string]any `json:"metadata"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } email := strings.TrimSpace(req.Email) if email == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email is required"}) + return errorJSON(c, fiber.StatusBadRequest, "email is required") } if !strings.Contains(email, "@") || !strings.Contains(email, ".") { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid email format"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid email format") } name := strings.TrimSpace(req.Name) if name == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"}) + return errorJSON(c, fiber.StatusBadRequest, "name is required") } password := strings.TrimSpace(req.Password) @@ -230,13 +230,13 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error { if password == "" { generated, genErr := utils.GeneratePasswordWithPolicy(policy) if genErr != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate password"}) + return errorJSON(c, fiber.StatusInternalServerError, "failed to generate password") } password = generated generatedPassword = generated } else { if err := utils.ValidatePasswordWithPolicy(policy, password); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusBadRequest, err.Error()) } } @@ -282,9 +282,9 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error { identityID, err := h.OryProvider.CreateUser(brokerUser, password) if err != nil { if strings.Contains(err.Error(), "already exists") { - return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "email already exists"}) + return errorJSON(c, fiber.StatusConflict, "email already exists") } - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // [New] Local DB Sync @@ -334,7 +334,7 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error { identity, err := h.KratosAdmin.GetIdentity(c.Context(), identityID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } if identity == nil { return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": identityID, "initialPassword": generatedPassword}) @@ -349,20 +349,20 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error { func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { if h.KratosAdmin == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available") } userID := strings.TrimSpace(c.Params("id")) if userID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "user id is required") } identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } if identity == nil { - return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"}) + return errorJSON(c, fiber.StatusNotFound, "user not found") } // [New] Check access scope @@ -370,7 +370,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { if requester != nil && requester.Role == domain.RoleTenantAdmin { compCode := extractTraitString(identity.Traits, "companyCode") if requester.CompanyCode == "" || compCode != requester.CompanyCode { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: cannot update user in another tenant"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot update user in another tenant") } } @@ -385,13 +385,13 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { Metadata map[string]any `json:"metadata"` } if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"}) + return errorJSON(c, fiber.StatusBadRequest, "invalid request body") } // [New] Tenant Admin restriction: Cannot change companyCode if requester != nil && requester.Role == domain.RoleTenantAdmin { if req.CompanyCode != nil && *req.CompanyCode != requester.CompanyCode { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: tenant admins cannot change user's tenant"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant") } } @@ -451,7 +451,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { state := normalizeKratosState(req.Status) updated, err := h.KratosAdmin.UpdateIdentity(c.Context(), userID, traits, state) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // [New] Local DB Sync @@ -519,7 +519,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { if req.Password != nil && *req.Password != "" { if err := h.KratosAdmin.UpdateIdentityPassword(c.Context(), userID, *req.Password); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } } @@ -528,12 +528,12 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { func (h *UserHandler) DeleteUser(c *fiber.Ctx) error { if h.KratosAdmin == nil { - return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"}) + return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available") } userID := strings.TrimSpace(c.Params("id")) if userID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"}) + return errorJSON(c, fiber.StatusBadRequest, "user id is required") } // [New] Check access scope before deletion @@ -543,13 +543,13 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error { if err == nil && identity != nil { compCode := extractTraitString(identity.Traits, "companyCode") if requester.CompanyCode == "" || compCode != requester.CompanyCode { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: cannot delete user in another tenant"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot delete user in another tenant") } } } if err := h.KratosAdmin.DeleteIdentity(c.Context(), userID); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } // [Keto] Cleanup relations (Best effort) diff --git a/backend/internal/middleware/error_helper.go b/backend/internal/middleware/error_helper.go new file mode 100644 index 00000000..31388152 --- /dev/null +++ b/backend/internal/middleware/error_helper.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "baron-sso-backend/internal/response" + + "github.com/gofiber/fiber/v2" +) + +// errorJSON은 legacy error 필드를 유지하면서 status 기반 code를 함께 반환합니다. +func errorJSON(c *fiber.Ctx, status int, message string) error { + return response.Error(c, status, response.StatusCode(status), message) +} + +// errorJSONCode는 상태코드 매핑과 다른 명시 코드가 필요할 때 사용합니다. +func errorJSONCode(c *fiber.Ctx, status int, code, message string) error { + return response.Error(c, status, code, message) +} diff --git a/backend/internal/middleware/rbac.go b/backend/internal/middleware/rbac.go index 4b8df4fd..0175974b 100644 --- a/backend/internal/middleware/rbac.go +++ b/backend/internal/middleware/rbac.go @@ -25,7 +25,7 @@ func RequireKetoPermission(config RBACConfig, namespace, relation string) fiber. return func(c *fiber.Ctx) error { profile, err := config.AuthHandler.GetEnrichedProfile(c) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized (trace:rbac_keto)"}) + return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_keto)") } // Store profile in locals for further use in handlers @@ -49,7 +49,7 @@ func RequireKetoPermission(config RBACConfig, namespace, relation string) fiber. if objectID == "" { slog.Error("RBAC Keto check failed: missing object id", "path", c.Path()) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "missing object id for permission check"}) + return errorJSON(c, fiber.StatusBadRequest, "missing object id for permission check") } slog.Info("Performing Keto permission check", "userID", profile.ID, "namespace", namespace, "objectID", objectID, "relation", relation) @@ -63,12 +63,12 @@ func RequireKetoPermission(config RBACConfig, namespace, relation string) fiber. allowed, err := config.KetoService.CheckPermission(c.Context(), profile.ID, namespace, objectID, relation) if err != nil { slog.Error("Keto service error", "error", err, "userID", profile.ID, "objectID", objectID) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"}) + return errorJSON(c, fiber.StatusInternalServerError, "permission check error") } if !allowed { slog.Warn("Keto permission denied", "userID", profile.ID, "namespace", namespace, "objectID", objectID, "relation", relation) - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: keto permission denied for " + namespace + ":" + objectID}) + return errorJSON(c, fiber.StatusForbidden, "forbidden: keto permission denied for "+namespace+":"+objectID) } return c.Next() @@ -85,9 +85,7 @@ func RequireRole(config RBACConfig) fiber.Handler { profile, err := config.AuthHandler.GetEnrichedProfile(c) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "unauthorized (trace:rbac_role): " + err.Error(), - }) + return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_role): "+err.Error()) } // Store profile in locals for further use in handlers @@ -114,9 +112,7 @@ func RequireRole(config RBACConfig) fiber.Handler { "allowedRoles", config.AllowedRoles, "path", c.Path(), ) - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "forbidden: insufficient permissions", - }) + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions") } // Store profile in locals for further use in handlers @@ -136,7 +132,7 @@ func RequireTenantMatch(config RBACConfig) fiber.Handler { profile, err := config.AuthHandler.GetEnrichedProfile(c) if err != nil { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized (trace:rbac_match)"}) + return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_match)") } // Store profile in locals for further use in handlers @@ -174,13 +170,11 @@ func RequireTenantMatch(config RBACConfig) fiber.Handler { if !isAllowed { slog.Warn("Tenant match failed", "userID", profile.ID, "targetTenantID", targetTenantID) - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "forbidden: you do not have access to this tenant", - }) + return errorJSON(c, fiber.StatusForbidden, "forbidden: you do not have access to this tenant") } return c.Next() } - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden"}) + return errorJSON(c, fiber.StatusForbidden, "forbidden") } } diff --git a/docs/test-plan.md b/docs/test-plan.md index 5c6ab8f9..75984c1a 100644 --- a/docs/test-plan.md +++ b/docs/test-plan.md @@ -13,6 +13,7 @@ - Backend(Go): **104개** - UserFront(Flutter): **47개** - AdminFront/DevFront(Playwright): **4개** + - UserFront WASM Playwright E2E: **42개** ### Backend 패키지별 커버리지 - `cmd/server`: 2.6% @@ -29,6 +30,7 @@ - UserFront 테스트 전수 목록: `docs/test-plan/userfront-test-inventory.md` - AdminFront/DevFront E2E 전수 목록: `docs/test-plan/web-e2e-test-inventory.md` - UserFront WASM Playwright E2E 확장 계획: `docs/test-plan/userfront-wasm-e2e-expansion-plan.md` +- UserFront WASM Playwright E2E 전수 목록: `docs/test-plan/userfront-wasm-e2e-route-inventory.md` ## 4) 실행 커맨드 - Backend 전체 테스트: `cd backend && go test ./...` @@ -36,7 +38,8 @@ - UserFront 테스트: `cd userfront && flutter test` - AdminFront E2E: `cd adminfront && npm test` - DevFront E2E: `cd devfront && npm test` -- UserFront WASM E2E(계획): `docs/test-plan/userfront-wasm-e2e-expansion-plan.md` 기준으로 Playwright 워크스페이스를 추가한 뒤 실행 +- UserFront WASM E2E: `cd userfront-e2e && npm run test:wasm` +- UserFront WASM E2E(테스트만): `cd userfront-e2e && npm test` ## 5) 유지 원칙 - 신규 기능은 관련 테스트를 반드시 추가합니다. diff --git a/docs/test-plan/userfront-wasm-e2e-expansion-plan.md b/docs/test-plan/userfront-wasm-e2e-expansion-plan.md index 26c8d90a..ce5d9b58 100644 --- a/docs/test-plan/userfront-wasm-e2e-expansion-plan.md +++ b/docs/test-plan/userfront-wasm-e2e-expansion-plan.md @@ -58,12 +58,46 @@ - 범위 6 구현 - null-check 복구 라우팅 검증 -## 4) 완료 기준 +## 4) 현재 구현 상태 (2026-02-24) +- Phase 0: 완료 + - `userfront-e2e/` 워크스페이스 추가 + - 로컬 SPA fallback 서버(`scripts/serve-userfront-build.mjs`) 추가 + - 실행 커맨드: `cd userfront-e2e && npm run test:wasm` + - CI 잡 연결: `.gitea/workflows/code_check.yml`의 `userfront-e2e-tests` +- Phase 1: 완료 + - `tests/auth-routing.spec.ts` 추가 + - 구현 시나리오: + - 비로그인 `/ko` → `/ko/signin` 리다이렉트 + - 로그인 상태 `/ko` 진입 + 새로고침 후 `/ko/dashboard` 유지 + - 비로그인 `/ko/approve?ref=...` 진입 시 `notice=qr_login_required`와 함께 signin 이동 + - 로그인 상태 `/ko/approve?ref=...`에서 approve API 호출 후 dashboard 이동 +- Phase 2: 완료 + - `tests/password-and-reset.spec.ts` 추가 + - 구현 시나리오: + - 비밀번호 로그인 성공 시 dashboard 이동 + 토큰 저장 확인 + - 비밀번호 로그인 실패 시 코드 기반 에러(`password_or_email_mismatch`)가 client-log로 기록되는지 확인 + - reset-password 성공 시 signin 이동 확인 + - 참고: + - WASM 렌더링에서는 접근성/DOM selector가 제한되어 로그인/리셋 폼은 `flt-glass-pane` 좌표 기반 입력으로 검증 + - 전수 인벤토리: + - `docs/test-plan/userfront-wasm-e2e-route-inventory.md` + - 라우트 22개 + 기능 회귀 12개(총 42 테스트) 코드화 완료 + - 프로필 소속 회귀 강화: + - `tests/profile-department.spec.ts` 추가 + - 구현 시나리오: + - 소속 수정 후 blur 저장 요청 전송 + - 입력 후 즉시 새로고침 시 저장 요청 미전송 재현 + - 동일값/빈값 입력 시 저장 요청 미전송 + - 수정 후 새로고침 뒤 재수정 저장 요청 누락 방지 + +## 5) 완료 기준 - 핵심 인증 플로우(로그인/새로고침/리다이렉트/QR)가 Playwright 회귀군으로 자동화됩니다. - 프로덕션 이슈 재발 건은 재현 테스트가 먼저 추가됩니다. - PR에서 E2E 결과 링크(성공/실패 로그) 확인이 가능합니다. -## 5) 운영 원칙 +## 6) 운영 원칙 - 버그는 반드시 재현 테스트를 먼저 추가합니다. - 재현 테스트가 실패하는 상태를 확인한 뒤 수정합니다. - 수정 후 동일 테스트를 반복 실행해 안정 통과까지 완료합니다. +- 테스트 하네스는 단계별로 초기화/정리합니다. + - 예: `beforeEach`에서 auth/mock state 재시드, `afterEach`에서 route mock 해제(`page.unroute`) 및 누수 상태 정리 diff --git a/docs/test-plan/userfront-wasm-e2e-route-inventory.md b/docs/test-plan/userfront-wasm-e2e-route-inventory.md new file mode 100644 index 00000000..2cf45a27 --- /dev/null +++ b/docs/test-plan/userfront-wasm-e2e-route-inventory.md @@ -0,0 +1,59 @@ +# UserFront WASM E2E 라우트/기능 전수 인벤토리 + +- 기준 소스: `userfront/lib/main.dart` +- 목적: 라우트 전수 항목을 Playwright 테스트로 코드화하고 CI에서 상시 검증 +- 현재 구현 파일: + - `userfront-e2e/tests/route-inventory.spec.ts` + - `userfront-e2e/tests/auth-routing.spec.ts` + - `userfront-e2e/tests/password-and-reset.spec.ts` + - `userfront-e2e/tests/profile-department.spec.ts` + +## 1) 라우트 전수 (main.dart 기준) + +| ID | Route | 검증 상태 | 테스트 파일 | +|---|---|---|---| +| R01 | `/` | 비로그인 시 `/{locale}/signin` 리다이렉트 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R02 | `/:locale` (`/ko`) | 비로그인 `signin` / 로그인 `dashboard` 분기 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts`, `userfront-e2e/tests/auth-routing.spec.ts` | +| R03 | `/:locale/dashboard` | 비로그인 `signin` / 로그인 유지 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts`, `userfront-e2e/tests/auth-routing.spec.ts` | +| R04 | `/:locale/profile` | 비로그인 `signin` / 로그인 유지 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R05 | `/:locale/signin` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R06 | `/:locale/login` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R07 | `/:locale/consent` | challenge 유무 케이스 진입 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R08 | `/:locale/signup` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R09 | `/:locale/registration` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R10 | `/:locale/verify` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R11 | `/:locale/verify/:token` | verify 경로 진입/처리 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R12 | `/:locale/verification` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R13 | `/:locale/l/:shortCode` | short code 경로 진입/처리 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R14 | `/:locale/forgot-password` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R15 | `/:locale/recovery` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R16 | `/:locale/reset-password` | token 기반 진입 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts`, `userfront-e2e/tests/password-and-reset.spec.ts` | +| R17 | `/:locale/error` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R18 | `/:locale/settings` | 진입 가능 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R19 | `/:locale/approve` | 비로그인 `signin?notice=...` / 로그인 `dashboard` 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts`, `userfront-e2e/tests/auth-routing.spec.ts` | +| R20 | `/:locale/ql/:ref` | 비로그인 `signin?notice=...` / 로그인 `dashboard` 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R21 | `/:locale/scan` | 비로그인 `signin` / 로그인 진입 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | +| R22 | `/:locale/admin/users` | 비로그인 `signin` / 로그인 진입 검증 완료 | `userfront-e2e/tests/route-inventory.spec.ts` | + +## 2) 기능 회귀 (핵심) + +| ID | 기능 | 검증 상태 | 테스트 파일 | +|---|---|---|---| +| F01 | `/ko` 비로그인 리다이렉트 | 완료 | `userfront-e2e/tests/auth-routing.spec.ts` | +| F02 | 로그인 후 `/ko` + 새로고침 세션 유지 | 완료 | `userfront-e2e/tests/auth-routing.spec.ts` | +| F03 | approve 경로 비로그인 보호 | 완료 | `userfront-e2e/tests/auth-routing.spec.ts` | +| F04 | approve 경로 로그인 자동 승인 | 완료 | `userfront-e2e/tests/auth-routing.spec.ts` | +| F05 | 비밀번호 로그인 성공 | 완료 | `userfront-e2e/tests/password-and-reset.spec.ts` | +| F06 | 비밀번호 로그인 실패 코드 처리 | 완료 | `userfront-e2e/tests/password-and-reset.spec.ts` | +| F07 | 비밀번호 재설정 완료 후 signin 이동 | 완료 | `userfront-e2e/tests/password-and-reset.spec.ts` | +| F08 | 프로필 소속 수정 후 blur 저장 요청 전송 | 완료 | `userfront-e2e/tests/profile-department.spec.ts` | +| F09 | 프로필 소속 입력 후 즉시 새로고침 시 저장 요청 미전송(재현) | 완료 | `userfront-e2e/tests/profile-department.spec.ts` | +| F10 | 프로필 소속 동일값 입력 시 저장 요청 미전송 | 완료 | `userfront-e2e/tests/profile-department.spec.ts` | +| F11 | 프로필 소속 빈값 입력 시 저장 요청 미전송 | 완료 | `userfront-e2e/tests/profile-department.spec.ts` | +| F12 | 프로필 소속 수정 후 새로고침 뒤 재수정 저장 요청 누락 방지 | 완료 | `userfront-e2e/tests/profile-department.spec.ts` | + +## 3) 실행/CI + +- 로컬 실행: `cd userfront-e2e && npm run test:wasm` +- CI 워크플로우: `.gitea/workflows/code_check.yml`의 `userfront-e2e-tests` 잡에서 매 실행 검증 +- 현재 스위트 수량: 총 42 테스트(라우트 30 + 인증/리다이렉트 4 + 비밀번호/리셋 3 + 프로필 소속 5) diff --git a/docs/trouble-shooting/issue-303-login-link-shortcode-locale-route.md b/docs/trouble-shooting/issue-303-login-link-shortcode-locale-route.md new file mode 100644 index 00000000..b03b7ee7 --- /dev/null +++ b/docs/trouble-shooting/issue-303-login-link-shortcode-locale-route.md @@ -0,0 +1,47 @@ +# #303 로그인 링크/코드 진입 실패 (`/{locale}/l/{shortCode}`) 대응 + +## 요약 +- 로그인 링크 발송은 정상이나, 링크 클릭 후 로그인 검증 진입이 실패하는 사례가 있었습니다. +- 특히 locale prefix가 포함된 short-code 경로(`/{locale}/l/{shortCode}`)에서 재현되었습니다. +- 원인은 라우터의 public path 판별과 short-code 추출 로직이 locale prefix 경로를 고려하지 못한 점이었습니다. + +## 증상 +- 링크 클릭 URL이 `https://.../ko/l/AB123456` 형태일 때 로그인 검증이 자동 시작되지 않음 +- 비로그인 상태에서 해당 경로 접근 시 signin으로 리다이렉트되어 short-code 검증이 끊김 + +## 원인 +1. Public path 판별에서 `/l/` 경로가 제외되어 있었음 +2. `LoginScreen`의 short-code 추출이 `uri.pathSegments.first == 'l'`에만 의존 + - `/{locale}/l/{shortCode}`에서는 첫 세그먼트가 locale이므로 추출 실패 + +## 조치 내용 +1. 경로 정책 분리 +- `userfront/lib/features/auth/domain/login_link_route_policy.dart` 신규 추가 +- public path 판별(`isPublicAuthPath`)과 short-code 추출(`extractLoginShortCode`)을 공용화 + +2. 라우터 반영 +- `userfront/lib/main.dart` redirect에서 `isPublicAuthPath` 사용 +- `/l/` 경로를 public path로 허용 + +3. 로그인 화면 반영 +- `userfront/lib/features/auth/presentation/login_screen.dart`에서 + `extractLoginShortCode(Uri.base)`로 short-code를 추출하도록 변경 +- locale prefix 유무와 관계없이 short-code 검증 진입 가능 + +## 테스트 +### 재현 테스트 (Failing first) +- `flutter test test/login_link_route_policy_test.dart` +- 초기 실패 확인: + - localized short-code 추출 실패 + - localized short-code public path 판별 실패 + +### 수정 후 회귀 테스트 +- `flutter test test/login_link_route_policy_test.dart` 통과 +- `flutter test test/router_redirect_widget_test.dart` 통과 + +## 영향 범위 +- 링크/코드 로그인 진입 라우팅 (`/l/{shortCode}` 및 `/{locale}/l/{shortCode}`) +- 기존 `/verify`, `/signin`, `/login` 경로에는 동작 변화 없음 + +## 관련 이슈 +- Gitea: #303 `[bug][auth] 링크 클릭/코드 입력 로그인 실패 재현 및 수정` diff --git a/locales/en.toml b/locales/en.toml index 6740edd9..b6f0fdd6 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -18,6 +18,24 @@ saman = "Saman" [err.common] unknown = "An unknown error occurred." +[err.backend] +authorization_pending = "Authentication approval is still pending." +bad_request = "Please check your request." +conflict = "The request conflicts with the current state." +expired_token = "The token has expired." +forbidden = "This request is not allowed." +internal_error = "An internal error occurred while processing the request." +invalid_code = "The verification code is invalid." +invalid_or_expired_code = "The verification code is invalid or expired." +invalid_session = "The session is invalid." +invalid_session_reference = "The session reference is invalid." +not_found = "The requested authentication flow was not found." +not_supported = "This login method is not supported." +password_or_email_mismatch = "Email or password does not match." +rate_limited = "Too many requests. Please try again later." +service_unavailable = "The authentication service is currently unavailable." +slow_down = "Requests are too frequent. Please try again shortly." + [err.userfront] [err.userfront.auth_proxy] diff --git a/locales/ko.toml b/locales/ko.toml index 01417815..8e09067a 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -18,6 +18,24 @@ saman = "삼안" [err.common] unknown = "알 수 없는 오류가 발생했습니다." +[err.backend] +authorization_pending = "인증 승인이 아직 완료되지 않았습니다." +bad_request = "요청 값을 확인해 주세요." +conflict = "요청이 현재 상태와 충돌합니다." +expired_token = "토큰이 만료되었습니다." +forbidden = "요청이 허용되지 않습니다." +internal_error = "요청 처리 중 내부 오류가 발생했습니다." +invalid_code = "인증 코드가 올바르지 않습니다." +invalid_or_expired_code = "인증 코드가 유효하지 않거나 만료되었습니다." +invalid_session = "세션이 유효하지 않습니다." +invalid_session_reference = "세션 참조 정보가 유효하지 않습니다." +not_found = "요청한 인증 흐름을 찾을 수 없습니다." +not_supported = "지원하지 않는 로그인 방식입니다." +password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다." +rate_limited = "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요." +service_unavailable = "인증 서비스를 현재 사용할 수 없습니다." +slow_down = "요청 간격이 너무 빠릅니다. 잠시 후 다시 시도해 주세요." + [err.userfront] [err.userfront.auth_proxy] diff --git a/locales/template.toml b/locales/template.toml index 39c88db5..a284b6df 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -18,6 +18,24 @@ saman = "" [err.common] unknown = "" +[err.backend] +authorization_pending = "" +bad_request = "" +conflict = "" +expired_token = "" +forbidden = "" +internal_error = "" +invalid_code = "" +invalid_or_expired_code = "" +invalid_session = "" +invalid_session_reference = "" +not_found = "" +not_supported = "" +password_or_email_mismatch = "" +rate_limited = "" +service_unavailable = "" +slow_down = "" + [err.userfront] [err.userfront.auth_proxy] diff --git a/userfront-e2e/.gitignore b/userfront-e2e/.gitignore new file mode 100644 index 00000000..945fcd0d --- /dev/null +++ b/userfront-e2e/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +playwright-report/ +test-results/ diff --git a/userfront-e2e/README.md b/userfront-e2e/README.md new file mode 100644 index 00000000..b90b2702 --- /dev/null +++ b/userfront-e2e/README.md @@ -0,0 +1,29 @@ +# UserFront WASM E2E + +`userfront` WASM 빌드 산출물을 Playwright로 검증하는 테스트 워크스페이스입니다. + +## 실행 방법 + +1. 의존성 설치 +```bash +cd userfront-e2e +npm install +``` + +2. 테스트 실행(빌드 포함) +```bash +cd userfront-e2e +npm run test:wasm +``` + +3. 이미 빌드가 있을 때 테스트만 실행 +```bash +cd userfront-e2e +npm test +``` + +## 환경변수 + +- `BASE_URL`: 외부 배포 URL을 테스트할 때 사용합니다. 설정하면 로컬 정적 서버를 띄우지 않습니다. +- `PORT`: 로컬 정적 서버 포트 (기본 `4173`) +- `LOCALE`: 브라우저 locale (기본 `ko-KR`) diff --git a/userfront-e2e/package-lock.json b/userfront-e2e/package-lock.json new file mode 100644 index 00000000..829a0589 --- /dev/null +++ b/userfront-e2e/package-lock.json @@ -0,0 +1,111 @@ +{ + "name": "userfront-e2e", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "userfront-e2e", + "version": "0.1.0", + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/node": "^24.3.0", + "typescript": "^5.9.2" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/userfront-e2e/package.json b/userfront-e2e/package.json new file mode 100644 index 00000000..3688dfd5 --- /dev/null +++ b/userfront-e2e/package.json @@ -0,0 +1,18 @@ +{ + "name": "userfront-e2e", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "test": "playwright test", + "test:ui": "playwright test --ui", + "serve:build": "node ./scripts/serve-userfront-build.mjs", + "build:userfront:wasm": "cd ../userfront && flutter build web --wasm --release", + "test:wasm": "npm run build:userfront:wasm && npm test" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@types/node": "^24.3.0", + "typescript": "^5.9.2" + } +} diff --git a/userfront-e2e/playwright.config.ts b/userfront-e2e/playwright.config.ts new file mode 100644 index 00000000..d7e33047 --- /dev/null +++ b/userfront-e2e/playwright.config.ts @@ -0,0 +1,39 @@ +import { defineConfig, devices } from '@playwright/test'; + +const port = Number.parseInt(process.env.PORT ?? '4173', 10); +const defaultBaseUrl = `http://127.0.0.1:${port}`; +const baseURL = process.env.BASE_URL ?? defaultBaseUrl; +const reuseExistingServer = !process.env.CI; + +export default defineConfig({ + testDir: './tests', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'html', + use: { + baseURL, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + locale: process.env.LOCALE ?? 'ko-KR', + serviceWorkers: 'block', + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + webServer: process.env.BASE_URL + ? undefined + : { + command: 'node ./scripts/serve-userfront-build.mjs', + url: defaultBaseUrl, + reuseExistingServer, + timeout: 120_000, + }, +}); diff --git a/userfront-e2e/scripts/serve-userfront-build.mjs b/userfront-e2e/scripts/serve-userfront-build.mjs new file mode 100644 index 00000000..6f74f203 --- /dev/null +++ b/userfront-e2e/scripts/serve-userfront-build.mjs @@ -0,0 +1,68 @@ +import { createReadStream, existsSync, statSync } from 'node:fs'; +import { dirname, extname, join, normalize } from 'node:path'; +import { createServer } from 'node:http'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const root = normalize(join(__dirname, '../../userfront/build/web')); + +if (!existsSync(root) || !statSync(root).isDirectory()) { + console.error( + '[userfront-e2e] userfront/build/web not found. Run: cd userfront && flutter build web --wasm --release', + ); + process.exit(1); +} + +const port = Number.parseInt(process.env.PORT ?? '4173', 10); + +const contentTypes = { + '.css': 'text/css; charset=utf-8', + '.html': 'text/html; charset=utf-8', + '.ico': 'image/x-icon', + '.js': 'application/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.mjs': 'application/javascript; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml; charset=utf-8', + '.txt': 'text/plain; charset=utf-8', + '.wasm': 'application/wasm', + '.webmanifest': 'application/manifest+json; charset=utf-8', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +}; + +const server = createServer((req, res) => { + const url = new URL(req.url ?? '/', 'http://localhost'); + const pathname = decodeURIComponent(url.pathname); + const relative = pathname === '/' ? '/index.html' : pathname; + const candidate = normalize(join(root, relative)); + + if (!candidate.startsWith(root)) { + res.statusCode = 403; + res.end('Forbidden'); + return; + } + + let filePath = candidate; + + if (!existsSync(filePath) || statSync(filePath).isDirectory()) { + // Flutter web 라우팅 경로(`/ko`, `/ko/signin`)도 index.html로 fallback 처리 + filePath = join(root, 'index.html'); + } + + const ext = extname(filePath); + const contentType = contentTypes[ext] ?? 'application/octet-stream'; + + res.setHeader('Content-Type', contentType); + createReadStream(filePath) + .on('error', () => { + res.statusCode = 500; + res.end('Internal Server Error'); + }) + .pipe(res); +}); + +server.listen(port, '127.0.0.1', () => { + console.log(`[userfront-e2e] serving ${root} at http://127.0.0.1:${port}`); +}); diff --git a/userfront-e2e/tests/auth-routing.spec.ts b/userfront-e2e/tests/auth-routing.spec.ts new file mode 100644 index 00000000..802c2569 --- /dev/null +++ b/userfront-e2e/tests/auth-routing.spec.ts @@ -0,0 +1,143 @@ +import { expect, test, type Page, type Route } from '@playwright/test'; + +type MockOptions = { + sessionStatus?: number; + captureApprove?: (pendingRef: string | null) => void; +}; + +async function seedTokenLogin(page: Page): Promise { + await page.addInitScript(() => { + window.localStorage.setItem('baron_auth_token', 'e30.e30.e30'); + window.localStorage.setItem('baron_auth_provider', 'ory'); + window.localStorage.removeItem('baron_auth_cookie_mode'); + window.localStorage.removeItem('baron_auth_pending_provider'); + }); +} + +async function mockUserfrontApis( + page: Page, + options: MockOptions = {}, +): Promise { + const sessionStatus = options.sessionStatus ?? 200; + + await page.route('**/api/v1/**', async (route: Route) => { + const requestUrl = new URL(route.request().url()); + const path = requestUrl.pathname; + + if (path.endsWith('/api/v1/user/me')) { + if (sessionStatus === 200) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + id: 'e2e-user', + email: 'e2e@example.com', + name: 'E2E User', + phone: '+821012341234', + department: 'QA', + affiliationType: 'employee', + companyCode: 'BARON', + tenant: { + id: 'tenant-1', + name: 'Baron', + slug: 'baron', + description: 'E2E tenant', + }, + }), + }); + return; + } + + await route.fulfill({ + status: sessionStatus, + contentType: 'application/json', + body: JSON.stringify({ error: 'unauthorized' }), + }); + return; + } + + if (path.endsWith('/api/v1/user/rp/linked')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [] }), + }); + return; + } + + if (path.endsWith('/api/v1/audit/auth/timeline')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [], next_cursor: '' }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/qr/approve')) { + const body = route.request().postDataJSON() as { pendingRef?: string }; + options.captureApprove?.(body.pendingRef ?? null); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ok: true }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); +} + +test.describe('UserFront WASM auth routing', () => { + test('비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다', async ({ page }) => { + await mockUserfrontApis(page, { sessionStatus: 401 }); + + await page.goto('/ko'); + + await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); + }); + + test('로그인 상태 /ko 진입 후 새로고침해도 /ko/dashboard 를 유지한다', async ({ + page, + }) => { + await seedTokenLogin(page); + await mockUserfrontApis(page); + + await page.goto('/ko'); + await expect(page).toHaveURL(/\/ko\/dashboard$/); + + await page.reload(); + await expect(page).toHaveURL(/\/ko\/dashboard$/); + }); + + test('비로그인 /ko/approve 는 signin(+notice)으로 이동한다', async ({ page }) => { + await mockUserfrontApis(page, { sessionStatus: 401 }); + + await page.goto('/ko/approve?ref=e2e-ref'); + + await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/); + }); + + test('로그인 상태 /ko/approve 는 승인 API 호출 후 dashboard로 이동한다', async ({ + page, + }) => { + let approvedRef: string | null = null; + + await seedTokenLogin(page); + await mockUserfrontApis(page, { + captureApprove: (pendingRef) => { + approvedRef = pendingRef; + }, + }); + + await page.goto('/ko/approve?ref=e2e-approve-ref'); + + await expect(page).toHaveURL(/\/ko\/dashboard$/); + expect(approvedRef).toBe('e2e-approve-ref'); + }); +}); diff --git a/userfront-e2e/tests/password-and-reset.spec.ts b/userfront-e2e/tests/password-and-reset.spec.ts new file mode 100644 index 00000000..4bf89a37 --- /dev/null +++ b/userfront-e2e/tests/password-and-reset.spec.ts @@ -0,0 +1,257 @@ +import { expect, test, type Page, type Route } from '@playwright/test'; + +type RequestCapture = { + loginBody?: Record; + resetBody?: Record; + resetToken?: string | null; + clientLogs: string[]; +}; + +const SIGNIN_PASSWORD_TAB_X = 522; +const SIGNIN_TAB_Y = 158; +const SIGNIN_LOGIN_ID_X = 640; +const SIGNIN_LOGIN_ID_Y = 245; +const SIGNIN_PASSWORD_X = 640; +const SIGNIN_PASSWORD_Y = 311; +const SIGNIN_SUBMIT_X = 640; +const SIGNIN_SUBMIT_Y = 381; + +const RESET_NEW_PASSWORD_X = 640; +const RESET_NEW_PASSWORD_Y = 401; +const RESET_CONFIRM_PASSWORD_X = 640; +const RESET_CONFIRM_PASSWORD_Y = 464; +const RESET_SUBMIT_X = 640; +const RESET_SUBMIT_Y = 534; + +async function clickPasswordTab(page: Page): Promise { + await page.waitForTimeout(900); + const pane = page.locator('flt-glass-pane'); + await pane.click({ + position: { x: SIGNIN_PASSWORD_TAB_X, y: SIGNIN_TAB_Y }, + force: true, + }); + await page.waitForTimeout(120); + await pane.click({ + position: { x: SIGNIN_PASSWORD_TAB_X, y: SIGNIN_TAB_Y }, + 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 mockAuthApis(page: Page, capture: RequestCapture): Promise { + await page.route('**/api/v1/**', async (route: Route) => { + const requestUrl = new URL(route.request().url()); + const path = requestUrl.pathname; + + if (path.endsWith('/api/v1/auth/password/login')) { + capture.loginBody = (route.request().postDataJSON() ?? {}) as Record< + string, + unknown + >; + + const loginId = String(capture.loginBody.loginId ?? ''); + const password = String(capture.loginBody.password ?? ''); + if (loginId === 'e2e@example.com' && password === 'ValidPass1!') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + sessionJwt: 'e30.e30.e30', + provider: 'ory', + }), + }); + return; + } + + await route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: 'password_or_email_mismatch' }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/password/policy')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + minLength: 12, + minCharacterTypes: 3, + lowercase: true, + uppercase: true, + number: true, + nonAlphanumeric: true, + }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/password/reset/complete')) { + capture.resetBody = (route.request().postDataJSON() ?? {}) as Record< + string, + unknown + >; + capture.resetToken = requestUrl.searchParams.get('token'); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'ok' }), + }); + return; + } + + if (path.endsWith('/api/v1/client-log')) { + const payload = (route.request().postDataJSON() ?? {}) as { + message?: string; + }; + if (payload.message != null) { + capture.clientLogs.push(payload.message); + } + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ok: true }), + }); + return; + } + + if (path.endsWith('/api/v1/user/me')) { + const authHeader = route.request().headers()['authorization'] ?? ''; + if (!authHeader.startsWith('Bearer ')) { + await route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: 'unauthorized' }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + id: 'e2e-user', + email: 'e2e@example.com', + name: 'E2E User', + phone: '+821012341234', + department: 'QA', + affiliationType: 'employee', + companyCode: 'BARON', + tenant: { + id: 'tenant-1', + name: 'Baron', + slug: 'baron', + description: 'E2E tenant', + }, + }), + }); + return; + } + + if (path.endsWith('/api/v1/user/rp/linked')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [] }), + }); + return; + } + + if (path.endsWith('/api/v1/audit/auth/timeline')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [], next_cursor: '' }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); +} + +test.describe('UserFront WASM password login and reset', () => { + test('비밀번호 로그인 성공 시 dashboard로 이동하고 토큰을 저장한다', async ({ page }) => { + const capture: RequestCapture = { clientLogs: [] }; + await mockAuthApis(page, capture); + + await page.goto('/ko/signin'); + await clickPasswordTab(page); + await fillAt(page, SIGNIN_LOGIN_ID_X, SIGNIN_LOGIN_ID_Y, 'e2e@example.com'); + await fillAt(page, SIGNIN_PASSWORD_X, SIGNIN_PASSWORD_Y, 'ValidPass1!'); + await page.locator('flt-glass-pane').click({ + position: { x: SIGNIN_SUBMIT_X, y: SIGNIN_SUBMIT_Y }, + force: true, + }); + + await expect(page).toHaveURL(/\/ko\/dashboard$/); + + expect(capture.loginBody?.loginId).toBe('e2e@example.com'); + expect(capture.loginBody?.password).toBe('ValidPass1!'); + + const storedToken = await page.evaluate(() => + window.localStorage.getItem('baron_auth_token'), + ); + expect(storedToken).toBe('e30.e30.e30'); + }); + + test('비밀번호 로그인 실패 시 에러 코드를 사용자에게 표시한다', async ({ page }) => { + const capture: RequestCapture = { clientLogs: [] }; + await mockAuthApis(page, capture); + + await page.goto('/ko/signin'); + await clickPasswordTab(page); + await fillAt(page, SIGNIN_LOGIN_ID_X, SIGNIN_LOGIN_ID_Y, 'e2e@example.com'); + await fillAt(page, SIGNIN_PASSWORD_X, SIGNIN_PASSWORD_Y, 'WrongPass1!'); + await page.locator('flt-glass-pane').click({ + position: { x: SIGNIN_SUBMIT_X, y: SIGNIN_SUBMIT_Y }, + force: true, + }); + + await expect(page).toHaveURL(/\/ko\/signin$/); + await expect + .poll(() => + capture.clientLogs.some((message) => + message.includes('password_or_email_mismatch'), + ), + ) + .toBe(true); + }); + + test('reset-password에서 변경 성공 시 signin으로 이동한다', async ({ page }) => { + const capture: RequestCapture = { clientLogs: [] }; + await mockAuthApis(page, capture); + + await page.goto('/ko/reset-password?token=reset-token-e2e'); + await page.waitForTimeout(900); + await fillAt(page, RESET_NEW_PASSWORD_X, RESET_NEW_PASSWORD_Y, 'ValidPass1!A'); + await fillAt( + page, + RESET_CONFIRM_PASSWORD_X, + RESET_CONFIRM_PASSWORD_Y, + 'ValidPass1!A', + ); + await page.locator('flt-glass-pane').click({ + position: { x: RESET_SUBMIT_X, y: RESET_SUBMIT_Y }, + force: true, + }); + + await expect(page).toHaveURL(/\/ko\/signin$/); + expect(capture.resetToken).toBe('reset-token-e2e'); + expect(capture.resetBody?.newPassword).toBe('ValidPass1!A'); + }); +}); diff --git a/userfront-e2e/tests/profile-department.spec.ts b/userfront-e2e/tests/profile-department.spec.ts new file mode 100644 index 00000000..d3a9a914 --- /dev/null +++ b/userfront-e2e/tests/profile-department.spec.ts @@ -0,0 +1,275 @@ +import { expect, test, type Page, type Route } from '@playwright/test'; + +type ProfileState = { + department: string; + getMeCount: number; + putBodies: Array>; +}; + +const PROFILE_DEPARTMENT_EDIT_X = 1170; +const PROFILE_DEPARTMENT_EDIT_Y = 680; +const PROFILE_DEPARTMENT_INPUT_X = 110; +const PROFILE_DEPARTMENT_INPUT_Y = 685; +const PROFILE_BLUR_X = 200; +const PROFILE_BLUR_Y = 260; + +async function seedTokenLogin(page: Page): Promise { + await page.addInitScript(() => { + window.localStorage.setItem('baron_auth_token', 'e30.e30.e30'); + window.localStorage.setItem('baron_auth_provider', 'ory'); + window.localStorage.removeItem('baron_auth_cookie_mode'); + window.localStorage.removeItem('baron_auth_pending_provider'); + }); +} + +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 openDepartmentEditor(page: Page): Promise { + await page.locator('flt-glass-pane').click({ + position: { x: PROFILE_DEPARTMENT_EDIT_X, y: PROFILE_DEPARTMENT_EDIT_Y }, + force: true, + }); + await page.waitForTimeout(200); +} + +async function blurDepartmentEditor(page: Page): Promise { + await page.locator('flt-glass-pane').click({ + position: { x: PROFILE_BLUR_X, y: PROFILE_BLUR_Y }, + force: true, + }); + await page.waitForTimeout(250); +} + +async function mockProfileApis(page: Page, state: ProfileState): Promise { + await page.route('**/api/v1/**', async (route: Route) => { + const request = route.request(); + const requestUrl = new URL(request.url()); + const path = requestUrl.pathname; + const method = request.method().toUpperCase(); + + if (path.endsWith('/api/v1/user/me') && method === 'GET') { + const authHeader = request.headers()['authorization'] ?? ''; + if (!authHeader.startsWith('Bearer ')) { + await route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: 'unauthorized' }), + }); + return; + } + state.getMeCount += 1; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + id: 'e2e-user', + email: 'e2e@example.com', + name: 'E2E User', + phone: '+821012341234', + department: state.department, + affiliationType: 'employee', + companyCode: 'BARON', + tenant: { + id: 'tenant-1', + name: 'Baron', + slug: 'baron', + description: 'E2E tenant', + }, + }), + }); + return; + } + + if (path.endsWith('/api/v1/user/me') && method === 'PUT') { + const body = (request.postDataJSON() ?? {}) as Record; + state.putBodies.push(body); + const nextDepartment = String(body.department ?? '').trim(); + if (nextDepartment !== '') { + state.department = nextDepartment; + } + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + updatedAt: '2026-02-24T00:00:00Z', + }), + }); + return; + } + + if (path.endsWith('/api/v1/user/rp/linked')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [] }), + }); + return; + } + + if (path.endsWith('/api/v1/audit/auth/timeline')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [], next_cursor: '' }), + }); + return; + } + + if (path.endsWith('/api/v1/client-log')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ok: true }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ok: true }), + }); + }); +} + +async function openProfilePage(page: Page): Promise { + await page.goto('/ko/profile'); + await expect(page).toHaveURL(/\/ko\/profile$/); + await page.waitForTimeout(1200); +} + +async function waitForInitialProfileLoad(state: ProfileState): Promise { + await expect.poll(() => state.getMeCount).toBeGreaterThan(0); +} + +test.describe('UserFront WASM profile department editing', () => { + test.afterEach(async ({ page }) => { + await page.unroute('**/api/v1/**'); + }); + + test('소속 수정 후 포커스 아웃하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({ + page, + }) => { + const state: ProfileState = { + department: 'QA', + getMeCount: 0, + putBodies: [], + }; + await seedTokenLogin(page); + await mockProfileApis(page, state); + await openProfilePage(page); + await waitForInitialProfileLoad(state); + + await openDepartmentEditor(page); + await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Updated'); + await blurDepartmentEditor(page); + + await expect.poll(() => state.putBodies.length).toBe(1); + expect(state.putBodies[0]?.department).toBe('QA-Updated'); + expect(state.department).toBe('QA-Updated'); + + const getCountBeforeReload = state.getMeCount; + await page.reload(); + await expect(page).toHaveURL(/\/ko\/profile$/); + await expect.poll(() => state.getMeCount).toBeGreaterThan(getCountBeforeReload); + }); + + test('재현: 소속 입력만 하고 즉시 새로고침하면 저장 요청이 전송되지 않는다', async ({ + page, + }) => { + const state: ProfileState = { + department: 'QA', + getMeCount: 0, + putBodies: [], + }; + await seedTokenLogin(page); + await mockProfileApis(page, state); + await openProfilePage(page); + await waitForInitialProfileLoad(state); + + await openDepartmentEditor(page); + await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Repro'); + + await page.reload(); + await expect(page).toHaveURL(/\/ko\/profile$/); + expect(state.putBodies).toHaveLength(0); + expect(state.department).toBe('QA'); + }); + + test('소속에 기존값을 그대로 입력하고 포커스 아웃하면 저장 요청이 전송되지 않는다', async ({ + page, + }) => { + const state: ProfileState = { + department: 'QA', + getMeCount: 0, + putBodies: [], + }; + await seedTokenLogin(page); + await mockProfileApis(page, state); + await openProfilePage(page); + await waitForInitialProfileLoad(state); + + await openDepartmentEditor(page); + await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA'); + await blurDepartmentEditor(page); + + expect(state.putBodies).toHaveLength(0); + }); + + test('소속을 빈 값으로 입력하면 저장 요청이 전송되지 않는다', async ({ page }) => { + const state: ProfileState = { + department: 'QA', + getMeCount: 0, + putBodies: [], + }; + await seedTokenLogin(page); + await mockProfileApis(page, state); + await openProfilePage(page); + await waitForInitialProfileLoad(state); + + await openDepartmentEditor(page); + await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, ''); + await blurDepartmentEditor(page); + + expect(state.putBodies).toHaveLength(0); + expect(state.department).toBe('QA'); + }); + + test('소속을 수정한 뒤 새로고침 후 다시 수정해도 저장 요청이 누락되지 않는다', async ({ page }) => { + const state: ProfileState = { + department: 'QA', + getMeCount: 0, + putBodies: [], + }; + await seedTokenLogin(page); + await mockProfileApis(page, state); + await openProfilePage(page); + await waitForInitialProfileLoad(state); + + await openDepartmentEditor(page); + await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-1'); + await blurDepartmentEditor(page); + await expect.poll(() => state.putBodies.length).toBe(1); + + await page.reload(); + await expect(page).toHaveURL(/\/ko\/profile$/); + await page.waitForTimeout(1200); + + await openDepartmentEditor(page); + await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-2'); + await blurDepartmentEditor(page); + await expect.poll(() => state.putBodies.length).toBe(2); + + expect(state.putBodies[0]?.department).toBe('QA-1'); + expect(state.putBodies[1]?.department).toBe('QA-2'); + expect(state.department).toBe('QA-2'); + }); +}); diff --git a/userfront-e2e/tests/route-inventory.spec.ts b/userfront-e2e/tests/route-inventory.spec.ts new file mode 100644 index 00000000..79f7b4b2 --- /dev/null +++ b/userfront-e2e/tests/route-inventory.spec.ts @@ -0,0 +1,320 @@ +import { expect, test, type Page, type Route } from '@playwright/test'; + +async function seedTokenLogin(page: Page): Promise { + await page.addInitScript(() => { + window.localStorage.setItem('baron_auth_token', 'e30.e30.e30'); + window.localStorage.setItem('baron_auth_provider', 'ory'); + window.localStorage.removeItem('baron_auth_cookie_mode'); + window.localStorage.removeItem('baron_auth_pending_provider'); + }); +} + +async function mockInventoryApis(page: Page): Promise { + await page.route('**/api/v1/**', async (route: Route) => { + const requestUrl = new URL(route.request().url()); + const path = requestUrl.pathname; + const method = route.request().method().toUpperCase(); + + if (path.endsWith('/api/v1/user/me')) { + const authHeader = route.request().headers()['authorization'] ?? ''; + if (authHeader.startsWith('Bearer ')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + id: 'e2e-user', + email: 'e2e@example.com', + name: 'E2E User', + phone: '+821012341234', + department: 'QA', + affiliationType: 'employee', + companyCode: 'BARON', + tenant: { + id: 'tenant-1', + name: 'Baron', + slug: 'baron', + description: 'E2E tenant', + }, + }), + }); + return; + } + + await route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ error: 'unauthorized' }), + }); + return; + } + + if (path.endsWith('/api/v1/user/rp/linked')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [] }), + }); + return; + } + + if (path.endsWith('/api/v1/audit/auth/timeline')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [], next_cursor: '' }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/password/policy')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + minLength: 12, + minCharacterTypes: 3, + lowercase: true, + uppercase: true, + number: true, + nonAlphanumeric: true, + }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/magic-link/verify')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'approved' }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/login/code/verify')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'approved' }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/login/code/verify-short')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'approved' }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/consent') && method === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + client: { + client_name: 'E2E Client', + client_id: 'e2e-client', + }, + requested_scope: ['openid'], + scope_details: { + openid: { + description: 'OpenID', + mandatory: true, + }, + }, + }), + }); + return; + } + + if (path.endsWith('/api/v1/auth/qr/approve')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ok: true }), + }); + return; + } + + if (path.endsWith('/api/v1/client-log')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ok: true }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); +} + +test.describe('UserFront WASM route inventory (unauth)', () => { + test.beforeEach(async ({ page }) => { + await mockInventoryApis(page); + }); + + test('route: /', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveURL(/\/(ko|en)\/signin(?:\?.*)?$/); + }); + + test('route: /ko', async ({ page }) => { + await page.goto('/ko'); + await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); + }); + + test('route: /ko/dashboard', async ({ page }) => { + await page.goto('/ko/dashboard'); + await expect(page).toHaveURL(/\/ko\/signin$/); + }); + + test('route: /ko/profile', async ({ page }) => { + await page.goto('/ko/profile'); + await expect(page).toHaveURL(/\/ko\/signin$/); + }); + + test('route: /ko/admin/users', async ({ page }) => { + await page.goto('/ko/admin/users'); + await expect(page).toHaveURL(/\/ko\/signin$/); + }); + + test('route: /ko/scan', async ({ page }) => { + await page.goto('/ko/scan'); + await expect(page).toHaveURL(/\/ko\/signin$/); + }); + + test('route: /ko/signin', async ({ page }) => { + await page.goto('/ko/signin'); + await expect(page).toHaveURL(/\/ko\/signin$/); + }); + + test('route: /ko/login', async ({ page }) => { + await page.goto('/ko/login'); + await expect(page).toHaveURL(/\/ko\/login$/); + }); + + test('route: /ko/signup', async ({ page }) => { + await page.goto('/ko/signup'); + await expect(page).toHaveURL(/\/ko\/signup$/); + }); + + test('route: /ko/registration', async ({ page }) => { + await page.goto('/ko/registration'); + await expect(page).toHaveURL(/\/ko\/registration$/); + }); + + test('route: /ko/verify', async ({ page }) => { + await page.goto('/ko/verify'); + await expect(page).toHaveURL(/\/ko\/verify$/); + }); + + test('route: /ko/verify/:token', async ({ page }) => { + await page.goto('/ko/verify/e2e-token'); + await expect(page).toHaveURL(/\/ko\/verify\/e2e-token$/); + }); + + test('route: /ko/verification', async ({ page }) => { + await page.goto('/ko/verification'); + await expect(page).toHaveURL(/\/ko\/verification$/); + }); + + test('route: /ko/l/:shortCode', async ({ page }) => { + await page.goto('/ko/l/AB123456'); + await expect(page).toHaveURL(/\/ko\/l\/AB123456$/); + }); + + test('route: /ko/forgot-password', async ({ page }) => { + await page.goto('/ko/forgot-password'); + await expect(page).toHaveURL(/\/ko\/forgot-password$/); + }); + + test('route: /ko/recovery', async ({ page }) => { + await page.goto('/ko/recovery'); + await expect(page).toHaveURL(/\/ko\/recovery$/); + }); + + test('route: /ko/reset-password', async ({ page }) => { + await page.goto('/ko/reset-password?token=e2e-reset-token'); + await expect(page).toHaveURL(/\/ko\/reset-password\?token=e2e-reset-token$/); + }); + + test('route: /ko/error', async ({ page }) => { + await page.goto('/ko/error?error=invalid_request'); + await expect(page).toHaveURL(/\/ko\/error\?error=invalid_request$/); + }); + + test('route: /ko/settings', async ({ page }) => { + await page.goto('/ko/settings'); + await expect(page).toHaveURL(/\/ko\/settings$/); + }); + + test('route: /ko/consent (missing challenge)', async ({ page }) => { + await page.goto('/ko/consent'); + await expect(page).toHaveURL(/\/ko\/consent$/); + }); + + test('route: /ko/consent?consent_challenge=...', async ({ page }) => { + await page.goto('/ko/consent?consent_challenge=e2e-consent'); + await expect(page).toHaveURL(/\/ko\/consent\?consent_challenge=e2e-consent$/); + }); + + test('route: /ko/approve?ref=...', async ({ page }) => { + await page.goto('/ko/approve?ref=e2e-ref'); + await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/); + }); + + test('route: /ko/ql/:ref', async ({ page }) => { + await page.goto('/ko/ql/e2e-ref'); + await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/); + }); +}); + +test.describe('UserFront WASM route inventory (authed)', () => { + test.beforeEach(async ({ page }) => { + await seedTokenLogin(page); + await mockInventoryApis(page); + }); + + test('route: /ko -> /ko/dashboard', async ({ page }) => { + await page.goto('/ko'); + await expect(page).toHaveURL(/\/ko\/dashboard$/); + }); + + test('route: /ko/dashboard', async ({ page }) => { + await page.goto('/ko/dashboard'); + await expect(page).toHaveURL(/\/ko\/dashboard$/); + }); + + test('route: /ko/profile', async ({ page }) => { + await page.goto('/ko/profile'); + await expect(page).toHaveURL(/\/ko\/profile$/); + }); + + test('route: /ko/admin/users', async ({ page }) => { + await page.goto('/ko/admin/users'); + await expect(page).toHaveURL(/\/ko\/admin\/users$/); + }); + + test('route: /ko/scan', async ({ page }) => { + await page.goto('/ko/scan'); + await expect(page).toHaveURL(/\/ko\/scan$/); + }); + + test('route: /ko/approve?ref=... -> /ko/dashboard', async ({ page }) => { + await page.goto('/ko/approve?ref=e2e-ref'); + await expect(page).toHaveURL(/\/ko\/dashboard$/); + }); + + test('route: /ko/ql/:ref -> /ko/dashboard', async ({ page }) => { + await page.goto('/ko/ql/e2e-ref'); + await expect(page).toHaveURL(/\/ko\/dashboard$/); + }); +}); diff --git a/userfront-e2e/tsconfig.json b/userfront-e2e/tsconfig.json new file mode 100644 index 00000000..1bde1f2a --- /dev/null +++ b/userfront-e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node", "@playwright/test"], + "strict": true, + "noEmit": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["playwright.config.ts", "tests/**/*.ts", "scripts/**/*.mjs"] +} diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 20824c45..37f47071 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -54,12 +54,12 @@ empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." error = "연동 정보를 불러오지 못했습니다." [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} 연동이 해지되었습니다." diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index 59cd813b..43f51370 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -1361,6 +1361,9 @@ class _LoginScreenState extends ConsumerState child: Column( children: [ TextField( + key: const ValueKey( + 'password_login_id_input', + ), controller: _passwordLoginIdController, decoration: InputDecoration( labelText: tr( @@ -1375,6 +1378,9 @@ class _LoginScreenState extends ConsumerState ), const SizedBox(height: 16), TextField( + key: const ValueKey( + 'password_login_password_input', + ), controller: _passwordController, obscureText: true, decoration: InputDecoration( @@ -1390,6 +1396,9 @@ class _LoginScreenState extends ConsumerState ), const SizedBox(height: 24), FilledButton( + key: const ValueKey( + 'password_login_submit_button', + ), onPressed: _handlePasswordLogin, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(50), diff --git a/userfront/lib/features/auth/presentation/reset_password_screen.dart b/userfront/lib/features/auth/presentation/reset_password_screen.dart index d2ccd716..ab470478 100644 --- a/userfront/lib/features/auth/presentation/reset_password_screen.dart +++ b/userfront/lib/features/auth/presentation/reset_password_screen.dart @@ -192,6 +192,7 @@ class _ResetPasswordScreenState extends State { ), const SizedBox(height: 40), TextFormField( + key: const ValueKey('reset_password_new_input'), controller: _passwordController, obscureText: _isPasswordObscured, decoration: InputDecoration( @@ -263,6 +264,7 @@ class _ResetPasswordScreenState extends State { ), const SizedBox(height: 16), TextFormField( + key: const ValueKey('reset_password_confirm_input'), controller: _confirmPasswordController, obscureText: _isConfirmPasswordObscured, decoration: InputDecoration( @@ -292,6 +294,7 @@ class _ResetPasswordScreenState extends State { ), const SizedBox(height: 24), FilledButton( + key: const ValueKey('reset_password_submit_button'), onPressed: _isLoading ? null : _handlePasswordReset, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(50), diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index e89e1cfe..3e30856f 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -31,6 +32,9 @@ class _DashboardScreenState extends ConsumerState { static const _surface = Colors.white; static const _border = Color(0xFFE5E7EB); static const _subtle = Color(0xFFF7F8FA); + static const double _historySessionMinWidth = 92; + static const double _historyOtherColumnsBaselineWidth = 780; + static const int _historySessionMinVisibleChars = 8; final ScrollController _pageScrollController = ScrollController(); final ScrollController _rpScrollController = ScrollController(); @@ -1370,6 +1374,9 @@ class _DashboardScreenState extends ConsumerState { children: [ LayoutBuilder( builder: (context, constraints) { + final sessionColumnWidth = _historySessionColumnWidth( + constraints.maxWidth, + ); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: ConstrainedBox( @@ -1379,10 +1386,13 @@ class _DashboardScreenState extends ConsumerState { horizontalMargin: 12, columns: [ DataColumn( - label: Text( - tr( - 'ui.userfront.audit.table.session_id', - fallback: 'Session ID', + label: SizedBox( + width: sessionColumnWidth, + child: Text( + tr( + 'ui.userfront.audit.table.session_id', + fallback: 'Session ID', + ), ), ), ), @@ -1426,10 +1436,14 @@ class _DashboardScreenState extends ConsumerState { return DataRow( cells: [ DataCell( - _selectableText( - log.sessionId.isEmpty - ? tr('ui.common.hyphen', fallback: '-') - : log.sessionId, + SizedBox( + width: sessionColumnWidth, + child: _buildHistorySessionIdCell( + log.sessionId.isEmpty + ? tr('ui.common.hyphen', fallback: '-') + : log.sessionId, + sessionColumnWidth, + ), ), ), DataCell( @@ -1474,6 +1488,36 @@ class _DashboardScreenState extends ConsumerState { ); } + double _historySessionColumnWidth(double maxWidth) { + return math.max( + _historySessionMinWidth, + maxWidth - _historyOtherColumnsBaselineWidth, + ); + } + + String _compactSessionId(String sessionId) { + if (sessionId.length <= _historySessionMinVisibleChars) { + return sessionId; + } + return '${sessionId.substring(0, _historySessionMinVisibleChars)}...'; + } + + Widget _buildHistorySessionIdCell(String sessionId, double columnWidth) { + final compactMode = columnWidth <= _historySessionMinWidth + 0.5; + final displayText = compactMode ? _compactSessionId(sessionId) : sessionId; + final textWidget = Text( + displayText, + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, + ); + + if (displayText == sessionId) { + return textWidget; + } + return Tooltip(message: sessionId, child: textWidget); + } + Widget _buildHistoryList(AuthTimelineState state) { return _buildHistoryContainer( child: Column( diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index 861a7b88..82e9b196 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -41,6 +41,7 @@ class _ProfilePageState extends ConsumerState { bool _phoneTouched = false; bool _phoneCodeTouched = false; bool _isSavingField = false; + String? _skipAutoSaveField; String _initialPhone = ''; bool _isPhoneChanged = false; @@ -354,6 +355,10 @@ class _ProfilePageState extends ConsumerState { void _autoSaveIfEditing(UserProfile profile, String field) { if (_editingField != field) return; + if (_skipAutoSaveField == field) { + _skipAutoSaveField = null; + return; + } if (_isVerifying) return; if (_isSavingField) return; if (!_hasFieldChanged(profile, field)) { @@ -375,6 +380,10 @@ class _ProfilePageState extends ConsumerState { void _handlePhoneFocusChange(UserProfile profile) { if (_editingField != 'phone') return; + if (_skipAutoSaveField == 'phone') { + _skipAutoSaveField = null; + return; + } if (_isVerifying) return; if (_isSavingField) return; if (_phoneFocus.hasFocus || _phoneCodeFocus.hasFocus) return; @@ -704,6 +713,7 @@ class _ProfilePageState extends ConsumerState { title: Text(label), subtitle: Text(displayValue), trailing: TextButton( + key: Key('profile-$field-edit-button'), onPressed: isUpdating ? null : () => _startEditing(field, profile), child: Text(tr('ui.common.edit')), ), @@ -720,6 +730,7 @@ class _ProfilePageState extends ConsumerState { children: [ Expanded( child: TextField( + key: Key('profile-$field-input'), controller: controller, focusNode: field == 'name' ? _nameFocus : _departmentFocus, textInputAction: TextInputAction.done, @@ -731,9 +742,15 @@ class _ProfilePageState extends ConsumerState { ), ), const SizedBox(width: 12), - OutlinedButton( - onPressed: isUpdating ? null : () => _cancelEditing(profile), - child: Text(tr('ui.common.cancel')), + Listener( + onPointerDown: (_) { + _skipAutoSaveField = field; + }, + child: OutlinedButton( + key: Key('profile-$field-cancel-button'), + onPressed: isUpdating ? null : () => _cancelEditing(profile), + child: Text(tr('ui.common.cancel')), + ), ), ], ), @@ -796,9 +813,14 @@ class _ProfilePageState extends ConsumerState { ), ), const SizedBox(width: 8), - OutlinedButton( - onPressed: isUpdating ? null : () => _cancelEditing(profile), - child: Text(tr('ui.common.cancel')), + Listener( + onPointerDown: (_) { + _skipAutoSaveField = 'phone'; + }, + child: OutlinedButton( + onPressed: isUpdating ? null : () => _cancelEditing(profile), + child: Text(tr('ui.common.cancel')), + ), ), ], ), diff --git a/userfront/test/profile_notifier_persistence_test.dart b/userfront/test/profile_notifier_persistence_test.dart new file mode 100644 index 00000000..f41b5186 --- /dev/null +++ b/userfront/test/profile_notifier_persistence_test.dart @@ -0,0 +1,112 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:userfront/features/profile/data/models/user_profile_model.dart'; +import 'package:userfront/features/profile/data/repositories/profile_repository.dart'; +import 'package:userfront/features/profile/domain/notifiers/profile_notifier.dart'; + +class _FakeProfileRepository extends ProfileRepository { + _FakeProfileRepository({ + required this.initialProfile, + required this.persistUpdate, + }) : _profile = initialProfile; + + final UserProfile initialProfile; + final bool persistUpdate; + UserProfile _profile; + + int updateCount = 0; + String? lastRequestedDepartment; + + @override + Future getMyProfile() async { + return _profile; + } + + @override + Future updateMyProfile({ + required String name, + required String phone, + required String department, + }) async { + updateCount += 1; + lastRequestedDepartment = department; + if (!persistUpdate) { + return; + } + _profile = _profile.copyWith( + name: name, + phone: phone, + department: department, + ); + } +} + +UserProfile _seedProfile({required String department}) { + return UserProfile( + id: 'user-1', + email: 'qa@example.com', + name: 'QA User', + phone: '01012345678', + department: department, + affiliationType: 'employee', + companyCode: 'BARON', + ); +} + +void main() { + test('재현: 저장소가 소속 변경을 반영하지 않으면 loadProfile 이후 이전 값으로 보인다', () async { + final repository = _FakeProfileRepository( + initialProfile: _seedProfile(department: 'Old Dept'), + persistUpdate: false, + ); + final container = ProviderContainer( + overrides: [profileRepositoryProvider.overrideWithValue(repository)], + ); + addTearDown(container.dispose); + + final initial = await container.read(profileProvider.future); + expect(initial?.department, 'Old Dept'); + + await container + .read(profileProvider.notifier) + .updateProfile( + name: 'QA User', + phone: '01012345678', + department: 'New Dept', + ); + + expect(repository.updateCount, 1); + expect(repository.lastRequestedDepartment, 'New Dept'); + + await container.read(profileProvider.notifier).loadProfile(); + expect(container.read(profileProvider).value?.department, 'Old Dept'); + }); + + test('소속 변경이 저장소에 반영되면 loadProfile 이후에도 변경값이 유지된다', () async { + final repository = _FakeProfileRepository( + initialProfile: _seedProfile(department: 'Old Dept'), + persistUpdate: true, + ); + final container = ProviderContainer( + overrides: [profileRepositoryProvider.overrideWithValue(repository)], + ); + addTearDown(container.dispose); + + final initial = await container.read(profileProvider.future); + expect(initial?.department, 'Old Dept'); + + await container + .read(profileProvider.notifier) + .updateProfile( + name: 'QA User', + phone: '01012345678', + department: 'New Dept', + ); + + expect(repository.updateCount, 1); + expect(repository.lastRequestedDepartment, 'New Dept'); + + await container.read(profileProvider.notifier).loadProfile(); + expect(container.read(profileProvider).value?.department, 'New Dept'); + }); +} From aeb418fb9fac5d3bb62b5d4870d8022a535f9661 Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Tue, 24 Feb 2026 15:38:55 +0900 Subject: [PATCH 02/22] feat: add env-aware client log policy and const lint fixes --- README.md | 16 ++ backend/cmd/server/main.go | 41 ++--- backend/internal/logger/client_log_policy.go | 143 ++++++++++++++++++ .../internal/logger/client_log_policy_test.go | 79 ++++++++++ docs/AGENTS.md | 9 ++ docs/client-log-policy.md | 67 ++++++++ .../lib/core/services/auth_proxy_service.dart | 19 ++- userfront/lib/core/services/log_policy.dart | 123 +++++++++++++++ .../lib/core/services/logger_service.dart | 42 ++++- .../presentation/forgot_password_screen.dart | 5 +- .../presentation/login_success_screen.dart | 5 +- .../presentation/reset_password_screen.dart | 2 +- .../presentation/pages/profile_page.dart | 83 ++++++++-- userfront/test/log_policy_test.dart | 114 ++++++++++++++ 14 files changed, 701 insertions(+), 47 deletions(-) create mode 100644 backend/internal/logger/client_log_policy.go create mode 100644 backend/internal/logger/client_log_policy_test.go create mode 100644 docs/client-log-policy.md create mode 100644 userfront/lib/core/services/log_policy.dart create mode 100644 userfront/test/log_policy_test.dart diff --git a/README.md b/README.md index cbf08d14..0378e249 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,22 @@ USERFRONT_URL=https://sso.example.com - `KRATOS_ALLOWED_RETURN_URLS_EXTRA`: 추가 허용 return URL (선택) - 빈값: `[]` - 다중값: `["https://a.example.com/callback","https://b.example.com/callback"]` 또는 `https://a.example.com/callback,https://b.example.com/callback` +- `CLIENT_LOG_DEBUG`: 클라이언트 로그 디버그 모드 강제 (기본: 비운영 `true`, 운영 `false`) + - 운영(`APP_ENV=production|prod`)에서 `true|1|on|yes` 설정 시 `INFO/DEBUG` 클라이언트 로그 수집 허용 + - 미설정(기본) 시 운영에서는 `WARN/ERROR`만 수집 +- `USERFRONT_DEBUG_LOG`: UserFront 측 디버그 로그 fallback 플래그 + - `CLIENT_LOG_DEBUG`가 없을 때만 UserFront에서 대체로 참조 + +### 클라이언트 로그 정책 (중요) +- 기본 원칙: 운영 환경에서는 민감정보 보호를 우선하며, 과도한 로그 수집을 제한합니다. +- 환경별 동작: + - 비운영(`dev/local/stage` 등): 디버그 로그 허용 (`INFO/DEBUG/WARN/ERROR`) + - 운영(`production/prod`) + `CLIENT_LOG_DEBUG` 미설정: `WARN/ERROR`만 수집 + - 운영(`production/prod`) + `CLIENT_LOG_DEBUG=true`: 디버그 로그 허용 +- 민감정보는 환경과 무관하게 마스킹합니다. + - 예: `password`, `newPassword`, `token`, `authorization`, `cookie`, `sessionJwt` + - 문자열 패턴(`token=...`, `authorization:...`, JSON body 내 민감 key)도 마스킹 +- 상세 정책 문서: `docs/client-log-policy.md` ### `.env` 작성 후 권장 점검 ```bash diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 2d019cc7..8472373f 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -277,6 +277,8 @@ func main() { // 3. Initialize Fiber appEnv := getEnv("APP_ENV", "dev") + clientLogDebugFlag := getEnv("CLIENT_LOG_DEBUG", "") + clientDebugEnabled := logger.ClientDebugEnabled(appEnv, clientLogDebugFlag) app := fiber.New(fiber.Config{ AppName: "Baron SSO Backend", DisableStartupMessage: true, // Clean logs @@ -367,6 +369,10 @@ func main() { } else { slog.Info("🔒 API Docs disabled in production") } + slog.Info("Client log policy configured", + "app_env", appEnv, + "client_debug_enabled", clientDebugEnabled, + ) // Routes app.Get("/", func(c *fiber.Ctx) error { @@ -630,12 +636,20 @@ func main() { if err := c.BodyParser(&req); err != nil { return c.SendStatus(fiber.StatusBadRequest) } + if !logger.ShouldAcceptClientLog(appEnv, clientLogDebugFlag, req.Level) { + return c.SendStatus(fiber.StatusOK) + } + level := logger.NormalizeClientLogLevel(req.Level) + if level == slog.LevelInfo && logger.ShouldFilterNoisyClientInfo(appEnv, clientLogDebugFlag, req.Message) { + return c.SendStatus(fiber.StatusOK) + } // Prepare attributes for flattening attrs := []any{ slog.String("source", "client"), } - for k, v := range req.Data { + sanitizedData := logger.SanitizeClientLogData(req.Data) + for k, v := range sanitizedData { // Skip svc if it's already set by the global logger to avoid confusion, // or keep it as client_svc if k == "svc" { @@ -644,30 +658,7 @@ func main() { attrs = append(attrs, slog.Any(k, v)) } } - - // Map and log with correct level - var level slog.Level - switch req.Level { - case "SEVERE", "ERROR": - level = slog.LevelError - case "WARNING", "WARN": - level = slog.LevelWarn - default: - level = slog.LevelInfo - } - - // Filter out noisy client navigation logs - if level == slog.LevelInfo { - msg := strings.ToLower(req.Message) - if strings.Contains(msg, "navigating to") || - strings.Contains(msg, "going to") || - strings.Contains(msg, "redirecting to") || - strings.Contains(msg, "full paths for routes") { - return c.SendStatus(fiber.StatusOK) - } - } - - slog.Log(c.Context(), level, req.Message, attrs...) + slog.Log(c.Context(), level, logger.SanitizeClientLogMessage(req.Message), attrs...) return c.SendStatus(fiber.StatusOK) }) diff --git a/backend/internal/logger/client_log_policy.go b/backend/internal/logger/client_log_policy.go new file mode 100644 index 00000000..c4be699b --- /dev/null +++ b/backend/internal/logger/client_log_policy.go @@ -0,0 +1,143 @@ +package logger + +import ( + "log/slog" + "regexp" + "strings" +) + +var sensitiveClientLogKeys = map[string]struct{}{ + "password": {}, + "currentpassword": {}, + "newpassword": {}, + "oldpassword": {}, + "token": {}, + "accesstoken": {}, + "refreshtoken": {}, + "secret": {}, + "clientsecret": {}, + "authorization": {}, + "cookie": {}, + "setcookie": {}, + "verificationcode": {}, + "code": {}, + "loginchallenge": {}, + "loginverifier": {}, + "sessionjwt": {}, + "accessjwt": {}, + "refreshjwt": {}, +} + +var ( + logJSONSensitivePattern = regexp.MustCompile(`(?i)"(password|currentpassword|newpassword|oldpassword|token|accesstoken|refreshtoken|secret|clientsecret|authorization|cookie|setcookie|verificationcode|code|loginchallenge|loginverifier|sessionjwt|accessjwt|refreshjwt)"\s*:\s*"[^"]*"`) + logKVPattern = regexp.MustCompile(`(?i)\b(password|current_password|currentpassword|new_password|newpassword|old_password|oldpassword|token|access_token|accesstoken|refresh_token|refreshtoken|authorization|cookie|session_jwt|sessionjwt|access_jwt|accessjwt|refresh_jwt|refreshjwt)\b\s*[:=]\s*([^\s,;]+)`) +) + +func IsProductionEnv(appEnv string) bool { + env := strings.ToLower(strings.TrimSpace(appEnv)) + return env == "prod" || env == "production" +} + +func parseBoolFlag(raw string) bool { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "1", "true", "yes", "y", "on": + return true + default: + return false + } +} + +func ClientDebugEnabled(appEnv, productionDebugFlag string) bool { + if !IsProductionEnv(appEnv) { + return true + } + return parseBoolFlag(productionDebugFlag) +} + +func NormalizeClientLogLevel(level string) slog.Level { + switch strings.ToUpper(strings.TrimSpace(level)) { + case "SEVERE", "ERROR": + return slog.LevelError + case "WARNING", "WARN": + return slog.LevelWarn + case "DEBUG", "FINE", "TRACE": + return slog.LevelDebug + default: + return slog.LevelInfo + } +} + +func ShouldAcceptClientLog(appEnv, productionDebugFlag, level string) bool { + if ClientDebugEnabled(appEnv, productionDebugFlag) { + return true + } + return NormalizeClientLogLevel(level) >= slog.LevelWarn +} + +func ShouldFilterNoisyClientInfo(appEnv, productionDebugFlag, message string) bool { + if ClientDebugEnabled(appEnv, productionDebugFlag) { + return false + } + msg := strings.ToLower(message) + return strings.Contains(msg, "navigating to") || + strings.Contains(msg, "going to") || + strings.Contains(msg, "redirecting to") || + strings.Contains(msg, "full paths for routes") +} + +func SanitizeClientLogMessage(message string) string { + if strings.TrimSpace(message) == "" { + return message + } + sanitized := logJSONSensitivePattern.ReplaceAllStringFunc(message, func(segment string) string { + parts := strings.SplitN(segment, ":", 2) + if len(parts) != 2 { + return segment + } + return parts[0] + `:"*****"` + }) + sanitized = logKVPattern.ReplaceAllString(sanitized, `$1=*****`) + return sanitized +} + +func SanitizeClientLogData(data map[string]interface{}) map[string]interface{} { + if len(data) == 0 { + return data + } + out := make(map[string]interface{}, len(data)) + for k, v := range data { + if isSensitiveClientLogKey(k) { + out[k] = "*****" + continue + } + out[k] = sanitizeClientLogValue(v) + } + return out +} + +func sanitizeClientLogValue(v interface{}) interface{} { + switch val := v.(type) { + case map[string]interface{}: + return SanitizeClientLogData(val) + case []interface{}: + next := make([]interface{}, len(val)) + for i := range val { + next[i] = sanitizeClientLogValue(val[i]) + } + return next + case string: + return SanitizeClientLogMessage(val) + default: + return val + } +} + +func isSensitiveClientLogKey(key string) bool { + normalized := strings.ToLower(strings.TrimSpace(key)) + normalized = strings.ReplaceAll(normalized, "-", "") + normalized = strings.ReplaceAll(normalized, "_", "") + normalized = strings.ReplaceAll(normalized, ".", "") + normalized = strings.ReplaceAll(normalized, " ", "") + _, ok := sensitiveClientLogKeys[normalized] + return ok +} diff --git a/backend/internal/logger/client_log_policy_test.go b/backend/internal/logger/client_log_policy_test.go new file mode 100644 index 00000000..10ee1235 --- /dev/null +++ b/backend/internal/logger/client_log_policy_test.go @@ -0,0 +1,79 @@ +package logger + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClientDebugEnabled(t *testing.T) { + t.Run("non production enables debug by default", func(t *testing.T) { + assert.True(t, ClientDebugEnabled("dev", "")) + assert.True(t, ClientDebugEnabled("local", "false")) + }) + + t.Run("production disables debug by default", func(t *testing.T) { + assert.False(t, ClientDebugEnabled("production", "")) + assert.False(t, ClientDebugEnabled("prod", "false")) + }) + + t.Run("production accepts explicit debug override", func(t *testing.T) { + assert.True(t, ClientDebugEnabled("production", "true")) + assert.True(t, ClientDebugEnabled("production", "1")) + assert.True(t, ClientDebugEnabled("prod", "on")) + }) +} + +func TestShouldAcceptClientLog(t *testing.T) { + assert.False(t, ShouldAcceptClientLog("production", "", "INFO")) + assert.False(t, ShouldAcceptClientLog("production", "", "DEBUG")) + assert.True(t, ShouldAcceptClientLog("production", "", "WARN")) + assert.True(t, ShouldAcceptClientLog("production", "", "ERROR")) + assert.True(t, ShouldAcceptClientLog("production", "true", "INFO")) + assert.True(t, ShouldAcceptClientLog("dev", "", "INFO")) +} + +func TestShouldFilterNoisyClientInfo(t *testing.T) { + assert.True(t, ShouldFilterNoisyClientInfo("production", "", "Navigating to /ko/signin")) + assert.False(t, ShouldFilterNoisyClientInfo("production", "true", "Navigating to /ko/signin")) + assert.False(t, ShouldFilterNoisyClientInfo("dev", "", "Navigating to /ko/signin")) +} + +func TestSanitizeClientLogData(t *testing.T) { + input := map[string]interface{}{ + "token": "raw-token", + "safe": "ok", + "nested": map[string]interface{}{ + "new_password": "secret-1", + "path": "/ko/profile", + }, + "arr": []interface{}{ + map[string]interface{}{"authorization": "Bearer abc"}, + "token=abc123", + }, + } + + result := SanitizeClientLogData(input) + + assert.Equal(t, "*****", result["token"]) + assert.Equal(t, "ok", result["safe"]) + + nested := result["nested"].(map[string]interface{}) + assert.Equal(t, "*****", nested["new_password"]) + assert.Equal(t, "/ko/profile", nested["path"]) + + arr := result["arr"].([]interface{}) + first := arr[0].(map[string]interface{}) + assert.Equal(t, "*****", first["authorization"]) + assert.Equal(t, "token=*****", arr[1]) +} + +func TestSanitizeClientLogMessage(t *testing.T) { + msg := `FLUTTER_ERROR token=abc123 payload={"password":"hello","safe":"ok"} authorization:BearerX` + sanitized := SanitizeClientLogMessage(msg) + assert.NotContains(t, sanitized, "abc123") + assert.NotContains(t, sanitized, `"password":"hello"`) + assert.Contains(t, sanitized, `"password":"*****"`) + assert.Contains(t, sanitized, "token=*****") + assert.Contains(t, sanitized, "authorization=*****") +} diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 650b3219..00565a0e 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -48,10 +48,19 @@ - 환경 변수 추가/변경 시 - `.env.sample` 반영 - 문서/가이드 갱신 + - 클라이언트 로그 정책 영향 확인 (`CLIENT_LOG_DEBUG`, `USERFRONT_DEBUG_LOG`) - 배포/운영 변경 시 - `Makefile`/compose 실행 절차 영향 확인 - 최소 Smoke 테스트 수행 + - 로그 수집 레벨이 운영 기본 정책(`WARN/ERROR`)을 유지하는지 확인 + +## 클라이언트 로그 정책 +- 상세 정책은 `docs/client-log-policy.md`를 기준으로 유지합니다. +- 원칙: + - 운영 기본값은 `WARN/ERROR`만 수집 + - 운영 디버그는 `CLIENT_LOG_DEBUG=true`로만 일시 허용 + - 민감정보 마스킹은 환경과 무관하게 항상 적용 ## 테스트 참고 - 테스트 계획 및 수동 실행 기준은 `docs/test-plan.md`를 따른다. diff --git a/docs/client-log-policy.md b/docs/client-log-policy.md new file mode 100644 index 00000000..6d781ef5 --- /dev/null +++ b/docs/client-log-policy.md @@ -0,0 +1,67 @@ +# Client Log Policy + +## 1. 목적 +- 운영 환경에서 클라이언트 로그를 최소권한으로 수집하고, 민감정보 유출을 방지합니다. +- 장애 분석이 필요한 경우에만 명시적 디버그 옵션으로 로그 레벨을 확장합니다. + +## 2. 환경별 수집 정책 + +### 2.1 기준 변수 +- `APP_ENV` +- `CLIENT_LOG_DEBUG` +- (UserFront fallback) `USERFRONT_DEBUG_LOG` + +### 2.2 동작 매트릭스 +- `APP_ENV != production|prod` + - 클라이언트 로그: `DEBUG/INFO/WARN/ERROR` 수집 허용 +- `APP_ENV == production|prod` AND `CLIENT_LOG_DEBUG` 미설정 + - 클라이언트 로그: `WARN/ERROR`만 수집 + - `INFO` 네비게이션 노이즈 로그는 필터 +- `APP_ENV == production|prod` AND `CLIENT_LOG_DEBUG=true|1|on|yes` + - 클라이언트 로그: `DEBUG/INFO/WARN/ERROR` 수집 허용 + +## 3. 민감정보 마스킹 규칙 + +### 3.1 Key 기반 마스킹 +아래 키는 값 전체를 `*****`로 치환합니다. +- `password`, `currentPassword`, `newPassword`, `oldPassword` +- `token`, `accessToken`, `refreshToken` +- `secret`, `clientSecret` +- `authorization`, `cookie`, `setCookie` +- `verificationCode`, `code` +- `loginChallenge`, `loginVerifier` +- `sessionJwt`, `accessJwt`, `refreshJwt` + +### 3.2 문자열 패턴 마스킹 +메시지 본문에서도 아래 패턴을 마스킹합니다. +- `token=...` +- `authorization:...` 또는 `authorization=...` +- JSON 문자열 내 민감 key/value + +## 4. 구현 위치 +- Backend + - 정책/마스킹 로직: `backend/internal/logger/client_log_policy.go` + - 수집 엔드포인트 적용: `backend/cmd/server/main.go` (`POST /api/v1/client-log`) +- UserFront + - 정책/마스킹 로직: `userfront/lib/core/services/log_policy.dart` + - 로그 출력/전송 정책: `userfront/lib/core/services/logger_service.dart` + - 전송 직전 마스킹: `userfront/lib/core/services/auth_proxy_service.dart` + +## 5. 검증 + +### 5.1 Backend +```bash +cd backend +go test ./internal/logger -count=1 +go test ./cmd/server -count=1 +``` + +### 5.2 UserFront +```bash +cd userfront +flutter test test/log_policy_test.dart +``` + +## 6. 운영 가이드 +- 운영에서 디버그 로그가 필요하면 `CLIENT_LOG_DEBUG=true`를 명시적으로 설정하고, 이슈 해결 후 즉시 원복합니다. +- 운영에서도 민감정보 마스킹은 항상 강제되며 비활성화할 수 없습니다. diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart index 3abe60bd..df798601 100644 --- a/userfront/lib/core/services/auth_proxy_service.dart +++ b/userfront/lib/core/services/auth_proxy_service.dart @@ -4,6 +4,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:userfront/i18n.dart'; import 'http_client.dart'; import 'auth_token_store.dart'; +import 'log_policy.dart'; class AuthProxyService { static String _envOrDefault(String key, String fallback) { @@ -793,15 +794,29 @@ class AuthProxyService { if (!_canSendClientLog()) { return; } + final appEnv = _envOrDefault('APP_ENV', 'dev'); + final productionDebugFlag = _envOrDefault( + 'CLIENT_LOG_DEBUG', + _envOrDefault('USERFRONT_DEBUG_LOG', ''), + ); + if (!LogPolicy.shouldRelayClientLog( + level: level, + appEnv: appEnv, + productionDebugFlag: productionDebugFlag, + )) { + return; + } final url = Uri.parse('$_baseUrl/api/v1/client-log'); + final sanitizedMessage = LogPolicy.sanitizeMessage(message); + final sanitizedData = data == null ? null : LogPolicy.sanitizeData(data); try { await http.post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'level': level, - 'message': message, - if (data != null) 'data': data, + 'message': sanitizedMessage, + if (sanitizedData != null) 'data': sanitizedData, }), ); _recordClientLogSuccess(); diff --git a/userfront/lib/core/services/log_policy.dart b/userfront/lib/core/services/log_policy.dart new file mode 100644 index 00000000..de0e4652 --- /dev/null +++ b/userfront/lib/core/services/log_policy.dart @@ -0,0 +1,123 @@ +class LogPolicy { + static const Set _sensitiveKeys = { + 'password', + 'currentpassword', + 'newpassword', + 'oldpassword', + 'token', + 'accesstoken', + 'refreshtoken', + 'secret', + 'clientsecret', + 'authorization', + 'cookie', + 'setcookie', + 'verificationcode', + 'code', + 'loginchallenge', + 'loginverifier', + 'sessionjwt', + 'accessjwt', + 'refreshjwt', + }; + + static bool isProductionEnv(String? appEnv) { + final env = (appEnv ?? '').trim().toLowerCase(); + return env == 'prod' || env == 'production'; + } + + static bool parseBoolFlag(String? raw) { + final value = (raw ?? '').trim().toLowerCase(); + return value == '1' || + value == 'true' || + value == 'yes' || + value == 'y' || + value == 'on'; + } + + static bool debugEnabled({ + required String? appEnv, + required String? productionDebugFlag, + }) { + if (!isProductionEnv(appEnv)) { + return true; + } + return parseBoolFlag(productionDebugFlag); + } + + static bool shouldRelayClientLog({ + required String level, + required String? appEnv, + required String? productionDebugFlag, + }) { + if (debugEnabled( + appEnv: appEnv, + productionDebugFlag: productionDebugFlag, + )) { + return true; + } + final normalized = level.trim().toUpperCase(); + return normalized == 'SEVERE' || + normalized == 'ERROR' || + normalized == 'WARNING' || + normalized == 'WARN'; + } + + static String sanitizeMessage(String message) { + if (message.trim().isEmpty) { + return message; + } + var sanitized = message.replaceAllMapped( + RegExp( + r'"(password|currentpassword|newpassword|oldpassword|token|accesstoken|refreshtoken|secret|clientsecret|authorization|cookie|setcookie|verificationcode|code|loginchallenge|loginverifier|sessionjwt|accessjwt|refreshjwt)"\s*:\s*"[^"]*"', + caseSensitive: false, + ), + (match) { + final key = match.group(1) ?? 'sensitive'; + return '"$key":"*****"'; + }, + ); + sanitized = sanitized.replaceAllMapped( + RegExp( + r'\b(password|current_password|currentpassword|new_password|newpassword|old_password|oldpassword|token|access_token|accesstoken|refresh_token|refreshtoken|authorization|cookie|session_jwt|sessionjwt|access_jwt|accessjwt|refresh_jwt|refreshjwt)\b\s*[:=]\s*([^\s,;]+)', + caseSensitive: false, + ), + (match) { + final key = match.group(1) ?? 'sensitive'; + return '$key=*****'; + }, + ); + return sanitized; + } + + static Map sanitizeData(Map input) { + final output = {}; + for (final entry in input.entries) { + if (_isSensitiveKey(entry.key)) { + output[entry.key] = '*****'; + } else { + output[entry.key] = _sanitizeValue(entry.value); + } + } + return output; + } + + static dynamic _sanitizeValue(dynamic value) { + if (value is Map) { + return sanitizeData(value); + } + if (value is List) { + return value.map(_sanitizeValue).toList(growable: false); + } + if (value is String) { + return sanitizeMessage(value); + } + return value; + } + + static bool _isSensitiveKey(String key) { + var normalized = key.trim().toLowerCase(); + normalized = normalized.replaceAll(RegExp(r'[-_.\s]'), ''); + return _sensitiveKeys.contains(normalized); + } +} diff --git a/userfront/lib/core/services/logger_service.dart b/userfront/lib/core/services/logger_service.dart index d878cc62..99802d10 100644 --- a/userfront/lib/core/services/logger_service.dart +++ b/userfront/lib/core/services/logger_service.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:logging/logging.dart' as std_log; import 'package:logger/logger.dart' as pretty_log; import 'auth_proxy_service.dart'; +import 'log_policy.dart'; /// Global Logger Service for Baron SSO Frontend class LoggerService { @@ -10,8 +12,20 @@ class LoggerService { factory LoggerService() => _instance; late final pretty_log.Logger _prettyLogger; + late final String _appEnv; + late final String _productionDebugFlag; LoggerService._internal() { + _appEnv = _envOrDefault('APP_ENV', 'dev'); + _productionDebugFlag = _envOrDefault( + 'CLIENT_LOG_DEBUG', + _envOrDefault('USERFRONT_DEBUG_LOG', ''), + ); + final debugEnabled = LogPolicy.debugEnabled( + appEnv: _appEnv, + productionDebugFlag: _productionDebugFlag, + ); + // 1. Initialize Pretty Logger for Dev _prettyLogger = pretty_log.Logger( printer: pretty_log.PrettyPrinter( @@ -25,9 +39,9 @@ class LoggerService { ); // 2. Configure Standard Logger (logging package) - std_log.Logger.root.level = kReleaseMode - ? std_log.Level.WARNING - : std_log.Level.ALL; + std_log.Logger.root.level = debugEnabled + ? std_log.Level.ALL + : std_log.Level.WARNING; std_log.Logger.root.onRecord.listen((record) { if (kReleaseMode) { @@ -40,6 +54,17 @@ class LoggerService { }); } + static String _envOrDefault(String key, String fallback) { + if (!dotenv.isInitialized) { + return fallback; + } + final value = dotenv.env[key]; + if (value == null || value.trim().isEmpty) { + return fallback; + } + return value; + } + /// Initialize the logger. Call this in main.dart static void init() { // Accessing the instance triggers the constructor @@ -64,10 +89,11 @@ class LoggerService { } void _logJson(std_log.LogRecord record) { + final sanitizedMessage = LogPolicy.sanitizeMessage(record.message); final logData = { 'time': record.time.toUtc().toIso8601String(), // Use UTC for consistency 'level': record.level.name, - 'msg': record.message, + 'msg': sanitizedMessage, 'svc': 'baron-userfront', if (record.error != null) 'error': record.error.toString(), if (record.stackTrace != null) 'stack': record.stackTrace.toString(), @@ -77,10 +103,14 @@ class LoggerService { debugPrint(jsonEncode(logData)); // 2. Relay to Backend (Docker Terminal) - if (record.level >= std_log.Level.WARNING) { + if (LogPolicy.shouldRelayClientLog( + level: record.level.name, + appEnv: _appEnv, + productionDebugFlag: _productionDebugFlag, + )) { AuthProxyService.sendLog( record.level.name, - record.message, + sanitizedMessage, data: { 'client_time': record.time.toUtc().toIso8601String(), 'logger': record.loggerName, diff --git a/userfront/lib/features/auth/presentation/forgot_password_screen.dart b/userfront/lib/features/auth/presentation/forgot_password_screen.dart index 1cca15c8..b3fe1d96 100644 --- a/userfront/lib/features/auth/presentation/forgot_password_screen.dart +++ b/userfront/lib/features/auth/presentation/forgot_password_screen.dart @@ -98,7 +98,10 @@ class _ForgotPasswordScreenState extends State { children: [ Text( tr('ui.userfront.forgot.heading'), - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.center, ), if (_drySendEnabled) ...[ diff --git a/userfront/lib/features/auth/presentation/login_success_screen.dart b/userfront/lib/features/auth/presentation/login_success_screen.dart index cb045820..81c89486 100644 --- a/userfront/lib/features/auth/presentation/login_success_screen.dart +++ b/userfront/lib/features/auth/presentation/login_success_screen.dart @@ -23,7 +23,10 @@ class LoginSuccessScreen extends StatelessWidget { const SizedBox(height: 24), Text( tr('ui.userfront.login_success.title'), - style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 16), Text( diff --git a/userfront/lib/features/auth/presentation/reset_password_screen.dart b/userfront/lib/features/auth/presentation/reset_password_screen.dart index ab470478..d9ebcb39 100644 --- a/userfront/lib/features/auth/presentation/reset_password_screen.dart +++ b/userfront/lib/features/auth/presentation/reset_password_screen.dart @@ -178,7 +178,7 @@ class _ResetPasswordScreenState extends State { children: [ Text( tr('ui.userfront.reset.subtitle'), - style: TextStyle( + style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index 82e9b196..7f0da220 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; 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'; @@ -22,6 +23,7 @@ class _ProfilePageState extends ConsumerState { static const _surface = Colors.white; static const _border = Color(0xFFE5E7EB); static const _subtle = Color(0xFFF7F8FA); + static final _log = Logger('ProfilePage'); UserProfile? _cachedProfile; String? _editingField; @@ -65,6 +67,22 @@ class _ProfilePageState extends ConsumerState { _phoneCodeFocus.addListener(_onPhoneCodeFocusChange); } + void _debugLog( + String event, { + String? field, + String? reason, + bool? changed, + bool? hasFocus, + }) { + final parts = ['event=$event']; + if (field != null) parts.add('field=$field'); + if (reason != null) parts.add('reason=$reason'); + if (changed != null) parts.add('changed=$changed'); + if (hasFocus != null) parts.add('hasFocus=$hasFocus'); + if (_editingField != null) parts.add('editing=$_editingField'); + _log.fine(parts.join(' ')); + } + void _onNameFocusChange() { if (!mounted) return; if (!_nameFocus.hasFocus && _nameTouched) { @@ -77,6 +95,11 @@ class _ProfilePageState extends ConsumerState { void _onDepartmentFocusChange() { if (!mounted) return; + _debugLog( + 'department_focus_change', + field: 'department', + hasFocus: _departmentFocus.hasFocus, + ); if (!_departmentFocus.hasFocus && _departmentTouched) { final profile = ref.read(profileProvider).value ?? _cachedProfile; if (profile != null) _autoSaveIfEditing(profile, 'department'); @@ -180,6 +203,7 @@ class _ProfilePageState extends ConsumerState { } void _startEditing(String field, UserProfile profile) { + _debugLog('start_editing', field: field); setState(() { _editingField = field; if (field == 'name') { @@ -356,12 +380,25 @@ class _ProfilePageState extends ConsumerState { void _autoSaveIfEditing(UserProfile profile, String field) { if (_editingField != field) return; if (_skipAutoSaveField == field) { + _debugLog('autosave_skip', field: field, reason: 'skip_flag'); _skipAutoSaveField = null; return; } - if (_isVerifying) return; - if (_isSavingField) return; + if (_isVerifying) { + _debugLog('autosave_skip', field: field, reason: 'verifying'); + return; + } + if (_isSavingField) { + _debugLog('autosave_skip', field: field, reason: 'saving_in_flight'); + return; + } if (!_hasFieldChanged(profile, field)) { + _debugLog( + 'autosave_skip', + field: field, + reason: 'unchanged', + changed: false, + ); setState(() { if (field == 'phone') { _resetPhoneState(); @@ -375,6 +412,7 @@ class _ProfilePageState extends ConsumerState { }); return; } + _debugLog('autosave_trigger', field: field, changed: true); _saveField(profile); } @@ -412,25 +450,33 @@ class _ProfilePageState extends ConsumerState { Future _saveField(UserProfile profile) async { if (_editingField == null) return; - if (_isSavingField) return; + if (_isSavingField) { + _debugLog('save_skip', reason: 'saving_in_flight'); + return; + } + final currentField = _editingField!; - final nextName = _editingField == 'name' + final nextName = currentField == 'name' ? _nameController!.text.trim() : profile.name; - final nextPhone = _editingField == 'phone' + final nextPhone = currentField == 'phone' ? _phoneController!.text.trim() : profile.phone; - final nextDepartment = _editingField == 'department' + final nextDepartment = currentField == 'department' ? _departmentController!.text.trim() : profile.department; - if (_editingField == 'name' && nextName.isEmpty) { + _debugLog('save_attempt', field: currentField); + + if (currentField == 'name' && nextName.isEmpty) { + _debugLog('save_skip', field: currentField, reason: 'empty_name'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('msg.userfront.profile.name_required'))), ); return; } - if (_editingField == 'department' && nextDepartment.isEmpty) { + if (currentField == 'department' && nextDepartment.isEmpty) { + _debugLog('save_skip', field: currentField, reason: 'empty_department'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(tr('msg.userfront.profile.department_required')), @@ -438,14 +484,20 @@ class _ProfilePageState extends ConsumerState { ); return; } - if (_editingField == 'phone') { + if (currentField == 'phone') { if (nextPhone.isEmpty) { + _debugLog('save_skip', field: currentField, reason: 'empty_phone'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('msg.userfront.profile.phone_required'))), ); return; } if (_isPhoneChanged && !_isPhoneVerified) { + _debugLog( + 'save_skip', + field: currentField, + reason: 'phone_not_verified', + ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(tr('msg.userfront.profile.phone_verify_required')), @@ -455,7 +507,13 @@ class _ProfilePageState extends ConsumerState { } } - if (!_hasFieldChanged(profile, _editingField!)) { + if (!_hasFieldChanged(profile, currentField)) { + _debugLog( + 'save_skip', + field: currentField, + reason: 'unchanged', + changed: false, + ); setState(() { if (_editingField == 'phone') { _resetPhoneState(); @@ -468,6 +526,7 @@ class _ProfilePageState extends ConsumerState { } _isSavingField = true; + _debugLog('save_dispatch', field: currentField, changed: true); try { await ref @@ -479,7 +538,7 @@ class _ProfilePageState extends ConsumerState { ); if (mounted) { setState(() { - if (_editingField == 'phone') { + if (currentField == 'phone') { _initialPhone = nextPhone; _resetPhoneState(); } @@ -487,11 +546,13 @@ class _ProfilePageState extends ConsumerState { _nameTouched = false; _departmentTouched = false; }); + _debugLog('save_success', field: currentField); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('msg.userfront.profile.update_success'))), ); } } catch (e) { + _debugLog('save_failed', field: currentField, reason: e.toString()); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/userfront/test/log_policy_test.dart b/userfront/test/log_policy_test.dart new file mode 100644 index 00000000..c7c3937b --- /dev/null +++ b/userfront/test/log_policy_test.dart @@ -0,0 +1,114 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:userfront/core/services/log_policy.dart'; + +void main() { + group('LogPolicy.debugEnabled', () { + test('non production enables debug by default', () { + expect( + LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: null), + isTrue, + ); + expect( + LogPolicy.debugEnabled(appEnv: 'staging', productionDebugFlag: 'false'), + isTrue, + ); + }); + + test('production disables debug unless explicitly enabled', () { + expect( + LogPolicy.debugEnabled(appEnv: 'production', productionDebugFlag: ''), + isFalse, + ); + expect( + LogPolicy.debugEnabled( + appEnv: 'production', + productionDebugFlag: 'true', + ), + isTrue, + ); + expect( + LogPolicy.debugEnabled(appEnv: 'prod', productionDebugFlag: '1'), + isTrue, + ); + }); + }); + + group('LogPolicy.shouldRelayClientLog', () { + test('production default forwards only warning or higher', () { + expect( + LogPolicy.shouldRelayClientLog( + level: 'INFO', + appEnv: 'production', + productionDebugFlag: '', + ), + isFalse, + ); + expect( + LogPolicy.shouldRelayClientLog( + level: 'WARNING', + appEnv: 'production', + productionDebugFlag: '', + ), + isTrue, + ); + expect( + LogPolicy.shouldRelayClientLog( + level: 'ERROR', + appEnv: 'production', + productionDebugFlag: '', + ), + isTrue, + ); + }); + + test('production debug option forwards info logs', () { + expect( + LogPolicy.shouldRelayClientLog( + level: 'INFO', + appEnv: 'production', + productionDebugFlag: 'true', + ), + isTrue, + ); + }); + }); + + group('LogPolicy.sanitize', () { + test('sanitizes sensitive message patterns', () { + const message = + 'token=abc123 payload={"password":"hello","safe":"ok"} authorization:BearerXYZ'; + final sanitized = LogPolicy.sanitizeMessage(message); + expect(sanitized, isNot(contains('abc123'))); + expect(sanitized, contains('token=*****')); + expect(sanitized, contains('"password":"*****"')); + expect(sanitized, contains('authorization=*****')); + }); + + test('sanitizes nested sensitive keys', () { + final data = { + 'token': 'tok', + 'ok': 'value', + 'nested': {'new_password': 'pw', 'safe': 'x'}, + 'arr': [ + {'authorization': 'Bearer secret'}, + 'cookie=session=raw', + ], + }; + + final sanitized = LogPolicy.sanitizeData(data); + expect(sanitized['token'], '*****'); + expect(sanitized['ok'], 'value'); + expect( + (sanitized['nested'] as Map)['new_password'], + '*****', + ); + expect((sanitized['nested'] as Map)['safe'], 'x'); + expect( + ((sanitized['arr'] as List).first + as Map)['authorization'], + '*****', + ); + expect((sanitized['arr'] as List)[1], 'cookie=*****'); + }); + }); +} From 7fee9c597a5e686a782fdfd79a726c1ff0863237 Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Tue, 24 Feb 2026 16:42:55 +0900 Subject: [PATCH 03/22] =?UTF-8?q?=EB=A1=9C=EC=BB=AC/CI=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/code_check.yml | 11 +- Makefile | 143 +++++++++++++----- adminfront/playwright-report/index.html | 2 +- docs/userfront_error_handling_policy.md | 12 +- scripts/run_adminfront_ci_tests.sh | 14 +- userfront-e2e/tests/auth-routing.spec.ts | 18 ++- .../tests/password-and-reset.spec.ts | 13 +- userfront/assets/translations/en.toml | 39 +++-- userfront/assets/translations/ko.toml | 55 ++++--- userfront/assets/translations/template.toml | 12 ++ userfront/test/error_screen_test.dart | 50 ++++++ 11 files changed, 277 insertions(+), 92 deletions(-) diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml index e5c27307..77d23ab5 100644 --- a/.gitea/workflows/code_check.yml +++ b/.gitea/workflows/code_check.yml @@ -95,11 +95,12 @@ jobs: npx biome check . --linter-enabled=false --organize-imports-enabled=false - name: Lint Go backend - uses: golangci/golangci-lint-action@v6 - with: - version: v1.59 - working-directory: backend - args: --enable-only=gofmt,gofumpt + run: | + docker run --rm \ + -v "${PWD}/backend:/app" \ + -w /app \ + golangci/golangci-lint:v2.10.1 \ + golangci-lint fmt -E gofmt -E gofumpt -d - name: Sync userfront locales run: | diff --git a/Makefile b/Makefile index bd75229a..87e0a8f0 100644 --- a/Makefile +++ b/Makefile @@ -107,11 +107,30 @@ logs-app: docker compose -f $(COMPOSE_APP) logs -f # --- 로컬 통합 코드 체크 --- -.PHONY: code-check code-check-i18n code-check-go-lint 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 +ifeq ($(CI),) +PLAYWRIGHT_INSTALL_ALL := npx playwright install +PLAYWRIGHT_INSTALL_CHROMIUM := npx playwright install chromium +else +PLAYWRIGHT_INSTALL_ALL := npx playwright install --with-deps +PLAYWRIGHT_INSTALL_CHROMIUM := npx playwright install --with-deps chromium +endif -code-check: code-check-i18n code-check-go-lint 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 +.PHONY: code-check code-check-lint code-check-test-jobs code-check-i18n 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 + +code-check: code-check-lint code-check-test-jobs @echo "code-check complete." +code-check-lint: code-check-i18n code-check-front-lint code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint + +code-check-test-jobs: + @echo "==> run CI-equivalent test jobs (parallel)" + @$(MAKE) --no-print-directory -j5 --output-sync=target \ + code-check-backend-tests \ + code-check-userfront-tests \ + code-check-userfront-e2e-tests \ + code-check-adminfront-tests \ + code-check-devfront-tests + code-check-i18n: @echo "==> i18n resource check" @mkdir -p reports @@ -122,60 +141,83 @@ code-check-i18n: code-check-go-lint: @echo "==> go lint/format check" @if command -v golangci-lint >/dev/null 2>&1; then \ - cd backend && golangci-lint run --enable-only=gofmt,gofumpt; \ + cd backend && golangci-lint fmt -E gofmt -E gofumpt -d; \ + elif command -v docker >/dev/null 2>&1; then \ + docker run --rm \ + -v "$$(pwd)/backend:/app" \ + -w /app \ + golangci/golangci-lint:v2.10.1 \ + golangci-lint fmt -E gofmt -E gofumpt -d; \ else \ - echo "WARN: golangci-lint not found, fallback to gofmt check only."; \ - unformatted="$$(cd backend && gofmt -l .)"; \ - if [ -n "$$unformatted" ]; then \ - echo "gofmt required:"; \ - echo "$$unformatted"; \ - exit 1; \ - fi; \ + echo "ERROR: golangci-lint not found and docker is unavailable."; \ + echo "Install golangci-lint v2.10.1 or Docker to match CI lint step."; \ + exit 1; \ fi +code-check-sync-userfront-locales: + @echo "==> sync userfront locales" + /bin/sh ./scripts/sync_userfront_locales.sh + +code-check-userfront-install: + @echo "==> install userfront dependencies" + cd userfront && flutter pub get + code-check-userfront-lint: @echo "==> userfront format/analyze" - cd userfront && flutter pub get - cd userfront && dart format --output=show --set-exit-if-changed lib test + cd userfront && dart format --output=none --set-exit-if-changed lib test cd userfront && flutter analyze --no-fatal-warnings --no-fatal-infos code-check-front-lint: @echo "==> adminfront biome lint/format check" + rm -rf adminfront/playwright-report adminfront/test-results cd adminfront && npm ci - cd adminfront && npx biome check src tests playwright.config.ts --formatter-enabled=false --organize-imports-enabled=false - cd adminfront && npx biome check src tests playwright.config.ts --linter-enabled=false --organize-imports-enabled=false + cd adminfront && npx biome check . --formatter-enabled=false --organize-imports-enabled=false + cd adminfront && npx biome check . --linter-enabled=false --organize-imports-enabled=false @echo "==> devfront biome lint/format check" + rm -rf devfront/playwright-report devfront/test-results cd devfront && npm ci - cd devfront && npx biome check src tests playwright.config.ts --formatter-enabled=false --organize-imports-enabled=false - cd devfront && npx biome check src tests playwright.config.ts --linter-enabled=false --organize-imports-enabled=false + cd devfront && npx biome check . --formatter-enabled=false --organize-imports-enabled=false + cd devfront && npx biome check . --linter-enabled=false --organize-imports-enabled=false code-check-backend-tests: @echo "==> backend tests" cd backend && go test -v ./... code-check-userfront-tests: - @echo "==> userfront tests" - cd userfront && flutter test + @echo "==> userfront tests (isolated workspace)" + @tmp_dir="$$(mktemp -d /tmp/baron-sso-userfront-tests.XXXXXX)"; \ + trap 'rm -rf "$$tmp_dir"' EXIT INT TERM; \ + mkdir -p "$$tmp_dir/scripts"; \ + cp scripts/sync_userfront_locales.sh "$$tmp_dir/scripts/"; \ + cp -R locales "$$tmp_dir/locales"; \ + if command -v rsync >/dev/null 2>&1; then \ + rsync -a --delete \ + --exclude '.dart_tool' \ + --exclude 'build' \ + --exclude '.pub-cache' \ + --exclude '.flutter-plugins' \ + --exclude '.flutter-plugins-dependencies' \ + userfront/ "$$tmp_dir/userfront/"; \ + else \ + cp -R userfront "$$tmp_dir/userfront"; \ + rm -rf "$$tmp_dir/userfront/.dart_tool" "$$tmp_dir/userfront/build"; \ + fi; \ + cd "$$tmp_dir" && /bin/sh ./scripts/sync_userfront_locales.sh; \ + cd "$$tmp_dir/userfront" && flutter test code-check-adminfront-tests: @echo "==> adminfront tests" - @mkdir -p reports/adminfront - @rm -rf reports/adminfront/playwright-report reports/adminfront/test-results - @status=0; \ - (cd adminfront && npx playwright install) || status=$$?; \ - if [ $$status -eq 0 ]; then \ - (cd adminfront && npm test) || status=$$?; \ - fi; \ - [ -d adminfront/playwright-report ] && cp -R adminfront/playwright-report reports/adminfront/ || true; \ - [ -d adminfront/test-results ] && cp -R adminfront/test-results reports/adminfront/ || true; \ - exit $$status + ./scripts/run_adminfront_ci_tests.sh adminfront-tests code-check-devfront-tests: @echo "==> devfront tests" @mkdir -p reports/devfront @rm -rf reports/devfront/playwright-report reports/devfront/test-results @status=0; \ - (cd devfront && npx playwright install) || status=$$?; \ + (cd devfront && npm ci) || status=$$?; \ + if [ $$status -eq 0 ]; then \ + (cd devfront && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \ + fi; \ if [ $$status -eq 0 ]; then \ (cd devfront && npm test) || status=$$?; \ fi; \ @@ -184,20 +226,47 @@ code-check-devfront-tests: exit $$status code-check-userfront-e2e-tests: - @echo "==> userfront wasm playwright e2e tests" + @echo "==> userfront wasm playwright e2e tests (isolated workspace)" @mkdir -p reports/userfront-e2e @rm -rf reports/userfront-e2e/playwright-report reports/userfront-e2e/test-results - @status=0; \ - (cd userfront && flutter build web --wasm --release) || status=$$?; \ + @tmp_dir="$$(mktemp -d /tmp/baron-sso-userfront-e2e-tests.XXXXXX)"; \ + trap 'rm -rf "$$tmp_dir"' EXIT INT TERM; \ + mkdir -p "$$tmp_dir/scripts"; \ + cp scripts/sync_userfront_locales.sh "$$tmp_dir/scripts/"; \ + cp -R locales "$$tmp_dir/locales"; \ + if command -v rsync >/dev/null 2>&1; then \ + rsync -a --delete \ + --exclude '.dart_tool' \ + --exclude 'build' \ + --exclude '.pub-cache' \ + --exclude '.flutter-plugins' \ + --exclude '.flutter-plugins-dependencies' \ + userfront/ "$$tmp_dir/userfront/"; \ + rsync -a --delete \ + --exclude 'node_modules' \ + --exclude 'playwright-report' \ + --exclude 'test-results' \ + userfront-e2e/ "$$tmp_dir/userfront-e2e/"; \ + else \ + cp -R userfront "$$tmp_dir/userfront"; \ + rm -rf "$$tmp_dir/userfront/.dart_tool" "$$tmp_dir/userfront/build"; \ + cp -R userfront-e2e "$$tmp_dir/userfront-e2e"; \ + rm -rf "$$tmp_dir/userfront-e2e/node_modules" "$$tmp_dir/userfront-e2e/playwright-report" "$$tmp_dir/userfront-e2e/test-results"; \ + fi; \ + status=0; \ + (cd "$$tmp_dir" && /bin/sh ./scripts/sync_userfront_locales.sh) || status=$$?; \ if [ $$status -eq 0 ]; then \ - (cd userfront-e2e && npm ci) || status=$$?; \ + (cd "$$tmp_dir/userfront-e2e" && npm ci) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ - (cd userfront-e2e && npx playwright install --with-deps chromium) || status=$$?; \ + (cd "$$tmp_dir/userfront" && flutter build web --wasm --release) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ - (cd userfront-e2e && npm test) || status=$$?; \ + (cd "$$tmp_dir/userfront-e2e" && $(PLAYWRIGHT_INSTALL_CHROMIUM)) || status=$$?; \ fi; \ - [ -d userfront-e2e/playwright-report ] && cp -R userfront-e2e/playwright-report reports/userfront-e2e/ || true; \ - [ -d userfront-e2e/test-results ] && cp -R userfront-e2e/test-results reports/userfront-e2e/ || true; \ + if [ $$status -eq 0 ]; then \ + (cd "$$tmp_dir/userfront-e2e" && npm test) || status=$$?; \ + fi; \ + [ -d "$$tmp_dir/userfront-e2e/playwright-report" ] && cp -R "$$tmp_dir/userfront-e2e/playwright-report" reports/userfront-e2e/ || true; \ + [ -d "$$tmp_dir/userfront-e2e/test-results" ] && cp -R "$$tmp_dir/userfront-e2e/test-results" reports/userfront-e2e/ || true; \ exit $$status diff --git a/adminfront/playwright-report/index.html b/adminfront/playwright-report/index.html index c67d9584..a369469d 100644 --- a/adminfront/playwright-report/index.html +++ b/adminfront/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+a.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/docs/userfront_error_handling_policy.md b/docs/userfront_error_handling_policy.md index c673c236..16abdbc3 100644 --- a/docs/userfront_error_handling_policy.md +++ b/docs/userfront_error_handling_policy.md @@ -66,7 +66,17 @@ cd userfront flutter test test/error_screen_test.dart ``` -## 6. 관련 이슈 +## 6. Backend `code` 주입/매핑 경로 +- 기본 매핑 함수: `backend/internal/response/error_response.go` + - `404 -> not_found` + - `429 -> rate_limited` +- legacy 응답 보강 미들웨어: `backend/internal/middleware/error_code_enricher.go` + - 핸들러가 `{"error": ...}`만 반환해도 status 기반 `code`를 주입 +- 신규 권장 패턴: `response.Error(...)` 또는 공통 helper(`errorJSON`, `errorJSONCode`)로 핸들러에서 명시 코드 반환 + +UserFront는 위 경로로 전달된 `code`를 기준으로 whitelist/ory/unknown 분기를 수행합니다. + +## 7. 관련 이슈 - `#164` `[UserFront] 에러 노출 whitelist 정의 및 적용` - `#259` `백엔드 i18n/에러 메시지 fallback 정책 재정리 및 반영 계획 수립` - `#260` `[Backend] 에러 응답 code 통일 구현 계획 (phase rollout)` diff --git a/scripts/run_adminfront_ci_tests.sh b/scripts/run_adminfront_ci_tests.sh index 4551582c..17a89245 100755 --- a/scripts/run_adminfront_ci_tests.sh +++ b/scripts/run_adminfront_ci_tests.sh @@ -5,6 +5,14 @@ job_name="${1:-adminfront-tests}" mkdir -p reports +if [ -n "${CI:-}" ]; then + playwright_install_cmd=(npx playwright install --with-deps) + playwright_install_desc="npx playwright install --with-deps" +else + playwright_install_cmd=(npx playwright install) + playwright_install_desc="npx playwright install" +fi + set +e ( cd adminfront @@ -36,7 +44,7 @@ fi set +e ( cd adminfront - npx playwright install --with-deps + "${playwright_install_cmd[@]}" ) 2>&1 | tee reports/adminfront-provision.log provision_exit_code=${PIPESTATUS[0]} set -e @@ -51,7 +59,7 @@ if [ "$provision_exit_code" -ne 0 ]; then echo "- Exit Code: \`$provision_exit_code\`" echo echo "## Command" - echo "\`cd adminfront && npx playwright install --with-deps\`" + echo "\`cd adminfront && ${playwright_install_desc}\`" echo echo "## Provision Log Tail (last 200 lines)" echo '```text' @@ -80,7 +88,7 @@ if [ "$test_exit_code" -ne 0 ]; then echo "## Commands" echo "1. \`cd adminfront\`" echo "2. \`npm ci\`" - echo "3. \`npx playwright install --with-deps\`" + echo "3. \`${playwright_install_desc}\`" echo "4. \`npm test\`" echo echo "## Log Tail (last 200 lines)" diff --git a/userfront-e2e/tests/auth-routing.spec.ts b/userfront-e2e/tests/auth-routing.spec.ts index 802c2569..a68f6684 100644 --- a/userfront-e2e/tests/auth-routing.spec.ts +++ b/userfront-e2e/tests/auth-routing.spec.ts @@ -75,8 +75,18 @@ async function mockUserfrontApis( } if (path.endsWith('/api/v1/auth/qr/approve')) { - const body = route.request().postDataJSON() as { pendingRef?: string }; - options.captureApprove?.(body.pendingRef ?? null); + if (route.request().method() == 'POST') { + let pendingRef: string | null = null; + try { + const body = (route.request().postDataJSON() ?? {}) as { + pendingRef?: string; + }; + pendingRef = body.pendingRef ?? null; + } catch (_) { + pendingRef = null; + } + options.captureApprove?.(pendingRef); + } await route.fulfill({ status: 200, contentType: 'application/json', @@ -137,7 +147,9 @@ test.describe('UserFront WASM auth routing', () => { await page.goto('/ko/approve?ref=e2e-approve-ref'); - await expect(page).toHaveURL(/\/ko\/dashboard$/); + await expect(page).toHaveURL(/\/ko\/dashboard(?:\?.*)?$/, { + timeout: 10_000, + }); expect(approvedRef).toBe('e2e-approve-ref'); }); }); diff --git a/userfront-e2e/tests/password-and-reset.spec.ts b/userfront-e2e/tests/password-and-reset.spec.ts index 4bf89a37..3de849fe 100644 --- a/userfront-e2e/tests/password-and-reset.spec.ts +++ b/userfront-e2e/tests/password-and-reset.spec.ts @@ -17,7 +17,7 @@ const SIGNIN_SUBMIT_X = 640; const SIGNIN_SUBMIT_Y = 381; const RESET_NEW_PASSWORD_X = 640; -const RESET_NEW_PASSWORD_Y = 401; +const RESET_NEW_PASSWORD_Y = 382; const RESET_CONFIRM_PASSWORD_X = 640; const RESET_CONFIRM_PASSWORD_Y = 464; const RESET_SUBMIT_X = 640; @@ -236,7 +236,13 @@ test.describe('UserFront WASM password login and reset', () => { const capture: RequestCapture = { clientLogs: [] }; await mockAuthApis(page, capture); + const policyLoaded = page.waitForResponse( + (response) => + response.url().includes('/api/v1/auth/password/policy') && + response.status() === 200, + ); await page.goto('/ko/reset-password?token=reset-token-e2e'); + await policyLoaded; await page.waitForTimeout(900); await fillAt(page, RESET_NEW_PASSWORD_X, RESET_NEW_PASSWORD_Y, 'ValidPass1!A'); await fillAt( @@ -250,7 +256,10 @@ test.describe('UserFront WASM password login and reset', () => { force: true, }); - await expect(page).toHaveURL(/\/ko\/signin$/); + await expect + .poll(() => capture.resetBody?.newPassword as string | undefined) + .toBe('ValidPass1!A'); + await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/, { timeout: 10_000 }); expect(capture.resetToken).toBe('reset-token-e2e'); expect(capture.resetBody?.newPassword).toBe('ValidPass1!A'); }); diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 5cd91c17..c8e88505 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -79,18 +79,8 @@ title_generic = "Title Generic" title_with_code = "Title With Code" type = "Type" -[msg.userfront.error.whitelist] -settings_disabled = "Account settings are currently unavailable." -invalid_session = "Your session has expired. Please sign in again." -verification_required = "Additional verification is required. Please follow the instructions." -recovery_expired = "The recovery link has expired. Please request a new one." -recovery_invalid = "The recovery link is invalid." -rate_limited = "Too many requests. Please try again later." -not_found = "The requested page could not be found." -bad_request = "Please check your input." -password_or_email_mismatch = "Email or password does not match." - [msg.userfront.error.ory] +"$normalizedCode" = "{error}" access_denied = "The user denied the consent request." consent_required = "Consent is required to continue." interaction_required = "Additional interaction is required. Please try again." @@ -105,6 +95,18 @@ temporarily_unavailable = "The authentication server is temporarily unavailable. unauthorized_client = "The client is not authorized for this request." unsupported_response_type = "The response type is not supported." +[msg.userfront.error.whitelist] +"$normalizedCode" = "{error}" +bad_request = "Please check your input." +invalid_session = "Your session has expired. Please sign in again." +not_found = "The requested page could not be found." +password_or_email_mismatch = "Email or password does not match." +rate_limited = "Too many requests. Please try again later." +recovery_expired = "The recovery link has expired. Please request a new one." +recovery_invalid = "The recovery link is invalid." +settings_disabled = "Account settings are currently unavailable." +verification_required = "Additional verification is required. Please follow the instructions." + [msg.userfront.forgot] description = "Description" dry_send = "Dry Send" @@ -233,8 +235,8 @@ disabled = "Disabled" [msg.userfront.signup] failed = "Failed" -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" -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" +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" +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.agreement] title = "Title" @@ -294,6 +296,8 @@ title = "Title" [ui.common] add = "Add" +admin_only = "Admin Only" +assign = "Assign" back = "Back" cancel = "Cancel" close = "Close" @@ -305,9 +309,13 @@ delete = "Delete" details = "Details" edit = "Edit" hyphen = "-" +language = "Language" +language_en = "English" +language_ko = "Language Ko" na = "N/A" never = "Never" next = "Next" +none = "None" page_of = "Page {page} of {total}" prev = "Prev" previous = "Previous" @@ -319,10 +327,9 @@ resend = "Resend" retry = "Retry" save = "Save" search = "Search" +select = "User Optional" +select_placeholder = "Select Placeholder" show_more = "Show More" -language = "Language" -language_ko = "한국어" -language_en = "English" theme_dark = "Dark" theme_light = "Light" theme_toggle = "Theme Toggle" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 37f47071..0ec335ff 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -54,12 +54,12 @@ empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." error = "연동 정보를 불러오지 못했습니다." [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} 연동이 해지되었습니다." @@ -79,18 +79,8 @@ title_generic = "오류가 발생했습니다" title_with_code = "오류: {code}" type = "오류 종류: {type}" -[msg.userfront.error.whitelist] -settings_disabled = "현재 계정 설정 화면은 준비 중입니다." -invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요." -verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요." -recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요." -recovery_invalid = "재설정 링크가 유효하지 않습니다." -rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요." -not_found = "요청한 페이지를 찾을 수 없습니다." -bad_request = "입력값을 확인해 주세요." -password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다." - [msg.userfront.error.ory] +"$normalizedCode" = "{error}" access_denied = "사용자가 동의를 거부했습니다." consent_required = "앱 접근 동의가 필요합니다." interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요." @@ -105,6 +95,18 @@ 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는 발송되지 않습니다." @@ -148,7 +150,7 @@ scan_hint = "모바일 앱으로 스캔하세요" invalid = "문자 2개와 숫자 6자리를 입력해 주세요." [msg.userfront.login.unregistered] -body = "가입되지 않은 정보입니다.\\\\n회원가입 후 이용해 주세요." +body = "가입되지 않은 정보입니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원가입 후 이용해 주세요." [msg.userfront.login.verification] approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다." @@ -233,15 +235,15 @@ disabled = "현재 계정 설정 화면은 준비 중입니다." [msg.userfront.signup] failed = "가입 실패: {error}" -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" -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" +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" +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.agreement] -title = "서비스 이용을 위해\\\\n약관에 동의해주세요" +title = "서비스 이용을 위해\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n약관에 동의해주세요" [msg.userfront.signup.auth] affiliate_notice = "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요." -title = "본인 확인을 위해\\\\n인증을 진행해주세요" +title = "본인 확인을 위해\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n인증을 진행해주세요" [msg.userfront.signup.email] code_mismatch = "인증코드가 일치하지 않습니다." @@ -257,7 +259,7 @@ lowercase_required = "소문자가 최소 1개 이상 포함되어야 합니다. mismatch = "비밀번호가 일치하지 않습니다." number_required = "숫자가 최소 1개 이상 포함되어야 합니다." symbol_required = "특수문자가 최소 1개 이상 포함되어야 합니다." -title = "마지막으로\\\\n비밀번호를 설정해주세요" +title = "마지막으로\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n비밀번호를 설정해주세요" uppercase_required = "대문자가 최소 1개 이상 포함되어야 합니다." [msg.userfront.signup.password.rule] @@ -286,7 +288,7 @@ uppercase = "대문자" [msg.userfront.signup.profile] affiliate_hint = "가족사 이메일 사용 시 자동으로 선택됩니다." -title = "회원님의\\\\n소속 정보를 알려주세요" +title = "회원님의\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n소속 정보를 알려주세요" [msg.userfront.signup.success] body = "성공적으로 가입되었습니다." @@ -294,6 +296,8 @@ title = "회원가입 완료" [ui.common] add = "추가" +admin_only = "관리자 전용" +assign = "할당" back = "돌아가기" cancel = "취소" close = "닫기" @@ -305,9 +309,13 @@ delete = "삭제" details = "상세정보" edit = "편집" hyphen = "-" +language = "언어" +language_en = "English" +language_ko = "한국어" na = "N/A" never = "Never" next = "Next" +none = "없음" page_of = "Page {page} of {total}" prev = "이전" previous = "Previous" @@ -319,10 +327,9 @@ resend = "재발송" retry = "다시 시도" save = "저장" search = "검색" +select = "사용자 선택" +select_placeholder = "사용자를 선택하세요" show_more = "+ 더보기" -language = "언어" -language_ko = "한국어" -language_en = "English" theme_dark = "Dark" theme_light = "Light" theme_toggle = "테마 전환" diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index cb29cf60..d1f5404d 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -80,6 +80,7 @@ title_with_code = "" type = "" [msg.userfront.error.whitelist] +"$normalizedCode" = "" settings_disabled = "" invalid_session = "" verification_required = "" @@ -91,6 +92,7 @@ bad_request = "" password_or_email_mismatch = "" [msg.userfront.error.ory] +"$normalizedCode" = "" access_denied = "" consent_required = "" interaction_required = "" @@ -558,3 +560,13 @@ verify = "" [ui.userfront.signup.success] action = "" + + +# Auto-added missing keys + +[ui.common] +admin_only = "" +assign = "" +none = "" +select = "" +select_placeholder = "" diff --git a/userfront/test/error_screen_test.dart b/userfront/test/error_screen_test.dart index 58dd8987..e80c7454 100644 --- a/userfront/test/error_screen_test.dart +++ b/userfront/test/error_screen_test.dart @@ -143,4 +143,54 @@ void main() { expect(find.text('원문 메시지'), findsNothing); expect(find.text(type), findsOneWidget); }); + + testWidgets('프로덕션은 not_found 코드를 whitelist 메시지로 노출한다 (404 매핑)', ( + WidgetTester tester, + ) async { + await _pumpErrorScreen( + tester, + errorCode: 'not_found', + description: '원문 메시지', + isProdOverride: true, + ); + + final detail = tr( + 'msg.userfront.error.whitelist.not_found', + fallback: internalErrorWhitelistMessages['not_found']!, + ); + final type = tr( + 'msg.userfront.error.type', + fallback: '오류 종류: {{type}}', + params: {'type': 'not_found'}, + ); + + expect(find.text(detail), findsOneWidget); + expect(find.text(type), findsOneWidget); + expect(find.text('원문 메시지'), findsNothing); + }); + + testWidgets('프로덕션은 rate_limited 코드를 whitelist 메시지로 노출한다 (429 매핑)', ( + WidgetTester tester, + ) async { + await _pumpErrorScreen( + tester, + errorCode: 'rate_limited', + description: '원문 메시지', + isProdOverride: true, + ); + + final detail = tr( + 'msg.userfront.error.whitelist.rate_limited', + fallback: internalErrorWhitelistMessages['rate_limited']!, + ); + final type = tr( + 'msg.userfront.error.type', + fallback: '오류 종류: {{type}}', + params: {'type': 'rate_limited'}, + ); + + expect(find.text(detail), findsOneWidget); + expect(find.text(type), findsOneWidget); + expect(find.text('원문 메시지'), findsNothing); + }); } From 077ff0e6a4e97f91538d9f79388e92c29dcaa474 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 24 Feb 2026 14:49:28 +0900 Subject: [PATCH 04/22] =?UTF-8?q?=EC=84=B8=EC=85=98=20=EB=A7=8C=EB=A3=8C?= =?UTF-8?q?=20=EB=A1=9C=EC=BC=80=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/locales/en.toml | 8 ++++++++ devfront/src/locales/ko.toml | 8 ++++++++ devfront/src/locales/template.toml | 8 ++++++++ locales/en.toml | 7 +++++++ locales/ko.toml | 7 +++++++ locales/template.toml | 6 ++++++ 6 files changed, 44 insertions(+) diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index 79dbcf4f..49e3d051 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -248,6 +248,7 @@ note = "Note" load_error = "Error loading client: {{error}}" loading = "Loading client..." saved = "Saved" +save_error = "Failed to save: {{error}}" [msg.dev.clients.general.identity] logo_help = "Logo Help" @@ -315,6 +316,7 @@ approved_device = "Approved Device" approved_ip = "Approve IP: {{ip}}" audit_empty = "Audit Empty" audit_load_error = "Audit Load Error" +render_error = "Dashboard render error: {{error}}" auth_method = "Auth Method" client_id = "Client ID: {{id}}" client_id_missing = "Client Id Missing" @@ -1122,6 +1124,12 @@ title = "Stack readiness" plane = "Dev Plane" subtitle = "Manage your applications" +[ui.dev.session] +active = "Checking expiration..." +unknown = "Unknown" +expired = "Session expired" +expiring = "Expiring soon: {{minutes}}m {{seconds}}s left" +remaining = "Expires in: {{minutes}}m {{seconds}}s" [ui.userfront] app_title = "Baron SW Portal" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index a8392019..1022c46b 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -248,6 +248,7 @@ note = "엔드포인트는 읽기 전용으로 유지하고, 비밀키 재발행 load_error = "Error loading client: {{error}}" loading = "Loading client..." saved = "설정이 저장되었습니다." +save_error = "저장 실패: {{error}}" [msg.dev.clients.general.identity] logo_help = "인증 화면에 표시될 PNG/SVG URL입니다." @@ -315,6 +316,7 @@ approved_device = "승인 기기: {{device}}" approved_ip = "승인 IP: {{ip}}" audit_empty = "최근 접속 이력이 없습니다." audit_load_error = "접속이력을 불러오지 못했습니다." +render_error = "대시보드 렌더링 오류: {{error}}" auth_method = "인증수단: {{method}}" client_id = "Client ID: {{id}}" client_id_missing = "Client ID 없음" @@ -1122,6 +1124,12 @@ title = "Stack readiness" plane = "Dev Plane" subtitle = "Manage your applications" +[ui.dev.session] +active = "만료 시간 확인 중..." +unknown = "확인 불가" +expired = "세션 만료" +expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음" +remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음" [ui.userfront] app_title = "Baron SW 포탈" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index fb270793..8fdb9d02 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -248,6 +248,7 @@ note = "" load_error = "" loading = "" saved = "" +save_error = "" [msg.dev.clients.general.identity] logo_help = "" @@ -315,6 +316,7 @@ approved_device = "" approved_ip = "" audit_empty = "" audit_load_error = "" +render_error = "" auth_method = "" client_id = "" client_id_missing = "" @@ -1134,6 +1136,12 @@ title = "" plane = "" subtitle = "" +[ui.dev.session] +active = "" +unknown = "" +expired = "" +expiring = "" +remaining = "" [ui.userfront] app_title = "" diff --git a/locales/en.toml b/locales/en.toml index 1781fe1c..98246ac5 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -1266,6 +1266,13 @@ title = "Stack readiness" plane = "Dev Plane" subtitle = "Manage your applications" +[ui.dev.session] +active = "Checking expiration..." +unknown = "Unknown" +expired = "Session expired" +expiring = "Expiring soon: {{minutes}}m {{seconds}}s left" +remaining = "Expires in: {{minutes}}m {{seconds}}s" + [ui.userfront] app_title = "Baron SW Portal" diff --git a/locales/ko.toml b/locales/ko.toml index 3d5e6d9a..1632965f 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -1266,6 +1266,13 @@ title = "Stack readiness" plane = "Dev Plane" subtitle = "Manage your applications" +[ui.dev.session] +active = "만료 시간 확인 중..." +unknown = "확인 불가" +expired = "세션 만료" +expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음" +remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음" + [ui.userfront] app_title = "Baron SW 포탈" diff --git a/locales/template.toml b/locales/template.toml index 4243afdb..c5986bcd 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -1154,6 +1154,12 @@ title = "" plane = "" subtitle = "" +[ui.dev.session] +active = "" +unknown = "" +expired = "" +expiring = "" +remaining = "" [ui.userfront] app_title = "" From c9a364a8ba1712221fc8c2f28b8ee7068c5c6401 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 24 Feb 2026 14:50:07 +0900 Subject: [PATCH 05/22] =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=83=81=EB=8B=A8=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B0=94=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 --- devfront/src/components/layout/AppLayout.tsx | 169 ++++++++++++++++++- docker/ory/hydra/hydra.yml | 4 + 2 files changed, 171 insertions(+), 2 deletions(-) diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx index e0a1d7bb..469a9362 100644 --- a/devfront/src/components/layout/AppLayout.tsx +++ b/devfront/src/components/layout/AppLayout.tsx @@ -1,5 +1,11 @@ -import { BadgeCheck, LogOut, Moon, ShieldHalf, Sun } from "lucide-react"; -import { useEffect, useState } from "react"; +import { + BadgeCheck, + LogOut, + Moon, + ShieldHalf, + Sun, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { useAuth } from "react-oidc-context"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { t } from "../../lib/i18n"; @@ -18,10 +24,14 @@ const navItems = [ function AppLayout() { const auth = useAuth(); const navigate = useNavigate(); + const profileMenuRef = useRef(null); const [theme, setTheme] = useState<"light" | "dark">(() => { const stored = window.localStorage.getItem("admin_theme"); return stored === "dark" ? "dark" : "light"; }); + const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); + const [isRefreshingSession, setIsRefreshingSession] = useState(false); + const [nowMs, setNowMs] = useState(() => Date.now()); const handleLogout = () => { if (window.confirm(t("msg.dev.logout_confirm", "로그아웃 하시겠습니까?"))) { @@ -41,10 +51,106 @@ function AppLayout() { window.localStorage.setItem("admin_theme", theme); }, [theme]); + useEffect(() => { + const timer = window.setInterval(() => { + setNowMs(Date.now()); + }, 1000); + return () => { + window.clearInterval(timer); + }; + }, []); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + profileMenuRef.current && + !profileMenuRef.current.contains(event.target as Node) + ) { + setIsProfileMenuOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + const toggleTheme = () => { setTheme((prev) => (prev === "light" ? "dark" : "light")); }; + const profileName = + auth.user?.profile?.name?.toString().trim() || + auth.user?.profile?.preferred_username?.toString().trim() || + auth.user?.profile?.nickname?.toString().trim() || + t("ui.dev.profile.unknown_name", "Unknown User"); + const profileEmail = + auth.user?.profile?.email?.toString().trim() || + t("ui.dev.profile.unknown_email", "unknown@example.com"); + const profileInitial = profileName.charAt(0).toUpperCase(); + const expiresAtSec = auth.user?.expires_at; + const remainingMs = + typeof expiresAtSec === "number" ? expiresAtSec * 1000 - nowMs : null; + const remainingTotalSec = + remainingMs !== null ? Math.max(0, Math.floor(remainingMs / 1000)) : null; + const remainingMinutes = + remainingTotalSec !== null ? Math.floor(remainingTotalSec / 60) : null; + const remainingSeconds = + remainingTotalSec !== null ? remainingTotalSec % 60 : null; + + let sessionToneClass = + "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"; + let sessionText = t("ui.dev.session.active", "세션 만료 시간 확인 중"); + + if (remainingMs === null) { + sessionToneClass = + "border-border bg-card text-muted-foreground"; + sessionText = t("ui.dev.session.unknown", "세션 만료 시간 확인 불가"); + } else if (remainingMs <= 0) { + sessionToneClass = + "border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300"; + sessionText = t("ui.dev.session.expired", "세션 만료됨"); + } else { + if (remainingMinutes !== null && remainingSeconds !== null && remainingMinutes <= 5) { + sessionToneClass = + "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"; + sessionText = t( + "ui.dev.session.expiring", + "만료 임박: {{minutes}}분 {{seconds}}초 남음", + { + minutes: remainingMinutes, + seconds: remainingSeconds, + }, + ); + } else { + sessionText = t( + "ui.dev.session.remaining", + "만료까지 {{minutes}}분 {{seconds}}초", + { + minutes: remainingMinutes ?? 0, + seconds: remainingSeconds ?? 0, + }, + ); + } + } + + const handleRefreshSessionExpiry = async () => { + if (isRefreshingSession) { + return; + } + setIsRefreshingSession(true); + try { + await auth.signinSilent(); + setNowMs(Date.now()); + setIsProfileMenuOpen(false); + } catch (error) { + console.error("Failed to refresh session expiry:", error); + } finally { + setIsRefreshingSession(false); + } + }; + return (
diff --git a/docker/ory/hydra/hydra.yml b/docker/ory/hydra/hydra.yml index 82f9fb37..7bf79b10 100644 --- a/docker/ory/hydra/hydra.yml +++ b/docker/ory/hydra/hydra.yml @@ -92,3 +92,7 @@ oidc: salt: youReallyNeedToChangeThis dynamic_client_registration: enabled: true + +ttl: + access_token: 15m + id_token: 15m From 75cde56761bc2781c441731cbc41126074506da1 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 24 Feb 2026 15:24:15 +0900 Subject: [PATCH 06/22] =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=95=B1(RP)=20?= =?UTF-8?q?=EC=9A=A9=EC=96=B4=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20i18n=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/features/clients/ClientConsentsPage.tsx | 2 +- devfront/src/features/clients/ClientDetailsPage.tsx | 2 +- devfront/src/features/clients/ClientGeneralPage.tsx | 5 ++++- devfront/src/features/clients/ClientsPage.tsx | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index 7bacc080..e82277ca 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -117,7 +117,7 @@ function ClientConsentsPage() { to={`/clients/${clientId}`} className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground" > - {t("ui.dev.clients.details.tab.connection", "Connection")} + {t("ui.dev.clients.details.tab.connection", "Federation")} {t("ui.dev.clients.details.tab.consents", "Consent & Users")} diff --git a/devfront/src/features/clients/ClientDetailsPage.tsx b/devfront/src/features/clients/ClientDetailsPage.tsx index 1882c04a..a372fef0 100644 --- a/devfront/src/features/clients/ClientDetailsPage.tsx +++ b/devfront/src/features/clients/ClientDetailsPage.tsx @@ -218,7 +218,7 @@ function ClientDetailsPage() { to={`/clients/${clientId}`} className="border-b-2 border-primary pb-3 text-sm font-bold text-primary" > - {t("ui.dev.clients.details.tab.connection", "Connection")} + {t("ui.dev.clients.details.tab.connection", "Federation")} - {t("ui.dev.clients.general.security.private", "Private")} + {t( + "ui.dev.clients.general.security.private", + "Server side App", + )} {t( diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 7cd7ad2a..cb43ae6b 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -159,7 +159,7 @@ function ClientsPage() { {t("ui.dev.clients.registry.title", "RP registry")}

- {t("ui.dev.clients.registry.subtitle", "Relying Parties")} + {t("ui.dev.clients.registry.subtitle", "연동 앱")} {t( @@ -315,7 +315,7 @@ function ClientsPage() { variant={client.type === "private" ? "success" : "muted"} > {client.type === "private" - ? t("ui.dev.clients.type.private", "Private") + ? t("ui.dev.clients.type.private", "Server side App") : t("ui.dev.clients.type.pkce", "PKCE")} From 38e0b6d80e7dc86c0b35d848b2a242eaf746c601 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 24 Feb 2026 15:24:35 +0900 Subject: [PATCH 07/22] =?UTF-8?q?=EC=9A=A9=EC=96=B4=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=A1=9C=EC=BC=80=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/locales/en.toml | 14 +++++++------- devfront/src/locales/ko.toml | 14 +++++++------- locales/en.toml | 14 +++++++------- locales/ko.toml | 14 +++++++------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index 49e3d051..fa5e4b56 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -255,14 +255,14 @@ logo_help = "Logo Help" subtitle = "Subtitle" [msg.dev.clients.general.redirect] -help = "Help" +help = "Enter the redirect URIs. You can modify them in the Federation tab after creation." [msg.dev.clients.general.scopes] empty = "Empty" subtitle = "Subtitle" [msg.dev.clients.general.security] -private_help = "Private App (Server-side): For apps that can safely store a client secret, such as Node.js or Java servers." +private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers." pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory." subtitle = "Select application type. Security level determines authentication method." @@ -981,7 +981,7 @@ user = "User" [ui.dev.clients.details.breadcrumb] current = "Current" -section = "Relying Parties" +section = "Applications" [ui.dev.clients.details.credentials] client_id = "Client ID" @@ -1008,7 +1008,7 @@ show = "Show" title = "Title" [ui.dev.clients.details.tab] -connection = "Connection" +connection = "Federation" consents = "Consent & Users" settings = "Settings" @@ -1053,7 +1053,7 @@ name = "Scope Name" delete = "Delete" [ui.dev.clients.general.security] -private = "Private" +private = "Server Side App" pkce = "PKCE" title = "Security Settings" @@ -1075,7 +1075,7 @@ subtitle = "Tenant admin on-call" title = "Owner" [ui.dev.clients.registry] -subtitle = "Relying Parties" +subtitle = "Applications" title = "RP registry" [ui.dev.clients.table] @@ -1087,7 +1087,7 @@ status = "Status" type = "Type" [ui.dev.clients.type] -private = "Private" +private = "Server side App" pkce = "PKCE" [ui.dev.dashboard] diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index 1022c46b..6959405d 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -255,14 +255,14 @@ logo_help = "인증 화면에 표시될 PNG/SVG URL입니다." subtitle = "앱 이름과 설명, 로고를 설정합니다." [msg.dev.clients.general.redirect] -help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 Connection 탭에서 수정 가능합니다." +help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다." [msg.dev.clients.general.scopes] empty = "등록된 스코프가 없습니다." subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다." [msg.dev.clients.general.security] -private_help = "Private 앱 (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다." +private_help = "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다." pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다." subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다." @@ -981,7 +981,7 @@ user = "User" [ui.dev.clients.details.breadcrumb] current = "연동 앱 상세" -section = "Relying Parties" +section = "연동 앱" [ui.dev.clients.details.credentials] client_id = "Client ID" @@ -1008,7 +1008,7 @@ show = "비밀키 보기" title = "보안 메모" [ui.dev.clients.details.tab] -connection = "Connection" +connection = "연동 설정" consents = "Consent & Users" settings = "Settings" @@ -1053,7 +1053,7 @@ name = "Scope Name" delete = "Delete" [ui.dev.clients.general.security] -private = "Private" +private = "Server side App" pkce = "PKCE" title = "보안 설정" @@ -1075,7 +1075,7 @@ subtitle = "Tenant admin on-call" title = "Owner" [ui.dev.clients.registry] -subtitle = "Relying Parties" +subtitle = "연동 앱" title = "RP registry" [ui.dev.clients.table] @@ -1087,7 +1087,7 @@ status = "상태" type = "유형" [ui.dev.clients.type] -private = "Private" +private = "Server side App" pkce = "PKCE" [ui.dev.dashboard] diff --git a/locales/en.toml b/locales/en.toml index 98246ac5..b5753efc 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -311,14 +311,14 @@ logo_help = "Logo Help" subtitle = "Subtitle" [msg.dev.clients.general.redirect] -help = "Help" +help = "Enter the redirect URIs. You can modify them in the Federation tab after creation." [msg.dev.clients.general.scopes] empty = "Empty" subtitle = "Subtitle" [msg.dev.clients.general.security] -private_help = "Private App (Server-side): For apps that can safely store a client secret, such as Node.js or Java servers." +private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers." pkce_help = "PKCE App (SPA/Mobile): For apps that cannot safely store a client secret. PKCE is mandatory." subtitle = "Select application type. Security level determines authentication method." @@ -1123,7 +1123,7 @@ user = "User" [ui.dev.clients.details.breadcrumb] current = "Current" -section = "Relying Parties" +section = "Applications" [ui.dev.clients.details.credentials] client_id = "Client ID" @@ -1150,7 +1150,7 @@ show = "Show" title = "Title" [ui.dev.clients.details.tab] -connection = "Connection" +connection = "Federation" consents = "Consent & Users" settings = "Settings" @@ -1195,7 +1195,7 @@ name = "Scope Name" delete = "Delete" [ui.dev.clients.general.security] -private = "Private" +private = "Server Side App" pkce = "PKCE" title = "Security Settings" @@ -1217,7 +1217,7 @@ subtitle = "Tenant admin on-call" title = "Owner" [ui.dev.clients.registry] -subtitle = "Relying Parties" +subtitle = "Applications" title = "RP registry" [ui.dev.clients.table] @@ -1229,7 +1229,7 @@ status = "Status" type = "Type" [ui.dev.clients.type] -private = "Private" +private = "Server side App" pkce = "PKCE" [ui.dev.dashboard] diff --git a/locales/ko.toml b/locales/ko.toml index 1632965f..847f2569 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -311,14 +311,14 @@ logo_help = "인증 화면에 표시될 PNG/SVG URL입니다." subtitle = "앱 이름과 설명, 로고를 설정합니다." [msg.dev.clients.general.redirect] -help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 Connection 탭에서 수정 가능합니다." +help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동 설정 탭에서 수정 가능합니다." [msg.dev.clients.general.scopes] empty = "등록된 스코프가 없습니다." subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다." [msg.dev.clients.general.security] -private_help = "Private 앱 (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다." +private_help = "Server side App (서버 사이드 앱): Node.js, Java 등 비밀키를 안전하게 보관 가능한 경우 사용합니다." pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다." subtitle = "앱 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다." @@ -1123,7 +1123,7 @@ user = "User" [ui.dev.clients.details.breadcrumb] current = "연동 앱 상세" -section = "Relying Parties" +section = "연동 앱" [ui.dev.clients.details.credentials] client_id = "Client ID" @@ -1150,7 +1150,7 @@ show = "비밀키 보기" title = "보안 메모" [ui.dev.clients.details.tab] -connection = "Connection" +connection = "연동 설정" consents = "Consent & Users" settings = "Settings" @@ -1195,7 +1195,7 @@ name = "Scope Name" delete = "Delete" [ui.dev.clients.general.security] -private = "Private" +private = "Server side App" pkce = "PKCE" title = "보안 설정" @@ -1217,7 +1217,7 @@ subtitle = "Tenant admin on-call" title = "Owner" [ui.dev.clients.registry] -subtitle = "Relying Parties" +subtitle = "연동 앱" title = "RP registry" [ui.dev.clients.table] @@ -1229,7 +1229,7 @@ status = "상태" type = "유형" [ui.dev.clients.type] -private = "Private" +private = "Server side App" pkce = "PKCE" [ui.dev.dashboard] From 558d88593a5fdbd89c4ea8df61db684216bd4430 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 24 Feb 2026 15:57:09 +0900 Subject: [PATCH 08/22] =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=B3=80=ED=99=94?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=A1=9C=EC=BC=80=EC=9D=BC=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/src/locales/en.toml | 4 ++++ devfront/src/locales/ko.toml | 4 ++++ devfront/src/locales/template.toml | 4 ++++ locales/en.toml | 4 ++++ locales/ko.toml | 4 ++++ locales/template.toml | 3 +++ 6 files changed, 23 insertions(+) diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index fa5e4b56..e01dd020 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -214,6 +214,9 @@ loading = "Loading apps..." showing = "Showing {{shown}} of {{total}} apps" status_update_error = "Failed to update client status" status_updated = "The app has been {{status}}." +deleted = "App deleted." +delete_error = "Failed to delete: {{error}}" +delete_confirm = "Are you sure you want to delete this app? This action cannot be undone." [msg.dev.clients.consents] empty = "No consents found." @@ -905,6 +908,7 @@ theme_dark = "Dark" theme_light = "Light" theme_toggle = "Theme Toggle" unknown = "Unknown" +view = "View" [ui.common.badge] admin_only = "Admin only" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index 6959405d..ef85680a 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -214,6 +214,9 @@ loading = "Loading apps..." showing = "Showing {{shown}} of {{total}} apps" status_update_error = "Failed to update client status" status_updated = "앱이 {{status}}되었습니다." +deleted = "앱이 삭제되었습니다." +delete_error = "삭제 실패: {{error}}" +delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." [msg.dev.clients.consents] empty = "No consents found." @@ -905,6 +908,7 @@ theme_dark = "Dark" theme_light = "Light" theme_toggle = "테마 전환" unknown = "Unknown" +view = "보기" [ui.common.badge] admin_only = "Admin only" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index 8fdb9d02..5c2934c5 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -214,6 +214,9 @@ loading = "" showing = "" status_update_error = "" status_updated = "" +deleted = "" +delete_error = "" +delete_confirm = "" [msg.dev.clients.consents] empty = "" @@ -917,6 +920,7 @@ theme_dark = "" theme_light = "" theme_toggle = "" unknown = "" +view = "" [ui.common.badge] admin_only = "" diff --git a/locales/en.toml b/locales/en.toml index b5753efc..a56f7c8b 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -270,6 +270,9 @@ loading = "Loading apps..." showing = "Showing {{shown}} of {{total}} apps" status_update_error = "Failed to update client status" status_updated = "The app has been {{status}}." +deleted = "App deleted." +delete_error = "Failed to delete: {{error}}" +delete_confirm = "Are you sure you want to delete this app? This action cannot be undone." [msg.dev.clients.consents] empty = "No consents found." @@ -1047,6 +1050,7 @@ theme_dark = "Dark" theme_light = "Light" theme_toggle = "Theme Toggle" unknown = "Unknown" +view = "View" [ui.common.badge] admin_only = "Admin only" diff --git a/locales/ko.toml b/locales/ko.toml index 847f2569..f7889a55 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -270,6 +270,9 @@ loading = "Loading apps..." showing = "Showing {{shown}} of {{total}} apps" status_update_error = "Failed to update client status" status_updated = "앱이 {{status}}되었습니다." +deleted = "앱이 삭제되었습니다." +delete_error = "삭제 실패: {{error}}" +delete_confirm = "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." [msg.dev.clients.consents] empty = "No consents found." @@ -1047,6 +1050,7 @@ theme_dark = "Dark" theme_light = "Light" theme_toggle = "테마 전환" unknown = "Unknown" +view = "보기" [ui.common.badge] admin_only = "Admin only" diff --git a/locales/template.toml b/locales/template.toml index c5986bcd..db96cadd 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -232,6 +232,9 @@ loading = "" showing = "" status_update_error = "" status_updated = "" +deleted = "" +delete_error = "" +delete_confirm = "" [msg.dev.clients.consents] empty = "" From 45dc68427a0ff6ccb66a6479fdf103acfe23db3f Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 24 Feb 2026 16:00:37 +0900 Subject: [PATCH 09/22] =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=95=B1=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=B0=8F=20=ED=8E=B8=EC=A7=91=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientGeneralPage.tsx | 176 ++++++++++++------ devfront/src/features/clients/ClientsPage.tsx | 115 ++---------- 2 files changed, 140 insertions(+), 151 deletions(-) diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 7bb0e8c0..cf00077a 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -16,7 +16,12 @@ import { Input } from "../../components/ui/input"; import { Label } from "../../components/ui/label"; import { Switch } from "../../components/ui/switch"; import { Textarea } from "../../components/ui/textarea"; -import { createClient, fetchClient, updateClient } from "../../lib/devApi"; +import { + createClient, + deleteClient, + fetchClient, + updateClient, +} from "../../lib/devApi"; import type { ClientStatus, ClientType, @@ -174,6 +179,39 @@ function ClientGeneralPage() { }, }); + const deleteMutation = useMutation({ + mutationFn: (id: string) => deleteClient(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["clients"] }); + alert(t("msg.dev.clients.deleted", "앱이 삭제되었습니다.")); + navigate("/clients"); + }, + onError: (err) => { + const errorMessage = + (err as AxiosError<{ error?: string }>).response?.data?.error ?? + (err as Error)?.message; + alert( + t("msg.dev.clients.delete_error", "삭제 실패: {{error}}", { + error: errorMessage, + }), + ); + }, + }); + + const handleDelete = () => { + if ( + clientId && + window.confirm( + t( + "msg.dev.clients.delete_confirm", + "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + ), + ) + ) { + deleteMutation.mutate(clientId); + } + }; + if (!isCreate && isLoading) { return (
@@ -220,14 +258,16 @@ function ClientGeneralPage() { : t("ui.dev.clients.general.title_edit", "Client Settings")}
- - {status === "active" - ? t("ui.common.status.active", "Active") - : t("ui.common.status.inactive", "Inactive")} - + {!isCreate && ( + + {status === "active" + ? t("ui.common.status.active", "Active") + : t("ui.common.status.inactive", "Inactive")} + + )}
{!isCreate && ( @@ -254,15 +294,49 @@ function ClientGeneralPage() { {/* 1. Application Identity */}
- - {t("ui.dev.clients.general.identity.title", "Application Identity")} - - - {t( - "msg.dev.clients.general.identity.subtitle", - "앱 이름과 설명, 로고를 설정합니다.", +
+
+ + {t( + "ui.dev.clients.general.identity.title", + "Application Identity", + )} + + + {t( + "msg.dev.clients.general.identity.subtitle", + "앱 이름과 설명, 로고를 설정합니다.", + )} + +
+ {!isCreate && ( +
+ +
+ + setStatus(checked ? "active" : "inactive") + } + /> + + {status === "active" + ? t("ui.common.status.active", "활성") + : t("ui.common.status.inactive", "비활성")} + +
+
)} - +
@@ -568,43 +642,41 @@ function ClientGeneralPage() { -
- - +
+
+ {!isCreate && ( + + )} +
+
+ + +
- {!isCreate && ( -
-
- - {t("ui.dev.clients.general.footer.client_id", "Client ID")} - - {data?.client?.id} -
-
- - {t("ui.dev.clients.general.footer.created_on", "Created On")} - - - {data?.client?.createdAt - ? new Date(data.client.createdAt).toLocaleString() - : "-"} - -
-
- )} +
); } diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index cb43ae6b..9d9def58 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -1,4 +1,4 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { BookOpenText, @@ -22,10 +22,8 @@ import { CardHeader, CardTitle, } from "../../components/ui/card"; -import { CopyButton } from "../../components/ui/copy-button"; import { Input } from "../../components/ui/input"; import { Separator } from "../../components/ui/separator"; -import { Switch } from "../../components/ui/switch"; import { Table, TableBody, @@ -34,56 +32,16 @@ import { TableHeader, TableRow, } from "../../components/ui/table"; -import { toast } from "../../components/ui/use-toast"; -import { - deleteClient, - fetchClients, - updateClientStatus, -} from "../../lib/devApi"; +import { fetchClients } from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { cn } from "../../lib/utils"; function ClientsPage() { const navigate = useNavigate(); - const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery({ queryKey: ["clients"], queryFn: fetchClients, }); - const updateStatusMutation = useMutation({ - mutationFn: (payload: { id: string; status: "active" | "inactive" }) => - updateClientStatus(payload.id, payload.status), - onSuccess: (_, variables) => { - const statusText = - variables.status === "active" - ? t("ui.common.status.active", "활성화") - : t("ui.common.status.inactive", "비활성화"); - toast( - t( - "msg.dev.clients.status_updated", - "클라이언트가 {{status}}되었습니다.", - { - status: statusText, - }, - ), - ); - queryClient.invalidateQueries({ queryKey: ["clients"] }); - }, - onError: (error: AxiosError<{ error?: string }>) => { - const errMsg = - error.response?.data?.error ?? - error.message ?? - t( - "msg.dev.clients.status_update_error", - "Failed to update client status", - ); - toast(errMsg, "error"); - }, - }); - const deleteMutation = useMutation({ - mutationFn: (clientId: string) => deleteClient(clientId), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["clients"] }), - }); const clients = data?.items || []; const totalClients = clients.length; @@ -267,7 +225,10 @@ function ClientsPage() { {clients.map((client) => ( -
+
{client.type === "private" ? ( @@ -284,30 +245,13 @@ function ClientsPage() { {t("ui.dev.clients.tenant_scoped", "Tenant-scoped")}

-
+
{client.id} - - toast( - t( - "msg.dev.clients.copy_client_id", - "클라이언트 ID가 복사되었습니다.", - ), - ) - } - />
@@ -320,33 +264,14 @@ function ClientsPage() { -
- - updateStatusMutation.mutate({ - id: client.id, - status: checked ? "active" : "inactive", - }) - } - /> - - {client.status === "active" - ? t("ui.common.status.active", "활성") - : t("ui.common.status.inactive", "비활성")} - -
+ + {client.status === "active" + ? t("ui.common.status.active", "Active") + : t("ui.common.status.inactive", "Inactive")} +
{client.createdAt @@ -357,17 +282,9 @@ function ClientsPage() {
-
From 75cf8355de03c1f344ac0197c69e70f333e251b1 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 24 Feb 2026 17:24:30 +0900 Subject: [PATCH 10/22] =?UTF-8?q?i18n=20=ED=82=A4=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BA=90=EC=8B=9C/=EC=82=B0=EC=B6=9C=EB=AC=BC=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + adminfront/playwright-report/index.html | 85 ------------------------- locales/en.toml | 9 +++ locales/ko.toml | 9 +++ locales/template.toml | 52 +++------------ tools/i18n-scanner/manual-keys.ts | 1 + 6 files changed, 30 insertions(+), 127 deletions(-) delete mode 100644 adminfront/playwright-report/index.html diff --git a/.gitignore b/.gitignore index 9c6d4a32..ba9819d2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.log *.out *.exe +.npm-cache/ reports reports/* diff --git a/adminfront/playwright-report/index.html b/adminfront/playwright-report/index.html deleted file mode 100644 index a369469d..00000000 --- a/adminfront/playwright-report/index.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - Playwright Test Report - - - - -
- - - \ No newline at end of file diff --git a/locales/en.toml b/locales/en.toml index a56f7c8b..19966500 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -258,6 +258,7 @@ error = "Error" loading = "Loading..." no_description = "No Description." saving = "Saving..." +requesting = "Requesting..." unknown_error = "unknown error" [msg.dev] @@ -1080,6 +1081,12 @@ scope_badge = "Scoped to /dev" clients = "Connected Application" logout = "Logout" +[ui.dev.profile] +menu_aria = "Open account menu" +menu_title = "Account" +unknown_email = "unknown@example.com" +unknown_name = "Unknown User" + [ui.dev.clients] copy_client_id = "Copy client id" new = "Add Connected Application" @@ -1276,6 +1283,8 @@ unknown = "Unknown" expired = "Session expired" expiring = "Expiring soon: {{minutes}}m {{seconds}}s left" remaining = "Expires in: {{minutes}}m {{seconds}}s" +refresh = "Refresh session expiry" +refreshing = "Refreshing session expiry..." [ui.userfront] app_title = "Baron SW Portal" diff --git a/locales/ko.toml b/locales/ko.toml index f7889a55..977a9b07 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -258,6 +258,7 @@ error = "오류가 발생했습니다." loading = "로딩 중..." no_description = "설명이 없습니다." saving = "저장 중..." +requesting = "요청 중..." unknown_error = "unknown error" [msg.dev] @@ -1080,6 +1081,12 @@ scope_badge = "Scoped to /dev" clients = "연동 앱" logout = "로그아웃" +[ui.dev.profile] +menu_aria = "계정 메뉴 열기" +menu_title = "계정" +unknown_email = "unknown@example.com" +unknown_name = "Unknown User" + [ui.dev.clients] copy_client_id = "Copy client id" new = "연동 앱 추가" @@ -1276,6 +1283,8 @@ unknown = "확인 불가" expired = "세션 만료" expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음" remaining = "만료 예정: {{minutes}}분 {{seconds}}초 남음" +refresh = "세션 만료 시간 갱신" +refreshing = "세션 만료 시간 갱신 중..." [ui.userfront] app_title = "Baron SW 포탈" diff --git a/locales/template.toml b/locales/template.toml index db96cadd..55567ffb 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -18,24 +18,6 @@ saman = "" [err.common] unknown = "" -[err.backend] -authorization_pending = "" -bad_request = "" -conflict = "" -expired_token = "" -forbidden = "" -internal_error = "" -invalid_code = "" -invalid_or_expired_code = "" -invalid_session = "" -invalid_session_reference = "" -not_found = "" -not_supported = "" -password_or_email_mismatch = "" -rate_limited = "" -service_unavailable = "" -slow_down = "" - [err.userfront] [err.userfront.auth_proxy] @@ -220,18 +202,16 @@ count = "" [msg.common] loading = "" saving = "" +requesting = "" unknown_error = "" [msg.dev] logout_confirm = "" [msg.dev.clients] -copy_client_id = "" load_error = "" loading = "" showing = "" -status_update_error = "" -status_updated = "" deleted = "" delete_error = "" delete_confirm = "" @@ -430,7 +410,6 @@ token_missing = "" verification_failed = "" [msg.userfront.login.link] -approved = "" helper = "" missing_login_id = "" missing_phone = "" @@ -493,8 +472,6 @@ organization = "" security = "" [msg.userfront.qr] -approve_error = "" -approve_success = "" camera_error = "" permission_error = "" permission_required = "" @@ -915,6 +892,7 @@ create = "" delete = "" details = "" edit = "" +view = "" hyphen = "" na = "" never = "" @@ -925,7 +903,6 @@ previous = "" qr = "" read_only = "" refresh = "" -requesting = "" resend = "" retry = "" save = "" @@ -967,8 +944,13 @@ scope_badge = "" clients = "" logout = "" +[ui.dev.profile] +menu_aria = "" +menu_title = "" +unknown_email = "" +unknown_name = "" + [ui.dev.clients] -copy_client_id = "" new = "" search_placeholder = "" tenant_scoped = "" @@ -1055,10 +1037,6 @@ title_edit = "" [ui.dev.clients.general.breadcrumb] section = "" -[ui.dev.clients.general.footer] -client_id = "" -created_on = "" - [ui.dev.clients.general.identity] description = "" description_placeholder = "" @@ -1163,6 +1141,8 @@ unknown = "" expired = "" expiring = "" remaining = "" +refresh = "" +refreshing = "" [ui.userfront] app_title = "" @@ -1239,12 +1219,9 @@ login_id = "" password = "" [ui.userfront.login.link] -action_label = "" code_only = "" -page_title = "" resend_with_time = "" send = "" -title = "" [ui.userfront.login.qr] expired = "" @@ -1314,9 +1291,7 @@ organization = "" security = "" [ui.userfront.qr] -request_permission = "" rescan = "" -result_failure = "" result_success = "" title = "" @@ -1393,8 +1368,6 @@ delete_confirm = "" delete_error = "" delete_success = "" empty = "" -import_error = "" -import_success = "" loading = "" [msg.admin.groups.members] @@ -1428,7 +1401,6 @@ no_description = "" [ui.admin.groups] add_unit = "" -import_csv = "" [ui.admin.groups.create] description = "" @@ -1448,10 +1420,6 @@ parent_none = "" unit_level_label = "" unit_level_placeholder = "" -[ui.admin.groups.table] -created_at = "" -level = "" - [ui.admin.tenants.admins] add_button = "" already_admin = "" diff --git a/tools/i18n-scanner/manual-keys.ts b/tools/i18n-scanner/manual-keys.ts index ee374a68..f6dff763 100644 --- a/tools/i18n-scanner/manual-keys.ts +++ b/tools/i18n-scanner/manual-keys.ts @@ -14,6 +14,7 @@ t("ui.admin.nav.audit_logs"); t("ui.admin.nav.auth_guard"); t("ui.admin.nav.logout"); t("ui.admin.nav.relying_parties"); +t("ui.dev.nav.clients"); // Common & Info t("err.common.unknown"); From 242c088730f73148960a79d8b01b959666bcf0c6 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 24 Feb 2026 17:28:39 +0900 Subject: [PATCH 11/22] =?UTF-8?q?make=20code-check=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/components/layout/AppLayout.tsx | 24 +++++++------- .../features/clients/ClientGeneralPage.tsx | 2 -- userfront/assets/translations/en.toml | 1 + userfront/assets/translations/ko.toml | 1 + userfront/assets/translations/template.toml | 10 +----- userfront/pubspec.lock | 32 +++++++------------ 6 files changed, 27 insertions(+), 43 deletions(-) diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx index 469a9362..9e798031 100644 --- a/devfront/src/components/layout/AppLayout.tsx +++ b/devfront/src/components/layout/AppLayout.tsx @@ -1,10 +1,4 @@ -import { - BadgeCheck, - LogOut, - Moon, - ShieldHalf, - Sun, -} from "lucide-react"; +import { BadgeCheck, LogOut, Moon, ShieldHalf, Sun } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useAuth } from "react-oidc-context"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; @@ -104,22 +98,25 @@ function AppLayout() { let sessionText = t("ui.dev.session.active", "세션 만료 시간 확인 중"); if (remainingMs === null) { - sessionToneClass = - "border-border bg-card text-muted-foreground"; + sessionToneClass = "border-border bg-card text-muted-foreground"; sessionText = t("ui.dev.session.unknown", "세션 만료 시간 확인 불가"); } else if (remainingMs <= 0) { sessionToneClass = "border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300"; sessionText = t("ui.dev.session.expired", "세션 만료됨"); } else { - if (remainingMinutes !== null && remainingSeconds !== null && remainingMinutes <= 5) { + if ( + remainingMinutes !== null && + remainingSeconds !== null && + remainingMinutes <= 5 + ) { sessionToneClass = "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"; sessionText = t( "ui.dev.session.expiring", "만료 임박: {{minutes}}분 {{seconds}}초 남음", { - minutes: remainingMinutes, + minutes: remainingMinutes, seconds: remainingSeconds, }, ); @@ -300,7 +297,10 @@ function AppLayout() { disabled={isRefreshingSession} > {isRefreshingSession - ? t("ui.dev.session.refreshing", "세션 만료 시간 갱신 중...") + ? t( + "ui.dev.session.refreshing", + "세션 만료 시간 갱신 중...", + ) : t("ui.dev.session.refresh", "세션 만료 시간 갱신")}
diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index cf00077a..ba910abd 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -675,8 +675,6 @@ function ClientGeneralPage() {
- -
); } diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index c8e88505..1acbecba 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -334,6 +334,7 @@ theme_dark = "Dark" theme_light = "Light" theme_toggle = "Theme Toggle" unknown = "Unknown" +view = "View" [ui.common.badge] admin_only = "Admin only" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 0ec335ff..6442beeb 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -334,6 +334,7 @@ theme_dark = "Dark" theme_light = "Light" theme_toggle = "테마 전환" unknown = "Unknown" +view = "보기" [ui.common.badge] admin_only = "Admin only" diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index d1f5404d..3911cf30 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -131,7 +131,6 @@ token_missing = "" verification_failed = "" [msg.userfront.login.link] -approved = "" helper = "" missing_login_id = "" missing_phone = "" @@ -194,8 +193,6 @@ organization = "" security = "" [msg.userfront.qr] -approve_error = "" -approve_success = "" camera_error = "" permission_error = "" permission_required = "" @@ -306,6 +303,7 @@ create = "" delete = "" details = "" edit = "" +view = "" hyphen = "" na = "" never = "" @@ -316,7 +314,6 @@ previous = "" qr = "" read_only = "" refresh = "" -requesting = "" resend = "" retry = "" save = "" @@ -423,12 +420,9 @@ login_id = "" password = "" [ui.userfront.login.link] -action_label = "" code_only = "" -page_title = "" resend_with_time = "" send = "" -title = "" [ui.userfront.login.qr] expired = "" @@ -498,9 +492,7 @@ organization = "" security = "" [ui.userfront.qr] -request_permission = "" rescan = "" -result_failure = "" result_success = "" title = "" diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock index fecd33f1..ae003ec3 100644 --- a/userfront/pubspec.lock +++ b/userfront/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" cli_config: dependency: transitive description: @@ -268,14 +268,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" leak_tracker: dependency: transitive description: @@ -328,18 +320,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -653,26 +645,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.15" toml: dependency: "direct main" description: From 34231782500c3df76770c057618e00a1f3dd3e84 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 25 Feb 2026 09:09:18 +0900 Subject: [PATCH 12/22] =?UTF-8?q?SSO=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9D=84=20=ED=8C=9D=EC=97=85=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/features/auth/AuthCallbackPage.tsx | 7 +++++++ devfront/src/features/auth/LoginPage.tsx | 12 +++++++++--- devfront/src/lib/apiClient.ts | 5 +++-- devfront/src/lib/auth.ts | 1 + 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/devfront/src/features/auth/AuthCallbackPage.tsx b/devfront/src/features/auth/AuthCallbackPage.tsx index 638a7924..339dade5 100644 --- a/devfront/src/features/auth/AuthCallbackPage.tsx +++ b/devfront/src/features/auth/AuthCallbackPage.tsx @@ -1,12 +1,19 @@ import { useEffect } from "react"; import { useAuth } from "react-oidc-context"; import { useNavigate } from "react-router-dom"; +import { userManager } from "../../lib/auth"; export default function AuthCallbackPage() { const auth = useAuth(); const navigate = useNavigate(); useEffect(() => { + // 팝업으로 열린 경우 signinPopupCallback 처리 + if (window.opener) { + userManager.signinPopupCallback(); + return; + } + if (auth.isAuthenticated) { navigate("/", { replace: true }); } else if (auth.error) { diff --git a/devfront/src/features/auth/LoginPage.tsx b/devfront/src/features/auth/LoginPage.tsx index 7004f4eb..4d592b68 100644 --- a/devfront/src/features/auth/LoginPage.tsx +++ b/devfront/src/features/auth/LoginPage.tsx @@ -1,5 +1,6 @@ import { ExternalLink, LogIn, ShieldHalf } from "lucide-react"; import { useAuth } from "react-oidc-context"; +import { useNavigate } from "react-router-dom"; import { Button } from "../../components/ui/button"; import { Card, @@ -11,10 +12,15 @@ import { function LoginPage() { const auth = useAuth(); + const navigate = useNavigate(); - const handleSSOLogin = () => { - // OIDC client-side authentication flow started here - auth.signinRedirect(); + const handleSSOLogin = async () => { + try { + await auth.signinPopup(); + navigate("/clients", { replace: true }); + } catch (error) { + console.error("Popup login failed", error); + } }; return ( diff --git a/devfront/src/lib/apiClient.ts b/devfront/src/lib/apiClient.ts index fd7cf54f..65a6995c 100644 --- a/devfront/src/lib/apiClient.ts +++ b/devfront/src/lib/apiClient.ts @@ -30,8 +30,9 @@ apiClient.interceptors.response.use( if (error.response?.status === 401) { // 401 발생 시 로그인 페이지로 리다이렉트 const isAuthPath = window.location.pathname.startsWith("/callback"); - if (!isAuthPath) { - userManager.signinRedirect(); + const isLoginPath = window.location.pathname === "/login"; + if (!isAuthPath && !isLoginPath) { + window.location.href = "/login"; } } return Promise.reject(error); diff --git a/devfront/src/lib/auth.ts b/devfront/src/lib/auth.ts index 90453199..f424d9d9 100644 --- a/devfront/src/lib/auth.ts +++ b/devfront/src/lib/auth.ts @@ -9,6 +9,7 @@ export const oidcConfig: AuthProviderProps = { response_type: "code", scope: "openid offline_access profile email", // offline_access for refresh token post_logout_redirect_uri: window.location.origin, + popup_redirect_uri: `${window.location.origin}/auth/callback`, userStore: new WebStorageStateStore({ store: window.localStorage }), automaticSilentRenew: true, }; From 85538ae672b1bce53e04f2866b605216e68d91b0 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 25 Feb 2026 14:36:07 +0900 Subject: [PATCH 13/22] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=A4=84=EB=B0=94=EA=BF=88=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.toml | 8 +- locales/ko.toml | 30 +- userfront/lib/i18n_data.dart | 661 +++++++++++++++++++++++++---------- 3 files changed, 487 insertions(+), 212 deletions(-) diff --git a/locales/en.toml b/locales/en.toml index 19966500..48fed153 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -168,7 +168,7 @@ description = "Description" [msg.admin.tenants] approve_confirm = "Approve Confirm" approve_success = "Approve Success" -delete_confirm = "Delete Tenant \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"{{name}}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"?" +delete_confirm = "Delete Tenant \"{{name}}\"?" delete_success = "Tenant deleted." empty = "Empty" fetch_error = "Fetch Error" @@ -202,7 +202,7 @@ empty = "Empty" count = "Count" [msg.admin.tenants.schema] -empty = "No custom fields defined. Click \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"Add Field\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" to begin." +empty = "No custom fields defined. Click \"Add Field\" to begin." missing_id = "Tenant ID missing" subtitle = "Define custom attributes for users in this tenant." update_error = "Failed to update schema" @@ -573,8 +573,8 @@ disabled = "Disabled" [msg.userfront.signup] failed = "Failed" -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" -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" +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" +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.agreement] title = "Title" diff --git a/locales/ko.toml b/locales/ko.toml index 977a9b07..07162c9c 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -90,7 +90,7 @@ notice_emphasis = "지금 한 번만" notice_suffix = "표시됩니다." [msg.admin.api_keys.list] -delete_confirm = "API 키 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"{{name}}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"를 삭제할까요?" +delete_confirm = "API 키 \"{{name}}\"를 삭제할까요?" empty = "등록된 API 키가 없습니다." fetch_error = "API 키 목록 조회에 실패했습니다." subtitle = "서버 간 통신(Machine-to-Machine)을 위한 API 키를 발급하고 관리합니다." @@ -168,7 +168,7 @@ description = "주요 운영 화면으로 바로 이동합니다." [msg.admin.tenants] approve_confirm = "이 테넌트를 승인하시겠습니까?" approve_success = "테넌트가 승인되었습니다." -delete_confirm = "테넌트 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"{{name}}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"를 삭제할까요?" +delete_confirm = "테넌트 \"{{name}}\"를 삭제할까요?" delete_success = "테넌트가 삭제되었습니다." empty = "아직 등록된 테넌트가 없습니다." fetch_error = "테넌트 목록 조회에 실패했습니다." @@ -202,7 +202,7 @@ empty = "소속된 사용자가 없습니다." count = "총 {{count}}개 테넌트" [msg.admin.tenants.schema] -empty = "No custom fields defined. Click \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"Add Field\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" to begin." +empty = "No custom fields defined. Click \"Add Field\" to begin." missing_id = "Tenant ID missing" subtitle = "Define custom attributes for users in this tenant." update_error = "Failed to update schema" @@ -245,7 +245,7 @@ name_required = "이름은 필수입니다." password_hint = "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다." [msg.admin.users.list] -delete_confirm = "사용자 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"{{name}}\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"을(를) 정말 삭제하시겠습니까?" +delete_confirm = "사용자 \"{{name}}\"을(를) 정말 삭제하시겠습니까?" empty = "검색 결과가 없습니다." fetch_error = "사용자 목록 조회에 실패했습니다." subtitle = "시스템 사용자를 조회하고 관리합니다. (Local DB)" @@ -290,7 +290,7 @@ load_error = "Error loading client: {{error}}" loading = "Loading client..." missing_id = "Client ID가 필요합니다." redirect_saved = "Redirect URIs가 저장되었습니다." -rotate_confirm = "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?" +rotate_confirm = "경고: Client Secret을 재발급하면 기존 시크릿은 즉시 무효화됩니다.\n연동된 애플리케이션이 중단될 수 있습니다. 계속하시겠습니까?" rotate_error = "재발급 실패: {{error}}" save_error = "저장 실패: {{error}}" secret_rotated = "Client Secret이 재발급되었습니다." @@ -392,12 +392,12 @@ empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." error = "연동 정보를 불러오지 못했습니다." [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}} 연동이 해지되었습니다." @@ -488,7 +488,7 @@ scan_hint = "모바일 앱으로 스캔하세요" invalid = "문자 2개와 숫자 6자리를 입력해 주세요." [msg.userfront.login.unregistered] -body = "가입되지 않은 정보입니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원가입 후 이용해 주세요." +body = "가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요." [msg.userfront.login.verification] approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다." @@ -573,15 +573,15 @@ disabled = "현재 계정 설정 화면은 준비 중입니다." [msg.userfront.signup] failed = "가입 실패: {{error}}" -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" -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" +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" +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.agreement] -title = "서비스 이용을 위해\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n약관에 동의해주세요" +title = "서비스 이용을 위해\n약관에 동의해주세요" [msg.userfront.signup.auth] affiliate_notice = "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요." -title = "본인 확인을 위해\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n인증을 진행해주세요" +title = "본인 확인을 위해\n인증을 진행해주세요" [msg.userfront.signup.email] code_mismatch = "인증코드가 일치하지 않습니다." @@ -597,7 +597,7 @@ lowercase_required = "소문자가 최소 1개 이상 포함되어야 합니다. mismatch = "비밀번호가 일치하지 않습니다." number_required = "숫자가 최소 1개 이상 포함되어야 합니다." symbol_required = "특수문자가 최소 1개 이상 포함되어야 합니다." -title = "마지막으로\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n비밀번호를 설정해주세요" +title = "마지막으로\n비밀번호를 설정해주세요" uppercase_required = "대문자가 최소 1개 이상 포함되어야 합니다." [msg.userfront.signup.password.rule] @@ -626,7 +626,7 @@ uppercase = "대문자" [msg.userfront.signup.profile] affiliate_hint = "가족사 이메일 사용 시 자동으로 선택됩니다." -title = "회원님의\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n소속 정보를 알려주세요" +title = "회원님의\n소속 정보를 알려주세요" [msg.userfront.signup.success] body = "성공적으로 가입되었습니다." diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index e252bd3d..98bc2601 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -8,6 +8,26 @@ const Map koStrings = { "domain.company.jangheon": "장헌", "domain.company.ptc": "PTC", "domain.company.saman": "삼안", + "domain.tenant_type.company": "COMPANY (일반 기업)", + "domain.tenant_type.company_group": "COMPANY_GROUP (그룹사/지주사)", + "domain.tenant_type.personal": "PERSONAL (개인 워크스페이스)", + "domain.tenant_type.user_group": "USER_GROUP (내부 부서/팀)", + "err.backend.authorization_pending": "인증 승인이 아직 완료되지 않았습니다.", + "err.backend.bad_request": "요청 값을 확인해 주세요.", + "err.backend.conflict": "요청이 현재 상태와 충돌합니다.", + "err.backend.expired_token": "토큰이 만료되었습니다.", + "err.backend.forbidden": "요청이 허용되지 않습니다.", + "err.backend.internal_error": "요청 처리 중 내부 오류가 발생했습니다.", + "err.backend.invalid_code": "인증 코드가 올바르지 않습니다.", + "err.backend.invalid_or_expired_code": "인증 코드가 유효하지 않거나 만료되었습니다.", + "err.backend.invalid_session": "세션이 유효하지 않습니다.", + "err.backend.invalid_session_reference": "세션 참조 정보가 유효하지 않습니다.", + "err.backend.not_found": "요청한 인증 흐름을 찾을 수 없습니다.", + "err.backend.not_supported": "지원하지 않는 로그인 방식입니다.", + "err.backend.password_or_email_mismatch": "이메일 혹은 비밀번호가 일치하지 않습니다.", + "err.backend.rate_limited": "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.", + "err.backend.service_unavailable": "인증 서비스를 현재 사용할 수 없습니다.", + "err.backend.slow_down": "요청 간격이 너무 빠릅니다. 잠시 후 다시 시도해 주세요.", "err.common.unknown": "알 수 없는 오류가 발생했습니다.", "err.userfront.auth_proxy.consent_accept": "동의 처리에 실패했습니다.", "err.userfront.auth_proxy.consent_fetch": "동의 정보를 가져오지 못했습니다.", @@ -29,69 +49,83 @@ 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.filters.empty": "필터 없음", "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": - "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", + "msg.admin.audit.subtitle": "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", + "msg.admin.groups.list.create_error": "생성 실패", + "msg.admin.groups.list.create_success": "조직 단위가 생성되었습니다.", + "msg.admin.groups.list.delete_confirm": "정말로 삭제하시겠습니까?", + "msg.admin.groups.list.delete_error": "삭제 실패", + "msg.admin.groups.list.delete_success": "조직 단위가 삭제되었습니다.", + "msg.admin.groups.list.empty": "테넌트에 등록된 조직 단위가 없습니다.", + "msg.admin.groups.list.import_error": "가져오기 실패", + "msg.admin.groups.list.import_success": "조직도가 임포트되었습니다.", + "msg.admin.groups.list.loading": "로딩 중...", "msg.admin.groups.list.subtitle": "이 테넌트에 정의된 사용자 그룹 목록입니다.", + "msg.admin.groups.members.add_success": "구성원이 추가되었습니다.", "msg.admin.groups.members.count": "{{count}} 명", "msg.admin.groups.members.empty": "멤버가 없습니다.", + "msg.admin.groups.members.remove_confirm": "제거하시겠습니까?", + "msg.admin.groups.members.remove_success": "구성원이 제외되었습니다.", "msg.admin.groups.members.title": "[{{name}}] 멤버 관리", "msg.admin.groups.prompt.user_id": "추가할 사용자의 UUID를 입력하세요:", + "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_success": "역할이 회수되었습니다.", "msg.admin.header.subtitle": "Tenant isolation & least privilege by default", "msg.admin.idp_env_prod": "IDP env: prod", - "msg.admin.notice.idp_policy": - "IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다.", + "msg.admin.logout_confirm": "로그아웃 하시겠습니까?", + "msg.admin.notice.idp_policy": "IDP 관리 키는 서버 내부 래핑 API로만 사용하며, 감사·레이트리밋을 기본 적용합니다.", "msg.admin.notice.scope": "관리 기능은 /admin 네임스페이스에서만 노출합니다.", "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.scope_admin": "Scoped to /admin", "msg.admin.session_ttl": "Session TTL: 15m admin", "msg.admin.tenant_headers": "Tenant-aware headers", - "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.admins.add_success": "관리자가 추가되었습니다.", + "msg.admin.tenants.admins.empty": "등록된 관리자가 없습니다.", + "msg.admin.tenants.admins.remove_confirm": "관리자를 삭제하시겠습니까?", + "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.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.empty": "소속된 사용자가 없습니다.", + "msg.admin.tenants.missing_id": "테넌트 ID가 없습니다.", "msg.admin.tenants.registry.count": "총 {{count}}개 테넌트", - "msg.admin.tenants.schema.empty": - "No custom fields defined. Click \\\"Add Field\\\" to begin.", + "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.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": "하위 테넌트가 없습니다.", @@ -101,96 +135,87 @@ const Map koStrings = { "msg.admin.users.create.error": "사용자 생성에 실패했습니다.", "msg.admin.users.create.form.email_required": "이메일은 필수입니다.", "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.detail.edit_subtitle": "{{email}} 계정의 정보를 수정합니다.", "msg.admin.users.detail.form.name_required": "이름은 필수입니다.", "msg.admin.users.detail.not_found": "사용자를 찾을 수 없습니다.", - "msg.admin.users.detail.security.password_hint": - "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다.", + "msg.admin.users.detail.security.password_hint": "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다.", "msg.admin.users.detail.update_error": "사용자 수정에 실패했습니다.", "msg.admin.users.detail.update_success": "사용자 정보가 수정되었습니다.", - "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}}명의 사용자가 등록되어 있습니다.", "msg.admin.users.list.subtitle": "시스템 사용자를 조회하고 관리합니다. (Local DB)", + "msg.common.error": "오류가 발생했습니다.", "msg.common.loading": "로딩 중...", + "msg.common.no_description": "설명이 없습니다.", + "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.dev.clients.consents.showing": "Showing {{from}} to {{to}} of {{total}} users", "msg.dev.clients.consents.subtitle": "OIDC Relying Party 사용자 권한을 검토·관리합니다.", - "msg.dev.clients.copy_client_id": "클라이언트 ID가 복사되었습니다.", + "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.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.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를 입력하세요. 생성 후 Connection 탭에서 수정 가능합니다.", + "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.confidential_help": - "서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관 가능한 경우.", - "msg.dev.clients.general.security.public_help": - "SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본 사용합니다.", - "msg.dev.clients.general.security.subtitle": - "클라이언트 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다.", - "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.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.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 clients...", - "msg.dev.clients.registry.description": - "OIDC 클라이언트, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.", + "msg.dev.clients.loading": "Loading apps...", + "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}} clients", + "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.status_updated": "앱이 {{status}}되었습니다.", + "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.logout_confirm": "로그아웃 하시겠습니까?", "msg.dev.sidebar.notice": "개발자 전용 콘솔입니다.", - "msg.dev.sidebar.notice_detail": "클라이언트 애플리케이션 등록 및 관리를 수행할 수 있습니다.", + "msg.dev.sidebar.notice_detail": "연동 앱 등록 및 관리를 수행할 수 있습니다.", "msg.info.saved_success": "저장이 완료되었습니다.", "msg.userfront.audit.date": "접속일자: {{value}}", "msg.userfront.audit.device": "접속환경: {{value}}", @@ -201,15 +226,12 @@ const Map koStrings = { "msg.userfront.audit.session_id": "Session ID: {{value}}", "msg.userfront.audit.status": "현황: (준비중)", "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": "접속이력을 불러오지 못했습니다.", @@ -220,8 +242,8 @@ 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.revoke.confirm": - "{{app}} 앱과의 연동을 해지하시겠습니까?\\\\\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다.", + "msg.userfront.dashboard.render_error": "대시보드 렌더링 오류: {{error}}", + "msg.userfront.dashboard.revoke.confirm": "{{app}} 앱과의 연동을 해지하시겠습니까?\n해지하면 다음 로그인 시 다시 동의가 필요합니다.", "msg.userfront.dashboard.revoke.error": "해지 실패: {{error}}", "msg.userfront.dashboard.revoke.success": "{{app}} 연동이 해지되었습니다.", "msg.userfront.dashboard.scopes.empty": "요청된 권한이 없습니다.", @@ -231,13 +253,35 @@ const Map koStrings = { "msg.userfront.error.detail_generic": "오류가 발생했습니다.", "msg.userfront.error.detail_request": "요청을 처리하는 중 문제가 발생했습니다.", "msg.userfront.error.id": "오류 ID: {{id}}", + "msg.userfront.error.ory.\"\$normalizedCode\"": "{{error}}", + "msg.userfront.error.ory.access_denied": "사용자가 동의를 거부했습니다.", + "msg.userfront.error.ory.consent_required": "앱 접근 동의가 필요합니다.", + "msg.userfront.error.ory.interaction_required": "추가 상호작용이 필요합니다. 다시 시도해 주세요.", + "msg.userfront.error.ory.invalid_client": "클라이언트 인증 정보가 유효하지 않습니다.", + "msg.userfront.error.ory.invalid_grant": "인증 요청이 만료되었거나 유효하지 않습니다.", + "msg.userfront.error.ory.invalid_request": "잘못된 요청입니다.", + "msg.userfront.error.ory.invalid_scope": "요청한 권한 범위가 유효하지 않습니다.", + "msg.userfront.error.ory.login_required": "로그인이 필요합니다.", + "msg.userfront.error.ory.request_forbidden": "요청이 거부되었습니다.", + "msg.userfront.error.ory.server_error": "인증 서버 오류가 발생했습니다.", + "msg.userfront.error.ory.temporarily_unavailable": "인증 서버를 일시적으로 사용할 수 없습니다.", + "msg.userfront.error.ory.unauthorized_client": "해당 클라이언트는 이 요청을 수행할 수 없습니다.", + "msg.userfront.error.ory.unsupported_response_type": "지원하지 않는 응답 타입입니다.", "msg.userfront.error.title": "인증 과정에서 오류가 발생했습니다", "msg.userfront.error.title_generic": "오류가 발생했습니다", "msg.userfront.error.title_with_code": "오류: {{code}}", "msg.userfront.error.type": "오류 종류: {{type}}", - "msg.userfront.error.whitelist.\$normalizedCode": "에러가 계속되면 관리자에게 문의해주세요", - "msg.userfront.forgot.description": - "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다.", + "msg.userfront.error.whitelist.\"\$normalizedCode\"": "{{error}}", + "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.rate_limited": "요청이 많습니다. 잠시 후 다시 시도해 주세요.", + "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.forgot.dry_send": "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다.", "msg.userfront.forgot.error": "전송에 실패했습니다: {{error}}", "msg.userfront.forgot.input_required": "이메일 또는 휴대폰 번호를 입력해주세요.", @@ -250,29 +294,26 @@ 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": "입력하신 이메일로 로그인 링크를 보냈습니다.", "msg.userfront.login.link_sent_phone": "입력하신 번호로 로그인 링크를 보냈습니다.", - "msg.userfront.login.link_timeout": "로그인 요청 시간이 초과되었습니다.", + "msg.userfront.login.link_timeout": "시간이 경과되었습니다.", "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": "QR 세션이 만료되었습니다.", + "msg.userfront.login.qr_expired": "시간이 경과되었습니다.", "msg.userfront.login.qr_init_failed": "QR 초기화에 실패했습니다: {{error}}", "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": "성공적으로 로그인되었습니다.", @@ -310,8 +351,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개 이상의 특수문자를 포함해야 합니다.", @@ -330,10 +370,9 @@ const Map koStrings = { "msg.userfront.sections.apps_subtitle": "현재 연결된 앱과 최근 인증 상태입니다.", "msg.userfront.sections.audit_subtitle": "Baron 로그인 기준의 최근 접근 기록입니다.", "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.title": "서비스 이용을 위해\n약관에 동의해주세요", + "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": "유효한 이메일 형식이 아닙니다.", @@ -342,8 +381,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": "소문자", @@ -353,9 +391,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": "✅ 휴대폰 인증 완료", @@ -368,17 +405,14 @@ 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": "\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": "가족사 이메일 사용 시 자동으로 선택됩니다.", - "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": "\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", "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 키 발급하기", @@ -429,20 +463,46 @@ 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.groups.create.description": "부서나 팀과 같은 새로운 조직 단위를 추가합니다.", "ui.admin.groups.create.title": "새 그룹 생성", + "ui.admin.groups.detail.breadcrumb_org": "조직 관리 목록으로 돌아가기", + "ui.admin.groups.detail.breadcrumb_tenant": "테넌트 상세", + "ui.admin.groups.detail.breadcrumb_unit": "조직 단위", + "ui.admin.groups.detail.members_subtitle": "이 조직에 소속된 사용자를 관리합니다.", + "ui.admin.groups.detail.members_title": "구성원 관리", + "ui.admin.groups.detail.permissions_subtitle": "이 조직이 다른 테넌트에 가지는 역할을 정의합니다.", + "ui.admin.groups.detail.permissions_title": "권한 관리", "ui.admin.groups.form.desc_label": "설명", "ui.admin.groups.form.desc_placeholder": "그룹 용도 설명", "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": "예: 본부, 팀", + "ui.admin.groups.import_csv": "CSV 임포트", "ui.admin.groups.list.title": "User Groups", "ui.admin.groups.members.table.email": "이메일", "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.nav.api_keys": "API 키", + "ui.admin.nav.audit_logs": "감사 로그", + "ui.admin.nav.auth_guard": "인증 가드", + "ui.admin.nav.logout": "로그아웃", + "ui.admin.nav.overview": "개요", + "ui.admin.nav.relying_parties": "애플리케이션(RP)", + "ui.admin.nav.tenant_dashboard": "테넌트 대시보드", + "ui.admin.nav.tenants": "테넌트", + "ui.admin.nav.user_groups": "유저 그룹", + "ui.admin.nav.users": "사용자", "ui.admin.overview.kicker": "Global Overview", "ui.admin.overview.playbook.title": "Admin playbook", "ui.admin.overview.quick_links.add_tenant": "테넌트 추가", @@ -455,26 +515,59 @@ const Map koStrings = { "ui.admin.role.tenant_admin": "TENANT ADMIN", "ui.admin.role.tenant_member": "TENANT MEMBER", "ui.admin.tenants.add": "테넌트 추가", + "ui.admin.tenants.admins.add_button": "관리자 추가", + "ui.admin.tenants.admins.already_admin": "이미 관리자", + "ui.admin.tenants.admins.dialog_description": "이름 또는 이메일로 사용자를 검색하세요.", + "ui.admin.tenants.admins.dialog_no_results": "검색 결과가 없습니다.", + "ui.admin.tenants.admins.dialog_search_hint": "검색어를 입력해 주세요.", + "ui.admin.tenants.admins.dialog_search_placeholder": "사용자 검색 (최소 2자)...", + "ui.admin.tenants.admins.dialog_title": "새 관리자 추가", + "ui.admin.tenants.admins.remove_title": "관리자 권한 회수", + "ui.admin.tenants.admins.table_actions": "액션", + "ui.admin.tenants.admins.table_email": "이메일", + "ui.admin.tenants.admins.table_name": "이름", + "ui.admin.tenants.admins.title": "테넌트 관리자", "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.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.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.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_profile": "프로필", + "ui.admin.tenants.detail.tab_schema": "사용자 스키마", + "ui.admin.tenants.detail.title": "상세", + "ui.admin.tenants.list.select_placeholder": "테넌트를 선택하세요", "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.profile.allowed_domains": "허용된 도메인 (콤마로 구분)", + "ui.admin.tenants.profile.allowed_domains_help": "이 도메인을 가진 이메일로 가입한 사용자는 자동으로 이 테넌트에 배정됩니다.", + "ui.admin.tenants.profile.approve_button": "테넌트 승인", + "ui.admin.tenants.profile.description": "설명", + "ui.admin.tenants.profile.name": "테넌트 이름", + "ui.admin.tenants.profile.slug": "슬러그 (Slug)", + "ui.admin.tenants.profile.status": "상태", + "ui.admin.tenants.profile.subtitle": "슬러그 및 상태 변경은 즉시 적용됩니다.", + "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.field.key": "Field Key (ID)", @@ -498,6 +591,7 @@ const Map koStrings = { "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": "테넌트 목록", "ui.admin.title": "Admin Control", @@ -511,12 +605,16 @@ 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.job_title": "직무", + "ui.admin.users.create.form.job_title_placeholder": "프론트엔드 개발", "ui.admin.users.create.form.name": "이름", "ui.admin.users.create.form.name_placeholder": "홍길동", "ui.admin.users.create.form.password": "비밀번호", "ui.admin.users.create.form.password_placeholder": "********", "ui.admin.users.create.form.phone": "전화번호", "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": "시스템 전역 (소속 없음)", @@ -530,10 +628,14 @@ const Map koStrings = { "ui.admin.users.detail.edit_title": "정보 수정", "ui.admin.users.detail.form.department": "부서", "ui.admin.users.detail.form.department_placeholder": "개발팀", + "ui.admin.users.detail.form.job_title": "직무", + "ui.admin.users.detail.form.job_title_placeholder": "프론트엔드 개발", "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.status": "상태", "ui.admin.users.detail.form.tenant": "테넌트 (Tenant)", @@ -552,14 +654,15 @@ const Map koStrings = { "ui.admin.users.list.table.actions": "ACTIONS", "ui.admin.users.list.table.created": "CREATED", "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.title": "사용자 관리", - "ui.btn.cancel": "취소", - "ui.btn.save": "저장", "ui.common.add": "추가", + "ui.common.admin_only": "관리자 전용", + "ui.common.assign": "할당", "ui.common.back": "돌아가기", "ui.common.badge.admin_only": "Admin only", "ui.common.badge.command_only": "Command only", @@ -574,9 +677,13 @@ const Map koStrings = { "ui.common.details": "상세정보", "ui.common.edit": "편집", "ui.common.hyphen": "-", + "ui.common.language": "언어", + "ui.common.language_en": "English", + "ui.common.language_ko": "한국어", "ui.common.na": "N/A", "ui.common.never": "Never", "ui.common.next": "Next", + "ui.common.none": "없음", "ui.common.page_of": "Page {{page}} of {{total}}", "ui.common.prev": "이전", "ui.common.previous": "Previous", @@ -590,6 +697,8 @@ const Map koStrings = { "ui.common.role.user": "User", "ui.common.save": "저장", "ui.common.search": "검색", + "ui.common.select": "사용자 선택", + "ui.common.select_placeholder": "사용자를 선택하세요", "ui.common.show_more": "+ 더보기", "ui.common.status.active": "Active", "ui.common.status.blocked": "Blocked", @@ -602,6 +711,7 @@ const Map koStrings = { "ui.common.theme_light": "Light", "ui.common.theme_toggle": "테마 전환", "ui.common.unknown": "Unknown", + "ui.common.view": "보기", "ui.dev.brand": "Baron 로그인", "ui.dev.clients.badge.admin_session": "관리자 세션", "ui.dev.clients.badge.tenant_selected": "테넌트: 선택됨", @@ -628,62 +738,59 @@ const Map koStrings = { "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": "클라이언트 상세", - "ui.dev.clients.details.breadcrumb.section": "Relying Parties", + "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.title": "클라이언트 자격 증명", + "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.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": "비밀키 숨기기", "ui.dev.clients.details.secret.rotate": "비밀키 재발급 (Rotate)", "ui.dev.clients.details.secret.show": "비밀키 보기", "ui.dev.clients.details.security.title": "보안 메모", - "ui.dev.clients.details.tab.connection": "Connection", + "ui.dev.clients.details.tab.connection": "연동 설정", "ui.dev.clients.details.tab.consents": "Consent & Users", "ui.dev.clients.details.tab.settings": "Settings", "ui.dev.clients.general.breadcrumb.section": "Applications", - "ui.dev.clients.general.create": "클라이언트 생성", - "ui.dev.clients.general.display_new": "새 클라이언트", + "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.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": "앱 이름", "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.redirect.placeholder": "https://app.example.com/callback, http://localhost:3000/auth/callback (콤마로 구분)", "ui.dev.clients.general.save": "설정 저장", "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.security.confidential": "Confidential", - "ui.dev.clients.general.security.public": "Public", + "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.help.docs_title": "Docs & Examples", "ui.dev.clients.help.title": "Need help with OIDC configuration?", "ui.dev.clients.help.view_guides": "View guides", - "ui.dev.clients.list.title": "클라이언트 목록", - "ui.dev.clients.new": "새 클라이언트", + "ui.dev.clients.list.title": "연동 앱 목록", + "ui.dev.clients.new": "연동 앱 추가", "ui.dev.clients.owner.avatar_alt": "ops user", "ui.dev.clients.owner.email": "admin@brsw.kr", "ui.dev.clients.owner.name": "AI Admin Bot", @@ -691,9 +798,9 @@ 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.subtitle": "Relying Parties", + "ui.dev.clients.registry.subtitle": "연동 앱", "ui.dev.clients.registry.title": "RP registry", - "ui.dev.clients.search_placeholder": "클라이언트 이름/ID로 검색...", + "ui.dev.clients.search_placeholder": "연동 앱 이름/ID로 검색...", "ui.dev.clients.table.actions": "액션", "ui.dev.clients.table.application": "애플리케이션", "ui.dev.clients.table.client_id": "Client ID", @@ -701,8 +808,8 @@ const Map koStrings = { "ui.dev.clients.table.status": "상태", "ui.dev.clients.table.type": "유형", "ui.dev.clients.tenant_scoped": "Tenant-scoped", - "ui.dev.clients.type.confidential": "기밀(Confidential)", - "ui.dev.clients.type.public": "Public", + "ui.dev.clients.type.pkce": "PKCE", + "ui.dev.clients.type.private": "Server side App", "ui.dev.clients.untitled": "Untitled", "ui.dev.console_title": "Developer Console", "ui.dev.dashboard.badge.consent_guard": "Consent guard ready", @@ -724,12 +831,24 @@ const Map koStrings = { "ui.dev.env_badge": "Env: dev", "ui.dev.header.plane": "Dev Plane", "ui.dev.header.subtitle": "Manage your applications", + "ui.dev.nav.clients": "연동 앱", + "ui.dev.nav.logout": "로그아웃", + "ui.dev.profile.menu_aria": "계정 메뉴 열기", + "ui.dev.profile.menu_title": "계정", + "ui.dev.profile.unknown_email": "unknown@example.com", + "ui.dev.profile.unknown_name": "Unknown User", "ui.dev.scope_badge": "Scoped to /dev", - "ui.nav.dashboard": "대시보드", + "ui.dev.session.active": "만료 시간 확인 중...", + "ui.dev.session.expired": "세션 만료", + "ui.dev.session.expiring": "만료 임박: {{minutes}}분 {{seconds}}초 남음", + "ui.dev.session.refresh": "세션 만료 시간 갱신", + "ui.dev.session.refreshing": "세션 만료 시간 갱신 중...", + "ui.dev.session.remaining": "만료 예정: {{minutes}}분 {{seconds}}초 남음", + "ui.dev.session.unknown": "확인 불가", "ui.userfront.app_label.admin_console": "Admin Console", "ui.userfront.app_label.baron": "Baron 로그인", "ui.userfront.app_label.dev_console": "Dev Console", - "ui.userfront.app_title": "Baron 로그인", + "ui.userfront.app_title": "Baron SW 포탈", "ui.userfront.audit.table.app": "애플리케이션", "ui.userfront.audit.table.auth_method": "인증수단", "ui.userfront.audit.table.date": "접속일자", @@ -864,6 +983,26 @@ const Map enStrings = { "domain.company.jangheon": "Jangheon", "domain.company.ptc": "PTC", "domain.company.saman": "Saman", + "domain.tenant_type.company": "Company", + "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.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.invalid_code": "The verification code is invalid.", + "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.common.unknown": "An unknown error occurred.", "err.userfront.auth_proxy.consent_accept": "Consent Accept", "err.userfront.auth_proxy.consent_fetch": "Consent Fetch", @@ -901,13 +1040,31 @@ const Map enStrings = { "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.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.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.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", @@ -922,22 +1079,28 @@ const Map enStrings = { "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.create.form.domains_help": - "Users with these email domains will be automatically assigned to this tenant.", + "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.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.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.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.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", @@ -964,16 +1127,21 @@ const Map enStrings = { "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.common.loading": "Loading...", + "msg.common.no_description": "No Description.", + "msg.common.requesting": "Requesting...", "msg.common.saving": "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.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.delete_error": "Failed to delete: {{error}}", + "msg.dev.clients.deleted": "App deleted.", "msg.dev.clients.details.copy_client_id": "Client ID copied.", "msg.dev.clients.details.copy_client_secret": "Copy Client Secret", "msg.dev.clients.details.copy_endpoint": "{{label}} copied.", @@ -994,26 +1162,25 @@ const Map enStrings = { "msg.dev.clients.general.identity.subtitle": "Subtitle", "msg.dev.clients.general.load_error": "Error loading client: {{error}}", "msg.dev.clients.general.loading": "Loading client...", - "msg.dev.clients.general.redirect.help": "Help", + "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.confidential_help": "Confidential Help", - "msg.dev.clients.general.security.public_help": "Public Help", - "msg.dev.clients.general.security.subtitle": "Subtitle", - "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.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.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 clients...", + "msg.dev.clients.loading": "Loading apps...", "msg.dev.clients.registry.description": "Description", "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}} clients", + "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 Updated", + "msg.dev.clients.status_updated": "The app has been {{status}}.", "msg.dev.dashboard.hero.body": "Body", "msg.dev.dashboard.hero.title_emphasis": "Title Emphasis", "msg.dev.dashboard.hero.title_prefix": "Title Prefix", @@ -1021,6 +1188,7 @@ const Map enStrings = { "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.logout_confirm": "Are you sure you want to log out?", "msg.dev.sidebar.notice": "Notice", "msg.dev.sidebar.notice_detail": "Notice Detail", "msg.info.saved_success": "Saved successfully.", @@ -1049,6 +1217,7 @@ const Map enStrings = { "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.render_error": "Dashboard render error: {{error}}", "msg.userfront.dashboard.revoke.confirm": "Confirm", "msg.userfront.dashboard.revoke.error": "Error", "msg.userfront.dashboard.revoke.success": "Success", @@ -1059,11 +1228,34 @@ const Map enStrings = { "msg.userfront.error.detail_generic": "Detail Generic", "msg.userfront.error.detail_request": "Detail Request", "msg.userfront.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.invalid_client": "Client authentication failed.", + "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.whitelist.\$normalizedCode": "\$NormalizedCode", + "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", @@ -1073,7 +1265,7 @@ const Map enStrings = { "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": "Helper", + "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", @@ -1082,14 +1274,14 @@ const Map enStrings = { "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.login.link_timeout": "Link Timeout", - "msg.userfront.login.no_account": "No Account", + "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.qr_expired": "QR Expired", + "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", @@ -1188,14 +1380,12 @@ const Map enStrings = { "msg.userfront.signup.policy.summary": "Summary", "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.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.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", "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", @@ -1248,20 +1438,46 @@ const Map enStrings = { "ui.admin.audit.title": "Title", "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.groups.detail.breadcrumb_org": "Breadcrumb Org", + "ui.admin.groups.detail.breadcrumb_tenant": "Tenant Details", + "ui.admin.groups.detail.breadcrumb_unit": "Breadcrumb Unit", + "ui.admin.groups.detail.members_subtitle": "Members Subtitle", + "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_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.submit": "Submit", + "ui.admin.groups.form.unit_level_label": "Unit Level Label", + "ui.admin.groups.form.unit_level_placeholder": "Unit Level Placeholder", + "ui.admin.groups.import_csv": "Import Csv", "ui.admin.groups.list.title": "User Groups", "ui.admin.groups.members.table.email": "Email", "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.nav.api_keys": "API Keys", + "ui.admin.nav.audit_logs": "Audit Logs", + "ui.admin.nav.auth_guard": "Auth Guard", + "ui.admin.nav.logout": "Logout", + "ui.admin.nav.overview": "Overview", + "ui.admin.nav.relying_parties": "Apps (RP)", + "ui.admin.nav.tenant_dashboard": "Tenant Dashboard", + "ui.admin.nav.tenants": "Tenants", + "ui.admin.nav.user_groups": "User Groups", + "ui.admin.nav.users": "Users", "ui.admin.overview.kicker": "Global Overview", "ui.admin.overview.playbook.title": "Admin playbook", "ui.admin.overview.quick_links.add_tenant": "Tenant Add", @@ -1274,26 +1490,59 @@ const Map enStrings = { "ui.admin.role.tenant_admin": "TENANT ADMIN", "ui.admin.role.tenant_member": "TENANT MEMBER", "ui.admin.tenants.add": "Tenant Add", + "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_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.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.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": "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.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.tab_federation": "Tab Federation", + "ui.admin.tenants.detail.tab_organization": "Organization Manage", + "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.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.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.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.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.key": "Field Key (ID)", @@ -1317,6 +1566,7 @@ const Map enStrings = { "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.title": "Admin Control", @@ -1330,12 +1580,16 @@ const Map enStrings = { "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.job_title": "Job Title", + "ui.admin.users.create.form.job_title_placeholder": "Job Title Placeholder", "ui.admin.users.create.form.name": "Name", "ui.admin.users.create.form.name_placeholder": "Name Placeholder", "ui.admin.users.create.form.password": "Password", "ui.admin.users.create.form.password_placeholder": "********", "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.role": "Role", "ui.admin.users.create.form.tenant": "Tenant (Tenant)", "ui.admin.users.create.form.tenant_global": "Tenant Global", @@ -1349,10 +1603,14 @@ const Map enStrings = { "ui.admin.users.detail.edit_title": "Edit Title", "ui.admin.users.detail.form.department": "Department", "ui.admin.users.detail.form.department_placeholder": "Department Placeholder", + "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.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.role": "Role", "ui.admin.users.detail.form.status": "Status", "ui.admin.users.detail.form.tenant": "Tenant (Tenant)", @@ -1371,14 +1629,15 @@ const Map enStrings = { "ui.admin.users.list.table.actions": "ACTIONS", "ui.admin.users.list.table.created": "CREATED", "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.title": "User Manage", - "ui.btn.cancel": "Cancel", - "ui.btn.save": "Save", "ui.common.add": "Add", + "ui.common.admin_only": "Admin Only", + "ui.common.assign": "Assign", "ui.common.back": "Back", "ui.common.badge.admin_only": "Admin only", "ui.common.badge.command_only": "Command only", @@ -1393,9 +1652,13 @@ const Map enStrings = { "ui.common.details": "Details", "ui.common.edit": "Edit", "ui.common.hyphen": "-", + "ui.common.language": "Language", + "ui.common.language_en": "English", + "ui.common.language_ko": "Language Ko", "ui.common.na": "N/A", "ui.common.never": "Never", "ui.common.next": "Next", + "ui.common.none": "None", "ui.common.page_of": "Page {{page}} of {{total}}", "ui.common.prev": "Prev", "ui.common.previous": "Previous", @@ -1409,6 +1672,8 @@ const Map enStrings = { "ui.common.role.user": "User", "ui.common.save": "Save", "ui.common.search": "Search", + "ui.common.select": "User Optional", + "ui.common.select_placeholder": "Select Placeholder", "ui.common.show_more": "Show More", "ui.common.status.active": "Active", "ui.common.status.blocked": "Blocked", @@ -1421,6 +1686,7 @@ const Map enStrings = { "ui.common.theme_light": "Light", "ui.common.theme_toggle": "Theme Toggle", "ui.common.unknown": "Unknown", + "ui.common.view": "View", "ui.dev.brand": "Brand", "ui.dev.clients.badge.admin_session": "Admin Session", "ui.dev.clients.badge.tenant_selected": "Tenant Selected", @@ -1448,7 +1714,7 @@ const Map enStrings = { "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": "Relying Parties", + "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", @@ -1456,28 +1722,25 @@ const Map enStrings = { "ui.dev.clients.details.endpoints.title": "Title", "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.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.tab.connection": "Connection", + "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.general.breadcrumb.section": "Applications", - "ui.dev.clients.general.create": "Create", - "ui.dev.clients.general.display_new": "Display New", + "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_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", @@ -1486,23 +1749,23 @@ const Map enStrings = { "ui.dev.clients.general.redirect.placeholder": "Placeholder", "ui.dev.clients.general.save": "Settings Save", "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.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.security.confidential": "Confidential", - "ui.dev.clients.general.security.public": "Public", + "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.title_create": "Create Client", "ui.dev.clients.general.title_edit": "Client Settings", "ui.dev.clients.help.docs_title": "Docs & Examples", "ui.dev.clients.help.title": "Need help with OIDC configuration?", "ui.dev.clients.help.view_guides": "View guides", - "ui.dev.clients.list.title": "Title", - "ui.dev.clients.new": "New", + "ui.dev.clients.list.title": "Connected Applications", + "ui.dev.clients.new": "Add Connected Application", "ui.dev.clients.owner.avatar_alt": "ops user", "ui.dev.clients.owner.email": "admin@brsw.kr", "ui.dev.clients.owner.name": "AI Admin Bot", @@ -1510,9 +1773,9 @@ 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.subtitle": "Relying Parties", + "ui.dev.clients.registry.subtitle": "Applications", "ui.dev.clients.registry.title": "RP registry", - "ui.dev.clients.search_placeholder": "Search Placeholder", + "ui.dev.clients.search_placeholder": "Search by app name or ID...", "ui.dev.clients.table.actions": "Actions", "ui.dev.clients.table.application": "Application", "ui.dev.clients.table.client_id": "Client ID", @@ -1520,8 +1783,8 @@ const Map enStrings = { "ui.dev.clients.table.status": "Status", "ui.dev.clients.table.type": "Type", "ui.dev.clients.tenant_scoped": "Tenant-scoped", - "ui.dev.clients.type.confidential": "Confidential", - "ui.dev.clients.type.public": "Public", + "ui.dev.clients.type.pkce": "PKCE", + "ui.dev.clients.type.private": "Server side App", "ui.dev.clients.untitled": "Untitled", "ui.dev.console_title": "Developer Console", "ui.dev.dashboard.badge.consent_guard": "Consent guard ready", @@ -1543,12 +1806,24 @@ const Map enStrings = { "ui.dev.env_badge": "Env: dev", "ui.dev.header.plane": "Dev Plane", "ui.dev.header.subtitle": "Manage your applications", + "ui.dev.nav.clients": "Connected Application", + "ui.dev.nav.logout": "Logout", + "ui.dev.profile.menu_aria": "Open account menu", + "ui.dev.profile.menu_title": "Account", + "ui.dev.profile.unknown_email": "unknown@example.com", + "ui.dev.profile.unknown_name": "Unknown User", "ui.dev.scope_badge": "Scoped to /dev", - "ui.nav.dashboard": "Dashboard", + "ui.dev.session.active": "Checking expiration...", + "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.remaining": "Expires in: {{minutes}}m {{seconds}}s", + "ui.dev.session.unknown": "Unknown", "ui.userfront.app_label.admin_console": "Admin Console", "ui.userfront.app_label.baron": "Baron", "ui.userfront.app_label.dev_console": "Dev Console", - "ui.userfront.app_title": "App Title", + "ui.userfront.app_title": "Baron SW Portal", "ui.userfront.audit.table.app": "App", "ui.userfront.audit.table.auth_method": "Auth Method", "ui.userfront.audit.table.date": "Date", @@ -1581,7 +1856,7 @@ const Map enStrings = { "ui.userfront.forgot.submit": "Submit", "ui.userfront.forgot.title": "Title", "ui.userfront.login.action.submit": "Submit", - "ui.userfront.login.field.login_id": "Login Id", + "ui.userfront.login.field.login_id": "Emain or Phone Number", "ui.userfront.login.field.password": "Password", "ui.userfront.login.forgot_password": "Forgot Password", "ui.userfront.login.link.action_label": "Action Label", @@ -1592,15 +1867,15 @@ const Map enStrings = { "ui.userfront.login.link.title": "Title", "ui.userfront.login.qr.expired": "Expired", "ui.userfront.login.qr.refresh": "Refresh", - "ui.userfront.login.qr.remaining": "Remaining", + "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", + "ui.userfront.login.tabs.link": "Link/Code", "ui.userfront.login.tabs.password": "Password", - "ui.userfront.login.tabs.qr": "QR", + "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", From 3cfece2a33e16231c9eb7fcc43d0649c5426e4dc Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 25 Feb 2026 16:07:41 +0900 Subject: [PATCH 14/22] =?UTF-8?q?devfront=20UI=20=EC=9D=BC=EA=B4=80?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=A0=95=EB=A0=AC=20=EC=A0=95=EC=B1=85?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/components/layout/AppLayout.tsx | 26 +- .../features/clients/ClientGeneralPage.tsx | 155 ++++---- .../clients/routes/ClientFederationPage.tsx | 356 +++++++++--------- devfront/src/locales/en.toml | 1 + devfront/src/locales/ko.toml | 1 + devfront/src/locales/template.toml | 1 + locales/en.toml | 2 +- locales/ko.toml | 2 +- 8 files changed, 287 insertions(+), 257 deletions(-) diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx index 9e798031..a5163929 100644 --- a/devfront/src/components/layout/AppLayout.tsx +++ b/devfront/src/components/layout/AppLayout.tsx @@ -197,7 +197,20 @@ function AppLayout() { ))}
-
+
+ +
+
+ +
+

{t("msg.dev.sidebar.notice", "개발자 전용 콘솔입니다.")}

{t( @@ -207,17 +220,6 @@ function AppLayout() {

- -
- -
diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index ba910abd..349bba58 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { Plus, Shield, Sparkles, Trash2, Upload } from "lucide-react"; +import { Plus, Save, Shield, Sparkles, Trash2, Upload } from "lucide-react"; import { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { Badge } from "../../components/ui/badge"; @@ -16,6 +16,7 @@ import { Input } from "../../components/ui/input"; import { Label } from "../../components/ui/label"; import { Switch } from "../../components/ui/switch"; import { Textarea } from "../../components/ui/textarea"; +import { toast } from "../../components/ui/use-toast"; import { createClient, deleteClient, @@ -128,6 +129,21 @@ function ClientGeneralPage() { setScopes(scopes.filter((s) => s.id !== id)); }; + const handleStatusChange = (nextStatus: ClientStatus) => { + setStatus(nextStatus); + const statusLabel = + nextStatus === "active" + ? t("ui.common.status.active", "Active") + : t("ui.common.status.inactive", "Inactive"); + toast( + t( + "msg.dev.clients.general.status_changed", + "상태가 {{status}}로 변경되었습니다.", + { status: statusLabel }, + ), + ); + }; + const mutation = useMutation({ mutationFn: async () => { const scopeNames = scopes.map((scope) => scope.name).filter(Boolean); @@ -204,7 +220,7 @@ function ClientGeneralPage() { window.confirm( t( "msg.dev.clients.delete_confirm", - "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "정말로 이 앱을 삭제하시습니까? 이 작업은 되돌릴 수 없습니다.", ), ) ) { @@ -309,33 +325,6 @@ function ClientGeneralPage() { )}
- {!isCreate && ( -
- -
- - setStatus(checked ? "active" : "inactive") - } - /> - - {status === "active" - ? t("ui.common.status.active", "활성") - : t("ui.common.status.inactive", "비활성")} - -
-
- )}
@@ -371,40 +360,66 @@ function ClientGeneralPage() { />
-
- -
-
- setLogoUrl(e.target.value)} - placeholder={t( - "ui.dev.clients.general.identity.logo_placeholder", - "https://example.com/logo.png", - )} - /> -

- {t( - "msg.dev.clients.general.identity.logo_help", - "인증 화면에 표시될 PNG/SVG URL입니다.", - )} -

-
-
- {logoUrl ? ( - {t( +
+ +
+
+ setLogoUrl(e.target.value)} + placeholder={t( + "ui.dev.clients.general.identity.logo_placeholder", + "https://example.com/logo.png", )} - className="h-full w-full object-contain" /> - ) : ( - - )} +

+ {t( + "msg.dev.clients.general.identity.logo_help", + "인증 화면에 표시될 PNG/SVG URL입니다.", + )} +

+
+
+ {logoUrl ? ( + {t( + ) : ( + + )} +
+
+
+ +
+ +
+ +
@@ -658,20 +673,30 @@ function ClientGeneralPage() { )}
-
+
diff --git a/devfront/src/features/clients/routes/ClientFederationPage.tsx b/devfront/src/features/clients/routes/ClientFederationPage.tsx index 898f4dc7..7fc7ca69 100644 --- a/devfront/src/features/clients/routes/ClientFederationPage.tsx +++ b/devfront/src/features/clients/routes/ClientFederationPage.tsx @@ -1,11 +1,30 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Plus, Trash2, Edit, Globe, Save } from "lucide-react"; import { useState } from "react"; import { useParams } from "react-router-dom"; +import { Button } from "../../../components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../../../components/ui/card"; +import { Input } from "../../../components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../../../components/ui/table"; import { createIdpConfigForClient, listIdpConfigsForClient, } from "../../../lib/devApi"; import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi"; +import { t } from "../../../lib/i18n"; // Proper Modal Component with Form const CreateIdpModal = ({ @@ -37,12 +56,10 @@ const CreateIdpModal = ({ onClose(); }, onError: (error) => { - // Basic error handling alert(`Failed to create configuration: ${error.message}`); }, }); - // 이 내용으로 교체해주세요 const handleChange = ( e: React.ChangeEvent, ) => { @@ -61,104 +78,89 @@ const CreateIdpModal = ({ if (!isOpen) return null; return ( -
-
-

Add New IdP Configuration

-
- {/* Display Name */} -
- - -
+
+ + + {t("ui.dev.clients.federation.add_title", "Add Identity Provider")} + + {t("msg.dev.clients.federation.add_subtitle", "Connect an external OIDC provider.")} + + + + +
+ + +
- {/* Issuer URL */} -
- - -
+
+ + +
- {/* Client ID */} -
- - -
+
+
+ + +
+
+ + +
+
- {/* Client Secret */} -
- - -
+
+ + +
- {/* Scopes */} -
- - -
- - {/* Action Buttons */} -
- - -
- -
+
+ + +
+ + +
); }; @@ -168,7 +170,7 @@ export function ClientFederationPage() { const [isCreateModalOpen, setCreateModalOpen] = useState(false); if (!clientId) { - return
Client ID is missing
; + return
Client ID is missing
; } const { data, isLoading, error } = useQuery({ @@ -177,94 +179,92 @@ export function ClientFederationPage() { }); return ( -
-

Identity Federation Settings

-

- Manage external identity providers for this application. -

+
+
+
+

+ + {t("ui.dev.clients.federation.title", "Identity Federation")} +

+

+ {t("msg.dev.clients.federation.subtitle", "Manage external identity providers for this application.")} +

+
+ +
-
- -
+ + + + + + Display Name + Provider Type + Status + Actions + + + + {isLoading ? ( + + + {t("msg.common.loading", "Loading...")} + + + ) : error ? ( + + + {(error as Error).message} + + + ) : data?.length === 0 ? ( + + + {t("msg.dev.clients.federation.empty", "No IdP configurations found.")} + + + ) : ( + data?.map((config: IdpConfig) => ( + + {config.display_name} + {config.provider_type.toUpperCase()} + + + {config.status} + + + +
+ + +
+
+ + )) + )} + +
+
+
setCreateModalOpen(false)} clientId={clientId} /> - - {isLoading &&
Loading configurations...
} - {error && ( -
- Failed to load configurations: {error.message} -
- )} - - {data && ( -
- - - - - - - - - - - {data.length === 0 ? ( - - - - ) : ( - data.map((config: IdpConfig) => ( - - - - - - - )) - )} - -
Display NameProvider TypeStatusActions
- No IdP configurations found. -
- {config.display_name} - - {config.provider_type.toUpperCase()} - - - {config.status} - - - - -
-
- )}
); } diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index e01dd020..6b0d3b9c 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -252,6 +252,7 @@ load_error = "Error loading client: {{error}}" loading = "Loading client..." saved = "Saved" save_error = "Failed to save: {{error}}" +status_changed = "Status changed to {{status}}." [msg.dev.clients.general.identity] logo_help = "Logo Help" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index ef85680a..dfea9909 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -252,6 +252,7 @@ load_error = "Error loading client: {{error}}" loading = "Loading client..." saved = "설정이 저장되었습니다." save_error = "저장 실패: {{error}}" +status_changed = "상태가 {{status}}로 변경되었습니다." [msg.dev.clients.general.identity] logo_help = "인증 화면에 표시될 PNG/SVG URL입니다." diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index 5c2934c5..dc467da6 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -252,6 +252,7 @@ load_error = "" loading = "" saved = "" save_error = "" +status_changed = "" [msg.dev.clients.general.identity] logo_help = "" diff --git a/locales/en.toml b/locales/en.toml index 48fed153..fb709370 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -1168,7 +1168,7 @@ settings = "Settings" [ui.dev.clients.general] create = "Create Application" display_new = "Add Connected Application" -save = "Settings Save" +save = "Save" title_create = "Create Client" title_edit = "Client Settings" diff --git a/locales/ko.toml b/locales/ko.toml index 07162c9c..f28333d8 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -1168,7 +1168,7 @@ settings = "Settings" [ui.dev.clients.general] create = "앱 생성" display_new = "연동 앱 추가" -save = "설정 저장" +save = "저장" title_create = "Create Client" title_edit = "Client Settings" From 6d80b04a55c24bad851e423cce9f5e661670266b Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 25 Feb 2026 16:48:11 +0900 Subject: [PATCH 15/22] =?UTF-8?q?locales=20sync=20=EB=B0=8F=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clients/routes/ClientFederationPage.tsx | 64 +++++++++++++++---- devfront/src/locales/en.toml | 11 +++- devfront/src/locales/ko.toml | 11 +++- devfront/src/locales/template.toml | 11 +++- locales/en.toml | 12 +++- locales/ko.toml | 12 +++- locales/template.toml | 12 +++- userfront/assets/translations/en.toml | 4 +- userfront/assets/translations/ko.toml | 20 +++--- 9 files changed, 125 insertions(+), 32 deletions(-) diff --git a/devfront/src/features/clients/routes/ClientFederationPage.tsx b/devfront/src/features/clients/routes/ClientFederationPage.tsx index 7fc7ca69..4d63eead 100644 --- a/devfront/src/features/clients/routes/ClientFederationPage.tsx +++ b/devfront/src/features/clients/routes/ClientFederationPage.tsx @@ -81,9 +81,14 @@ const CreateIdpModal = ({
- {t("ui.dev.clients.federation.add_title", "Add Identity Provider")} + + {t("ui.dev.clients.federation.add_title", "Add Identity Provider")} + - {t("msg.dev.clients.federation.add_subtitle", "Connect an external OIDC provider.")} + {t( + "msg.dev.clients.federation.add_subtitle", + "Connect an external OIDC provider.", + )} @@ -146,16 +151,22 @@ const CreateIdpModal = ({ -
@@ -170,7 +181,11 @@ export function ClientFederationPage() { const [isCreateModalOpen, setCreateModalOpen] = useState(false); if (!clientId) { - return
Client ID is missing
; + return ( +
+ Client ID is missing +
+ ); } const { data, isLoading, error } = useQuery({ @@ -187,7 +202,10 @@ export function ClientFederationPage() { {t("ui.dev.clients.federation.title", "Identity Federation")}

- {t("msg.dev.clients.federation.subtitle", "Manage external identity providers for this application.")} + {t( + "msg.dev.clients.federation.subtitle", + "Manage external identity providers for this application.", + )}

-
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index 6b0d3b9c..45da9846 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -254,6 +254,11 @@ saved = "Saved" save_error = "Failed to save: {{error}}" status_changed = "Status changed to {{status}}." +[msg.dev.clients.federation] +subtitle = "Manage external identity providers for this application." +add_subtitle = "Connect an external OIDC provider." +empty = "No IdP configurations found." + [msg.dev.clients.general.identity] logo_help = "Logo Help" subtitle = "Subtitle" @@ -1020,10 +1025,14 @@ settings = "Settings" [ui.dev.clients.general] create = "Create Application" display_new = "Add Connected Application" -save = "Settings Save" title_create = "Create Client" title_edit = "Client Settings" +[ui.dev.clients.federation] +title = "Identity Federation" +add_title = "Add Identity Provider" +add_btn = "Add Provider" + [ui.dev.clients.general.breadcrumb] section = "Applications" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index dfea9909..35a3da00 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -254,6 +254,11 @@ saved = "설정이 저장되었습니다." save_error = "저장 실패: {{error}}" status_changed = "상태가 {{status}}로 변경되었습니다." +[msg.dev.clients.federation] +subtitle = "이 애플리케이션의 외부 IdP 설정을 관리합니다." +add_subtitle = "외부 OIDC 제공자를 연결합니다." +empty = "등록된 IdP 설정이 없습니다." + [msg.dev.clients.general.identity] logo_help = "인증 화면에 표시될 PNG/SVG URL입니다." subtitle = "앱 이름과 설명, 로고를 설정합니다." @@ -1020,10 +1025,14 @@ settings = "Settings" [ui.dev.clients.general] create = "앱 생성" display_new = "연동 앱 추가" -save = "설정 저장" title_create = "Create Client" title_edit = "Client Settings" +[ui.dev.clients.federation] +title = "Identity Federation" +add_title = "Add Identity Provider" +add_btn = "Add Provider" + [ui.dev.clients.general.breadcrumb] section = "Applications" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index dc467da6..b1cb4d35 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -254,6 +254,11 @@ saved = "" save_error = "" status_changed = "" +[msg.dev.clients.federation] +subtitle = "" +add_subtitle = "" +empty = "" + [msg.dev.clients.general.identity] logo_help = "" subtitle = "" @@ -1032,10 +1037,14 @@ settings = "" [ui.dev.clients.general] create = "" display_new = "" -save = "" title_create = "" title_edit = "" +[ui.dev.clients.federation] +title = "" +add_title = "" +add_btn = "" + [ui.dev.clients.general.breadcrumb] section = "" diff --git a/locales/en.toml b/locales/en.toml index fb709370..2430cdf3 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -309,6 +309,12 @@ load_error = "Error loading client: {{error}}" loading = "Loading client..." saved = "Saved" save_error = "Failed to save: {{error}}" +status_changed = "Status changed to {{status}}." + +[msg.dev.clients.federation] +subtitle = "Manage external identity providers for this application." +add_subtitle = "Connect an external OIDC provider." +empty = "No IdP configurations found." [msg.dev.clients.general.identity] logo_help = "Logo Help" @@ -1168,10 +1174,14 @@ settings = "Settings" [ui.dev.clients.general] create = "Create Application" display_new = "Add Connected Application" -save = "Save" title_create = "Create Client" title_edit = "Client Settings" +[ui.dev.clients.federation] +title = "Identity Federation" +add_title = "Add Identity Provider" +add_btn = "Add Provider" + [ui.dev.clients.general.breadcrumb] section = "Applications" diff --git a/locales/ko.toml b/locales/ko.toml index f28333d8..3622ae6f 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -309,6 +309,12 @@ load_error = "Error loading client: {{error}}" loading = "Loading client..." saved = "설정이 저장되었습니다." save_error = "저장 실패: {{error}}" +status_changed = "상태가 {{status}}로 변경되었습니다." + +[msg.dev.clients.federation] +subtitle = "이 애플리케이션의 외부 IdP 설정을 관리합니다." +add_subtitle = "외부 OIDC 제공자를 연결합니다." +empty = "등록된 IdP 설정이 없습니다." [msg.dev.clients.general.identity] logo_help = "인증 화면에 표시될 PNG/SVG URL입니다." @@ -1168,10 +1174,14 @@ settings = "Settings" [ui.dev.clients.general] create = "앱 생성" display_new = "연동 앱 추가" -save = "저장" title_create = "Create Client" title_edit = "Client Settings" +[ui.dev.clients.federation] +title = "Identity Federation" +add_title = "Add Identity Provider" +add_btn = "Add Provider" + [ui.dev.clients.general.breadcrumb] section = "Applications" diff --git a/locales/template.toml b/locales/template.toml index 55567ffb..69dd2124 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -250,6 +250,12 @@ load_error = "" loading = "" saved = "" save_error = "" +status_changed = "" + +[msg.dev.clients.federation] +subtitle = "" +add_subtitle = "" +empty = "" [msg.dev.clients.general.identity] logo_help = "" @@ -1030,10 +1036,14 @@ settings = "" [ui.dev.clients.general] create = "" display_new = "" -save = "" title_create = "" title_edit = "" +[ui.dev.clients.federation] +title = "" +add_title = "" +add_btn = "" + [ui.dev.clients.general.breadcrumb] section = "" diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 1acbecba..4e906a09 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -235,8 +235,8 @@ disabled = "Disabled" [msg.userfront.signup] failed = "Failed" -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" -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" +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" +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.agreement] title = "Title" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 6442beeb..1c180d38 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -54,12 +54,12 @@ empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다." error = "연동 정보를 불러오지 못했습니다." [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} 연동이 해지되었습니다." @@ -150,7 +150,7 @@ scan_hint = "모바일 앱으로 스캔하세요" invalid = "문자 2개와 숫자 6자리를 입력해 주세요." [msg.userfront.login.unregistered] -body = "가입되지 않은 정보입니다.\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n회원가입 후 이용해 주세요." +body = "가입되지 않은 정보입니다.\n회원가입 후 이용해 주세요." [msg.userfront.login.verification] approved = "승인되었습니다. 로그인은 요청하신 창에서 완료됩니다." @@ -235,15 +235,15 @@ disabled = "현재 계정 설정 화면은 준비 중입니다." [msg.userfront.signup] failed = "가입 실패: {error}" -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" -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" +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" +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.agreement] -title = "서비스 이용을 위해\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n약관에 동의해주세요" +title = "서비스 이용을 위해\n약관에 동의해주세요" [msg.userfront.signup.auth] affiliate_notice = "가족사 회원의 경우 반드시 회사 공식 이메일을 입력해주세요." -title = "본인 확인을 위해\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n인증을 진행해주세요" +title = "본인 확인을 위해\n인증을 진행해주세요" [msg.userfront.signup.email] code_mismatch = "인증코드가 일치하지 않습니다." @@ -259,7 +259,7 @@ lowercase_required = "소문자가 최소 1개 이상 포함되어야 합니다. mismatch = "비밀번호가 일치하지 않습니다." number_required = "숫자가 최소 1개 이상 포함되어야 합니다." symbol_required = "특수문자가 최소 1개 이상 포함되어야 합니다." -title = "마지막으로\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n비밀번호를 설정해주세요" +title = "마지막으로\n비밀번호를 설정해주세요" uppercase_required = "대문자가 최소 1개 이상 포함되어야 합니다." [msg.userfront.signup.password.rule] @@ -288,7 +288,7 @@ uppercase = "대문자" [msg.userfront.signup.profile] affiliate_hint = "가족사 이메일 사용 시 자동으로 선택됩니다." -title = "회원님의\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n소속 정보를 알려주세요" +title = "회원님의\n소속 정보를 알려주세요" [msg.userfront.signup.success] body = "성공적으로 가입되었습니다." From 24f9fb6904f1ce04e1ba0daabac3fbf1ff40fc8b Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 25 Feb 2026 17:07:08 +0900 Subject: [PATCH 16/22] =?UTF-8?q?flutter=20=EB=A6=B0=ED=8A=B8=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- userfront/lib/i18n_data.dart | 303 ++++++++++++++++++++++++----------- 1 file changed, 206 insertions(+), 97 deletions(-) diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart index 98bc2601..ced42124 100644 --- a/userfront/lib/i18n_data.dart +++ b/userfront/lib/i18n_data.dart @@ -49,7 +49,8 @@ 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": "표시됩니다.", @@ -57,14 +58,16 @@ const Map koStrings = { "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.filters.empty": "필터 없음", "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": "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", + "msg.admin.audit.subtitle": + "Command 요청 기반 ClickHouse 로그를 조회합니다. 사용자/테넌트는 추후 세션 연동 시 자동 채워집니다.", "msg.admin.groups.list.create_error": "생성 실패", "msg.admin.groups.list.create_success": "조직 단위가 생성되었습니다.", "msg.admin.groups.list.delete_confirm": "정말로 삭제하시겠습니까?", @@ -85,20 +88,25 @@ 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_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.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.scope_admin": "Scoped to /admin", @@ -111,10 +119,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": "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": + "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.subtitle": "글로벌 운영 기준의 신규 테넌트를 등록합니다.", "msg.admin.tenants.delete_confirm": "테넌트 \"{{name}}\"를 삭제할까요?", "msg.admin.tenants.delete_success": "테넌트가 삭제되었습니다.", @@ -123,9 +135,11 @@ const Map koStrings = { "msg.admin.tenants.members.empty": "소속된 사용자가 없습니다.", "msg.admin.tenants.missing_id": "테넌트 ID가 없습니다.", "msg.admin.tenants.registry.count": "총 {{count}}개 테넌트", - "msg.admin.tenants.schema.empty": "No custom fields defined. Click \"Add Field\" to begin.", + "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.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": "하위 테넌트가 없습니다.", @@ -135,16 +149,19 @@ const Map koStrings = { "msg.admin.users.create.error": "사용자 생성에 실패했습니다.", "msg.admin.users.create.form.email_required": "이메일은 필수입니다.", "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.detail.edit_subtitle": "{{email}} 계정의 정보를 수정합니다.", "msg.admin.users.detail.form.name_required": "이름은 필수입니다.", "msg.admin.users.detail.not_found": "사용자를 찾을 수 없습니다.", - "msg.admin.users.detail.security.password_hint": "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다.", + "msg.admin.users.detail.security.password_hint": + "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다.", "msg.admin.users.detail.update_error": "사용자 수정에 실패했습니다.", "msg.admin.users.detail.update_success": "사용자 정보가 수정되었습니다.", "msg.admin.users.list.delete_confirm": "사용자 \"{{name}}\"을(를) 정말 삭제하시겠습니까?", @@ -161,7 +178,8 @@ const Map koStrings = { "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.showing": + "Showing {{from}} to {{to}} of {{total}} users", "msg.dev.clients.consents.subtitle": "OIDC Relying Party 사용자 권한을 검토·관리합니다.", "msg.dev.clients.copy_client_id": "Client ID가 복사되었습니다.", "msg.dev.clients.delete_confirm": "정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", @@ -173,40 +191,56 @@ const Map koStrings = { "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가 필요합니다.", - "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.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.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.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.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.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": "에서 관리합니다.", @@ -226,12 +260,15 @@ const Map koStrings = { "msg.userfront.audit.session_id": "Session ID: {{value}}", "msg.userfront.audit.status": "현황: (준비중)", "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": "접속이력을 불러오지 못했습니다.", @@ -243,7 +280,8 @@ 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": "요청된 권한이 없습니다.", @@ -275,13 +313,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": "이메일 또는 휴대폰 번호를 입력해주세요.", @@ -294,7 +336,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": "입력하신 이메일로 로그인 링크를 보냈습니다.", @@ -303,7 +346,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": "시간이 경과되었습니다.", @@ -313,7 +357,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": "성공적으로 로그인되었습니다.", @@ -351,7 +396,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개 이상의 특수문자를 포함해야 합니다.", @@ -371,7 +417,8 @@ const Map koStrings = { "msg.userfront.sections.audit_subtitle": "Baron 로그인 기준의 최근 접근 기록입니다.", "msg.userfront.settings.disabled": "현재 계정 설정 화면은 준비 중입니다.", "msg.userfront.signup.agreement.title": "서비스 이용을 위해\n약관에 동의해주세요", - "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": "이미 가입된 이메일입니다.", @@ -381,7 +428,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": "소문자", @@ -392,7 +440,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": "✅ 휴대폰 인증 완료", @@ -405,14 +454,17 @@ 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": + "\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": "가족사 이메일 사용 시 자동으로 선택됩니다.", "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": + "\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", "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 키 발급하기", @@ -532,7 +584,8 @@ const Map koStrings = { "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.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": "상위 테넌트 (선택)", @@ -559,7 +612,8 @@ const Map koStrings = { "ui.admin.tenants.members.table.status": "STATUS", "ui.admin.tenants.members.title": "Tenant Members ({{count}})", "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.name": "테넌트 이름", @@ -747,7 +801,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": "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": "Redirect URIs 저장", "ui.dev.clients.details.redirect.title": "리디렉션 URI 설정", "ui.dev.clients.details.secret.hide": "비밀키 숨기기", @@ -757,22 +812,27 @@ const Map koStrings = { "ui.dev.clients.details.tab.connection": "연동 설정", "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.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.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": "앱 이름", "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.save": "설정 저장", + "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", @@ -987,22 +1047,27 @@ 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", @@ -1060,7 +1125,8 @@ const Map enStrings = { "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_confirm": + "msg.admin.groups.roles.remove_confirm", "msg.admin.groups.roles.remove_success": "Remove Success", "msg.admin.header.subtitle": "Tenant isolation & least privilege by default", "msg.admin.idp_env_prod": "IDP env: prod", @@ -1086,7 +1152,8 @@ const Map enStrings = { "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.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", @@ -1098,9 +1165,11 @@ const Map enStrings = { "msg.admin.tenants.members.empty": "Empty", "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.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.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", @@ -1136,10 +1205,12 @@ const Map enStrings = { "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.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.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.", @@ -1158,20 +1229,32 @@ const Map enStrings = { "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.federation.empty": "No IdP configurations found.", + "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.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.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.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.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.load_error": "Error loading clients: {{error}}", "msg.dev.clients.loading": "Loading apps...", "msg.dev.clients.registry.description": "Description", @@ -1229,33 +1312,49 @@ const Map enStrings = { "msg.userfront.error.detail_request": "Detail Request", "msg.userfront.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": "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.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.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", @@ -1380,12 +1479,14 @@ const Map enStrings = { "msg.userfront.signup.policy.summary": "Summary", "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.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.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", "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", @@ -1495,7 +1596,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", @@ -1507,7 +1609,8 @@ const Map enStrings = { "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.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": "Parent", @@ -1722,7 +1825,8 @@ const Map enStrings = { "ui.dev.clients.details.endpoints.title": "Title", "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.secret.hide": "Hide", @@ -1732,24 +1836,29 @@ const Map enStrings = { "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.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_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.redirect.label": "Redirect URIs", "ui.dev.clients.general.redirect.placeholder": "Placeholder", - "ui.dev.clients.general.save": "Settings Save", "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.delete": "Delete", "ui.dev.clients.general.scopes.table.description": "Description", From 099a8c768c2d5c321fc9dc4a672c5b241ff97535 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 26 Feb 2026 12:39:56 +0900 Subject: [PATCH 17/22] =?UTF-8?q?consent=20=EC=B2=A0=ED=9A=8C=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8B=AC=20=EB=B0=8F=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=20UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientConsentsPage.tsx | 88 +++++++++++++------ .../clients/routes/ClientFederationPage.tsx | 2 +- devfront/src/lib/devApi.ts | 11 ++- devfront/src/locales/ko.toml | 43 ++++----- 4 files changed, 96 insertions(+), 48 deletions(-) diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index e82277ca..113855d7 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -33,6 +33,8 @@ function ClientConsentsPage() { const clientId = params.id ?? ""; const [subjectInput, setSubjectInput] = useState(""); const [subject, setSubject] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const { data: clientData } = useQuery({ queryKey: ["client", clientId], queryFn: () => fetchClient(clientId), @@ -44,9 +46,9 @@ function ClientConsentsPage() { error, refetch, } = useQuery({ - queryKey: ["consents", clientId, subject], - queryFn: () => fetchConsents(subject, clientId), - enabled: clientId.length > 0, // Removed subject.length > 0 check + queryKey: ["consents", clientId, subject, statusFilter], + queryFn: () => fetchConsents(subject, clientId, statusFilter), + enabled: clientId.length > 0, }); const revokeMutation = useMutation({ mutationFn: (payload: { subject: string }) => @@ -56,6 +58,19 @@ function ClientConsentsPage() { }, }); + const handleRevoke = (sub: string) => { + if ( + window.confirm( + t( + "msg.dev.clients.consents.revoke_confirm", + "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다.", + ), + ) + ) { + revokeMutation.mutate({ subject: sub }); + } + }; + const rows = consentsData?.items ?? []; return ( @@ -150,14 +165,18 @@ function ClientConsentsPage() { {t("ui.dev.clients.consents.status_label", "Status:")} - setStatusFilter(e.target.value)} + > + - - @@ -226,7 +245,7 @@ function ClientConsentsPage() { {t( "ui.dev.clients.consents.table.last_auth", - "Last Authenticated", + "Last Authenticated / Revoked", )} @@ -243,7 +262,10 @@ function ClientConsentsPage() { ) : ( rows.map((row) => ( - +
@@ -273,9 +295,15 @@ function ClientConsentsPage() {
- - {t("ui.common.status.active", "Active")} - + {row.status === "active" ? ( + + {t("ui.common.status.active", "Active")} + + ) : ( + + {t("ui.dev.clients.consents.status_revoked", "Revoked")} + + )}
@@ -294,20 +322,28 @@ function ClientConsentsPage() { {new Date(row.createdAt).toLocaleString()} - {row.authenticatedAt - ? new Date(row.authenticatedAt).toLocaleString() - : "-"} + {row.status === "revoked" && row.deletedAt ? ( + + {t("ui.dev.clients.consents.revoked_at", "Revoked: ")} + {new Date(row.deletedAt).toLocaleString()} + + ) : row.authenticatedAt ? ( + new Date(row.authenticatedAt).toLocaleString() + ) : ( + "-" + )} - + {row.status === "active" && ( + + )} )) @@ -349,7 +385,9 @@ function ClientConsentsPage() { "Active Grants", )}

- {rows.length} + + {rows.filter((r) => r.status === "active").length} + diff --git a/devfront/src/features/clients/routes/ClientFederationPage.tsx b/devfront/src/features/clients/routes/ClientFederationPage.tsx index 4d63eead..ac6c14a8 100644 --- a/devfront/src/features/clients/routes/ClientFederationPage.tsx +++ b/devfront/src/features/clients/routes/ClientFederationPage.tsx @@ -156,7 +156,7 @@ const CreateIdpModal = ({ disabled={ mutation.isPending || formData.display_name.trim() === "" || - formData.issuer_url.trim() === "" + (formData.issuer_url?.trim() ?? "") === "" } > {mutation.isPending ? ( diff --git a/devfront/src/lib/devApi.ts b/devfront/src/lib/devApi.ts index 5aa29992..b20e8cd0 100644 --- a/devfront/src/lib/devApi.ts +++ b/devfront/src/lib/devApi.ts @@ -57,6 +57,8 @@ export type ConsentSummary = { grantedScopes: string[]; authenticatedAt?: string; createdAt: string; + deletedAt?: string; + status: "active" | "revoked"; tenantId?: string; tenantName?: string; }; @@ -148,11 +150,18 @@ export async function deleteClient(clientId: string) { await apiClient.delete(`/dev/clients/${clientId}`); } -export async function fetchConsents(subject: string, clientId?: string) { +export async function fetchConsents( + subject: string, + clientId?: string, + status?: string, +) { const params: Record = { subject }; if (clientId) { params.client_id = clientId; } + if (status && status !== "all") { + params.status = status; + } const { data } = await apiClient.get("/dev/consents", { params, }); diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index 35a3da00..d832d1e4 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -956,36 +956,37 @@ admin_session = "관리자 세션" tenant_selected = "테넌트: 선택됨" [ui.dev.clients.consents] -export_csv = "Export CSV" -revoke = "Revoke" +export_csv = "CSV 내보내기" +revoke = "권한 철회" +revoked_at = "철회일: " search_placeholder = "사용자 ID, 이름, 이메일로 검색" -status_all = "All Statuses" -status_label = "Status:" -status_revoked = "Revoked" -subject = "Subject" -title = "User Consent Grants" +status_all = "모든 상태" +status_label = "상태:" +status_revoked = "철회됨" +subject = "사용자 ID" +title = "사용자 동의 권한 관리" [ui.dev.clients.consents.breadcrumb] -clients = "Clients" -current = "User Consent Grants" -home = "Home" +clients = "애플리케이션" +current = "사용자 동의 권한" +home = "홈" [ui.dev.clients.consents.filters] -advanced = "Advanced Filters" +advanced = "상세 필터" [ui.dev.clients.consents.stats] -active_grants = "Active Grants" -avg_scopes = "Avg. Scopes per User" -total_scopes = "Total Scopes Issued" +active_grants = "활성 권한" +avg_scopes = "사용자당 평균 권한 수" +total_scopes = "전체 부여된 권한 수" [ui.dev.clients.consents.table] -action = "Action" -first_granted = "First Granted" -last_auth = "Last Authenticated" -scopes = "Granted Scopes" -status = "Status" -tenant = "Tenant" -user = "User" +action = "작업" +first_granted = "최초 동의" +last_auth = "최근 인증 / 철회" +scopes = "부여된 권한 (Scopes)" +status = "상태" +tenant = "테넌트" +user = "사용자" [ui.dev.clients.details] From c8f39c15e0cca3f703f0c176659a0d7b298a8fe1 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 26 Feb 2026 12:40:43 +0900 Subject: [PATCH 18/22] =?UTF-8?q?conent=20=EC=9D=B4=EB=A0=A5=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20soft=20delete=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=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 | 10 ++++- backend/internal/handler/dev_handler.go | 40 ++++++++++++++----- .../repository/client_consent_repository.go | 13 +++--- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index d45024fb..4d195458 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -3423,6 +3423,12 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { continue } + // 삭제된 권한일 경우 + status := "inactive" + if dc.DeletedAt.Valid { + status = "revoked" + } + // Hydra에서 클라이언트 정보 조회 (메타데이터용) client, err := h.Hydra.GetClient(c.Context(), dc.ClientID) if err != nil { @@ -3432,7 +3438,7 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { linkedRpSummary: linkedRpSummary{ ID: dc.ClientID, Name: dc.ClientID, - Status: "inactive", + Status: status, Scopes: dc.GrantedScopes, }, lastAuth: dc.UpdatedAt, @@ -3458,7 +3464,7 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { Name: name, Logo: extractHydraClientLogo(client.Metadata), URL: clientURL, - Status: "inactive", + Status: status, Scopes: dc.GrantedScopes, }, lastAuth: dc.UpdatedAt, diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index b216001d..bbc31675 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -93,15 +93,17 @@ type clientEndpoints struct { } type consentSummary struct { - Subject string `json:"subject"` - UserName string `json:"userName,omitempty"` - ClientID string `json:"clientId"` - ClientName string `json:"clientName,omitempty"` - GrantedScopes []string `json:"grantedScopes"` - AuthenticatedAt string `json:"authenticatedAt,omitempty"` - CreatedAt time.Time `json:"createdAt"` - TenantID string `json:"tenantId,omitempty"` - TenantName string `json:"tenantName,omitempty"` + Subject string `json:"subject"` + UserName string `json:"userName,omitempty"` + ClientID string `json:"clientId"` + ClientName string `json:"clientName,omitempty"` + GrantedScopes []string `json:"grantedScopes"` + AuthenticatedAt string `json:"authenticatedAt,omitempty"` + CreatedAt time.Time `json:"createdAt"` + DeletedAt *time.Time `json:"deletedAt,omitempty"` + Status string `json:"status"` + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` } type consentListResponse struct { @@ -648,6 +650,7 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error { // [Isolation] Get admin tenant ID from header or locals adminTenantID := c.Get("X-Tenant-ID") // Assume middleware sets this or trusted in dev + statusFilter := strings.ToLower(strings.TrimSpace(c.Query("status"))) var consents []domain.ClientConsentWithTenantInfo var total int64 @@ -686,6 +689,23 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error { continue } + var deletedAt *time.Time + status := "active" + if consent.DeletedAt.Valid { + deletedAt = &consent.DeletedAt.Time + status = "revoked" + } + + // Filter by status if requested + if statusFilter != "" && statusFilter != "all" { + if statusFilter == "active" && status != "active" { + continue + } + if statusFilter == "revoked" && status != "revoked" { + continue + } + } + userName := "" identity, err := h.KratosAdmin.GetIdentity(c.Context(), consent.Subject) if err == nil && identity != nil { @@ -703,6 +723,8 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error { GrantedScopes: consent.GrantedScopes, AuthenticatedAt: consent.UpdatedAt.Format(time.RFC3339), CreatedAt: consent.CreatedAt, + DeletedAt: deletedAt, + Status: status, TenantID: consent.TenantID, TenantName: consent.TenantName, }) diff --git a/backend/internal/repository/client_consent_repository.go b/backend/internal/repository/client_consent_repository.go index ee85daba..9ca6a8c5 100644 --- a/backend/internal/repository/client_consent_repository.go +++ b/backend/internal/repository/client_consent_repository.go @@ -24,11 +24,12 @@ func NewClientConsentRepository(db *gorm.DB) ClientConsentRepository { } func (r *clientConsentRepo) Upsert(ctx context.Context, consent *domain.ClientConsent) error { - return r.db.WithContext(ctx). + return r.db.WithContext(ctx).Unscoped(). Where("client_id = ? AND subject = ?", consent.ClientID, consent.Subject). Assign(map[string]interface{}{ "granted_scopes": consent.GrantedScopes, "updated_at": gorm.Expr("NOW()"), + "deleted_at": nil, }). FirstOrCreate(consent).Error } @@ -44,13 +45,13 @@ func (r *clientConsentRepo) List(ctx context.Context, clientID string, limit, of var total int64 // Base query for counting - countQuery := r.db.WithContext(ctx).Model(&domain.ClientConsent{}).Where("client_id = ?", clientID) + countQuery := r.db.WithContext(ctx).Unscoped().Model(&domain.ClientConsent{}).Where("client_id = ?", clientID) if err := countQuery.Count(&total).Error; err != nil { return nil, 0, err } // Query for fetching data - query := r.db.WithContext(ctx). + query := r.db.WithContext(ctx).Unscoped(). Model(&domain.ClientConsent{}). Select("client_consents.*, users.tenant_id, tenants.name as tenant_name"). Joins("LEFT JOIN users ON users.id::text = client_consents.subject"). @@ -66,7 +67,7 @@ func (r *clientConsentRepo) ListByTenant(ctx context.Context, clientID, tenantID var total int64 // Base query for counting - countQuery := r.db.WithContext(ctx). + countQuery := r.db.WithContext(ctx).Unscoped(). Model(&domain.ClientConsent{}). Joins("JOIN users ON users.id::text = client_consents.subject"). Where("client_consents.client_id = ? AND users.tenant_id = ?", clientID, tenantID) @@ -76,7 +77,7 @@ func (r *clientConsentRepo) ListByTenant(ctx context.Context, clientID, tenantID } // Query for fetching data - query := r.db.WithContext(ctx). + query := r.db.WithContext(ctx).Unscoped(). Model(&domain.ClientConsent{}). Select("client_consents.*, users.tenant_id, tenants.name as tenant_name"). Joins("JOIN users ON users.id::text = client_consents.subject"). @@ -94,7 +95,7 @@ func (r *clientConsentRepo) ListByTenant(ctx context.Context, clientID, tenantID func (r *clientConsentRepo) ListBySubject(ctx context.Context, subject string) ([]domain.ClientConsent, error) { var consents []domain.ClientConsent - err := r.db.WithContext(ctx). + err := r.db.WithContext(ctx).Unscoped(). Where("subject = ?", subject). Order("updated_at DESC"). Find(&consents).Error From d60bc1d5d5f54005b2cb8a4464c72526bb530b2b Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 26 Feb 2026 13:24:50 +0900 Subject: [PATCH 19/22] =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=95=B1=20?= =?UTF-8?q?=EB=B0=8F=20Consent=20=EB=AA=A9=EB=A1=9D=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientConsentsPage.tsx | 163 ++++++++++++------ devfront/src/features/clients/ClientsPage.tsx | 136 ++++++++++++--- .../clients/routes/ClientFederationPage.tsx | 2 +- 3 files changed, 224 insertions(+), 77 deletions(-) diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index 113855d7..e7e17685 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -27,6 +27,7 @@ import { } from "../../components/ui/table"; import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi"; import { t } from "../../lib/i18n"; +import { cn } from "../../lib/utils"; function ClientConsentsPage() { const params = useParams(); @@ -34,6 +35,8 @@ function ClientConsentsPage() { const [subjectInput, setSubjectInput] = useState(""); const [subject, setSubject] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); + const [scopeFilter, setScopeFilter] = useState("all"); + const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false); const { data: clientData } = useQuery({ queryKey: ["client", clientId], @@ -72,6 +75,10 @@ function ClientConsentsPage() { }; const rows = consentsData?.items ?? []; + const allScopes = Array.from(new Set(rows.flatMap((r) => r.grantedScopes))); + const filteredRows = rows.filter((row) => { + return scopeFilter === "all" || row.grantedScopes.includes(scopeFilter); + }); return (
@@ -147,59 +154,105 @@ function ClientConsentsPage() { - -
-
- - +
+
+
+ + setSubjectInput(e.target.value)} + /> +
+
+
+
-
- - {t("ui.dev.clients.consents.status_label", "Status:")} - - + + {t( + "ui.dev.clients.consents.filters.advanced", + "Advanced Filters", + )} + + +
-
- - - -
+ + {isAdvancedFilterOpen && ( +
+
+ + {t("ui.dev.clients.consents.status_label", "Status:")} + + +
+ +
+ + {t("ui.dev.clients.consents.scope_label", "Scope:")} + + +
+ + +
+ )} @@ -254,14 +307,14 @@ function ClientConsentsPage() { - {rows.length === 0 && !isLoading ? ( + {filteredRows.length === 0 && !isLoading ? ( {t("msg.dev.clients.consents.empty", "No consents found.")} ) : ( - rows.map((row) => ( + filteredRows.map((row) => ( 0 ? 1 : 0, - to: rows.length, + from: filteredRows.length > 0 ? 1 : 0, + to: filteredRows.length, total: rows.length, }, )} @@ -366,7 +419,7 @@ function ClientConsentsPage() { -
-
-
- - -
-
- - {t("ui.dev.clients.badge.tenant_selected", "테넌트: 선택됨")} - - - {t("ui.dev.clients.badge.admin_session", "관리자 세션")} - +
+
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ +
+ + {t("ui.dev.clients.badge.tenant_selected", "테넌트: 선택됨")} + + + {t("ui.dev.clients.badge.admin_session", "관리자 세션")} + +
+
+ + {isAdvancedFilterOpen && ( +
+
+ + {t("ui.dev.clients.filter.type_label", "Type:")} + + +
+
+ + {t("ui.dev.clients.consents.status_label", "Status:")} + + +
+ +
+ )}
@@ -222,7 +318,7 @@ function ClientsPage() { - {clients.map((client) => ( + {filteredClients.map((client) => (
diff --git a/devfront/src/features/clients/routes/ClientFederationPage.tsx b/devfront/src/features/clients/routes/ClientFederationPage.tsx index ac6c14a8..c61c20be 100644 --- a/devfront/src/features/clients/routes/ClientFederationPage.tsx +++ b/devfront/src/features/clients/routes/ClientFederationPage.tsx @@ -1,5 +1,5 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Plus, Trash2, Edit, Globe, Save } from "lucide-react"; +import { Edit, Globe, Plus, Save, Trash2 } from "lucide-react"; import { useState } from "react"; import { useParams } from "react-router-dom"; import { Button } from "../../../components/ui/button"; From 5a7231eba62adc891344c47ef7f12d8ee63235e8 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 26 Feb 2026 13:25:08 +0900 Subject: [PATCH 20/22] =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A0=A8=20=EB=8B=A4=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=20=ED=82=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.toml | 6 ++++++ locales/ko.toml | 6 ++++++ locales/template.toml | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/locales/en.toml b/locales/en.toml index 2430cdf3..35990817 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -281,6 +281,7 @@ load_error = "Error loading consents: {{error}}" loading = "Loading consents..." showing = "Showing {{from}} to {{to}} of {{total}} users" subtitle = "Subtitle" +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.details] copy_client_id = "Client ID copied." @@ -1104,9 +1105,14 @@ untitled = "Untitled" admin_session = "Admin Session" tenant_selected = "Tenant Selected" +[ui.dev.clients.filter] +status_all = "All Statuses" +type_all = "All Types" + [ui.dev.clients.consents] export_csv = "Export CSV" revoke = "Revoke" +revoked_at = "Revoked: " search_placeholder = "Search Placeholder" status_all = "All Statuses" status_label = "Status:" diff --git a/locales/ko.toml b/locales/ko.toml index 3622ae6f..ee27d7db 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -281,6 +281,7 @@ load_error = "Error loading consents: {{error}}" loading = "Loading consents..." showing = "Showing {{from}} to {{to}} of {{total}} users" subtitle = "OIDC Relying Party 사용자 권한을 검토·관리합니다." +revoke_confirm = "정말로 이 사용자의 권한을 철회하시겠습니까? 철회 시 사용자는 다음 접속 시 다시 동의해야 합니다." [msg.dev.clients.details] copy_client_id = "Client ID가 복사되었습니다." @@ -1104,9 +1105,14 @@ untitled = "Untitled" admin_session = "관리자 세션" tenant_selected = "테넌트: 선택됨" +[ui.dev.clients.filter] +status_all = "모든 상태" +type_all = "모든 유형" + [ui.dev.clients.consents] export_csv = "Export CSV" revoke = "Revoke" +revoked_at = "철회일: " search_placeholder = "사용자 ID, 이름, 이메일로 검색" status_all = "All Statuses" status_label = "Status:" diff --git a/locales/template.toml b/locales/template.toml index 69dd2124..c9190ae6 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -222,6 +222,7 @@ load_error = "" loading = "" showing = "" subtitle = "" +revoke_confirm = "" [msg.dev.clients.details] copy_client_id = "" @@ -966,9 +967,14 @@ untitled = "" admin_session = "" tenant_selected = "" +[ui.dev.clients.filter] +status_all = "" +type_all = "" + [ui.dev.clients.consents] export_csv = "" revoke = "" +revoked_at = "" search_placeholder = "" status_all = "" status_label = "" From 39062e1773136cbe8e13dbc9e6304e6e121ea5c7 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 26 Feb 2026 13:58:20 +0900 Subject: [PATCH 21/22] =?UTF-8?q?Consent=20=EB=AA=A9=EB=A1=9D=20CSV=20?= =?UTF-8?q?=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientConsentsPage.tsx | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index e7e17685..75bafe57 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -80,6 +80,56 @@ function ClientConsentsPage() { return scopeFilter === "all" || row.grantedScopes.includes(scopeFilter); }); + const handleExportCSV = () => { + if (filteredRows.length === 0) return; + + const headers = [ + t("ui.dev.clients.consents.table.user", "User"), + t("ui.dev.clients.consents.table.tenant", "Tenant"), + t("ui.dev.clients.table.status", "Status"), + t("ui.dev.clients.consents.table.scopes", "Granted Scopes"), + t("ui.dev.clients.consents.table.first_granted", "First Granted"), + t( + "ui.dev.clients.consents.table.last_auth", + "Last Authenticated / Revoked", + ), + ]; + + const csvContent = [ + headers.join(","), + ...filteredRows.map((row) => { + const lastAuthRevoked = + row.status === "revoked" && row.deletedAt + ? `${t("ui.dev.clients.consents.status_revoked", "Revoked")}: ${new Date(row.deletedAt).toLocaleString()}` + : row.authenticatedAt + ? new Date(row.authenticatedAt).toLocaleString() + : "-"; + + return [ + `"${row.subject} (${row.userName || ""})"`, + `"${row.tenantName || row.tenantId || ""}"`, + `"${row.status}"`, + `"${row.grantedScopes.join(", ")}"`, + `"${new Date(row.createdAt).toLocaleString()}"`, + `"${lastAuthRevoked}"`, + ].join(","); + }), + ].join("\n"); + + const blob = new Blob(["\uFEFF" + csvContent], { + type: "text/csv;charset=utf-8;", + }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + const date = new Date().toISOString().split("T")[0]; + link.setAttribute("href", url); + link.setAttribute("download", `consents_${clientId}_${date}.csv`); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + return (
@@ -191,7 +241,11 @@ function ClientConsentsPage() { > {t("ui.common.search", "검색")} -
@@ -455,6 +509,8 @@ function ClientConsentsPage() { {rows.reduce((acc, row) => acc + row.grantedScopes.length, 0)} + +

{t( From 9a409689ee40f69932242215c3ac571d6941ac1f Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 26 Feb 2026 14:04:28 +0900 Subject: [PATCH 22/22] =?UTF-8?q?i18n=20=EB=88=84=EB=9D=BD=20=ED=82=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20flutter=20=EB=A6=B0=ED=8A=B8?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/features/clients/ClientConsentsPage.tsx | 2 +- devfront/src/features/clients/ClientsPage.tsx | 5 ++++- locales/en.toml | 4 ++++ locales/ko.toml | 4 ++++ locales/template.toml | 4 ++++ userfront/assets/translations/en.toml | 1 + userfront/assets/translations/ko.toml | 1 + userfront/assets/translations/template.toml | 1 + 8 files changed, 20 insertions(+), 2 deletions(-) diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx index 75bafe57..1f44f2f6 100644 --- a/devfront/src/features/clients/ClientConsentsPage.tsx +++ b/devfront/src/features/clients/ClientConsentsPage.tsx @@ -116,7 +116,7 @@ function ClientConsentsPage() { }), ].join("\n"); - const blob = new Blob(["\uFEFF" + csvContent], { + const blob = new Blob([`\uFEFF${csvContent}`], { type: "text/csv;charset=utf-8;", }); const url = URL.createObjectURL(blob); diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 2f7fe72d..4a8472cb 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -188,7 +188,10 @@ function ClientsPage() {

- {t("ui.dev.clients.badge.tenant_selected", "테넌트: 선택됨")} + {t( + "ui.dev.clients.badge.tenant_selected", + "테넌트: 선택됨", + )} {t("ui.dev.clients.badge.admin_session", "관리자 세션")} diff --git a/locales/en.toml b/locales/en.toml index 35990817..4ac4562c 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -1046,6 +1046,7 @@ previous = "Previous" qr = "QR" read_only = "Read Only" refresh = "Refresh" +reset = "Reset" requesting = "Requesting" resend = "Resend" retry = "Retry" @@ -1108,11 +1109,14 @@ tenant_selected = "Tenant Selected" [ui.dev.clients.filter] status_all = "All Statuses" type_all = "All Types" +type_label = "Type:" [ui.dev.clients.consents] export_csv = "Export CSV" revoke = "Revoke" revoked_at = "Revoked: " +scope_all = "All Scopes" +scope_label = "Scope:" search_placeholder = "Search Placeholder" status_all = "All Statuses" status_label = "Status:" diff --git a/locales/ko.toml b/locales/ko.toml index ee27d7db..d69953e6 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -1046,6 +1046,7 @@ previous = "Previous" qr = "QR" read_only = "읽기 전용" refresh = "새로고침" +reset = "초기화" requesting = "요청 중..." resend = "재발송" retry = "다시 시도" @@ -1108,11 +1109,14 @@ tenant_selected = "테넌트: 선택됨" [ui.dev.clients.filter] status_all = "모든 상태" type_all = "모든 유형" +type_label = "유형:" [ui.dev.clients.consents] export_csv = "Export CSV" revoke = "Revoke" revoked_at = "철회일: " +scope_all = "모든 권한" +scope_label = "권한:" search_placeholder = "사용자 ID, 이름, 이메일로 검색" status_all = "All Statuses" status_label = "Status:" diff --git a/locales/template.toml b/locales/template.toml index c9190ae6..ecd149df 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -908,6 +908,7 @@ page_of = "" prev = "" previous = "" qr = "" +reset = "" read_only = "" refresh = "" resend = "" @@ -970,11 +971,14 @@ tenant_selected = "" [ui.dev.clients.filter] status_all = "" type_all = "" +type_label = "" [ui.dev.clients.consents] export_csv = "" revoke = "" revoked_at = "" +scope_all = "" +scope_label = "" search_placeholder = "" status_all = "" status_label = "" diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index 4e906a09..db6738c6 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -322,6 +322,7 @@ previous = "Previous" qr = "QR" read_only = "Read Only" refresh = "Refresh" +reset = "Reset" requesting = "Requesting" resend = "Resend" retry = "Retry" diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index 1c180d38..ec26fe14 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -322,6 +322,7 @@ previous = "Previous" qr = "QR" read_only = "읽기 전용" refresh = "새로고침" +reset = "초기화" requesting = "요청 중..." resend = "재발송" retry = "다시 시도" diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index 3911cf30..0a335640 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -312,6 +312,7 @@ page_of = "" prev = "" previous = "" qr = "" +reset = "" read_only = "" refresh = "" resend = ""