From e98ab39dfe6cc22b7e69165398f5001928590044 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 23 Mar 2026 15:36:00 +0900 Subject: [PATCH] =?UTF-8?q?code=20check=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 --- Makefile | 14 +- adminfront/playwright.config.ts | 6 +- devfront/playwright.config.ts | 6 +- userfront-e2e/playwright.config.ts | 5 +- .../tests/profile-department.spec.ts | 15 +- userfront/assets/translations/en.toml | 6 + userfront/assets/translations/ko.toml | 7 +- userfront/assets/translations/template.toml | 7 +- .../auth/presentation/consent_screen.dart | 4 +- .../auth/presentation/signup_screen.dart | 44 +++--- .../test/profile_page_edit_flow_test.dart | 145 ++++++++++-------- 11 files changed, 155 insertions(+), 104 deletions(-) diff --git a/Makefile b/Makefile index 0eea204a..8466a2b2 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,10 @@ endif .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_TEST_JOBS ?= 1 +PLAYWRIGHT_WORKERS ?= 1 +FLUTTER_TEST_CONCURRENCY ?= 1 + code-check: code-check-lint code-check-test-jobs @echo "code-check complete." @@ -124,7 +128,7 @@ code-check-lint: code-check-i18n code-check-front-lint code-check-go-lint code-c code-check-test-jobs: @echo "==> run CI-equivalent test jobs (parallel)" - @$(MAKE) --no-print-directory -j5 --output-sync=target \ + @$(MAKE) --no-print-directory -j$(CODE_CHECK_TEST_JOBS) --output-sync=target \ code-check-backend-tests \ code-check-userfront-tests \ code-check-userfront-e2e-tests \ @@ -203,11 +207,11 @@ code-check-userfront-tests: 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 + cd "$$tmp_dir/userfront" && flutter test --concurrency=$(FLUTTER_TEST_CONCURRENCY) code-check-adminfront-tests: @echo "==> adminfront tests" - ./scripts/run_adminfront_ci_tests.sh adminfront-tests + PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) ./scripts/run_adminfront_ci_tests.sh adminfront-tests code-check-devfront-tests: @echo "==> devfront tests" @@ -219,7 +223,7 @@ code-check-devfront-tests: (cd devfront && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ - (cd devfront && npm test) || status=$$?; \ + (cd devfront && PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) npm test) || status=$$?; \ fi; \ [ -d devfront/playwright-report ] && cp -R devfront/playwright-report reports/devfront/ || true; \ [ -d devfront/test-results ] && cp -R devfront/test-results reports/devfront/ || true; \ @@ -267,7 +271,7 @@ code-check-userfront-e2e-tests: if [ $$status -eq 0 ]; then \ port="$$(node -e "const net=require('node:net'); const s=net.createServer(); s.listen(0,'127.0.0.1',()=>{console.log(s.address().port); s.close();});")"; \ echo "==> userfront-e2e using PORT=$$port"; \ - (cd "$$tmp_dir/userfront-e2e" && PORT=$$port npm test) || status=$$?; \ + (cd "$$tmp_dir/userfront-e2e" && PORT=$$port PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) 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; \ diff --git a/adminfront/playwright.config.ts b/adminfront/playwright.config.ts index f4bfe8e1..2acbace2 100644 --- a/adminfront/playwright.config.ts +++ b/adminfront/playwright.config.ts @@ -1,5 +1,9 @@ import { defineConfig, devices } from "@playwright/test"; +const configuredWorkers = process.env.PLAYWRIGHT_WORKERS + ? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10) + : undefined; + /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -24,7 +28,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: configuredWorkers ?? (process.env.CI ? 1 : undefined), /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/devfront/playwright.config.ts b/devfront/playwright.config.ts index 9606c1ce..b52394df 100644 --- a/devfront/playwright.config.ts +++ b/devfront/playwright.config.ts @@ -1,5 +1,9 @@ import { defineConfig, devices } from "@playwright/test"; +const configuredWorkers = process.env.PLAYWRIGHT_WORKERS + ? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10) + : undefined; + /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -20,7 +24,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: configuredWorkers ?? (process.env.CI ? 1 : undefined), /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/userfront-e2e/playwright.config.ts b/userfront-e2e/playwright.config.ts index d7e33047..fcc38967 100644 --- a/userfront-e2e/playwright.config.ts +++ b/userfront-e2e/playwright.config.ts @@ -4,13 +4,16 @@ 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; +const configuredWorkers = process.env.PLAYWRIGHT_WORKERS + ? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10) + : undefined; export default defineConfig({ testDir: './tests', fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, + workers: configuredWorkers ?? (process.env.CI ? 1 : undefined), reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'html', use: { baseURL, diff --git a/userfront-e2e/tests/profile-department.spec.ts b/userfront-e2e/tests/profile-department.spec.ts index 4d8b468d..e22db24d 100644 --- a/userfront-e2e/tests/profile-department.spec.ts +++ b/userfront-e2e/tests/profile-department.spec.ts @@ -47,6 +47,11 @@ async function blurDepartmentEditor(page: Page): Promise { await page.waitForTimeout(250); } +async function submitDepartmentEditor(page: Page): Promise { + await page.keyboard.press('Enter'); + await page.waitForTimeout(250); +} + async function mockProfileApis(page: Page, state: ProfileState): Promise { await page.route('**/api/v1/**', async (route: Route) => { const request = route.request(); @@ -155,7 +160,7 @@ test.describe('UserFront WASM profile department editing', () => { await page.unroute('**/api/v1/**'); }); - test('소속 수정 후 포커스 아웃하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({ + test('소속 수정 후 명시 저장하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({ page, }) => { const state: ProfileState = { @@ -170,7 +175,7 @@ test.describe('UserFront WASM profile department editing', () => { await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Updated'); - await blurDepartmentEditor(page); + await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(1); expect(state.putBodies[0]?.department).toBe('QA-Updated'); @@ -248,7 +253,7 @@ test.describe('UserFront WASM profile department editing', () => { expect(state.department).toBe('QA'); }); - test('소속을 수정한 뒤 새로고침 후 다시 수정해도 저장 요청이 누락되지 않는다', async ({ page }) => { + test('소속을 저장한 뒤 새로고침 후 다시 저장해도 저장 요청이 누락되지 않는다', async ({ page }) => { const state: ProfileState = { department: 'QA', getMeCount: 0, @@ -261,7 +266,7 @@ test.describe('UserFront WASM profile department editing', () => { await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-1'); - await blurDepartmentEditor(page); + await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(1); await page.reload(); @@ -270,7 +275,7 @@ test.describe('UserFront WASM profile department editing', () => { await openDepartmentEditor(page); await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-2'); - await blurDepartmentEditor(page); + await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(2); expect(state.putBodies[0]?.department).toBe('QA-1'); diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml index c3525849..538eeb5b 100644 --- a/userfront/assets/translations/en.toml +++ b/userfront/assets/translations/en.toml @@ -280,6 +280,11 @@ privacy_full = "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 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] +all_hint = "Agree to both required documents to continue to the next step." +description = "Review the service terms and privacy collection notice, then agree to continue." +privacy_summary = "Review what personal data is collected, why it is used, and how it is retained." +progress = "{count} of {total} required agreements completed" +tos_summary = "Review the service terms, usage conditions, and responsibilities." title = "Please review and agree to the terms to continue." [msg.userfront.signup.auth] @@ -592,6 +597,7 @@ title = "Sign up" [ui.userfront.signup.agreement] all = "Agree to all" privacy_title = "Privacy Policy (Required)" +required = "Required" tos_title = "Terms of Service (Required)" [ui.userfront.signup.auth] diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml index ea883439..18d2b303 100644 --- a/userfront/assets/translations/ko.toml +++ b/userfront/assets/translations/ko.toml @@ -277,6 +277,11 @@ privacy_full = "개인정보 수집 및 이용 동의 전문..." tos_full = "서비스 이용약관 전문..." [msg.userfront.signup.agreement] +all_hint = "필수 약관 2개를 모두 확인하고 동의하면 다음 단계로 진행할 수 있습니다." +description = "계속 진행하려면 서비스 이용 조건과 개인정보 수집·이용 항목을 확인한 뒤 동의해주세요." +privacy_summary = "개인정보 수집 항목, 이용 목적, 보관 기준을 안내합니다." +progress = "필수 약관 {total}개 중 {count}개 동의 완료" +tos_summary = "서비스 이용 조건과 책임 범위를 확인할 수 있습니다." title = "서비스 이용을 위해\n약관에 동의해주세요" [msg.userfront.signup.auth] @@ -583,6 +588,7 @@ title = "회원가입" [ui.userfront.signup.agreement] all = "모두 동의합니다" privacy_title = "개인정보 수집 및 이용 동의 (필수)" +required = "필수" tos_title = "바론 소프트웨어 이용약관 (필수)" [ui.userfront.signup.auth] @@ -616,4 +622,3 @@ verify = "본인인증" [ui.userfront.signup.success] action = "로그인하기" - diff --git a/userfront/assets/translations/template.toml b/userfront/assets/translations/template.toml index c59a8780..44c85800 100644 --- a/userfront/assets/translations/template.toml +++ b/userfront/assets/translations/template.toml @@ -277,6 +277,11 @@ privacy_full = "" tos_full = "" [msg.userfront.signup.agreement] +all_hint = "" +description = "" +privacy_summary = "" +progress = "" +tos_summary = "" title = "" [msg.userfront.signup.auth] @@ -583,6 +588,7 @@ title = "" [ui.userfront.signup.agreement] all = "" privacy_title = "" +required = "" tos_title = "" [ui.userfront.signup.auth] @@ -616,4 +622,3 @@ verify = "" [ui.userfront.signup.success] action = "" - diff --git a/userfront/lib/features/auth/presentation/consent_screen.dart b/userfront/lib/features/auth/presentation/consent_screen.dart index b8c57597..0d3acd05 100644 --- a/userfront/lib/features/auth/presentation/consent_screen.dart +++ b/userfront/lib/features/auth/presentation/consent_screen.dart @@ -238,8 +238,8 @@ class _ConsentScreenState extends State { final clientName = (clientRawName != null && clientRawName.isNotEmpty) ? clientRawName : (clientId != '-' - ? clientId - : tr('msg.userfront.consent.client_unknown')); + ? clientId + : tr('msg.userfront.consent.client_unknown')); final clientLogo = _consentInfo?['client']?['logo_uri']; final requestedScopes = (_consentInfo?['requested_scope'] as List?)?.cast() ?? diff --git a/userfront/lib/features/auth/presentation/signup_screen.dart b/userfront/lib/features/auth/presentation/signup_screen.dart index e4f5a302..715c993d 100644 --- a/userfront/lib/features/auth/presentation/signup_screen.dart +++ b/userfront/lib/features/auth/presentation/signup_screen.dart @@ -1494,8 +1494,7 @@ class _SignupScreenState extends State { labelText: tr( 'ui.userfront.signup.profile.company', ), - border: - const OutlineInputBorder(), + border: const OutlineInputBorder(), ), items: [ DropdownMenuItem( @@ -1557,7 +1556,9 @@ class _SignupScreenState extends State { _buildProfileFieldGroup( title: _affiliationType == 'AFFILIATE' ? tr('ui.userfront.signup.profile.department') - : tr('ui.userfront.signup.profile.department_optional'), + : tr( + 'ui.userfront.signup.profile.department_optional', + ), description: _affiliationType == 'AFFILIATE' ? '가족사 사용자는 부서명을 입력해주세요.' : '선택 입력 항목입니다.', @@ -1677,10 +1678,7 @@ class _SignupScreenState extends State { ], ), ), - if (trailing != null) ...[ - const SizedBox(width: 12), - trailing, - ], + if (trailing != null) ...[const SizedBox(width: 12), trailing], ], ), SizedBox(height: isDesktop ? 18 : 14), @@ -1793,13 +1791,22 @@ class _SignupScreenState extends State { hasTypeCount, ), if (requiresUpper) - _cryptoCheck(tr('msg.userfront.signup.password.rule.uppercase'), hasUpper), + _cryptoCheck( + tr('msg.userfront.signup.password.rule.uppercase'), + hasUpper, + ), if (requiresLower) - _cryptoCheck(tr('msg.userfront.signup.password.rule.lowercase'), hasLower), + _cryptoCheck( + tr('msg.userfront.signup.password.rule.lowercase'), + hasLower, + ), if (requiresNumber) _cryptoCheck(tr('msg.userfront.signup.password.rule.number'), hasDigit), if (requiresSymbol) - _cryptoCheck(tr('msg.userfront.signup.password.rule.symbol'), hasSpecial), + _cryptoCheck( + tr('msg.userfront.signup.password.rule.symbol'), + hasSpecial, + ), ]; return LayoutBuilder( @@ -1861,14 +1868,15 @@ class _SignupScreenState extends State { obscureText: _isPasswordObscured, onChanged: (_) => setState(() {}), decoration: InputDecoration( - labelText: tr('ui.userfront.signup.password.label'), + labelText: tr( + 'ui.userfront.signup.password.label', + ), border: const OutlineInputBorder(), errorText: _passwordError, suffixIcon: IconButton( onPressed: () { setState(() { - _isPasswordObscured = - !_isPasswordObscured; + _isPasswordObscured = !_isPasswordObscured; }); }, icon: Icon( @@ -1897,8 +1905,8 @@ class _SignupScreenState extends State { obscureText: _isConfirmPasswordObscured, onChanged: (val) { setState(() { - _confirmPasswordError = (val != - _passwordController.text) + _confirmPasswordError = + (val != _passwordController.text) ? tr('msg.userfront.signup.password.mismatch') : null; }); @@ -2032,11 +2040,7 @@ class _SignupScreenState extends State { ), child: Padding( padding: EdgeInsets.all(isDesktop ? 16 : 14), - child: Wrap( - spacing: 12, - runSpacing: 10, - children: checks, - ), + child: Wrap(spacing: 12, runSpacing: 10, children: checks), ), ); } diff --git a/userfront/test/profile_page_edit_flow_test.dart b/userfront/test/profile_page_edit_flow_test.dart index 0abfe01f..8ba7025b 100644 --- a/userfront/test/profile_page_edit_flow_test.dart +++ b/userfront/test/profile_page_edit_flow_test.dart @@ -10,7 +10,7 @@ class MockProfileNotifier extends ProfileNotifier { UserProfile? _profile; bool updateCalled = false; String? updatedName; - + @override Future build() async { _profile = UserProfile( @@ -33,7 +33,11 @@ class MockProfileNotifier extends ProfileNotifier { } @override - Future updateProfile({String? name, String? phone, String? department}) async { + Future updateProfile({ + String? name, + String? phone, + String? department, + }) async { updateCalled = true; updatedName = name; _profile = _profile!.copyWith( @@ -46,75 +50,82 @@ class MockProfileNotifier extends ProfileNotifier { } void main() { - testWidgets('ProfilePage explicit save button UX flow (Edit -> Cancel -> Edit -> Save)', (tester) async { - final recordedErrors = []; - final previousOnError = FlutterError.onError; - FlutterError.onError = (details) { - final text = details.exceptionAsString(); - if (text.contains('A RenderFlex overflowed')) { - return; - } - recordedErrors.add(details); - }; - addTearDown(() { - FlutterError.onError = previousOnError; - }); + testWidgets( + 'ProfilePage explicit save button UX flow (Edit -> Cancel -> Edit -> Save)', + (tester) async { + final recordedErrors = []; + final previousOnError = FlutterError.onError; + FlutterError.onError = (details) { + final text = details.exceptionAsString(); + if (text.contains('A RenderFlex overflowed')) { + return; + } + recordedErrors.add(details); + }; + addTearDown(() { + FlutterError.onError = previousOnError; + }); - tester.view.physicalSize = const Size(1920, 1080); - tester.view.devicePixelRatio = 1.0; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); + tester.view.physicalSize = const Size(1920, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); - final mockNotifier = MockProfileNotifier(); - - await tester.pumpWidget( - ProviderScope( - overrides: [ - profileProvider.overrideWith(() => mockNotifier), - ], - child: const MaterialApp( - home: Scaffold(body: ProfilePage()), + final mockNotifier = MockProfileNotifier(); + + await tester.pumpWidget( + ProviderScope( + overrides: [profileProvider.overrideWith(() => mockNotifier)], + child: const MaterialApp(home: Scaffold(body: ProfilePage())), ), - ), - ); + ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - // 1. Entering edit mode - final editButton = find.byKey(const Key('profile-name-edit-button')); - expect(editButton, findsOneWidget); - await tester.tap(editButton); - await tester.pumpAndSettle(); + // 1. Entering edit mode + final editButton = find.byKey(const Key('profile-name-edit-button')); + expect(editButton, findsOneWidget); + await tester.tap(editButton); + await tester.pumpAndSettle(); - final inputField = find.byKey(const Key('profile-name-input')); - expect(inputField, findsOneWidget); - - // 2. Testing cancel flow - await tester.enterText(inputField, 'Changed Name'); - await tester.pumpAndSettle(); - - final cancelButton = find.byKey(const Key('profile-name-cancel-button')); - await tester.tap(cancelButton); - await tester.pumpAndSettle(); - - // After cancellation, the field should be read-only again. - expect(find.byKey(const Key('profile-name-input')), findsNothing); - // Find text could be part of ListTile - expect(find.text('Original Name'), findsWidgets); - - // 3. Re-enter edit mode and explicitly save - await tester.tap(find.byKey(const Key('profile-name-edit-button'))); - await tester.pumpAndSettle(); - - await tester.enterText(find.byKey(const Key('profile-name-input')), 'Saved Name'); - await tester.pumpAndSettle(); - - final saveButton = find.byKey(const Key('profile-name-save-button')); - await tester.tap(saveButton); - await tester.pumpAndSettle(); - - // Verify the mock received the update - expect(mockNotifier.updateCalled, isTrue); - expect(mockNotifier.updatedName, 'Saved Name'); - }); + final inputField = find.byKey(const Key('profile-name-input')); + expect(inputField, findsOneWidget); + + // 2. Testing cancel flow + await tester.enterText(inputField, 'Changed Name'); + await tester.pumpAndSettle(); + + final cancelButton = find.byKey(const Key('profile-name-cancel-button')); + await tester.tap(cancelButton); + await tester.pumpAndSettle(); + + // After cancellation, the field should be read-only again. + expect(find.byKey(const Key('profile-name-input')), findsNothing); + // Find text could be part of ListTile + expect(find.text('Original Name'), findsWidgets); + + // 3. Re-enter edit mode and explicitly save + await tester.tap(find.byKey(const Key('profile-name-edit-button'))); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byKey(const Key('profile-name-input')), + 'Saved Name', + ); + await tester.pumpAndSettle(); + + final saveButton = find.byKey(const Key('profile-name-save-button')); + await tester.tap(saveButton); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 4)); + await tester.pumpAndSettle(); + + FlutterError.onError = previousOnError; + + // Verify the mock received the update + expect(mockNotifier.updateCalled, isTrue); + expect(mockNotifier.updatedName, 'Saved Name'); + expect(recordedErrors, isEmpty); + }, + ); }