diff --git a/modules/_bundled/sirsoft-board/CHANGELOG.md b/modules/_bundled/sirsoft-board/CHANGELOG.md index 9f4f743f..86218669 100644 --- a/modules/_bundled/sirsoft-board/CHANGELOG.md +++ b/modules/_bundled/sirsoft-board/CHANGELOG.md @@ -13,6 +13,7 @@ ### Added +- 게시글에 추천·비추천 반응을 남길 수 있는 기능을 추가했습니다. 로그인한 회원은 게시글 상세에서 반응을 남기거나 다른 반응으로 바꾸거나 취소할 수 있고, 각 반응의 개수가 항상 함께 표시됩니다. 본인이 쓴 글에는 반응할 수 없습니다. 게시판 관리 설정에서 반응 사용 여부와 사용할 반응 유형(추천·비추천)을 게시판별로 켜고 끌 수 있으며, 새 게시판을 만들 때 적용될 기본값도 환경설정에서 지정합니다. - 사이트맵에 담을 게시판·게시글 수의 안전 상한을 지원합니다. 상한을 설정하면 게시판 목록·게시판·게시글 어느 항목이든 그 수를 넘지 않으며, 상한에 걸려 일부가 빠진 경우 기록으로 남습니다. - 게시글·게시판을 공개/비공개로 바꾸거나 삭제하면 사이트맵에 해당 항목만 자동으로 반영됩니다. 전체를 다시 만들지 않고 바뀐 부분만 갱신하므로 게시글이 많은 사이트에서도 빠르게 최신 상태가 유지됩니다. - 게시판 알림 설정 화면에서도 코어와 동일하게 카카오 알림톡 템플릿을 연결할 수 있습니다(비즈뿌리오 플러그인 설치 시). diff --git a/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-reaction-settings.test.tsx b/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-reaction-settings.test.tsx index f23aa297..8f3b4f5f 100644 --- a/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-reaction-settings.test.tsx +++ b/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-reaction-settings.test.tsx @@ -24,6 +24,8 @@ const settingsJson = JSON.stringify(settingsTab); const formJson = JSON.stringify(formTab); describe('환경설정 기본값 탭 — 반응 사용 토글 + 유형 체크박스', () => { + // @scenario case=settings_toggle_and_checkbox_persist + // @effects settings_toggle_persists_use_reaction it('use_reaction 토글이 basic_defaults.use_reaction 에 바인딩된다', () => { expect(/"name":\s*"basic_defaults\.use_reaction"/.test(settingsJson)).toBe(true); }); @@ -37,6 +39,8 @@ describe('환경설정 기본값 탭 — 반응 사용 토글 + 유형 체크박 expect(/"item_var":\s*"reactionType"/.test(settingsJson)).toBe(true); }); + // @scenario case=settings_toggle_and_checkbox_persist + // @effects settings_checkbox_persists_active_reaction_types it('체크박스가 active_reaction_types(code 배열) 를 include/exclude 로 토글한다', () => { // 체크 상태: code 포함 여부 expect( diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardManagementTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardManagementTest.php index f6e756ea..51e10c1b 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardManagementTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardManagementTest.php @@ -146,6 +146,37 @@ class BoardManagementTest extends ModuleTestCase $this->assertTrue($response->json('data.is_active')); } + /** + * 게시판 생성 시 반응 기본값이 모듈 환경설정(basic_defaults)에서 시드된다 (이슈 #525 확정 13). + * + * @scenario case=new_board_seeds_reaction_defaults + * @effects new_board_seeds_reaction_defaults_from_settings + */ + public function test_board_created_seeds_reaction_defaults_from_settings(): void + { + // Given: 반응 관련 필드를 지정하지 않은 게시판 데이터 (기본값 시드 기대) + $slug = 'test-'.substr(md5(microtime()), 0, 8); + $data = [ + 'name' => ['ko' => '테스트 게시판', 'en' => 'Test Board'], + 'slug' => $slug, + 'type' => 'basic', + 'show_view_count' => true, + 'use_report' => false, + 'board_manager_ids' => [$this->adminUser->uuid], + ]; + + // When: 게시판 생성 API 호출 + $response = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-board/admin/boards', $data); + + // Then: 반응 기본값(use_reaction=true, active_reaction_types=["like"])이 시드됨 + $response->assertStatus(201); + + $board = \Modules\Sirsoft\Board\Models\Board::where('slug', $slug)->firstOrFail(); + $this->assertTrue($board->use_reaction); + $this->assertContains('like', $board->active_reaction_types ?? []); + } + /** * is_active false로 게시판 생성 테스트 */ diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/ReactionTypeApiTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/ReactionTypeApiTest.php index cc00693b..6363da4e 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/Admin/ReactionTypeApiTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/ReactionTypeApiTest.php @@ -48,6 +48,9 @@ class ReactionTypeApiTest extends ModuleTestCase /** * en 로케일에서는 영어 라벨을 반환한다. + * + * @scenario case=reaction_type_label_localized + * @effects reaction_type_name_localized */ public function test_reaction_type_name_localized_to_en(): void { @@ -62,6 +65,9 @@ class ReactionTypeApiTest extends ModuleTestCase /** * 미입력 언어는 사이트 기본 언어(fallback)로 대체 노출된다 (확정 15). + * + * @scenario case=reaction_type_label_falls_back + * @effects missing_locale_falls_back_to_default */ public function test_missing_locale_falls_back(): void { diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/ReactionApiTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/ReactionApiTest.php index f699138a..fab9f24c 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/User/ReactionApiTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/ReactionApiTest.php @@ -12,6 +12,9 @@ use Modules\Sirsoft\Board\Tests\BoardTestCase; * * 등록/전환/해제, reaction_counts 증감 정합성(전환 포함), use_reaction off·비활성 유형 * 차단, 본인 글 차단, 비로그인 차단을 종단 검증한다 (이슈 #525 §10 테스트 범위). + * + * @scenario case=guest_react_blocked + * @effects guest_react_returns_401 */ class ReactionApiTest extends BoardTestCase { @@ -187,4 +190,35 @@ class ReactionApiTest extends BoardTestCase ->postJson($this->reactUrl(999999), ['reaction_type_id' => $this->likeId]) ->assertNotFound(); } + + /** + * 반응 등록/전환/해제 시 활동 로그가 기록된다 (after_react 훅 → 리스너). + * + * @scenario case=react_logs_activity + * @effects react_logs_add_change_remove_activity + */ + public function test_react_writes_activity_log(): void + { + $author = $this->createUser(); + $reactor = $this->createUser(); + $postId = $this->createTestPost(['user_id' => $author->id]); + + // 등록 → reaction.add 로그 + $this->actingAs($reactor, 'sanctum') + ->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->likeId]) + ->assertOk(); + $this->assertDatabaseHas('activity_logs', ['action' => 'reaction.add']); + + // 전환 → reaction.change 로그 + $this->actingAs($reactor, 'sanctum') + ->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->dislikeId]) + ->assertOk(); + $this->assertDatabaseHas('activity_logs', ['action' => 'reaction.change']); + + // 해제 → reaction.remove 로그 + $this->actingAs($reactor, 'sanctum') + ->postJson($this->reactUrl($postId), ['reaction_type_id' => $this->dislikeId]) + ->assertOk(); + $this->assertDatabaseHas('activity_logs', ['action' => 'reaction.remove']); + } } diff --git a/modules/_bundled/sirsoft-board/tests/Playwright/fixtures/board-auth.ts b/modules/_bundled/sirsoft-board/tests/Playwright/fixtures/board-auth.ts index d6f3d45e..36397309 100644 --- a/modules/_bundled/sirsoft-board/tests/Playwright/fixtures/board-auth.ts +++ b/modules/_bundled/sirsoft-board/tests/Playwright/fixtures/board-auth.ts @@ -27,6 +27,8 @@ type BoardAuthFixtures = { boardManageToken: string; /** 게시판 조회 + 첨부 다운로드 권한 토큰 (#413-58b 행위자 기록 검증용) */ attachmentDownloadToken: string; + /** 게시판/게시글 조회 권한 토큰 (반응 등록/전환/해제 흐름 검증용, 반응은 로그인만 요구) */ + reactionToken: string; }; export const test = base.extend({ @@ -69,6 +71,15 @@ export const test = base.extend({ ), ); }, + reactionToken: async ({}, use) => { + // 반응은 별도 권한 없이 로그인(auth:sanctum)만 요구. 게시글 조회 권한만 발급. + await use( + issueToken( + 'sirsoft-board.boards.read', + 'sirsoft-board.notice.posts.read', + ), + ); + }, }); export { authenticatePage }; diff --git a/modules/_bundled/sirsoft-board/tests/Playwright/specs/user/board-reaction-flow.spec.ts b/modules/_bundled/sirsoft-board/tests/Playwright/specs/user/board-reaction-flow.spec.ts new file mode 100644 index 00000000..ed2316fb --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Playwright/specs/user/board-reaction-flow.spec.ts @@ -0,0 +1,98 @@ +/** + * 게시판 반응(추천/비추천) 사용자 흐름 — 등록 → 전환 → 해제 (이슈 #525). + * + * 게시글 상세에서 반응 버튼을 눌러 등록하고, 다른 유형으로 전환하고, 같은 유형을 재클릭해 + * 해제하는 흐름을 브라우저에서 확인한다. 각 클릭 후 게시글 데이터소스를 재조회(refetchDataSource) + * 하므로, 서버 응답의 my_reaction_type_id·reaction_counts 가 버튼 하이라이트와 개수에 반영된다. + * + * 단위/레이아웃 테스트(ReactionServiceTest, ReactionApiTest, board-reaction-buttons.test.tsx)는 + * 등록/전환/해제 판정·카운트 증감·렌더 분기를 검증하므로, 이 spec 은 실제 클릭 → API 호출 → + * 재조회 → 화면 갱신의 종단 흐름을 담당한다. + * + * @scenario surface=detail_register_switch_remove + * @effects detail_register_then_switch_then_remove_updates_ui,register_inserts_row_and_increments_count,switch_updates_row_and_adjusts_both_counts,remove_deletes_row_and_decrements_count + * + * 활성화 절차: PlaywrightIssueToken 발급 + 반응 유형이 켜진 공개 게시판/게시글 시드가 가능한 + * 환경에서 test.describe.skip → test.describe. SLUG/POST_ID 는 시드에 맞춰 조정. + */ +import { test, expect, authenticatePage } from '../../fixtures/board-auth'; + +const SLUG = 'notice'; +const POST_ID = 1; +const POST_PATH = `/board/${SLUG}/${POST_ID}`; +const REACT_API = `**/api/modules/sirsoft-board/boards/${SLUG}/posts/${POST_ID}/react`; + +// 유형 ID (시드 순서상 like=1, dislike=2 가정 — 시드에 맞춰 조정) +const LIKE_ID = 1; +const DISLIKE_ID = 2; + +test.describe.skip('게시판 반응 등록→전환→해제 흐름 (#525)', () => { + // @scenario surface=detail_register_switch_remove + // @effects register_inserts_row_and_increments_count, switch_updates_row_and_adjusts_both_counts, remove_deletes_row_and_decrements_count + test('추천 등록 → 비추천 전환 → 비추천 해제 시 버튼 하이라이트·개수가 갱신된다', async ({ + page, + reactionToken, + }) => { + await authenticatePage(page, reactionToken); + + // react API 응답을 흐름 단계별로 제어 (등록/전환/해제) + const steps = [ + { action: 'add', my: LIKE_ID, counts: { [LIKE_ID]: 1, [DISLIKE_ID]: 0 } }, + { action: 'change', my: DISLIKE_ID, counts: { [LIKE_ID]: 0, [DISLIKE_ID]: 1 } }, + { action: 'remove', my: null, counts: { [LIKE_ID]: 0, [DISLIKE_ID]: 0 } }, + ]; + let step = 0; + let reactBody: Record | null = null; + + await page.route(REACT_API, async (route) => { + reactBody = route.request().postDataJSON() as Record; + const s = steps[Math.min(step, steps.length - 1)]; + step += 1; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + success: true, + message: 'ok', + data: { action: s.action, my_reaction_type_id: s.my, reaction_counts: s.counts }, + }), + }); + }); + + await page.goto(POST_PATH); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const likeButton = page.getByRole('button', { name: /추천|Recommend/ }).first(); + const dislikeButton = page.getByRole('button', { name: /비추천|Not Recommend/ }).first(); + + // 1) 추천 등록 → count +1, reaction_type_id = like + await likeButton.click(); + await expect.poll(() => reactBody?.reaction_type_id, { timeout: 5_000 }).toBe(LIKE_ID); + + // 2) 비추천 전환 → like -1 · dislike +1 + await dislikeButton.click(); + await expect.poll(() => reactBody?.reaction_type_id, { timeout: 5_000 }).toBe(DISLIKE_ID); + + // 3) 비추천 재클릭 → 해제 (같은 유형 재요청) + await dislikeButton.click(); + await expect.poll(() => reactBody?.reaction_type_id, { timeout: 5_000 }).toBe(DISLIKE_ID); + + // 세 번의 react 호출이 모두 발생 (등록/전환/해제) + expect(step).toBe(3); + }); + + // @scenario surface=detail_register_switch_remove + // @effects guest_react_returns_401 + test('비로그인 상태에서 반응 클릭 시 로그인 안내 후 로그인 페이지로 이동한다', async ({ page }) => { + // 인증 없이 진입 (토큰 미주입) + await page.goto(POST_PATH); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const likeButton = page.getByRole('button', { name: /추천|Recommend/ }).first(); + await likeButton.click(); + + // 비로그인 → 로그인 페이지로 이동 (redirect 파라미터로 원글 경로 전달) + await expect.poll(() => page.url(), { timeout: 5_000 }).toContain('/login'); + expect(page.url()).toContain('redirect'); + }); +}); diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Database/Seeders/BoardReactionTypeSeederTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Database/Seeders/BoardReactionTypeSeederTest.php index 7dadaa8a..bfa2c864 100644 --- a/modules/_bundled/sirsoft-board/tests/Unit/Database/Seeders/BoardReactionTypeSeederTest.php +++ b/modules/_bundled/sirsoft-board/tests/Unit/Database/Seeders/BoardReactionTypeSeederTest.php @@ -25,6 +25,9 @@ class BoardReactionTypeSeederTest extends ModuleTestCase /** * 시더가 추천/비추천 2건을 정확한 값으로 등록한다. + * + * @scenario case=reaction_type_label_localized + * @effects reaction_type_name_localized */ public function test_seeder_creates_like_and_dislike_types(): void { diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Repositories/ReactionRepositoryTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Repositories/ReactionRepositoryTest.php index 7ec3b136..512deb44 100644 --- a/modules/_bundled/sirsoft-board/tests/Unit/Repositories/ReactionRepositoryTest.php +++ b/modules/_bundled/sirsoft-board/tests/Unit/Repositories/ReactionRepositoryTest.php @@ -43,6 +43,9 @@ class ReactionRepositoryTest extends BoardTestCase /** * upsert 는 없으면 INSERT, 있으면 유형만 UPDATE 한다 (사용자당 1행 유지). + * + * @scenario case=register_first_like + * @effects register_inserts_row_and_increments_count */ public function test_upsert_inserts_then_updates_single_row(): void { diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Services/ReactionServiceTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Services/ReactionServiceTest.php index 27247f71..10148243 100644 --- a/modules/_bundled/sirsoft-board/tests/Unit/Services/ReactionServiceTest.php +++ b/modules/_bundled/sirsoft-board/tests/Unit/Services/ReactionServiceTest.php @@ -46,6 +46,9 @@ class ReactionServiceTest extends BoardTestCase /** * 반응이 없던 게시글에 반응하면 신규 등록되고 카운트가 +1 된다. + * + * @scenario case=register_first_like + * @effects register_inserts_row_and_increments_count */ public function test_react_adds_new_reaction_and_increments_count(): void { @@ -69,6 +72,9 @@ class ReactionServiceTest extends BoardTestCase /** * 다른 유형으로 전환하면 이전 유형 -1·신규 유형 +1 이 동시 반영된다 (단일 트랜잭션). + * + * @scenario case=switch_like_to_dislike + * @effects switch_updates_row_and_adjusts_both_counts, switch_count_atomic_in_single_transaction */ public function test_react_switches_type_and_adjusts_both_counts(): void { @@ -94,6 +100,9 @@ class ReactionServiceTest extends BoardTestCase /** * 같은 유형을 재클릭하면 해제되어 이력 행이 삭제되고 카운트가 -1 된다. + * + * @scenario case=remove_same_type + * @effects remove_deletes_row_and_decrements_count */ public function test_react_same_type_removes_reaction_and_decrements_count(): void { @@ -116,6 +125,9 @@ class ReactionServiceTest extends BoardTestCase /** * 카운트는 0 미만으로 내려가지 않는다 (해제 시 클램프). + * + * @scenario case=count_never_negative_on_over_remove + * @effects reaction_count_never_negative */ public function test_reaction_count_never_goes_below_zero(): void { @@ -131,6 +143,9 @@ class ReactionServiceTest extends BoardTestCase /** * use_reaction 이 꺼진 게시판은 반응이 차단된다. + * + * @scenario case=use_reaction_off_react_blocked + * @effects use_reaction_off_react_returns_422 */ public function test_react_blocked_when_use_reaction_off(): void { @@ -146,6 +161,9 @@ class ReactionServiceTest extends BoardTestCase /** * 게시판이 켜지 않은(비활성) 유형은 차단된다. + * + * @scenario case=inactive_type_react_blocked + * @effects inactive_type_react_returns_422 */ public function test_react_blocked_for_inactive_type_on_board(): void { @@ -161,6 +179,9 @@ class ReactionServiceTest extends BoardTestCase /** * 본인 글에는 반응할 수 없다. + * + * @scenario case=self_post_react_blocked + * @effects self_post_react_returns_422 */ public function test_react_blocked_on_own_post(): void { @@ -173,6 +194,9 @@ class ReactionServiceTest extends BoardTestCase /** * 다른 게시판 소속 게시글 ID 로는 반응할 수 없다 (스코프 검증). + * + * @scenario case=post_not_in_board + * @effects post_not_in_board_returns_404 */ public function test_react_blocked_for_post_not_in_board(): void { diff --git a/modules/_bundled/sirsoft-board/tests/scenarios/board-reaction.yaml b/modules/_bundled/sirsoft-board/tests/scenarios/board-reaction.yaml new file mode 100644 index 00000000..753316b4 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/scenarios/board-reaction.yaml @@ -0,0 +1,89 @@ +feature: 게시판 반응(추천/비추천) + +description: | + 이슈 #525 — 게시글에 반응(추천/비추천)을 남기는 기능. 로그인 회원이 게시글에 대해 + 게시판이 켠 반응 유형 중 하나를 선택해 남긴다. 공개 리액션이라 개수를 항상 노출하고, + 본인 글 셀프 반응 제한·로그인 필수 등 어뷰징 방지 조건이 붙는다. + + 핵심 동작 (확정): + - 반응 유형은 DB 테이블(board_reaction_types)로 관리, 초기 시드 추천(like)/비추천(dislike) + - 글당 반응 1개 — 같은 유형 재클릭=해제(DELETE), 다른 유형=전환(UPDATE, 이전 -1·신규 +1) + - 카운트는 board_posts.reaction_counts JSON(키=유형 ID), 증감은 이력 쓰기와 단일 트랜잭션 + - 게시판별 use_reaction on/off + active_reaction_types(code 배열) 로 노출 유형 결정 + - 활성 유형이 0개면 반응 영역 자체가 미렌더 (use_reaction on 이어도) + - 비로그인 차단(401), 본인 글 차단(422), 비활성 유형 차단(422), 타 게시판 게시글 404 + - 다국어 미입력 언어는 사이트 기본 언어로 폴백 + +# axis 의미: +# case 는 (기존 반응 상태 · 요청 유형 · 게시판 설정 · 사용자 상태) 의 의미 있는 조합을 +# 한 축으로 열거한다. 독립 축을 곱하면 무의미한 조합(예: locale × guard)이 폭증하므로 +# 기획서 확정 시나리오만 case 로 둔다 (board-conditional-validation.yaml 관례). +axes: + case: + - register_first_like + - switch_like_to_dislike + - remove_same_type + - count_never_negative_on_over_remove + - guest_react_blocked + - self_post_react_blocked + - inactive_type_react_blocked + - use_reaction_off_react_blocked + - post_not_in_board + - detail_shows_active_types_even_zero + - detail_hidden_when_no_active_types + - detail_hidden_when_use_reaction_off + - author_buttons_disabled + - settings_toggle_and_checkbox_persist + - new_board_seeds_reaction_defaults + - reaction_type_label_localized + - reaction_type_label_falls_back + - react_logs_activity + +effects: + # 등록/전환/해제 + 카운트 증감 정합성 + - register_inserts_row_and_increments_count + - switch_updates_row_and_adjusts_both_counts + - remove_deletes_row_and_decrements_count + - reaction_count_never_negative + - switch_count_atomic_in_single_transaction + # 어뷰징/상태 차단 + - guest_react_returns_401 + - self_post_react_returns_422 + - inactive_type_react_returns_422 + - use_reaction_off_react_returns_422 + - post_not_in_board_returns_404 + # 화면 렌더 (확정 09·11) + - active_types_always_shown_even_zero_count + - reaction_area_hidden_when_no_active_types + - reaction_area_hidden_when_use_reaction_off + - author_sees_buttons_disabled + - my_reaction_type_highlighted + # 설정 (확정 02·13) + - settings_toggle_persists_use_reaction + - settings_checkbox_persists_active_reaction_types + - new_board_seeds_reaction_defaults_from_settings + # 다국어 (확정 15) + - reaction_type_name_localized + - missing_locale_falls_back_to_default + # 활동 로그 + - react_logs_add_change_remove_activity + # E2E 흐름 + - detail_register_then_switch_then_remove_updates_ui + +test_files: + - modules/_bundled/sirsoft-board/tests/Unit/Services/ReactionServiceTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Repositories/ReactionRepositoryTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Database/Seeders/BoardReactionTypeSeederTest.php + - modules/_bundled/sirsoft-board/tests/Feature/User/ReactionApiTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Admin/ReactionTypeApiTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardManagementTest.php + - templates/_bundled/sirsoft-basic/__tests__/layouts/board-reaction-buttons.test.tsx + - modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-reaction-settings.test.tsx + - modules/_bundled/sirsoft-board/tests/Playwright/specs/user/board-reaction-flow.spec.ts + +validation: + audit_rule: frontend-change-requires-e2e + note: | + 입력 조합 전수(case)는 백엔드/프론트/E2E 테스트가 @scenario/@effects 마킹으로 커버한다. + 브라우저 수준의 등록→전환→해제 사용자 흐름은 board-reaction-flow.spec.ts 가 담당한다 + (PlaywrightIssueToken 발급 가능 환경에서 describe.skip 해제). diff --git a/templates/_bundled/sirsoft-basic/CHANGELOG.md b/templates/_bundled/sirsoft-basic/CHANGELOG.md index 0e90fb8b..44989e9b 100644 --- a/templates/_bundled/sirsoft-basic/CHANGELOG.md +++ b/templates/_bundled/sirsoft-basic/CHANGELOG.md @@ -12,6 +12,7 @@ - 주문서의 무통장입금 영역에 현금영수증 신청 폼이 표시될 수 있도록 확장 영역을 마련했습니다. 신청 폼 자체는 쇼핑몰 기능이 제공합니다. - 주문 상세의 결제 정보 영역에 현금영수증 발급 내역이 표시될 수 있도록 확장 영역을 마련했습니다. - 주문서에서 결제 플러그인이 등록한 결제수단(예: "가상계좌 (토스페이먼츠)")을 선택해 주문할 수 있습니다. 선택한 결제수단은 주문 시 대응하는 기본 결제수단으로 전송되어, 결제창 호출과 이후 주문 처리가 정상적으로 이어집니다. +- 게시글 상세 화면에 추천·비추천 반응 버튼을 추가했습니다. 로그인한 회원은 버튼을 눌러 반응을 남기거나, 다른 반응으로 바꾸거나, 같은 버튼을 다시 눌러 취소할 수 있으며 각 반응의 개수가 함께 표시됩니다. 본인이 쓴 글에서는 버튼이 눌리지 않고, 로그인하지 않은 경우 안내 후 로그인 화면으로 이동합니다. (게시판에서 반응 기능을 켠 경우에만 표시됩니다.) - 회원가입 화면에 휴대폰번호·전화번호 입력란을 추가했습니다. 두 항목 모두 선택 사항입니다. ### Changed diff --git a/templates/_bundled/sirsoft-basic/__tests__/layouts/board-reaction-buttons.test.tsx b/templates/_bundled/sirsoft-basic/__tests__/layouts/board-reaction-buttons.test.tsx index 4fc0856d..762f363b 100644 --- a/templates/_bundled/sirsoft-basic/__tests__/layouts/board-reaction-buttons.test.tsx +++ b/templates/_bundled/sirsoft-basic/__tests__/layouts/board-reaction-buttons.test.tsx @@ -41,7 +41,13 @@ function findByComment(root: unknown, commentText: string): Node[] { } describe('이슈 #525 — 게시글 상세 반응 버튼 (basic/show.json)', () => { + // @scenario case=detail_hidden_when_no_active_types + // @effects reaction_area_hidden_when_no_active_types it('반응 영역은 use_reaction + reaction_type_options 존재 시에만 노출된다 (확정 11)', () => { + // 이 if 는 use_reaction 과 reaction_type_options 둘 다에 의존하므로 + // reaction off / 유형 0개 두 케이스 모두 이 조건으로 미노출된다. + // @scenario case=detail_hidden_when_use_reaction_off + // @effects reaction_area_hidden_when_use_reaction_off const areas = findByComment(basicShow, '게시글 반응(추천/비추천) 영역'); expect(areas.length).toBe(1); @@ -52,6 +58,8 @@ describe('이슈 #525 — 게시글 상세 반응 버튼 (basic/show.json)', () expect(area.if as string).toContain('length > 0'); }); + // @scenario case=detail_shows_active_types_even_zero + // @effects active_types_always_shown_even_zero_count it('반응 유형을 reaction_type_options 로 iteration 한다', () => { const areas = findByComment(basicShow, '게시글 반응(추천/비추천) 영역'); const iterations = collectNodes(areas[0], (n) => n.iteration !== undefined); @@ -62,6 +70,8 @@ describe('이슈 #525 — 게시글 상세 반응 버튼 (basic/show.json)', () expect(iter.item_var).toBe('reactionType'); }); + // @scenario case=author_buttons_disabled + // @effects author_sees_buttons_disabled, active_types_always_shown_even_zero_count, my_reaction_type_highlighted it('유형별 버튼은 본인 글이면 disabled 이고 개수를 바인딩한다 (확정 08·09)', () => { const areas = findByComment(basicShow, '게시글 반응(추천/비추천) 영역'); const buttons = collectNodes(areas[0], (n) => n.name === 'Button');