fix(ecommerce,basic): 배송정책 스냅샷 형태 정규화로 주문 상세 배송정보 누락 해소
마이페이지·비회원 주문 상세에서 상품별 배송정책명과 개별 배송비 줄이 아무 안내 없이 통째로 사라졌다. 주문에 보관하는 배송정책 스냅샷이 항목 목록과 배송지 메타를 한 배열에 섞어 담고 있어, PHP 배열이 non-sequential 이 되면서 json_encode 가 리스트가 아니라 객체로 직렬화했기 때문이다. 서버 소비자는 정수 키 판별로 관용 처리해 무사했고 배열을 전제한 화면만 죽었다. 한 컬럼에 두 의미를 담은 구조 자체를 items + address 로 분리했다. 이미 저장된 주문은 화면만 고쳐서는 낫지 않으므로 업그레이드 스텝으로 함께 정규화한다.
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
- 상품 리뷰 목록의 작성자 이름과 쿠폰 목록의 쿠폰 이름을 키보드로 선택해 이동할 수 있게 했습니다. 이전에는 마우스 클릭으로만 이동할 수 있었습니다.
|
||||
- 주문 검색 프리셋 관리 창의 수정·삭제 아이콘이 의도한 크기보다 작게 보이던 문제를 수정했습니다.
|
||||
- 할인코드 목록에서 선택 개수·전체 건수·페이지당 표시 개수 문구에 숫자가 채워지지 않고 자리표시자가 그대로 보이던 문제를 수정했습니다.
|
||||
- 마이페이지와 비회원 주문 조회의 주문 상세에서 상품별 배송정책명과 개별 배송비가 아무 안내 없이 표시되지 않던 문제를 수정했습니다. 주문에 보관된 배송정책 기록의 저장 형태가 화면이 기대하는 형태와 달라 해당 줄이 통째로 빠져 있었습니다. 이미 저장된 주문도 업그레이드 시 함께 정리됩니다.
|
||||
- 미입금 자동취소 기한을 비운 채 저장하면 "저장되었습니다"라고 안내되면서도 값은 사라지던 문제를 수정했습니다. 숫자 항목에 숫자가 아닌 값을 입력하면 입력칸이 비워지는데, 그대로 저장하면 설정이 없는 상태로 남아 화면에 보이는 값과 실제 동작이 어긋났습니다. 이제 비워 둔 채로는 저장되지 않고 값을 입력하라고 안내합니다.
|
||||
- 미입금 자동취소 기한 입력 안내가 실제 입력 하한과 다른 "0일 이상"으로 표시되던 문구를 "1일 이상"으로 바로잡았습니다.
|
||||
- 쿠폰 수정 화면에서 유효기간을 "발급일로부터"로 두고 일수를 비운 채 저장할 수 있던 문제를 수정했습니다. 그렇게 저장된 쿠폰은 발급되는 즉시 만료 상태가 되어, 받은 회원이 사용할 수 없으면서도 아무 오류가 나지 않았습니다. 이제 저장 단계에서 일수를 입력하도록 안내하고, 이미 그렇게 저장돼 있던 쿠폰은 발급 시 유효기간이 설정되지 않았다고 알려 줍니다.
|
||||
|
||||
@@ -36,6 +36,7 @@ use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderRepositoryInterface;
|
||||
use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductOptionRepositoryInterface;
|
||||
use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductRepositoryInterface;
|
||||
use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ShippingTypeRepositoryInterface;
|
||||
use Modules\Sirsoft\Ecommerce\Support\ShippingPolicySnapshot;
|
||||
use Modules\Sirsoft\Ecommerce\Support\VatCalculator;
|
||||
|
||||
/**
|
||||
@@ -1428,10 +1429,10 @@ class OrderProcessingService
|
||||
*/
|
||||
protected function buildShippingPolicyAppliedSnapshot(OrderCalculationResult $result, array $shippingInfo = []): array
|
||||
{
|
||||
$policies = [];
|
||||
$items = [];
|
||||
foreach ($result->items as $item) {
|
||||
if ($item->appliedShippingPolicy) {
|
||||
$policies[] = [
|
||||
$items[] = [
|
||||
'product_option_id' => $item->productOptionId,
|
||||
'policy' => $item->appliedShippingPolicy->toArray(),
|
||||
];
|
||||
@@ -1439,13 +1440,16 @@ class OrderProcessingService
|
||||
}
|
||||
|
||||
// 배송지(국가/우편번호) 스냅샷 보존 (B5 — 환불/취소 재계산 시 도서산간/국가별 정책 판단 복원).
|
||||
// OrderAdjustmentService::buildRecalcInput 가 'address' 키로 ShippingAddress 를 복원한다.
|
||||
$policies['address'] = [
|
||||
//
|
||||
// 항목 목록과 배송지 메타는 **별도 키**로 분리한다. 종전에는 `$policies[]` 리스트에
|
||||
// `$policies['address']` 를 덧붙여 한 배열에 섞었고, 그 결과 PHP 배열이
|
||||
// non-sequential 이 되어 `json_encode` 가 객체로 직렬화했다. 서버는 `is_int($key)` 로
|
||||
// 버텼지만 배열을 전제하는 프론트에서 `.find is not a function` 이 나며 마이페이지·
|
||||
// 비회원 주문 상세의 배송정책 블록이 조용히 사라졌다.
|
||||
return ShippingPolicySnapshot::make($items, [
|
||||
'country_code' => strtoupper((string) ($shippingInfo['country_code'] ?? 'KR')),
|
||||
'zipcode' => $shippingInfo['zipcode'] ?? $shippingInfo['intl_postal_code'] ?? null,
|
||||
];
|
||||
|
||||
return $policies;
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Ecommerce\Support;
|
||||
|
||||
/**
|
||||
* 배송정책 적용 스냅샷의 구조 SSoT.
|
||||
*
|
||||
* `ecommerce_orders.shipping_policy_applied_snapshot` 은 두 가지를 담는다 —
|
||||
* 옵션별 적용 정책 **목록**과 주문 시점 배송지 **메타**(국가/우편번호). 종전에는 이 둘을
|
||||
* 한 배열에 섞어(`$policies[] = ...` 뒤에 `$policies['address'] = ...`) 저장했다.
|
||||
* PHP 배열이 non-sequential 이 되면 `json_encode` 가 리스트가 아니라 객체
|
||||
* `{"0": {...}, "address": {...}}` 로 직렬화한다. 서버측 소비자는 `is_int($key)` 로
|
||||
* 관용 처리해 무사했지만, **프론트는 배열을 전제**하므로
|
||||
* `(snapshot ?? []).find(...)` 가 `TypeError: .find is not a function` 으로 죽었고
|
||||
* 마이페이지·비회원 주문 상세의 배송정책 표시 블록이 예외도 없이 통째로 사라졌다.
|
||||
*
|
||||
* 한 컬럼에 두 의미를 섞지 않도록 `{items: [...], address: {...}}` 로 분리하고,
|
||||
* 판별·변환 로직을 이 클래스 하나로 모은다. 구형 혼합 배열은 `normalize()` 가 흡수하므로
|
||||
* 백필 전 기존 주문도 같은 경로로 동작한다.
|
||||
*
|
||||
* @since 1.0.5
|
||||
*/
|
||||
class ShippingPolicySnapshot
|
||||
{
|
||||
/** 항목 목록 키 */
|
||||
public const ITEMS = 'items';
|
||||
|
||||
/** 배송지 메타 키 */
|
||||
public const ADDRESS = 'address';
|
||||
|
||||
/**
|
||||
* 신형 스냅샷 구조를 생성합니다.
|
||||
*
|
||||
* @param array $items 옵션별 적용 정책 목록 (각 항목: product_option_id, policy)
|
||||
* @param array $address 배송지 메타 (country_code, zipcode)
|
||||
* @return array{items: array<int, array>, address: array} 신형 스냅샷
|
||||
*/
|
||||
public static function make(array $items, array $address): array
|
||||
{
|
||||
return [
|
||||
self::ITEMS => array_values($items),
|
||||
self::ADDRESS => $address,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 신형/구형 어느 형태든 신형 구조로 정규화합니다 (멱등).
|
||||
*
|
||||
* 구형은 정수 키에 항목이, `'address'` 문자열 키에 배송지가 섞여 있다.
|
||||
*
|
||||
* @param array|null $snapshot 원본 스냅샷 (신형 · 구형 · null 모두 허용)
|
||||
* @return array{items: array<int, array>, address: array} 신형 스냅샷
|
||||
*/
|
||||
public static function normalize(?array $snapshot): array
|
||||
{
|
||||
if (empty($snapshot)) {
|
||||
return self::make([], []);
|
||||
}
|
||||
|
||||
// 이미 신형이면 items 만 재인덱싱해 반환 (멱등)
|
||||
if (array_key_exists(self::ITEMS, $snapshot)) {
|
||||
return self::make(
|
||||
is_array($snapshot[self::ITEMS]) ? $snapshot[self::ITEMS] : [],
|
||||
is_array($snapshot[self::ADDRESS] ?? null) ? $snapshot[self::ADDRESS] : []
|
||||
);
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$address = [];
|
||||
|
||||
foreach ($snapshot as $key => $entry) {
|
||||
if ($key === self::ADDRESS) {
|
||||
$address = is_array($entry) ? $entry : [];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($entry)) {
|
||||
$items[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return self::make($items, $address);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스냅샷에서 배송지 메타만 추출합니다.
|
||||
*
|
||||
* @param array|null $snapshot 원본 스냅샷 (신형 · 구형 모두 허용)
|
||||
* @return array 배송지 메타 (country_code, zipcode)
|
||||
*/
|
||||
public static function address(?array $snapshot): array
|
||||
{
|
||||
return self::normalize($snapshot)[self::ADDRESS];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스냅샷에서 항목 목록만 추출합니다.
|
||||
*
|
||||
* @param array|null $snapshot 원본 스냅샷 (신형 · 구형 모두 허용)
|
||||
* @return array<int, array> 옵션별 적용 정책 목록
|
||||
*/
|
||||
public static function items(?array $snapshot): array
|
||||
{
|
||||
return self::normalize($snapshot)[self::ITEMS];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스냅샷을 `product_option_id => policy` 맵으로 변환합니다.
|
||||
*
|
||||
* `OrderCalculationService::groupByShippingPolicy()` 가 `$snapshots[$optionId]` 로
|
||||
* 접근하므로 재계산 경로에서 필요하다. 테스트 헬퍼 등이 직접 구성한
|
||||
* `optionId => policy` 형태도 그대로 통과시킨다.
|
||||
*
|
||||
* @param array|null $snapshot 원본 스냅샷 (신형 · 구형 · 옵션 키 맵 모두 허용)
|
||||
* @return array<int|string, array> product_option_id => 정책 데이터
|
||||
*/
|
||||
public static function toOptionPolicyMap(?array $snapshot): array
|
||||
{
|
||||
if (empty($snapshot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$map = [];
|
||||
|
||||
// 신형이 아니면서 정수 키에 policy_id 를 직접 갖는 형태 = optionId => policy 맵
|
||||
if (! array_key_exists(self::ITEMS, $snapshot)) {
|
||||
foreach ($snapshot as $key => $entry) {
|
||||
if ($key === self::ADDRESS || ! is_array($entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! isset($entry['product_option_id'], $entry['policy'])) {
|
||||
$map[$key] = $entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::items($snapshot) as $entry) {
|
||||
if (isset($entry['product_option_id'], $entry['policy'])) {
|
||||
$map[$entry['product_option_id']] = $entry['policy'];
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정한 옵션 ID 집합에 해당하는 항목만 추립니다.
|
||||
*
|
||||
* @param array|null $snapshot 원본 스냅샷 (신형 · 구형 모두 허용)
|
||||
* @param array<int, bool> $optionIds product_option_id => true 형태의 대상 집합
|
||||
* @return array<int, array> 추려진 항목 목록
|
||||
*/
|
||||
public static function itemsForOptions(?array $snapshot, array $optionIds): array
|
||||
{
|
||||
$picked = [];
|
||||
|
||||
foreach (self::items($snapshot) as $entry) {
|
||||
if (isset($entry['product_option_id'], $entry['policy'])
|
||||
&& isset($optionIds[$entry['product_option_id']])) {
|
||||
$picked[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return $picked;
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services;
|
||||
|
||||
use Modules\Sirsoft\Ecommerce\Support\ShippingPolicySnapshot;
|
||||
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
|
||||
|
||||
/**
|
||||
* 배송정책 스냅샷 구조 테스트
|
||||
*
|
||||
* `shipping_policy_applied_snapshot` 은 종전에 항목 리스트(정수 키)와 배송지 메타(`'address'`
|
||||
* 문자열 키)를 **한 배열에 섞어** 저장했다. PHP 배열이 non-sequential 이 되어 `json_encode` 가
|
||||
* 객체 `{"0": {...}, "address": {...}}` 로 직렬화했고, 그 결과 프론트의
|
||||
* `(snapshot ?? []).find(...)` 가 `TypeError: .find is not a function` 으로 죽어
|
||||
* 마이페이지/비회원 주문 상세의 **배송정책 표시 블록이 통째로 사라졌다**
|
||||
* (실측: 저장된 주문 18건 중 18건 전부 객체형).
|
||||
*
|
||||
* 이 테스트는 구조가 `{items: [...], address: {...}}` 로 분리되어 있고, JSON 왕복 후에도
|
||||
* `items` 가 **배열로 유지**되는지 잠근다. 구형 혼합 배열도 정규화로 흡수되어야 한다.
|
||||
*/
|
||||
class ShippingPolicySnapshotShapeTest extends ModuleTestCase
|
||||
{
|
||||
/**
|
||||
* 신형 스냅샷은 JSON 왕복 후에도 items 가 배열(리스트)로 유지된다.
|
||||
*/
|
||||
public function test_신형_스냅샷은_json_왕복_후에도_items_가_배열이다(): void
|
||||
{
|
||||
$snapshot = ShippingPolicySnapshot::make(
|
||||
[
|
||||
['product_option_id' => 490, 'policy' => ['policy_id' => 1, 'policy_name' => '국내 무료배송']],
|
||||
['product_option_id' => 491, 'policy' => ['policy_id' => 2, 'policy_name' => '조건부 무료']],
|
||||
],
|
||||
['country_code' => 'KR', 'zipcode' => '06234']
|
||||
);
|
||||
|
||||
$decoded = json_decode(json_encode($snapshot), true);
|
||||
|
||||
$this->assertArrayHasKey('items', $decoded);
|
||||
$this->assertArrayHasKey('address', $decoded);
|
||||
$this->assertSame([0, 1], array_keys($decoded['items']), 'items 는 0부터 연속된 리스트여야 한다');
|
||||
$this->assertSame('KR', $decoded['address']['country_code']);
|
||||
|
||||
// JSON 문자열에서 items 가 배열 리터럴(`[`)로 시작해야 한다 — 객체(`{`)면 프론트 .find 가 죽는다
|
||||
$this->assertStringContainsString('"items":[', json_encode($snapshot));
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목이 없어도 items 는 객체가 아니라 빈 배열이어야 한다.
|
||||
*/
|
||||
public function test_항목이_없어도_items_는_빈_배열이다(): void
|
||||
{
|
||||
$snapshot = ShippingPolicySnapshot::make([], ['country_code' => 'KR', 'zipcode' => null]);
|
||||
|
||||
$this->assertStringContainsString('"items":[]', json_encode($snapshot));
|
||||
}
|
||||
|
||||
/**
|
||||
* 구형(항목 + address 혼합) 스냅샷도 신형으로 정규화된다.
|
||||
*/
|
||||
public function test_구형_혼합_배열은_신형으로_정규화된다(): void
|
||||
{
|
||||
$legacy = [
|
||||
0 => ['product_option_id' => 490, 'policy' => ['policy_id' => 1]],
|
||||
1 => ['product_option_id' => 491, 'policy' => ['policy_id' => 2]],
|
||||
'address' => ['country_code' => 'KR', 'zipcode' => '06234'],
|
||||
];
|
||||
|
||||
$normalized = ShippingPolicySnapshot::normalize($legacy);
|
||||
|
||||
$this->assertCount(2, $normalized['items']);
|
||||
$this->assertSame(490, $normalized['items'][0]['product_option_id']);
|
||||
$this->assertSame('KR', $normalized['address']['country_code']);
|
||||
$this->assertStringContainsString('"items":[', json_encode($normalized));
|
||||
}
|
||||
|
||||
/**
|
||||
* 신형 스냅샷을 다시 정규화해도 형태가 유지된다(멱등).
|
||||
*/
|
||||
public function test_정규화는_멱등이다(): void
|
||||
{
|
||||
$modern = ShippingPolicySnapshot::make(
|
||||
[['product_option_id' => 490, 'policy' => ['policy_id' => 1]]],
|
||||
['country_code' => 'JP', 'zipcode' => null]
|
||||
);
|
||||
|
||||
$this->assertSame($modern, ShippingPolicySnapshot::normalize($modern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 스냅샷/널도 안전하게 빈 구조를 반환한다.
|
||||
*/
|
||||
public function test_빈_스냅샷은_빈_구조를_반환한다(): void
|
||||
{
|
||||
$normalized = ShippingPolicySnapshot::normalize([]);
|
||||
|
||||
$this->assertSame([], $normalized['items']);
|
||||
$this->assertSame([], $normalized['address']);
|
||||
}
|
||||
|
||||
/**
|
||||
* product_option_id 키 맵 변환은 신형/구형 양쪽에서 같은 결과를 낸다.
|
||||
*/
|
||||
public function test_옵션_키_맵_변환은_신형_구형_동일_결과(): void
|
||||
{
|
||||
$legacy = [
|
||||
0 => ['product_option_id' => 490, 'policy' => ['policy_id' => 1, 'policy_name' => 'A']],
|
||||
'address' => ['country_code' => 'KR'],
|
||||
];
|
||||
$modern = ShippingPolicySnapshot::normalize($legacy);
|
||||
|
||||
$this->assertSame(
|
||||
ShippingPolicySnapshot::toOptionPolicyMap($legacy),
|
||||
ShippingPolicySnapshot::toOptionPolicyMap($modern)
|
||||
);
|
||||
$this->assertSame(
|
||||
['policy_id' => 1, 'policy_name' => 'A'],
|
||||
ShippingPolicySnapshot::toOptionPolicyMap($modern)[490]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ use App\Extension\AbstractUpgradeStep;
|
||||
* 반영되므로 업그레이드 시점에 동일 인덱스를 적용한다. 데이터가 많은 쇼핑몰에서는
|
||||
* ALTER TABLE 이 수 분 걸리며 그동안 해당 테이블 쓰기가 대기한다.
|
||||
*
|
||||
* 3. 주문에 보관된 배송정책 스냅샷을 리스트+메타 구조로 정규화한다.
|
||||
* 항목 목록과 배송지 메타를 한 배열에 섞어 담아 json_encode 가 리스트가 아닌 객체로
|
||||
* 직렬화했고, 배열을 전제한 주문 상세 화면에서 배송정책 줄이 통째로 사라졌다.
|
||||
*
|
||||
* 모든 비즈니스 로직은 data/1.0.5/migrations/ 로 격리(AbstractUpgradeStep 규약).
|
||||
*
|
||||
* @upgrade-path A
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Upgrades\Data\Ext\Modules\SirsoftEcommerce\V1_0_5\Migrations;
|
||||
|
||||
use App\Extension\Upgrade\DataMigration;
|
||||
use App\Extension\UpgradeContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 주문의 배송정책 적용 스냅샷을 `{items: [...], address: {...}}` 구조로 정규화합니다.
|
||||
*
|
||||
* 종전 쓰기 지점은 항목 리스트(정수 키)에 배송지 메타(`'address'` 문자열 키)를 덧붙여
|
||||
* 한 배열에 섞어 저장했습니다. PHP 배열이 non-sequential 이 되면 `json_encode` 가 리스트가
|
||||
* 아니라 객체 `{"0": {...}, "address": {...}}` 로 직렬화합니다. 서버는 키 타입을 보고
|
||||
* 관용 처리해 문제가 없었지만, 배열을 전제하는 화면에서는 배송정책 표시 블록이 오류 표시
|
||||
* 없이 통째로 사라졌습니다(마이페이지·비회원 주문 상세).
|
||||
*
|
||||
* 소스 수정만으로는 이미 저장된 주문이 낫지 않으므로 기존 행을 함께 옮깁니다.
|
||||
*
|
||||
* idempotent: 이미 `items` 키를 가진 행은 건드리지 않습니다.
|
||||
* V-1 안전: Facades\DB/Schema 만 사용하며 모델·서비스에 의존하지 않습니다.
|
||||
*/
|
||||
class NormalizeShippingPolicySnapshotShape implements DataMigration
|
||||
{
|
||||
private const ORDERS_TABLE = 'ecommerce_orders';
|
||||
|
||||
private const COLUMN = 'shipping_policy_applied_snapshot';
|
||||
|
||||
/** @var int 청크 크기 */
|
||||
private const CHUNK = 200;
|
||||
|
||||
/**
|
||||
* 마이그레이션 식별자를 반환합니다.
|
||||
*
|
||||
* @return string 마이그레이션 이름
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return 'NormalizeShippingPolicySnapshotShape';
|
||||
}
|
||||
|
||||
/**
|
||||
* 구형 혼합 배열 스냅샷을 신형 구조로 옮깁니다.
|
||||
*
|
||||
* @param UpgradeContext $context 업그레이드 컨텍스트
|
||||
*/
|
||||
public function run(UpgradeContext $context): void
|
||||
{
|
||||
if (! Schema::hasTable(self::ORDERS_TABLE)
|
||||
|| ! Schema::hasColumn(self::ORDERS_TABLE, self::COLUMN)) {
|
||||
$context->logger->info('[ecommerce:1.0.5] 배송정책 스냅샷 정규화 — 대상 스키마 부재로 스킵');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$lastId = 0;
|
||||
|
||||
// 갱신값이 필터(whereNotNull) 소속을 유지하므로 키셋 순회로 누락 없이 처리한다.
|
||||
while (true) {
|
||||
$orders = DB::table(self::ORDERS_TABLE)
|
||||
->where('id', '>', $lastId)
|
||||
->whereNotNull(self::COLUMN)
|
||||
->orderBy('id')
|
||||
->limit(self::CHUNK)
|
||||
->get(['id', self::COLUMN]);
|
||||
|
||||
if ($orders->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$lastId = (int) $order->id;
|
||||
|
||||
$decoded = json_decode((string) $order->{self::COLUMN}, true);
|
||||
if (! is_array($decoded) || $decoded === []) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 신형이면 건드리지 않는다 (멱등)
|
||||
if (array_key_exists('items', $decoded)) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$address = [];
|
||||
|
||||
foreach ($decoded as $key => $entry) {
|
||||
if ($key === 'address') {
|
||||
$address = is_array($entry) ? $entry : [];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($entry)) {
|
||||
$items[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
DB::table(self::ORDERS_TABLE)
|
||||
->where('id', $order->id)
|
||||
->update([
|
||||
self::COLUMN => json_encode(
|
||||
['items' => array_values($items), 'address' => $address],
|
||||
JSON_UNESCAPED_UNICODE
|
||||
),
|
||||
]);
|
||||
|
||||
$updated++;
|
||||
}
|
||||
}
|
||||
|
||||
$context->logger->info(
|
||||
"[ecommerce:1.0.5] 배송정책 스냅샷 정규화 — 갱신 {$updated}건, 유지 {$skipped}건"
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+14
-13
File diff suppressed because one or more lines are too long
+1
-1
@@ -58,5 +58,5 @@ export interface AvatarUploaderProps {
|
||||
/** 레이아웃 편집기 주입 속성 (편집 모드 전용, 루트에 spread) */
|
||||
editorAttrs?: EditorAttrs;
|
||||
}
|
||||
export declare function AvatarUploader({ src, fallbackText, size, uploadEndpoint, deleteEndpoint, onUploadSuccess, onUploadError, onDeleteSuccess, onDeleteError, showDeleteButton, accept, maxSize, className, uploadButtonText, deleteButtonText, readOnly, confirmDelete, deleteConfirmMessage, confirmUpload, uploadConfirmMessage, uploadSuccessActions, uploadErrorActions, deleteSuccessActions, deleteErrorActions, id, editorAttrs, }: AvatarUploaderProps): import("react/jsx-runtime").JSX.Element;
|
||||
export declare function AvatarUploader({ src, fallbackText, size, uploadEndpoint, deleteEndpoint, onUploadSuccess, onUploadError, onDeleteSuccess, onDeleteError, showDeleteButton, accept, maxSize, className, uploadButtonText, deleteButtonText, readOnly, confirmDelete, deleteConfirmMessage, confirmUpload, uploadConfirmMessage, uploadSuccessActions, uploadErrorActions, deleteSuccessActions, deleteErrorActions, id, editorAttrs, }: AvatarUploaderProps): import("react").JSX.Element;
|
||||
export default AvatarUploader;
|
||||
|
||||
@@ -2867,8 +2867,20 @@
|
||||
}
|
||||
},
|
||||
"shipping_policy_applied_snapshot": {
|
||||
"policy_name": "기본 배송 정책",
|
||||
"shipping_amount": 3000
|
||||
"items": [
|
||||
{
|
||||
"product_option_id": 9001,
|
||||
"policy": {
|
||||
"policy_name": "기본 배송 정책",
|
||||
"standalone_shipping_amount": 3000,
|
||||
"standalone_shipping_amount_formatted": "3,000원"
|
||||
}
|
||||
}
|
||||
],
|
||||
"address": {
|
||||
"country_code": "KR",
|
||||
"zipcode": "06234"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -496,7 +496,7 @@
|
||||
"comment": "적용 배송정책",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(order.data.shipping_policy_applied_snapshot ?? []).find(sp => sp.product_option_id === item.product_option_id)}}",
|
||||
"if": "{{(order.data.shipping_policy_applied_snapshot?.items ?? []).find(sp => sp.product_option_id === item.product_option_id)}}",
|
||||
"props": { "className": "mt-1.5 flex items-center gap-1.5" },
|
||||
"children": [
|
||||
{
|
||||
@@ -508,7 +508,7 @@
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": { "className": "text-xs text-gray-500 dark:text-gray-400" },
|
||||
"text": "{{(order.data.shipping_policy_applied_snapshot ?? []).find(sp => sp.product_option_id === item.product_option_id)?.policy?.policy_name ?? ''}}"
|
||||
"text": "{{(order.data.shipping_policy_applied_snapshot?.items ?? []).find(sp => sp.product_option_id === item.product_option_id)?.policy?.policy_name ?? ''}}"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
@@ -520,15 +520,15 @@
|
||||
"comment": "배송비가 있을 때",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{((order.data.shipping_policy_applied_snapshot ?? []).find(sp => sp.product_option_id === item.product_option_id)?.policy?.standalone_shipping_amount ?? 0) > 0}}",
|
||||
"if": "{{((order.data.shipping_policy_applied_snapshot?.items ?? []).find(sp => sp.product_option_id === item.product_option_id)?.policy?.standalone_shipping_amount ?? 0) > 0}}",
|
||||
"props": { "className": "text-xs text-gray-500 dark:text-gray-400" },
|
||||
"text": "{{(order.data.shipping_policy_applied_snapshot ?? []).find(sp => sp.product_option_id === item.product_option_id)?.policy?.standalone_shipping_amount_formatted ?? ''}}"
|
||||
"text": "{{(order.data.shipping_policy_applied_snapshot?.items ?? []).find(sp => sp.product_option_id === item.product_option_id)?.policy?.standalone_shipping_amount_formatted ?? ''}}"
|
||||
},
|
||||
{
|
||||
"comment": "무료배송일 때",
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{((order.data.shipping_policy_applied_snapshot ?? []).find(sp => sp.product_option_id === item.product_option_id)?.policy?.standalone_shipping_amount ?? 0) === 0}}",
|
||||
"if": "{{((order.data.shipping_policy_applied_snapshot?.items ?? []).find(sp => sp.product_option_id === item.product_option_id)?.policy?.standalone_shipping_amount ?? 0) === 0}}",
|
||||
"props": { "className": "text-xs text-green-600 dark:text-green-400 font-medium" },
|
||||
"text": "$t:mypage.order_detail.free_shipping"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user