Files
HeuJung 6bde24bc75 fix(ecommerce): 숫자 설정 문자열 영속으로 인한 주문 생성 실패 해소
관리자 환경설정에서 미입금 자동취소 기한을 저장하면 HTML number 입력의
문자열이 그대로 영속되고, Carbon 3.13 의 strict 타입 경계에서 TypeError 가
발생해 무통장입금·가상계좌 주문이 전면 실패했다(공개 제보). 한 곳만 막으면
다른 저장 경로가 남으므로 저장·조회·소비·모델 네 계층에 각각 방어를 두고,
저장소 전역에서 같은 형태가 더 없음을 audit 룰로 확정했다.

같은 계획서 수행 중 드러난 인접 결함도 함께 해소했다.
- 쿠폰 수정 경로로 (발급일 기준, 일수 NULL) 조합이 저장되어 발급 즉시
 만료된 쿠폰이 조용히 나가던 문제 — 저장·발급 양쪽에서 차단
- 리뷰 작성 기한을 화면과 저장 판정이 서로 다른 기준으로 계산하던 문제
- 결제 플러그인이 PaymentMethodEnum 과 다른 어휘로 지원 결제수단을 선언해
 가상계좌·계좌이체·휴대폰결제에 PG사를 지정할 수 없던 문제

엔진에서는 `_localInit` 적용 여부를 전역 해시로만 판정해 인스턴스별 저장소가
리셋되지 않던 결함(engine-v1.54.5)과, 제거 대상이 없어도 상태 갱신·캐시
무효화가 상시 발생하던 판정 결함(engine-v1.54.6)을 고쳤다.

Playwright MCP 정밀 점검(T1~T11)에서 설정이 빈 문자열로 남은 사이트의
주문서 입금기한 안내가 미치환 플레이스홀더를 노출하는 것을 실측해, 화면
폴백을 서버와 같은 규칙으로 맞췄다.
2026-08-02 18:35:28 +09:00

83 lines
3.0 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Tests\Unit\Models;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Models\Board;
use Modules\Sirsoft\Board\Models\Post;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
use PHPUnit\Framework\Attributes\Test;
/**
* new_display_hours 정수 캐스트 회귀 테스트
*
* Post::isNew() 는 게시판의 new_display_hours 를 Carbon subHours() 에 넘긴다.
* subHours 는 값을 음수화하며 넘기므로 숫자 문자열은 우연히 통과하지만(비숫자 문자열은
* "Unsupported operand types" 로 실패), 같은 값이 addHours 계열에 닿는 순간 TypeError 가 된다.
*
* 더 직접적인 결함은 타입 불일치다 — DB 왕복 후에는 드라이버가 INT 컬럼을 정수로 반환하지만,
* 요청 입력으로 모델에 대입된 직후(재조회 전)에는 문자열이 그대로 남아 API 응답/비교 연산의
* 타입이 조회 시점에 따라 달라진다. 형제 숫자 컬럼(max_reply_depth/posts_count 등)과
* 동일하게 integer 캐스트로 통일한다.
*
* @effects board_new_display_hours_is_integer
*/
class NewDisplayHoursCastTest extends BoardTestCase
{
#[Test]
public function new_display_hours_is_integer_right_after_mass_assignment(): void
{
// 요청 입력(HTML number → 문자열)으로 모델을 만든 직후, 재조회 없이 읽는 경로
$board = new Board(['new_display_hours' => '48']);
$this->assertSame(48, $board->new_display_hours);
}
#[Test]
public function new_display_hours_is_integer_after_create_without_refresh(): void
{
$board = Board::create([
'slug' => 'cast-new-display-hours',
'name' => ['ko' => '캐스트 테스트', 'en' => 'Cast Test'],
'is_active' => true,
'new_display_hours' => '48',
]);
$this->assertSame(48, $board->new_display_hours);
}
#[Test]
public function new_display_hours_is_cast_to_integer_when_loaded_from_db(): void
{
DB::table('boards')->where('id', $this->board->id)->update(['new_display_hours' => 12]);
$board = Board::findOrFail($this->board->id);
$this->assertSame(12, $board->new_display_hours);
}
#[Test]
public function post_is_new_does_not_crash_when_board_attribute_is_string(): void
{
$postId = $this->createTestPost();
$post = Post::with('board')->findOrFail($postId);
// 문자열 유입 재현 (캐스트 도입 후 정수로 해석되어야 함 — 비회귀 가드)
$post->board->new_display_hours = '24';
$this->assertTrue($post->isNew());
}
#[Test]
public function post_is_new_returns_false_for_old_post(): void
{
$postId = $this->createTestPost();
DB::table('board_posts')->where('id', $postId)->update(['created_at' => now()->subHours(5)]);
$post = Post::with('board')->findOrFail($postId);
$post->board->new_display_hours = '1';
$this->assertFalse($post->isNew());
}
}