์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿšš ๊ณ ๋„๋ชฐ ์ƒํ’ˆ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง ๊ตฌํ˜„: ๋ฐฐ์†ก๋น„ ์ตœ์ ํ™”์˜ ๋ชจ๋“  ๊ฒƒ

๐Ÿšš ๊ณ ๋„๋ชฐ ์ƒํ’ˆ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง ๊ตฌํ˜„: ๋ฐฐ์†ก๋น„ ์ตœ์ ํ™”์˜ ๋ชจ๋“  ๊ฒƒ

์‡ผํ•‘๋ชฐ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์‹ค์ „ ๋ฐฐ์†ก ๋กœ์ง ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ“ฆ ์™œ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์ด ์ค‘์š”ํ• ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ ๊ณ ๋„๋ชฐ์—์„œ ์ƒํ’ˆ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ์‡ผํ•‘๋ชฐ์„ ์šด์˜ํ•˜๋‹ค ๋ณด๋ฉด ๊ณ ๊ฐ๋“ค์ด ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ํ•œ ๋ฒˆ์— ์ฃผ๋ฌธํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ •๋ง ๋งŽ์ž–์•„? ์ด๋•Œ ๋ฐฐ์†ก๋น„๋ฅผ ์–ด๋–ป๊ฒŒ ๊ณ„์‚ฐํ• ์ง€๊ฐ€ ์ •๋ง ์ค‘์š”ํ•œ ๋ฌธ์ œ์•ผ. ๐Ÿค”

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๊ณ ๊ฐ์ด A์ƒํ’ˆ(๋ฐฐ์†ก๋น„ 3,000์›)๊ณผ B์ƒํ’ˆ(๋ฐฐ์†ก๋น„ 3,000์›)์„ ํ•จ๊ป˜ ์ฃผ๋ฌธํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž. ์ด๋•Œ ๋ฐฐ์†ก๋น„๋ฅผ 6,000์›์œผ๋กœ ๋ฐ›์„ ๊ฑด๊ฐ€, ์•„๋‹ˆ๋ฉด 3,000์›๋งŒ ๋ฐ›์„ ๊ฑด๊ฐ€? ๊ฐ™์€ ๋ฐฐ์†ก์ง€๋กœ ๋ณด๋‚ด๋Š”๋ฐ ๋‘ ๋ฒˆ ๋ฐ›๋Š” ๊ฑด ๊ณ ๊ฐ ์ž…์žฅ์—์„œ ์–ต์šธํ•˜๊ฒ ์ง€? ๋ฐ”๋กœ ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒŒ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์ด์•ผ.

๐Ÿ’ก ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์˜ ํ•ต์‹ฌ ๊ฐ€์น˜ โ€ข ๊ณ ๊ฐ ๋งŒ์กฑ๋„ ํ–ฅ์ƒ: ํ•ฉ๋ฆฌ์ ์ธ ๋ฐฐ์†ก๋น„ ๋ถ€๊ณผ
โ€ข ๊ตฌ๋งค ์ „ํ™˜์œจ ์ฆ๊ฐ€: ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ดํƒˆ๋ฅ  ๊ฐ์†Œ
โ€ข ์šด์˜ ํšจ์œจ์„ฑ: ์ž๋™ํ™”๋œ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
โ€ข ๋ฌผ๋ฅ˜ ์ตœ์ ํ™”: ๋™์ผ ๋ฐฐ์†ก์ง€ ์ƒํ’ˆ ํ†ตํ•ฉ ์ฒ˜๋ฆฌ

์‹ค์ œ๋กœ ์žฌ๋Šฅ๋„ท์—์„œ๋„ ๋‹ค์–‘ํ•œ ์‡ผํ•‘๋ชฐ ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•˜๋Š”๋ฐ, ๋ฐฐ์†ก ๋กœ์ง ๊ตฌํ˜„์€ ํ•ญ์ƒ ์ƒ์œ„๊ถŒ์— ๋žญํฌ๋˜๋Š” ์š”์ฒญ์‚ฌํ•ญ์ด์•ผ. ๊ทธ๋งŒํผ ์ค‘์š”ํ•˜๊ณ  ๊นŒ๋‹ค๋กœ์šด ๋ถ€๋ถ„์ด๋ผ๋Š” ๊ฑฐ์ง€! ๐Ÿ˜Š

์ƒํ’ˆ A ๋ฐฐ์†ก๋น„ 3,000์› ์ƒํ’ˆ B ๋ฐฐ์†ก๋น„ 3,000์› ์ƒํ’ˆ C ๋ฐฐ์†ก๋น„ 2,500์› ๋ฌถ์Œ ๋ฐฐ์†ก ์ตœ์ข… 3,000์› ๐Ÿ‘ค ๋งŒ์กฑํ•œ ๊ณ ๊ฐ

๐ŸŽฏ ๊ณ ๋„๋ชฐ์˜ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ๋ฐฉ์‹ ์ดํ•ดํ•˜๊ธฐ

๋ณธ๊ฒฉ์ ์œผ๋กœ ๋กœ์ง ๊ตฌํ˜„์— ๋“ค์–ด๊ฐ€๊ธฐ ์ „์—, ๊ณ ๋„๋ชฐ์˜ ๊ธฐ๋ณธ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ๋ฐฉ์‹์„ ์ดํ•ดํ•ด์•ผ ํ•ด. ๊ณ ๋„๋ชฐ์€ ๋‹ค์–‘ํ•œ ๋ฐฐ์†ก๋น„ ์ •์ฑ…์„ ์ง€์›ํ•˜๋Š”๋ฐ, ํฌ๊ฒŒ ๋„ค ๊ฐ€์ง€ ๋ฐฉ์‹์œผ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์–ด.

๋ฐฐ์†ก๋น„ ๋ฐฉ์‹ ์„ค๋ช… ์ ์šฉ ์‚ฌ๋ก€
๊ณ ์ • ๋ฐฐ์†ก๋น„ ์ƒํ’ˆ์— ๊ด€๊ณ„์—†์ด ์ผ์ • ๊ธˆ์•ก ๋ถ€๊ณผ ์ผ๋ฐ˜ ์˜๋ฅ˜, ์†Œํ˜• ์žกํ™”
์กฐ๊ฑด๋ถ€ ๋ฌด๋ฃŒ๋ฐฐ์†ก ํŠน์ • ๊ธˆ์•ก ์ด์ƒ ๊ตฌ๋งค ์‹œ ๋ฌด๋ฃŒ 3๋งŒ์› ์ด์ƒ ๋ฌด๋ฃŒ๋ฐฐ์†ก
์ˆ˜๋Ÿ‰๋ณ„ ๋ฐฐ์†ก๋น„ ๊ตฌ๋งค ์ˆ˜๋Ÿ‰์— ๋”ฐ๋ผ ๋ฐฐ์†ก๋น„ ์ฐจ๋“ฑ ๋Œ€ํ˜• ๊ฐ€์ „, ๊ฐ€๊ตฌ
์ง€์—ญ๋ณ„ ์ฐจ๋“ฑ ๋ฐฐ์†ก๋น„ ๋ฐฐ์†ก ์ง€์—ญ์— ๋”ฐ๋ผ ์ถ”๊ฐ€ ๋น„์šฉ ์ œ์ฃผ/๋„์„œ์‚ฐ๊ฐ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„

์ด ์ค‘์—์„œ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์€ ์ฃผ๋กœ ๊ณ ์ • ๋ฐฐ์†ก๋น„์™€ ์กฐ๊ฑด๋ถ€ ๋ฌด๋ฃŒ๋ฐฐ์†ก ๋ฐฉ์‹์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ํ™œ์šฉ๋ผ. ์™œ๋ƒํ•˜๋ฉด ์—ฌ๋Ÿฌ ์ƒํ’ˆ์„ ๋ฌถ์–ด์„œ ๋ฐฐ์†ก๋น„๋ฅผ ๊ณ„์‚ฐํ•  ๋•Œ ๊ธฐ์ค€์ด ๋ช…ํ™•ํ•ด์•ผ ํ•˜๊ฑฐ๋“ ! ๐ŸŽช

๐Ÿ” ๊ณ ๋„๋ชฐ ๋ฐฐ์†ก๋น„ ์„ค์ • ์œ„์น˜ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ โ†’ ์ƒํ’ˆ๊ด€๋ฆฌ โ†’ ๋ฐฐ์†ก์ •์ฑ… ๊ด€๋ฆฌ โ†’ ๋ฐฐ์†ก๋น„ ์„ค์ •

์—ฌ๊ธฐ์„œ ๊ฐ ๋ฐฐ์†ก ์ •์ฑ…๋ณ„๋กœ ๋ฌถ์Œ ๋ฐฐ์†ก ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด. ๊ธฐ๋ณธ์ ์œผ๋กœ '๋ฐฐ์†ก๋น„ ํ•ฉ์‚ฐ' ๋˜๋Š” '๋ฐฐ์†ก๋น„ ์„ ํƒ' ์˜ต์…˜์„ ์ œ๊ณตํ•˜์ง€.

โš™๏ธ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์˜ ํ•ต์‹ฌ ์•Œ๊ณ ๋ฆฌ์ฆ˜

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์˜ ํ•ต์‹ฌ์€ ๊ทธ๋ฃนํ•‘(Grouping)๊ณผ ์šฐ์„ ์ˆœ์œ„ ๊ฒฐ์ •(Priority)์ด์•ผ. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ์–ด๋–ค ์ƒํ’ˆ๋“ค์„ ๋ฌถ์„ ์ˆ˜ ์žˆ๋Š”์ง€ ํŒ๋‹จํ•˜๊ณ , ๊ทธ์ค‘์—์„œ ์–ด๋–ค ๋ฐฐ์†ก๋น„๋ฅผ ์ ์šฉํ• ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ๊ฑฐ์ง€.

๐Ÿ“Œ 1๋‹จ๊ณ„: ๋ฐฐ์†ก ๊ทธ๋ฃน ๋ถ„๋ฅ˜

๋จผ์ € ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด๊ธด ์ƒํ’ˆ๋“ค์„ ๋ฐฐ์†ก ๊ทธ๋ฃน๋ณ„๋กœ ๋ถ„๋ฅ˜ํ•ด์•ผ ํ•ด. ๊ฐ™์€ ๊ทธ๋ฃน์— ์†ํ•œ ์ƒํ’ˆ๋“ค๋งŒ ๋ฌถ์Œ ๋ฐฐ์†ก์ด ๊ฐ€๋Šฅํ•˜๊ฑฐ๋“ !

1 ๋ฐฐ์†ก ์ •์ฑ… ID๋กœ ๊ทธ๋ฃนํ•‘

๊ฐ ์ƒํ’ˆ์€ ๋ฐฐ์†ก ์ •์ฑ… ID๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด. ์ด ID๊ฐ€ ๊ฐ™์€ ์ƒํ’ˆ๋“ค๋ผ๋ฆฌ ๋ฌถ์„ ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด '์ผ๋ฐ˜๋ฐฐ์†ก' ์ •์ฑ…(ID: 1)์„ ์‚ฌ์šฉํ•˜๋Š” ์ƒํ’ˆ๋“ค์€ ํ•˜๋‚˜์˜ ๊ทธ๋ฃน์ด ๋˜๋Š” ๊ฑฐ์ง€.
2 ๋ฐฐ์†ก ์ถœ๋ฐœ์ง€ ํ™•์ธ

๊ฐ™์€ ๋ฐฐ์†ก ์ •์ฑ…์ด๋ผ๋„ ์ถœ๋ฐœ์ง€๊ฐ€ ๋‹ค๋ฅด๋ฉด ๋ณ„๋„ ๊ทธ๋ฃน์œผ๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•ด. A์ฐฝ๊ณ ์—์„œ ์ถœ๋ฐœํ•˜๋Š” ์ƒํ’ˆ๊ณผ B์ฐฝ๊ณ ์—์„œ ์ถœ๋ฐœํ•˜๋Š” ์ƒํ’ˆ์€ ๋”ฐ๋กœ ๋ฐฐ์†ก๋˜๋‹ˆ๊นŒ!
3 ๋ฐฐ์†ก ๋ถˆ๊ฐ€ ์กฐํ•ฉ ์ฒดํฌ

์ผ๋ถ€ ์ƒํ’ˆ์€ ๋‹ค๋ฅธ ์ƒํ’ˆ๊ณผ ํ•จ๊ป˜ ๋ฐฐ์†กํ•  ์ˆ˜ ์—†์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด ๋ƒ‰์žฅ/๋ƒ‰๋™ ์‹ํ’ˆ์€ ์ผ๋ฐ˜ ์ƒํ’ˆ๊ณผ ๋ฌถ์„ ์ˆ˜ ์—†์ง€. ์ด๋Ÿฐ ์˜ˆ์™ธ ์ผ€์ด์Šค๋ฅผ ์ฒดํฌํ•ด์•ผ ํ•ด.

๐Ÿ“Œ 2๋‹จ๊ณ„: ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ๋กœ์ง

๊ทธ๋ฃน์ด ๋‚˜๋‰˜์—ˆ์œผ๋ฉด ์ด์ œ ๊ฐ ๊ทธ๋ฃน์˜ ๋ฐฐ์†ก๋น„๋ฅผ ๊ณ„์‚ฐํ•  ์ฐจ๋ก€์•ผ. ์—ฌ๊ธฐ์„œ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฐฉ์‹์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

// ๊ธฐ๋ณธ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ๋กœ์ง ์˜ˆ์‹œ (PHP)
function calculateShippingFee($cartItems) {
    $shippingGroups = [];
    
    // 1๋‹จ๊ณ„: ๋ฐฐ์†ก ๊ทธ๋ฃน ๋ถ„๋ฅ˜
    foreach ($cartItems as $item) {
        $groupKey = $item['delivery_sno'] . '_' . $item['warehouse_code'];
        
        if (!isset($shippingGroups[$groupKey])) {
            $shippingGroups[$groupKey] = [
                'items' => [],
                'total_price' => 0,
                'base_fee' => $item['delivery_fee'],
                'free_condition' => $item['free_delivery_price']
            ];
        }
        
        $shippingGroups[$groupKey]['items'][] = $item;
        $shippingGroups[$groupKey]['total_price'] += $item['price'] * $item['quantity'];
    }
    
    // 2๋‹จ๊ณ„: ๊ฐ ๊ทธ๋ฃน๋ณ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
    $totalShippingFee = 0;
    
    foreach ($shippingGroups as $group) {
        // ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ํ™•์ธ
        if ($group['total_price'] >= $group['free_condition']) {
            continue; // ๋ฌด๋ฃŒ๋ฐฐ์†ก
        }
        
        // ๋ฐฐ์†ก๋น„ ํ•ฉ์‚ฐ ๋ฐฉ์‹์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ
        $totalShippingFee += $group['base_fee'];
    }
    
    return $totalShippingFee;
}
๐Ÿ’ก ๊ฐœ๋ฐœ ํŒ ์œ„ ์ฝ”๋“œ๋Š” ๊ธฐ๋ณธ ๊ตฌ์กฐ์•ผ. ์‹ค์ œ๋กœ๋Š” ๋” ๋งŽ์€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด ๋„์„œ์‚ฐ๊ฐ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„, ์ˆ˜๋Ÿ‰๋ณ„ ๋ฐฐ์†ก๋น„ ์ฆ๊ฐ€, ๋ถ€ํ”ผ/๋ฌด๊ฒŒ ๊ธฐ๋ฐ˜ ๊ณ„์‚ฐ ๋“ฑ์„ ์ถ”๊ฐ€๋กœ ๊ณ ๋ คํ•ด์•ผ ํ•ด!

๐Ÿ› ๏ธ ๊ณ ๋„๋ชฐ ์‹ค์ „ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ์•Œ์•„๋ดค์œผ๋‹ˆ, ์ด์ œ ๊ณ ๋„๋ชฐ์—์„œ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๋‹จ๊ณ„๋ณ„๋กœ ์‚ดํŽด๋ณผ๊ฒŒ. ๊ณ ๋„๋ชฐ์€ ์ž์ฒด ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํŠน์ • ํŒŒ์ผ๋“ค์„ ์ˆ˜์ •ํ•ด์•ผ ํ•ด. ๐Ÿ˜Ž

๐Ÿ”ง Step 1: ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ๋ชจ๋“ˆ ์ˆ˜์ •

๊ณ ๋„๋ชฐ์˜ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ์€ ์ฃผ๋กœ module/Order/OrderCalculation.php ํŒŒ์ผ์—์„œ ์ฒ˜๋ฆฌ๋ผ. ์ด ํŒŒ์ผ์„ ์—ด์–ด์„œ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ๋ฉ”์„œ๋“œ๋ฅผ ์ฐพ์•„์•ผ ํ•ด.

// module/Order/OrderCalculation.php ๋‚ด๋ถ€

class OrderCalculation {
    
    /**
     * ๋ฌถ์Œ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
     * @param array $cartData ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐ์ดํ„ฐ
     * @return array ๋ฐฐ์†ก๋น„ ์ •๋ณด
     */
    public function getDeliveryGroupCharge($cartData) {
        $deliveryGroup = [];
        $deliveryCharge = [];
        
        // ๋ฐฐ์†ก๋น„ ๊ทธ๋ฃน ์ƒ์„ฑ
        foreach ($cartData as $key => $val) {
            // ๋ฐฐ์†ก ์ •์ฑ…๋ณ„ ๊ทธ๋ฃนํ•‘
            $groupKey = $val['deliverySno'] . '_' . $val['scmNo'];
            
            if (!isset($deliveryGroup[$groupKey])) {
                $deliveryGroup[$groupKey] = [
                    'deliverySno' => $val['deliverySno'],
                    'scmNo' => $val['scmNo'],
                    'goodsPrice' => 0,
                    'deliveryFee' => $val['deliveryFee'],
                    'deliveryFreePrice' => $val['deliveryFreePrice'],
                    'deliveryMethod' => $val['deliveryMethod'],
                    'goods' => []
                ];
            }
            
            // ์ƒํ’ˆ ์ •๋ณด ์ถ”๊ฐ€
            $deliveryGroup[$groupKey]['goods'][] = $val;
            $deliveryGroup[$groupKey]['goodsPrice'] += $val['price'] * $val['goodsCnt'];
        }
        
        // ๊ฐ ๊ทธ๋ฃน๋ณ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
        foreach ($deliveryGroup as $groupKey => $group) {
            $fee = 0;
            
            // ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ๋ฐฉ์‹์— ๋”ฐ๋ผ ๋ถ„๊ธฐ
            switch ($group['deliveryMethod']) {
                case 'fixed': // ๊ณ ์ • ๋ฐฐ์†ก๋น„
                    if ($group['goodsPrice'] < $group['deliveryFreePrice']) {
                        $fee = $group['deliveryFee'];
                    }
                    break;
                    
                case 'quantity': // ์ˆ˜๋Ÿ‰๋ณ„ ๋ฐฐ์†ก๋น„
                    $totalQty = 0;
                    foreach ($group['goods'] as $goods) {
                        $totalQty += $goods['goodsCnt'];
                    }
                    $fee = $this->calculateQuantityBasedFee($totalQty, $group);
                    break;
                    
                case 'weight': // ๋ฌด๊ฒŒ๋ณ„ ๋ฐฐ์†ก๋น„
                    $totalWeight = 0;
                    foreach ($group['goods'] as $goods) {
                        $totalWeight += $goods['weight'] * $goods['goodsCnt'];
                    }
                    $fee = $this->calculateWeightBasedFee($totalWeight, $group);
                    break;
            }
            
            // ์ง€์—ญ๋ณ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
            $fee += $this->getRegionalExtraFee($group);
            
            $deliveryCharge[$groupKey] = [
                'fee' => $fee,
                'group' => $group
            ];
        }
        
        return $deliveryCharge;
    }
    
    /**
     * ์ˆ˜๋Ÿ‰๋ณ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
     */
    private function calculateQuantityBasedFee($quantity, $group) {
        // ์ˆ˜๋Ÿ‰ ๊ตฌ๊ฐ„๋ณ„ ๋ฐฐ์†ก๋น„ ์„ค์ • ์กฐํšŒ
        $feeConfig = $this->getQuantityFeeConfig($group['deliverySno']);
        
        foreach ($feeConfig as $config) {
            if ($quantity >= $config['minQty'] && $quantity <= $config['maxQty']) {
                return $config['fee'];
            }
        }
        
        return $group['deliveryFee']; // ๊ธฐ๋ณธ ๋ฐฐ์†ก๋น„
    }
    
    /**
     * ๋ฌด๊ฒŒ๋ณ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
     */
    private function calculateWeightBasedFee($weight, $group) {
        // ๋ฌด๊ฒŒ ๊ตฌ๊ฐ„๋ณ„ ๋ฐฐ์†ก๋น„ ์„ค์ • ์กฐํšŒ
        $feeConfig = $this->getWeightFeeConfig($group['deliverySno']);
        
        foreach ($feeConfig as $config) {
            if ($weight >= $config['minWeight'] && $weight <= $config['maxWeight']) {
                return $config['fee'];
            }
        }
        
        return $group['deliveryFee']; // ๊ธฐ๋ณธ ๋ฐฐ์†ก๋น„
    }
    
    /**
     * ์ง€์—ญ๋ณ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„
     */
    private function getRegionalExtraFee($group) {
        // ๋ฐฐ์†ก์ง€ ์ฃผ์†Œ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        $address = $this->getDeliveryAddress();
        
        // ์ œ์ฃผ/๋„์„œ์‚ฐ๊ฐ„ ์ฒดํฌ
        if ($this->isRemoteArea($address)) {
            return $group['remoteAreaFee'] ?? 3000; // ๊ธฐ๋ณธ 3,000์›
        }
        
        return 0;
    }
}
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ ๊ณ ๋„๋ชฐ์˜ ์ฝ”์–ด ํŒŒ์ผ์„ ์ง์ ‘ ์ˆ˜์ •ํ•˜๋ฉด ์—…๋ฐ์ดํŠธ ์‹œ ๋ฎ์–ด์”Œ์›Œ์งˆ ์ˆ˜ ์žˆ์–ด. ๊ฐ€๋Šฅํ•˜๋ฉด user_define ํด๋”์— ์˜ค๋ฒ„๋ผ์ด๋“œ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ ์ž‘์—…ํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด!

๐Ÿ”ง Step 2: ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€ ์—ฐ๋™

๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ๋กœ์ง์„ ๋งŒ๋“ค์—ˆ์œผ๋ฉด, ์ด์ œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€์—์„œ ์ด๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ด. ๊ณ ๋„๋ชฐ์˜ ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋Š” module/Order/Cart.php์—์„œ ์ฒ˜๋ฆฌ๋ผ.

// module/Order/Cart.php ๋‚ด๋ถ€

class Cart extends OrderCalculation {
    
    /**
     * ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ณด ์กฐํšŒ (๋ฐฐ์†ก๋น„ ํฌํ•จ)
     */
    public function getCartInfo() {
        // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ ์กฐํšŒ
        $cartData = $this->getCartData();
        
        if (empty($cartData)) {
            return [];
        }
        
        // ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
        $deliveryCharge = $this->getDeliveryGroupCharge($cartData);
        
        // ์ด ์ฃผ๋ฌธ ๊ธˆ์•ก ๊ณ„์‚ฐ
        $totalGoodsPrice = 0;
        $totalDeliveryFee = 0;
        
        foreach ($cartData as $item) {
            $totalGoodsPrice += $item['price'] * $item['goodsCnt'];
        }
        
        foreach ($deliveryCharge as $charge) {
            $totalDeliveryFee += $charge['fee'];
        }
        
        return [
            'cartData' => $cartData,
            'deliveryCharge' => $deliveryCharge,
            'totalGoodsPrice' => $totalGoodsPrice,
            'totalDeliveryFee' => $totalDeliveryFee,
            'totalOrderPrice' => $totalGoodsPrice + $totalDeliveryFee
        ];
    }
    
    /**
     * ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐฐ์†ก๋น„ ๊ทธ๋ฃน ํ‘œ์‹œ์šฉ ๋ฐ์ดํ„ฐ
     */
    public function getDeliveryGroupDisplay() {
        $cartInfo = $this->getCartInfo();
        $displayData = [];
        
        foreach ($cartInfo['deliveryCharge'] as $groupKey => $charge) {
            $group = $charge['group'];
            
            $displayData[] = [
                'groupName' => $this->getDeliveryPolicyName($group['deliverySno']),
                'goodsList' => $group['goods'],
                'goodsPrice' => number_format($group['goodsPrice']),
                'deliveryFee' => number_format($charge['fee']),
                'freeCondition' => number_format($group['deliveryFreePrice']),
                'remainForFree' => max(0, $group['deliveryFreePrice'] - $group['goodsPrice'])
            ];
        }
        
        return $displayData;
    }
}

๐Ÿ”ง Step 3: ํ”„๋ก ํŠธ์—”๋“œ ํ™”๋ฉด ๊ตฌ์„ฑ

๋ฐฑ์—”๋“œ ๋กœ์ง์ด ์™„์„ฑ๋˜์—ˆ์œผ๋ฉด, ์ด์ œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ํ™”๋ฉด์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด. ๊ณ ๋„๋ชฐ์˜ ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ์€ skin/ ํด๋” ์•„๋ž˜์— ์žˆ์–ด.

<!-- skin/front/[์Šคํ‚จ๋ช…]/order/cart.html -->

<div class="cart-delivery-group">
    <h3>๋ฐฐ์†ก๋น„ ์•ˆ๋‚ด</h3>
    
    <!--{ @deliveryGroup }-->
    <div class="delivery-group-item">
        <div class="group-header">
            <strong>{=groupName}</strong>
            <span class="delivery-fee">๋ฐฐ์†ก๋น„: {=deliveryFee}์›</span>
        </div>
        
        <div class="group-goods">
            <!--{ @goodsList }-->
            <div class="goods-item">
                <img src="{=imageUrl}" alt="{=goodsNm}">
                <span>{=goodsNm}</span>
                <span>{=price}์› x {=goodsCnt}๊ฐœ</span>
            </div>
            <!--{ / }-->
        </div>
        
        <!--{ @if remainForFree > 0 }-->
        <div class="free-delivery-guide">
            <p>{=remainForFree}์› ๋” ๊ตฌ๋งคํ•˜์‹œ๋ฉด ๋ฌด๋ฃŒ๋ฐฐ์†ก!</p>
            <div class="progress-bar">
                <div class="progress" style="width: {=progressPercent}%"></div>
            </div>
        </div>
        <!--{ / }-->
    </div>
    <!--{ / }-->
    
    <div class="total-delivery-fee">
        <strong>์ด ๋ฐฐ์†ก๋น„</strong>
        <span>{=totalDeliveryFee}์›</span>
    </div>
</div>

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์—์„œ ๋ฐฐ์†ก ๊ทธ๋ฃน๋ณ„๋กœ ๋ฌถ์ธ ์ƒํ’ˆ๋“ค๊ณผ ๊ฐ ๊ทธ๋ฃน์˜ ๋ฐฐ์†ก๋น„๋ฅผ ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ์–ด! ๐ŸŽจ

๋ฌถ์Œ ๋ฐฐ์†ก ์ฒ˜๋ฆฌ ํ๋ฆ„๋„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ ์กฐํšŒ ๋ฐฐ์†ก ์ •์ฑ…๋ณ„ ๊ทธ๋ฃนํ•‘ (deliverySno ๊ธฐ์ค€) ๊ทธ๋ฃน A ์ผ๋ฐ˜๋ฐฐ์†ก ์ƒํ’ˆ ๊ทธ๋ฃน B ํŠน์ˆ˜๋ฐฐ์†ก ์ƒํ’ˆ ๊ทธ๋ฃน C ๋ƒ‰์žฅ๋ฐฐ์†ก ์ƒํ’ˆ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ 3,000์› ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ 5,000์› ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ 4,000์› ์ด ๋ฐฐ์†ก๋น„ ํ•ฉ์‚ฐ 12,000์›

๐ŸŽจ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

๊ธฐ๋ณธ์ ์ธ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ๋‹ค๋ฉด, ์ด์ œ ์ข€ ๋” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณผ ์ฐจ๋ก€์•ผ. ์‹ค์ œ ์‡ผํ•‘๋ชฐ์—์„œ๋Š” ๋‹ค์–‘ํ•œ ์˜ˆ์™ธ ์ƒํ™ฉ๊ณผ ํŠน์ˆ˜ ์ผ€์ด์Šค๋“ค์ด ์žˆ๊ฑฐ๋“ ! ๐Ÿ’ช

๐ŸŒŸ ๊ธฐ๋Šฅ 1: ์Šค๋งˆํŠธ ๋ฐฐ์†ก๋น„ ์ตœ์ ํ™”

๊ณ ๊ฐ์ด ์—ฌ๋Ÿฌ ๋ฐฐ์†ก ๊ทธ๋ฃน์˜ ์ƒํ’ˆ์„ ๋‹ด์•˜์„ ๋•Œ, ์–ด๋–ค ์กฐํ•ฉ์ด ๊ฐ€์žฅ ์ €๋ ดํ•œ์ง€ ์ž๋™์œผ๋กœ ๊ณ„์‚ฐํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด A๊ทธ๋ฃน ์ƒํ’ˆ 2๊ฐœ์™€ B๊ทธ๋ฃน ์ƒํ’ˆ 1๊ฐœ๋ฅผ ๋‹ด์•˜๋Š”๋ฐ, A๊ทธ๋ฃน์—์„œ 1๊ฐœ๋งŒ ๋” ์‚ฌ๋ฉด ๋ฌด๋ฃŒ๋ฐฐ์†ก์ด ๋œ๋‹ค๋ฉด ์ด๋ฅผ ์•Œ๋ ค์ฃผ๋Š” ๊ฑฐ์ง€!

/**
 * ๋ฐฐ์†ก๋น„ ์ตœ์ ํ™” ์ œ์•ˆ
 */
public function suggestOptimalDelivery($cartData) {
    $suggestions = [];
    $deliveryGroups = $this->getDeliveryGroupCharge($cartData);
    
    foreach ($deliveryGroups as $groupKey => $charge) {
        $group = $charge['group'];
        
        // ๋ฌด๋ฃŒ๋ฐฐ์†ก๊นŒ์ง€ ๋‚จ์€ ๊ธˆ์•ก ๊ณ„์‚ฐ
        if ($group['goodsPrice'] < $group['deliveryFreePrice']) {
            $remainAmount = $group['deliveryFreePrice'] - $group['goodsPrice'];
            
            // ํ•ด๋‹น ๊ทธ๋ฃน์˜ ์ถ”์ฒœ ์ƒํ’ˆ ์กฐํšŒ
            $recommendGoods = $this->getRecommendGoodsByPrice(
                $group['deliverySno'],
                $remainAmount
            );
            
            if (!empty($recommendGoods)) {
                $suggestions[] = [
                    'groupName' => $this->getDeliveryPolicyName($group['deliverySno']),
                    'currentFee' => $charge['fee'],
                    'remainAmount' => $remainAmount,
                    'recommendGoods' => $recommendGoods,
                    'message' => number_format($remainAmount) . '์›๋งŒ ๋” ๊ตฌ๋งคํ•˜์‹œ๋ฉด ๋ฐฐ์†ก๋น„ ' . 
                                 number_format($charge['fee']) . '์›์ด ๋ฌด๋ฃŒ!'
                ];
            }
        }
    }
    
    return $suggestions;
}

/**
 * ๊ฐ€๊ฒฉ๋Œ€๋ณ„ ์ถ”์ฒœ ์ƒํ’ˆ ์กฐํšŒ
 */
private function getRecommendGoodsByPrice($deliverySno, $targetPrice) {
    // ํ•ด๋‹น ๋ฐฐ์†ก ์ •์ฑ…์„ ์‚ฌ์šฉํ•˜๋Š” ์ƒํ’ˆ ์ค‘
    // ๋ชฉํ‘œ ๊ฐ€๊ฒฉ ์ดํ•˜์˜ ์ธ๊ธฐ ์ƒํ’ˆ ์กฐํšŒ
    $db = \App::load('DB');
    
    $sql = "SELECT goodsNo, goodsNm, goodsPrice, goodsImage 
            FROM es_goods 
            WHERE deliverySno = ? 
            AND goodsPrice <= ? 
            AND goodsSellFl = 'y'
            ORDER BY orderCnt DESC 
            LIMIT 5";
    
    return $db->query_fetch($sql, [$deliverySno, $targetPrice]);
}

๐ŸŒŸ ๊ธฐ๋Šฅ 2: ์‹ค์‹œ๊ฐ„ ๋ฐฐ์†ก๋น„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ

๊ณ ๊ฐ์ด ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ˆ˜๋Ÿ‰์„ ๋ณ€๊ฒฝํ•  ๋•Œ๋งˆ๋‹ค ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐฐ์†ก๋น„๊ฐ€ ์–ด๋–ป๊ฒŒ ๋ณ€ํ•˜๋Š”์ง€ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด์•ผ. AJAX๋ฅผ ํ™œ์šฉํ•ด์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด!

// JavaScript (jQuery ์‚ฌ์šฉ)
function updateDeliveryFeePreview() {
    var cartItems = [];
    
    // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ƒํ’ˆ ์ •๋ณด ์ˆ˜์ง‘
    $('.cart-item').each(function() {
        cartItems.push({
            goodsNo: $(this).data('goods-no'),
            goodsCnt: $(this).find('.quantity-input').val(),
            deliverySno: $(this).data('delivery-sno')
        });
    });
    
    // AJAX ์š”์ฒญ
    $.ajax({
        url: '/order/ajax_calculate_delivery',
        type: 'POST',
        data: {
            cartItems: JSON.stringify(cartItems)
        },
        success: function(response) {
            if (response.success) {
                // ๋ฐฐ์†ก๋น„ ์ •๋ณด ์—…๋ฐ์ดํŠธ
                updateDeliveryDisplay(response.deliveryCharge);
                
                // ๋ฌด๋ฃŒ๋ฐฐ์†ก๊นŒ์ง€ ๋‚จ์€ ๊ธˆ์•ก ํ‘œ์‹œ
                updateFreeDeliveryProgress(response.deliveryCharge);
                
                // ์ด ์ฃผ๋ฌธ๊ธˆ์•ก ์—…๋ฐ์ดํŠธ
                $('#total-order-price').text(
                    number_format(response.totalOrderPrice) + '์›'
                );
            }
        }
    });
}

// ๋ฐฐ์†ก๋น„ ์ •๋ณด ํ™”๋ฉด ์—…๋ฐ์ดํŠธ
function updateDeliveryDisplay(deliveryCharge) {
    $('.delivery-group-item').each(function(index) {
        var groupKey = $(this).data('group-key');
        var charge = deliveryCharge[groupKey];
        
        if (charge) {
            $(this).find('.delivery-fee').text(
                '๋ฐฐ์†ก๋น„: ' + number_format(charge.fee) + '์›'
            );
            
            // ๋ฌด๋ฃŒ๋ฐฐ์†ก ๋‹ฌ์„ฑ ์‹œ ํ‘œ์‹œ
            if (charge.fee === 0) {
                $(this).find('.free-badge').show();
            } else {
                $(this).find('.free-badge').hide();
            }
        }
    });
}

// ๋ฌด๋ฃŒ๋ฐฐ์†ก ์ง„ํ–‰๋ฅ  ์—…๋ฐ์ดํŠธ
function updateFreeDeliveryProgress(deliveryCharge) {
    $('.delivery-group-item').each(function() {
        var groupKey = $(this).data('group-key');
        var charge = deliveryCharge[groupKey];
        
        if (charge && charge.group.deliveryFreePrice > 0) {
            var percent = Math.min(100, 
                (charge.group.goodsPrice / charge.group.deliveryFreePrice) * 100
            );
            
            $(this).find('.progress').css('width', percent + '%');
            
            var remainAmount = Math.max(0, 
                charge.group.deliveryFreePrice - charge.group.goodsPrice
            );
            
            if (remainAmount > 0) {
                $(this).find('.remain-amount').text(
                    number_format(remainAmount) + '์›'
                );
            } else {
                $(this).find('.free-delivery-guide').html(
                    '<strong>๐ŸŽ‰ ๋ฌด๋ฃŒ๋ฐฐ์†ก ๋‹ฌ์„ฑ!</strong>'
                );
            }
        }
    });
}

// ์ˆ˜๋Ÿ‰ ๋ณ€๊ฒฝ ์‹œ ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ
$(document).on('change', '.quantity-input', function() {
    updateDeliveryFeePreview();
});

// ์ƒํ’ˆ ์‚ญ์ œ ์‹œ ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ
$(document).on('click', '.btn-delete-item', function() {
    // ์ƒํ’ˆ ์‚ญ์ œ ํ›„
    updateDeliveryFeePreview();
});

๐ŸŒŸ ๊ธฐ๋Šฅ 3: ๋ฐฐ์†ก์ง€ ๋ถ„๋ฆฌ ์ฃผ๋ฌธ

ํ•˜๋‚˜์˜ ์ฃผ๋ฌธ์—์„œ ์—ฌ๋Ÿฌ ๋ฐฐ์†ก์ง€๋กœ ๋ณด๋‚ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด ์„ ๋ฌผ์šฉ์œผ๋กœ ๊ฐ๊ฐ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์—๊ฒŒ ๋ณด๋‚ด๋Š” ๊ฒฝ์šฐ์ง€. ์ด๋•Œ๋Š” ๋ฐฐ์†ก์ง€๋ณ„๋กœ ๋ฐฐ์†ก๋น„๋ฅผ ๋”ฐ๋กœ ๊ณ„์‚ฐํ•ด์•ผ ํ•ด!

/**
 * ๋ฐฐ์†ก์ง€ ๋ถ„๋ฆฌ ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ
 */
public function processMultipleDeliveryAddress($orderData) {
    $deliveryAddresses = $orderData['deliveryAddresses']; // ๋ฐฐ์†ก์ง€ ๋ชฉ๋ก
    $cartItems = $orderData['cartItems'];
    
    $deliveryGroups = [];
    
    // ๋ฐฐ์†ก์ง€๋ณ„๋กœ ์ƒํ’ˆ ๊ทธ๋ฃนํ•‘
    foreach ($cartItems as $item) {
        $addressKey = $item['deliveryAddressKey'];
        $deliveryKey = $item['deliverySno'] . '_' . $addressKey;
        
        if (!isset($deliveryGroups[$deliveryKey])) {
            $deliveryGroups[$deliveryKey] = [
                'address' => $deliveryAddresses[$addressKey],
                'deliverySno' => $item['deliverySno'],
                'items' => [],
                'goodsPrice' => 0
            ];
        }
        
        $deliveryGroups[$deliveryKey]['items'][] = $item;
        $deliveryGroups[$deliveryKey]['goodsPrice'] += $item['price'] * $item['goodsCnt'];
    }
    
    // ๊ฐ ๋ฐฐ์†ก์ง€๋ณ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
    $totalDeliveryFee = 0;
    $deliveryCharges = [];
    
    foreach ($deliveryGroups as $deliveryKey => $group) {
        // ๊ธฐ๋ณธ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
        $baseFee = $this->calculateBasicDeliveryFee($group);
        
        // ์ง€์—ญ๋ณ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„
        $extraFee = $this->calculateRegionalExtraFee($group['address']);
        
        $totalFee = $baseFee + $extraFee;
        
        $deliveryCharges[$deliveryKey] = [
            'baseFee' => $baseFee,
            'extraFee' => $extraFee,
            'totalFee' => $totalFee,
            'address' => $group['address']
        ];
        
        $totalDeliveryFee += $totalFee;
    }
    
    return [
        'deliveryGroups' => $deliveryGroups,
        'deliveryCharges' => $deliveryCharges,
        'totalDeliveryFee' => $totalDeliveryFee
    ];
}

/**
 * ์ง€์—ญ๋ณ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ (์ƒ์„ธ)
 */
private function calculateRegionalExtraFee($address) {
    $extraFee = 0;
    
    // ์ œ์ฃผ๋„ ์ฒดํฌ
    if (strpos($address['address1'], '์ œ์ฃผ') !== false) {
        $extraFee += 3000;
    }
    
    // ๋„์„œ์‚ฐ๊ฐ„ ์ฒดํฌ (์šฐํŽธ๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜)
    $remoteAreaZipCodes = $this->getRemoteAreaZipCodes();
    if (in_array($address['zipcode'], $remoteAreaZipCodes)) {
        $extraFee += 5000;
    }
    
    // ์‚ฐ๊ฐ„์ง€์—ญ ์ถ”๊ฐ€ ์ฒดํฌ
    $mountainKeywords = ['์‚ฐ', '๋ฉด', '๋ฆฌ'];
    foreach ($mountainKeywords as $keyword) {
        if (strpos($address['address2'], $keyword) !== false) {
            $extraFee += 2000;
            break;
        }
    }
    
    return $extraFee;
}
๐Ÿ’ก ์‹ค๋ฌด ํŒ ๋ฐฐ์†ก์ง€ ๋ถ„๋ฆฌ ์ฃผ๋ฌธ์€ ์ฃผ๋ฌธ์„œ ์ž‘์„ฑ ๋‹จ๊ณ„์—์„œ UI/UX๊ฐ€ ๋งค์šฐ ์ค‘์š”ํ•ด. ๊ณ ๊ฐ์ด ์‰ฝ๊ฒŒ ๋ฐฐ์†ก์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ฐ ์ƒํ’ˆ์„ ์–ด๋А ๋ฐฐ์†ก์ง€๋กœ ๋ณด๋‚ผ์ง€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง๊ด€์ ์ธ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ด. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๋ฐฉ์‹์„ ์ถ”์ฒœํ•ด!

๐Ÿ› ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฌธ์ œ์— ๋ถ€๋”ชํžˆ๊ฒŒ ๋ผ. ๋‚ด๊ฐ€ ์‹ค์ œ๋กœ ๊ฒช์—ˆ๋˜ ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๊ณต์œ ํ• ๊ฒŒ! ๐Ÿ˜…

โŒ ๋ฌธ์ œ 1: ๋ฐฐ์†ก๋น„๊ฐ€ ์ค‘๋ณต์œผ๋กœ ๊ณ„์‚ฐ๋˜๋Š” ๊ฒฝ์šฐ

์ฆ์ƒ ๊ฐ™์€ ๋ฐฐ์†ก ์ •์ฑ…์˜ ์ƒํ’ˆ์ธ๋ฐ๋„ ๋ฐฐ์†ก๋น„๊ฐ€ ๊ฐ๊ฐ ๋ถ€๊ณผ๋˜์–ด ์ด ๋ฐฐ์†ก๋น„๊ฐ€ ๋น„์ •์ƒ์ ์œผ๋กœ ๋†’๊ฒŒ ๋‚˜์˜ด.

์›์ธ: ๋ฐฐ์†ก ๊ทธ๋ฃน ํ‚ค(groupKey)๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ๊ณ ์œ ํ•˜์ง€ ์•Š์€ ๊ฐ’์„ ์‚ฌ์šฉํ–ˆ๊ฑฐ๋‚˜, ๊ทธ๋ฃนํ•‘ ๋กœ์ง์—์„œ ์กฐ๊ฑด ์ฒดํฌ๊ฐ€ ๋ˆ„๋ฝ๋จ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

// ์ž˜๋ชป๋œ ์˜ˆ์‹œ
$groupKey = $item['deliverySno']; // ๋ฐฐ์†ก ์ •์ฑ… ID๋งŒ ์‚ฌ์šฉ

// ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ์‹œ
$groupKey = $item['deliverySno'] . '_' . 
            $item['scmNo'] . '_' . 
            $item['warehouseCode'] . '_' .
            ($item['separateDelivery'] ? uniqid() : ''); // ๊ฐœ๋ณ„ ๋ฐฐ์†ก ์ƒํ’ˆ์€ ๋ณ„๋„ ๊ทธ๋ฃน

// ์ถ”๊ฐ€๋กœ ๋ฐฐ์†ก ๋ถˆ๊ฐ€ ์กฐํ•ฉ ์ฒดํฌ
if ($this->isNonMixableGoods($item)) {
    $groupKey .= '_' . $item['goodsNo']; // ์ƒํ’ˆ๋ณ„๋กœ ๋ณ„๋„ ๊ทธ๋ฃน
}

โŒ ๋ฌธ์ œ 2: ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด์ด ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์ง€ ์•Š์Œ

์ฆ์ƒ ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ๊ธˆ์•ก์„ ๋„˜์—ˆ๋Š”๋ฐ๋„ ๋ฐฐ์†ก๋น„๊ฐ€ ๋ถ€๊ณผ๋˜๊ฑฐ๋‚˜, ๋ฐ˜๋Œ€๋กœ ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜์ง€ ๋ชปํ–ˆ๋Š”๋ฐ ๋ฌด๋ฃŒ๋ฐฐ์†ก์ด ์ ์šฉ๋จ.

์›์ธ: ์ƒํ’ˆ ๊ธˆ์•ก ํ•ฉ์‚ฐ ์‹œ ํ• ์ธ์ด๋‚˜ ์ฟ ํฐ์„ ๊ณ ๋ คํ•˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜, ๊ทธ๋ฃน๋ณ„ ๊ธˆ์•ก ๊ณ„์‚ฐ์ด ์ž˜๋ชป๋จ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

/**
 * ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ์ฒดํฌ (ํ• ์ธ ์ ์šฉ ํ›„ ๊ธˆ์•ก ๊ธฐ์ค€)
 */
private function checkFreeDeliveryCondition($group, $discountInfo) {
    // ์ƒํ’ˆ ๊ธˆ์•ก ํ•ฉ์‚ฐ
    $totalGoodsPrice = 0;
    
    foreach ($group['items'] as $item) {
        $itemPrice = $item['price'] * $item['goodsCnt'];
        
        // ์ƒํ’ˆ๋ณ„ ํ• ์ธ ์ ์šฉ
        if (isset($discountInfo[$item['goodsNo']])) {
            $itemPrice -= $discountInfo[$item['goodsNo']]['discountAmount'];
        }
        
        $totalGoodsPrice += $itemPrice;
    }
    
    // ๊ทธ๋ฃน ์ „์ฒด ํ• ์ธ ์ ์šฉ (์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ฟ ํฐ ๋“ฑ)
    if (isset($discountInfo['groupDiscount'][$group['deliverySno']])) {
        $totalGoodsPrice -= $discountInfo['groupDiscount'][$group['deliverySno']];
    }
    
    // ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ์ฒดํฌ
    $freeDeliveryPrice = $group['deliveryFreePrice'];
    
    // ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด์ด 0์ด๋ฉด ํ•ญ์ƒ ์œ ๋ฃŒ
    if ($freeDeliveryPrice == 0) {
        return false;
    }
    
    // ์กฐ๊ฑด ๊ธˆ์•ก ์ด์ƒ์ด๋ฉด ๋ฌด๋ฃŒ๋ฐฐ์†ก
    return ($totalGoodsPrice >= $freeDeliveryPrice);
}

โŒ ๋ฌธ์ œ 3: ์ˆ˜๋Ÿ‰ ๋ณ€๊ฒฝ ์‹œ ๋ฐฐ์†ก๋น„๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Œ

์ฆ์ƒ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์—์„œ ์ƒํ’ˆ ์ˆ˜๋Ÿ‰์„ ๋ณ€๊ฒฝํ•ด๋„ ๋ฐฐ์†ก๋น„๊ฐ€ ๊ทธ๋Œ€๋กœ ์œ ์ง€๋˜์–ด ๊ณ ๊ฐ์ด ํ˜ผ๋ž€์Šค๋Ÿฌ์›Œํ•จ.

์›์ธ: ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ˆ˜๋Ÿ‰ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋ฅผ ์ œ๋Œ€๋กœ ๊ฐ์ง€ํ•˜์ง€ ๋ชปํ•˜๊ฑฐ๋‚˜, AJAX ์š”์ฒญ์ด ์‹คํŒจํ•จ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

// ๋””๋ฐ”์šด์‹ฑ์„ ์ ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
var deliveryUpdateTimer;

function scheduleDeliveryUpdate() {
    // ๊ธฐ์กด ํƒ€์ด๋จธ ์ทจ์†Œ
    clearTimeout(deliveryUpdateTimer);
    
    // 0.5์ดˆ ํ›„ ์—…๋ฐ์ดํŠธ (์—ฐ์† ์ž…๋ ฅ ๋ฐฉ์ง€)
    deliveryUpdateTimer = setTimeout(function() {
        updateDeliveryFeePreview();
    }, 500);
}

// ์ˆ˜๋Ÿ‰ ์ž…๋ ฅ ์ด๋ฒคํŠธ
$(document).on('input', '.quantity-input', function() {
    // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ
    var value = parseInt($(this).val());
    if (isNaN(value) || value < 1) {
        $(this).val(1);
    }
    
    scheduleDeliveryUpdate();
});

// +/- ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
$(document).on('click', '.btn-quantity-plus, .btn-quantity-minus', function() {
    var $input = $(this).siblings('.quantity-input');
    var currentValue = parseInt($input.val());
    var newValue;
    
    if ($(this).hasClass('btn-quantity-plus')) {
        newValue = currentValue + 1;
    } else {
        newValue = Math.max(1, currentValue - 1);
    }
    
    $input.val(newValue);
    scheduleDeliveryUpdate();
});

// AJAX ์—๋Ÿฌ ์ฒ˜๋ฆฌ
function updateDeliveryFeePreview() {
    $.ajax({
        url: '/order/ajax_calculate_delivery',
        type: 'POST',
        data: {
            cartItems: getCartItemsData()
        },
        success: function(response) {
            if (response.success) {
                updateDeliveryDisplay(response.deliveryCharge);
            } else {
                console.error('๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ์‹คํŒจ:', response.message);
                showErrorMessage('๋ฐฐ์†ก๋น„๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
            }
        },
        error: function(xhr, status, error) {
            console.error('AJAX ์š”์ฒญ ์‹คํŒจ:', error);
            showErrorMessage('์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
        }
    });
}

โŒ ๋ฌธ์ œ 4: ๋ชจ๋ฐ”์ผ์—์„œ ๋ฐฐ์†ก๋น„ ์ •๋ณด๊ฐ€ ์ œ๋Œ€๋กœ ํ‘œ์‹œ๋˜์ง€ ์•Š์Œ

์ฆ์ƒ PC์—์„œ๋Š” ์ •์ƒ์ ์œผ๋กœ ๋ณด์ด๋Š”๋ฐ ๋ชจ๋ฐ”์ผ์—์„œ๋Š” ๋ฐฐ์†ก๋น„ ์ •๋ณด๊ฐ€ ์ž˜๋ฆฌ๊ฑฐ๋‚˜ ๋ ˆ์ด์•„์›ƒ์ด ๊นจ์ง.

์›์ธ: ๋ฐ˜์‘ํ˜• CSS๊ฐ€ ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜, ํ…Œ์ด๋ธ” ํ˜•ํƒœ์˜ ๋ ˆ์ด์•„์›ƒ์„ ์‚ฌ์šฉํ•ด์„œ ๋ชจ๋ฐ”์ผ์—์„œ ๊ฐ€๋กœ ์Šคํฌ๋กค์ด ๋ฐœ์ƒํ•จ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

/* ๋ฐ˜์‘ํ˜• ๋ฐฐ์†ก๋น„ ํ‘œ์‹œ CSS */
.delivery-group-item {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 15px;
    margin-bottom: 15px;
}

.group-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
    flex-wrap: wrap; /* ๋ชจ๋ฐ”์ผ์—์„œ ์ค„๋ฐ”๊ฟˆ */
}

.delivery-fee {
    color: #e74c3c;
    font-weight: bold;
    font-size: 1.1em;
}

/* ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™” */
@media (max-width: 768px) {
    .group-header {
        flex-direction: column;
        align-items: flex-start;
    }
    
    .delivery-fee {
        margin-top: 5px;
        font-size: 1em;
    }
    
    .group-goods {
        display: flex;
        flex-direction: column;
        gap: 10px;
    }
    
    .goods-item {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 10px;
        background: white;
        border-radius: 5px;
    }
    
    .goods-item img {
        width: 60px;
        height: 60px;
        object-fit: cover;
        border-radius: 5px;
    }
    
    .free-delivery-guide {
        padding: 10px;
        background: #e8f5e9;
        border-radius: 5px;
        margin-top: 10px;
    }
    
    .progress-bar {
        width: 100%;
        height: 8px;
        background: #ddd;
        border-radius: 4px;
        overflow: hidden;
        margin-top: 8px;
    }
    
    .progress {
        height: 100%;
        background: linear-gradient(90deg, #66bb6a, #4caf50);
        transition: width 0.3s ease;
    }
}
โš ๏ธ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ฃผ์˜ ์‹ค์‹œ๊ฐ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ์€ ์„œ๋ฒ„์— ๋ถ€ํ•˜๋ฅผ ์ค„ ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ๋™์‹œ ์ ‘์†์ž๊ฐ€ ๋งŽ์€ ์‹œ๊ฐ„๋Œ€์—๋Š” ์บ์‹ฑ์„ ์ ๊ทน ํ™œ์šฉํ•ด์•ผ ํ•ด. ๋ฐฐ์†ก ์ •์ฑ… ์ •๋ณด๋Š” Redis๋‚˜ Memcached์— ์บ์‹ฑํ•˜๊ณ , ๋™์ผํ•œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ตฌ์„ฑ์— ๋Œ€ํ•ด์„œ๋Š” ์ผ์ • ์‹œ๊ฐ„(์˜ˆ: 5๋ถ„) ๋™์•ˆ ์บ์‹œ๋œ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„!

๐Ÿ” ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ ๋ฐฉ๋ฒ•

๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ๋‹ค๋ฉด ๋ฐ˜๋“œ์‹œ ์ฒ ์ €ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฑฐ์ณ์•ผ ํ•ด. ๋ฐฐ์†ก๋น„๋Š” ๋ˆ๊ณผ ์ง๊ฒฐ๋˜๋Š” ๋ถ€๋ถ„์ด๋ผ ์ž‘์€ ์˜ค๋ฅ˜๋„ ํฐ ๋ฌธ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿงช

โœ… ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

1 ๊ธฐ๋ณธ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ

โ€ข ๋‹จ์ผ ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ ๋ฐฐ์†ก๋น„ ์ •์ƒ ๊ณ„์‚ฐ
โ€ข ๋™์ผ ๋ฐฐ์†ก ์ •์ฑ… ์ƒํ’ˆ 2๊ฐœ ์ด์ƒ ์ฃผ๋ฌธ ์‹œ ๋ฌถ์Œ ๋ฐฐ์†ก๋น„ ์ ์šฉ
โ€ข ๋‹ค๋ฅธ ๋ฐฐ์†ก ์ •์ฑ… ์ƒํ’ˆ ํ˜ผํ•ฉ ์ฃผ๋ฌธ ์‹œ ๊ฐ๊ฐ ๋ฐฐ์†ก๋น„ ๋ถ€๊ณผ
โ€ข ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ๋‹ฌ์„ฑ ์‹œ ๋ฐฐ์†ก๋น„ 0์› ์ฒ˜๋ฆฌ
2 ๊ฒฝ๊ณ„๊ฐ’ ํ…Œ์ŠคํŠธ

โ€ข ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ๊ธˆ์•ก ์ •ํ™•ํžˆ ์ผ์น˜ ์‹œ
โ€ข ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ๊ธˆ์•ก๋ณด๋‹ค 1์› ๋ถ€์กฑ ์‹œ
โ€ข ์ตœ์†Œ ์ฃผ๋ฌธ ๊ธˆ์•ก ๊ฒฝ๊ณ„๊ฐ’
โ€ข ์ตœ๋Œ€ ์ˆ˜๋Ÿ‰ ์ œํ•œ ๊ฒฝ๊ณ„๊ฐ’
3 ํ• ์ธ ์ ์šฉ ํ…Œ์ŠคํŠธ

โ€ข ์ƒํ’ˆ ํ• ์ธ ์ ์šฉ ํ›„ ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ์ฒดํฌ
โ€ข ์ฟ ํฐ ์‚ฌ์šฉ ํ›„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
โ€ข ์ ๋ฆฝ๊ธˆ ์‚ฌ์šฉ ์‹œ ๋ฐฐ์†ก๋น„ ์˜ํ–ฅ ํ™•์ธ
โ€ข ํ”„๋กœ๋ชจ์…˜ ํ• ์ธ๊ณผ ๋ฐฐ์†ก๋น„ ์กฐํ•ฉ
4 ํŠน์ˆ˜ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ

โ€ข ์ œ์ฃผ/๋„์„œ์‚ฐ๊ฐ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„
โ€ข ๋ƒ‰์žฅ/๋ƒ‰๋™ ์ƒํ’ˆ ๋ณ„๋„ ๋ฐฐ์†ก
โ€ข ๋Œ€ํ˜• ์ƒํ’ˆ ํŠน๋ณ„ ๋ฐฐ์†ก๋น„
โ€ข ์˜ˆ์•ฝ ์ƒํ’ˆ๊ณผ ์ผ๋ฐ˜ ์ƒํ’ˆ ํ˜ผํ•ฉ ์ฃผ๋ฌธ

๐Ÿงช ์ž๋™ํ™” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์˜ˆ์‹œ

// PHPUnit ํ…Œ์ŠคํŠธ ์˜ˆ์‹œ
class DeliveryCalculationTest extends TestCase {
    
    private $calculator;
    
    protected function setUp(): void {
        parent::setUp();
        $this->calculator = new OrderCalculation();
    }
    
    /**
     * ๊ธฐ๋ณธ ๋ฌถ์Œ ๋ฐฐ์†ก ํ…Œ์ŠคํŠธ
     */
    public function testBasicBundleDelivery() {
        $cartData = [
            [
                'goodsNo' => 1,
                'price' => 10000,
                'goodsCnt' => 1,
                'deliverySno' => 1,
                'deliveryFee' => 3000,
                'deliveryFreePrice' => 50000,
                'scmNo' => 1
            ],
            [
                'goodsNo' => 2,
                'price' => 15000,
                'goodsCnt' => 1,
                'deliverySno' => 1,
                'deliveryFee' => 3000,
                'deliveryFreePrice' => 50000,
                'scmNo' => 1
            ]
        ];
        
        $result = $this->calculator->getDeliveryGroupCharge($cartData);
        
        // ๊ฐ™์€ ๋ฐฐ์†ก ์ •์ฑ…์ด๋ฏ€๋กœ ๋ฐฐ์†ก๋น„๋Š” ํ•œ ๋ฒˆ๋งŒ ๋ถ€๊ณผ
        $this->assertEquals(3000, array_sum(array_column($result, 'fee')));
        
        // ๋ฐฐ์†ก ๊ทธ๋ฃน์€ 1๊ฐœ๋งŒ ์ƒ์„ฑ
        $this->assertCount(1, $result);
    }
    
    /**
     * ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ๋‹ฌ์„ฑ ํ…Œ์ŠคํŠธ
     */
    public function testFreeDeliveryCondition() {
        $cartData = [
            [
                'goodsNo' => 1,
                'price' => 30000,
                'goodsCnt' => 2,
                'deliverySno' => 1,
                'deliveryFee' => 3000,
                'deliveryFreePrice' => 50000,
                'scmNo' => 1
            ]
        ];
        
        $result = $this->calculator->getDeliveryGroupCharge($cartData);
        
        // 60,000์›์ด๋ฏ€๋กœ ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ๋‹ฌ์„ฑ
        $this->assertEquals(0, array_sum(array_column($result, 'fee')));
    }
    
    /**
     * ๋‹ค์ค‘ ๋ฐฐ์†ก ๊ทธ๋ฃน ํ…Œ์ŠคํŠธ
     */
    public function testMultipleDeliveryGroups() {
        $cartData = [
            [
                'goodsNo' => 1,
                'price' => 10000,
                'goodsCnt' => 1,
                'deliverySno' => 1,
                'deliveryFee' => 3000,
                'deliveryFreePrice' => 50000,
                'scmNo' => 1
            ],
            [
                'goodsNo' => 2,
                'price' => 20000,
                'goodsCnt' => 1,
                'deliverySno' => 2,
                'deliveryFee' => 5000,
                'deliveryFreePrice' => 100000,
                'scmNo' => 1
            ]
        ];
        
        $result = $this->calculator->getDeliveryGroupCharge($cartData);
        
        // ๋‹ค๋ฅธ ๋ฐฐ์†ก ์ •์ฑ…์ด๋ฏ€๋กœ ๊ฐ๊ฐ ๋ฐฐ์†ก๋น„ ๋ถ€๊ณผ
        $this->assertEquals(8000, array_sum(array_column($result, 'fee')));
        
        // ๋ฐฐ์†ก ๊ทธ๋ฃน์€ 2๊ฐœ ์ƒ์„ฑ
        $this->assertCount(2, $result);
    }
    
    /**
     * ์ง€์—ญ๋ณ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„ ํ…Œ์ŠคํŠธ
     */
    public function testRegionalExtraFee() {
        $cartData = [
            [
                'goodsNo' => 1,
                'price' => 10000,
                'goodsCnt' => 1,
                'deliverySno' => 1,
                'deliveryFee' => 3000,
                'deliveryFreePrice' => 50000,
                'scmNo' => 1
            ]
        ];
        
        // ์ œ์ฃผ ์ง€์—ญ ์„ค์ •
        $address = [
            'address1' => '์ œ์ฃผํŠน๋ณ„์ž์น˜๋„ ์ œ์ฃผ์‹œ',
            'zipcode' => '63000'
        ];
        
        $result = $this->calculator->getDeliveryGroupCharge($cartData, $address);
        
        // ๊ธฐ๋ณธ ๋ฐฐ์†ก๋น„ 3,000์› + ์ œ์ฃผ ์ถ”๊ฐ€ 3,000์›
        $this->assertEquals(6000, array_sum(array_column($result, 'fee')));
    }
}

์ด๋ ‡๊ฒŒ ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋‘๋ฉด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ๋งˆ๋‹ค ๊ธฐ์กด ๊ธฐ๋Šฅ์ด ์ •์ƒ ๋™์ž‘ํ•˜๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ‘

๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ์€ ์ฃผ๋ฌธ ํ”„๋กœ์„ธ์Šค์—์„œ ๋งค์šฐ ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚˜๋Š” ์ž‘์—…์ด์•ผ. ํŠนํžˆ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜๋Ÿ‰์„ ๋ณ€๊ฒฝํ•  ๋•Œ๋งˆ๋‹ค ๊ณ„์‚ฐ์ด ์ด๋ฃจ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ์ •๋ง ์ค‘์š”ํ•ด! โšก

๐Ÿš€ ์ตœ์ ํ™” ๊ธฐ๋ฒ• 1: ๋ฐฐ์†ก ์ •์ฑ… ์บ์‹ฑ

๋ฐฐ์†ก ์ •์ฑ… ์ •๋ณด๋Š” ์ž์ฃผ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์œผ๋‹ˆ๊นŒ ์บ์‹œ์— ์ €์žฅํ•ด๋‘๊ณ  ์‚ฌ์šฉํ•˜๋ฉด DB ์กฐํšŒ๋ฅผ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ์–ด.

/**
 * ๋ฐฐ์†ก ์ •์ฑ… ์บ์‹ฑ (Redis ์‚ฌ์šฉ)
 */
class DeliveryPolicyCache {
    
    private $redis;
    private $cachePrefix = 'delivery_policy:';
    private $cacheTTL = 3600; // 1์‹œ๊ฐ„
    
    public function __construct() {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
    }
    
    /**
     * ๋ฐฐ์†ก ์ •์ฑ… ์กฐํšŒ (์บ์‹œ ์šฐ์„ )
     */
    public function getDeliveryPolicy($deliverySno) {
        $cacheKey = $this->cachePrefix . $deliverySno;
        
        // ์บ์‹œ์—์„œ ์กฐํšŒ
        $cached = $this->redis->get($cacheKey);
        if ($cached !== false) {
            return json_decode($cached, true);
        }
        
        // DB์—์„œ ์กฐํšŒ
        $policy = $this->fetchFromDatabase($deliverySno);
        
        // ์บ์‹œ์— ์ €์žฅ
        $this->redis->setex(
            $cacheKey,
            $this->cacheTTL,
            json_encode($policy)
        );
        
        return $policy;
    }
    
    /**
     * ๋ฐฐ์†ก ์ •์ฑ… ์ผ๊ด„ ์กฐํšŒ
     */
    public function getMultipleDeliveryPolicies($deliverySnoList) {
        $policies = [];
        $missingKeys = [];
        
        // ์บ์‹œ์—์„œ ์ผ๊ด„ ์กฐํšŒ
        $cacheKeys = array_map(function($sno) {
            return $this->cachePrefix . $sno;
        }, $deliverySnoList);
        
        $cachedData = $this->redis->mget($cacheKeys);
        
        foreach ($deliverySnoList as $index => $sno) {
            if ($cachedData[$index] !== false) {
                $policies[$sno] = json_decode($cachedData[$index], true);
            } else {
                $missingKeys[] = $sno;
            }
        }
        
        // ์บ์‹œ์— ์—†๋Š” ๊ฒƒ๋งŒ DB ์กฐํšŒ
        if (!empty($missingKeys)) {
            $dbPolicies = $this->fetchMultipleFromDatabase($missingKeys);
            
            foreach ($dbPolicies as $sno => $policy) {
                $policies[$sno] = $policy;
                
                // ์บ์‹œ์— ์ €์žฅ
                $this->redis->setex(
                    $this->cachePrefix . $sno,
                    $this->cacheTTL,
                    json_encode($policy)
                );
            }
        }
        
        return $policies;
    }
    
    /**
     * ์บ์‹œ ๋ฌดํšจํ™”
     */
    public function invalidateCache($deliverySno) {
        $cacheKey = $this->cachePrefix . $deliverySno;
        $this->redis->del($cacheKey);
    }
}

๐Ÿš€ ์ตœ์ ํ™” ๊ธฐ๋ฒ• 2: ์ฟผ๋ฆฌ ์ตœ์ ํ™”

๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ๊ฐ€์ ธ์˜ค๋„๋ก ์ตœ์ ํ™”ํ•ด์•ผ ํ•ด.

/**
 * ์ตœ์ ํ™”๋œ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐ์ดํ„ฐ ์กฐํšŒ
 */
public function getOptimizedCartData($memNo) {
    $db = \App::load('DB');
    
    // JOIN์„ ํ™œ์šฉํ•œ ๋‹จ์ผ ์ฟผ๋ฆฌ๋กœ ๋ชจ๋“  ์ •๋ณด ์กฐํšŒ
    $sql = "
        SELECT 
            c.cartSno,
            c.goodsNo,
            c.goodsCnt,
            g.goodsNm,
            g.goodsPrice as price,
            g.goodsWeight as weight,
            d.sno as deliverySno,
            d.deliveryFee,
            d.deliveryFreePrice,
            d.deliveryMethod,
            d.scmNo,
            d.warehouseCode,
            s.companyNm as scmName,
            CASE 
                WHEN g.goodsSellFl = 'n' THEN 'soldout'
                WHEN g.stockFl = 'y' AND g.totalStock < c.goodsCnt THEN 'shortage'
                ELSE 'available'
            END as stockStatus
        FROM es_cart c
        INNER JOIN es_goods g ON c.goodsNo = g.goodsNo
        INNER JOIN es_delivery d ON g.deliverySno = d.sno
        LEFT JOIN es_scm s ON d.scmNo = s.sno
        WHERE c.memNo = ?
        AND c.cartSno > 0
        ORDER BY d.sno, d.scmNo
    ";
    
    $cartData = $db->query_fetch($sql, [$memNo]);
    
    return $cartData;
}

๐Ÿš€ ์ตœ์ ํ™” ๊ธฐ๋ฒ• 3: ํ”„๋ก ํŠธ์—”๋“œ ์ตœ์ ํ™”

๋ถˆํ•„์š”ํ•œ AJAX ์š”์ฒญ์„ ์ค„์ด๊ณ , ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ.

// ๋””๋ฐ”์šด์‹ฑ๊ณผ ์“ฐ๋กœํ‹€๋ง์„ ํ™œ์šฉํ•œ ์ตœ์ ํ™”
class DeliveryFeeUpdater {
    constructor() {
        this.updateTimer = null;
        this.lastUpdateTime = 0;
        this.minUpdateInterval = 1000; // ์ตœ์†Œ 1์ดˆ ๊ฐ„๊ฒฉ
        this.debounceDelay = 500; // 0.5์ดˆ ๋””๋ฐ”์šด์Šค
    }
    
    /**
     * ๋””๋ฐ”์šด์‹ฑ ์ ์šฉ ์—…๋ฐ์ดํŠธ
     */
    scheduleUpdate() {
        clearTimeout(this.updateTimer);
        
        this.updateTimer = setTimeout(() => {
            this.performUpdate();
        }, this.debounceDelay);
    }
    
    /**
     * ์“ฐ๋กœํ‹€๋ง ์ฒดํฌ ํ›„ ์—…๋ฐ์ดํŠธ
     */
    performUpdate() {
        const now = Date.now();
        const timeSinceLastUpdate = now - this.lastUpdateTime;
        
        if (timeSinceLastUpdate < this.minUpdateInterval) {
            // ๋„ˆ๋ฌด ๋น ๋ฅธ ์š”์ฒญ์€ ์ง€์—ฐ
            setTimeout(() => {
                this.performUpdate();
            }, this.minUpdateInterval - timeSinceLastUpdate);
            return;
        }
        
        this.lastUpdateTime = now;
        this.updateDeliveryFee();
    }
    
    /**
     * ์‹ค์ œ ๋ฐฐ์†ก๋น„ ์—…๋ฐ์ดํŠธ
     */
    updateDeliveryFee() {
        const cartData = this.getCartData();
        
        // ๋กœ์ปฌ ์บ์‹œ ์ฒดํฌ
        const cacheKey = this.generateCacheKey(cartData);
        const cached = this.getFromLocalCache(cacheKey);
        
        if (cached && !this.isCacheExpired(cached)) {
            this.updateUI(cached.data);
            return;
        }
        
        // AJAX ์š”์ฒญ
        $.ajax({
            url: '/order/ajax_calculate_delivery',
            type: 'POST',
            data: { cartData: JSON.stringify(cartData) },
            success: (response) => {
                if (response.success) {
                    // ๋กœ์ปฌ ์บ์‹œ์— ์ €์žฅ
                    this.saveToLocalCache(cacheKey, response, 60); // 60์ดˆ ์บ์‹œ
                    this.updateUI(response);
                }
            }
        });
    }
    
    /**
     * ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์บ์‹œ
     */
    getFromLocalCache(key) {
        try {
            const cached = localStorage.getItem('delivery_' + key);
            return cached ? JSON.parse(cached) : null;
        } catch (e) {
            return null;
        }
    }
    
    saveToLocalCache(key, data, ttl) {
        try {
            const cacheData = {
                data: data,
                timestamp: Date.now(),
                ttl: ttl * 1000
            };
            localStorage.setItem('delivery_' + key, JSON.stringify(cacheData));
        } catch (e) {
            // ์Šคํ† ๋ฆฌ์ง€ ์šฉ๋Ÿ‰ ์ดˆ๊ณผ ์‹œ ์˜ค๋ž˜๋œ ์บ์‹œ ์‚ญ์ œ
            this.clearOldCache();
        }
    }
    
    isCacheExpired(cached) {
        return (Date.now() - cached.timestamp) > cached.ttl;
    }
    
    /**
     * ์บ์‹œ ํ‚ค ์ƒ์„ฑ
     */
    generateCacheKey(cartData) {
        const keyData = cartData.map(item => 
            `${item.goodsNo}_${item.goodsCnt}`
        ).join('|');
        
        // ๊ฐ„๋‹จํ•œ ํ•ด์‹œ ์ƒ์„ฑ
        let hash = 0;
        for (let i = 0; i < keyData.length; i++) {
            hash = ((hash << 5) - hash) + keyData.charCodeAt(i);
            hash = hash & hash;
        }
        return Math.abs(hash).toString(36);
    }
}
๐Ÿ’ก ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ ์„ฑ๋Šฅ์„ ์ง€์†์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„, ์บ์‹œ ํžˆํŠธ์œจ, ์—๋Ÿฌ ๋ฐœ์ƒ๋ฅ  ๋“ฑ์„ ์ถ”์ ํ•˜๊ณ , ์ž„๊ณ„๊ฐ’์„ ์ดˆ๊ณผํ•˜๋ฉด ์•Œ๋ฆผ์„ ๋ฐ›๋„๋ก ์„ค์ •ํ•ด๋‘๋ฉด ์ข‹์•„. New Relic์ด๋‚˜ DataDog ๊ฐ™์€ APM ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํŽธ๋ฆฌํ•ด!

๐ŸŽ“ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

์ด๋ก ๊ณผ ์ฝ”๋“œ๋งŒ ๋ด์„œ๋Š” ๊ฐ์ด ์•ˆ ์˜ฌ ์ˆ˜ ์žˆ์œผ๋‹ˆ, ์‹ค์ œ ์‡ผํ•‘๋ชฐ์—์„œ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ์‚ฌ๋ก€๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ! ๐Ÿช

๐Ÿ“ฑ ์‚ฌ๋ก€ 1: ํŒจ์…˜ ์‡ผํ•‘๋ชฐ - ๋ธŒ๋žœ๋“œ๋ณ„ ๋ฌถ์Œ ๋ฐฐ์†ก

์—ฌ๋Ÿฌ ๋ธŒ๋žœ๋“œ๋ฅผ ์ž…์ ์‹œํ‚จ ํŒจ์…˜ ์‡ผํ•‘๋ชฐ์—์„œ๋Š” ๋ธŒ๋žœ๋“œ(๊ณต๊ธ‰์‚ฌ)๋ณ„๋กœ ๋ฐฐ์†ก๋น„๋ฅผ ๋‹ค๋ฅด๊ฒŒ ์ฑ…์ •ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„. ์˜ˆ๋ฅผ ๋“ค์–ด A๋ธŒ๋žœ๋“œ๋Š” 3๋งŒ์› ์ด์ƒ ๋ฌด๋ฃŒ๋ฐฐ์†ก, B๋ธŒ๋žœ๋“œ๋Š” 5๋งŒ์› ์ด์ƒ ๋ฌด๋ฃŒ๋ฐฐ์†ก ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ง์ด์•ผ.

/**
 * ๋ธŒ๋žœ๋“œ๋ณ„ ๋ฐฐ์†ก๋น„ ์ •์ฑ… ์ ์šฉ
 */
public function calculateBrandBasedDelivery($cartData) {
    $brandGroups = [];
    
    // ๋ธŒ๋žœ๋“œ(SCM)๋ณ„๋กœ ๊ทธ๋ฃนํ•‘
    foreach ($cartData as $item) {
        $brandKey = $item['scmNo'];
        
        if (!isset($brandGroups[$brandKey])) {
            $brandGroups[$brandKey] = [
                'brandName' => $item['scmName'],
                'items' => [],
                'totalPrice' => 0,
                'deliveryPolicy' => $this->getBrandDeliveryPolicy($item['scmNo'])
            ];
        }
        
        $brandGroups[$brandKey]['items'][] = $item;
        $brandGroups[$brandKey]['totalPrice'] += $item['price'] * $item['goodsCnt'];
    }
    
    // ๊ฐ ๋ธŒ๋žœ๋“œ๋ณ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
    $deliveryCharges = [];
    
    foreach ($brandGroups as $brandKey => $group) {
        $policy = $group['deliveryPolicy'];
        $fee = 0;
        
        // ๋ธŒ๋žœ๋“œ๋ณ„ ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด ์ฒดํฌ
        if ($group['totalPrice'] < $policy['freeDeliveryPrice']) {
            $fee = $policy['deliveryFee'];
        }
        
        // ๋ธŒ๋žœ๋“œ๋ณ„ ํŠน๋ณ„ ํ”„๋กœ๋ชจ์…˜ ์ฒดํฌ
        if ($this->hasSpecialPromotion($brandKey)) {
            $fee = 0; // ํ”„๋กœ๋ชจ์…˜ ๊ธฐ๊ฐ„ ๋ฌด๋ฃŒ๋ฐฐ์†ก
        }
        
        $deliveryCharges[$brandKey] = [
            'brandName' => $group['brandName'],
            'fee' => $fee,
            'freeCondition' => $policy['freeDeliveryPrice'],
            'remainAmount' => max(0, $policy['freeDeliveryPrice'] - $group['totalPrice'])
        ];
    }
    
    return $deliveryCharges;
}

๐ŸŽ ์‚ฌ๋ก€ 2: ์‹ํ’ˆ ์‡ผํ•‘๋ชฐ - ์˜จ๋„๋Œ€๋ณ„ ๋ฐฐ์†ก ๋ถ„๋ฆฌ

์‹ ์„ ์‹ํ’ˆ์„ ํŒ๋งคํ•˜๋Š” ์‡ผํ•‘๋ชฐ์—์„œ๋Š” ์ƒ์˜จ/๋ƒ‰์žฅ/๋ƒ‰๋™ ์ƒํ’ˆ์„ ๊ฐ๊ฐ ๋‹ค๋ฅธ ๋ฐฐ์†ก์œผ๋กœ ๋ณด๋‚ด์•ผ ํ•ด. ์ด๋•Œ ์˜จ๋„๋Œ€๋ณ„๋กœ ๋ฐฐ์†ก๋น„๋ฅผ ๊ณ„์‚ฐํ•ด์•ผ ํ•˜์ง€.

/**
 * ์˜จ๋„๋Œ€๋ณ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
 */
public function calculateTemperatureBasedDelivery($cartData) {
    $tempGroups = [
        'normal' => ['name' => '์ƒ์˜จ', 'items' => [], 'fee' => 3000],
        'cold' => ['name' => '๋ƒ‰์žฅ', 'items' => [], 'fee' => 5000],
        'frozen' => ['name' => '๋ƒ‰๋™', 'items' => [], 'fee' => 5000]
    ];
    
    // ์˜จ๋„๋Œ€๋ณ„ ๋ถ„๋ฅ˜
    foreach ($cartData as $item) {
        $tempType = $this->getGoodsTemperatureType($item['goodsNo']);
        
        if (isset($tempGroups[$tempType])) {
            $tempGroups[$tempType]['items'][] = $item;
        }
    }
    
    // ๊ฐ ์˜จ๋„๋Œ€๋ณ„ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
    $deliveryCharges = [];
    
    foreach ($tempGroups as $tempType => $group) {
        if (empty($group['items'])) {
            continue; // ํ•ด๋‹น ์˜จ๋„๋Œ€ ์ƒํ’ˆ์ด ์—†์œผ๋ฉด ์Šคํ‚ต
        }
        
        $totalPrice = 0;
        foreach ($group['items'] as $item) {
            $totalPrice += $item['price'] * $item['goodsCnt'];
        }
        
        $fee = $group['fee'];
        
        // ์˜จ๋„๋Œ€๋ณ„ ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด (์˜ˆ: 5๋งŒ์› ์ด์ƒ)
        if ($totalPrice >= 50000) {
            $fee = 0;
        }
        
        $deliveryCharges[$tempType] = [
            'name' => $group['name'],
            'fee' => $fee,
            'itemCount' => count($group['items']),
            'totalPrice' => $totalPrice
        ];
    }
    
    return $deliveryCharges;
}

/**
 * ์ƒํ’ˆ์˜ ์˜จ๋„๋Œ€ ์กฐํšŒ
 */
private function getGoodsTemperatureType($goodsNo) {
    // ์ƒํ’ˆ ์ •๋ณด์—์„œ ์˜จ๋„๋Œ€ ์ •๋ณด ์กฐํšŒ
    $db = \App::load('DB');
    
    $sql = "SELECT temperatureType FROM es_goods WHERE goodsNo = ?";
    $result = $db->query_fetch($sql, [$goodsNo], false);
    
    return $result['temperatureType'] ?? 'normal';
}

๐Ÿช‘ ์‚ฌ๋ก€ 3: ๊ฐ€๊ตฌ ์‡ผํ•‘๋ชฐ - ๋ถ€ํ”ผ/๋ฌด๊ฒŒ ๊ธฐ๋ฐ˜ ๋ฐฐ์†ก๋น„

๋Œ€ํ˜• ๊ฐ€๊ตฌ๋ฅผ ํŒ๋งคํ•˜๋Š” ์‡ผํ•‘๋ชฐ์—์„œ๋Š” ์ƒํ’ˆ์˜ ๋ถ€ํ”ผ์™€ ๋ฌด๊ฒŒ์— ๋”ฐ๋ผ ๋ฐฐ์†ก๋น„๋ฅผ ์ฐจ๋“ฑ ์ ์šฉํ•ด์•ผ ํ•ด.

/**
 * ๋ถ€ํ”ผ/๋ฌด๊ฒŒ ๊ธฐ๋ฐ˜ ๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ
 */
public function calculateVolumeWeightBasedDelivery($cartData) {
    $deliveryGroups = [];
    
    foreach ($cartData as $item) {
        // ์ƒํ’ˆ์˜ ๋ถ€ํ”ผ ๋ฌด๊ฒŒ ๊ณ„์‚ฐ
        $volumeWeight = $this->calculateVolumeWeight($item);
        $actualWeight = $item['weight'] * $item['goodsCnt'];
        
        // ๋ถ€ํ”ผ ๋ฌด๊ฒŒ์™€ ์‹ค์ œ ๋ฌด๊ฒŒ ์ค‘ ํฐ ๊ฐ’ ์ ์šฉ
        $chargeableWeight = max($volumeWeight, $actualWeight);
        
        // ๋ฌด๊ฒŒ ๊ตฌ๊ฐ„๋ณ„ ๋ฐฐ์†ก๋น„ ๊ฒฐ์ •
        $deliveryFee = $this->getDeliveryFeeByWeight($chargeableWeight);
        
        // ์„ค์น˜ ์„œ๋น„์Šค ํ•„์š” ์—ฌ๋ถ€ ์ฒดํฌ
        if ($item['needInstallation']) {
            $deliveryFee += $item['installationFee'];
        }
        
        $deliveryGroups[] = [
            'goodsNo' => $item['goodsNo'],
            'goodsNm' => $item['goodsNm'],
            'chargeableWeight' => $chargeableWeight,
            'deliveryFee' => $deliveryFee,
            'installationFee' => $item['needInstallation'] ? $item['installationFee'] : 0
        ];
    }
    
    return $deliveryGroups;
}

/**
 * ๋ถ€ํ”ผ ๋ฌด๊ฒŒ ๊ณ„์‚ฐ (๊ฐ€๋กœ x ์„ธ๋กœ x ๋†’์ด / 5000)
 */
private function calculateVolumeWeight($item) {
    $volume = $item['width'] * $item['length'] * $item['height'];
    return $volume / 5000; // ํƒ๋ฐฐ์‚ฌ ๊ธฐ์ค€
}

/**
 * ๋ฌด๊ฒŒ ๊ตฌ๊ฐ„๋ณ„ ๋ฐฐ์†ก๋น„
 */
private function getDeliveryFeeByWeight($weight) {
    $weightRanges = [
        ['max' => 5, 'fee' => 5000],
        ['max' => 10, 'fee' => 8000],
        ['max' => 20, 'fee' => 12000],
        ['max' => 30, 'fee' => 18000],
        ['max' => PHP_INT_MAX, 'fee' => 25000]
    ];
    
    foreach ($weightRanges as $range) {
        if ($weight <= $range['max']) {
            return $range['fee'];
        }
    }
    
    return 25000; // ๊ธฐ๋ณธ๊ฐ’
}

์ด๋Ÿฐ ์‹์œผ๋กœ ์—…์ข…๊ณผ ์ƒํ’ˆ ํŠน์„ฑ์— ๋งž๊ฒŒ ๋ฐฐ์†ก๋น„ ๋กœ์ง์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ์–ด! ์‹ค์ œ๋กœ ์žฌ๋Šฅ๋„ท์—์„œ๋„ ์ด๋Ÿฐ ๋งž์ถคํ˜• ๋ฐฐ์†ก ๋กœ์ง ๊ฐœ๋ฐœ ์˜๋ขฐ๊ฐ€ ์ •๋ง ๋งŽ์ด ๋“ค์–ด์™€. ๊ฐ ์‡ผํ•‘๋ชฐ๋งˆ๋‹ค ํŠน์ˆ˜ํ•œ ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ๊ธฐ ๋งˆ๋ จ์ด๊ฑฐ๋“ ! ๐Ÿ˜Š

๐Ÿ” ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

๋ฐฐ์†ก๋น„ ๊ณ„์‚ฐ์€ ๊ธˆ์•ก๊ณผ ๊ด€๋ จ๋œ ๋ฏผ๊ฐํ•œ ๋ถ€๋ถ„์ด๋ผ ๋ณด์•ˆ์—๋„ ์‹ ๊ฒฝ ์จ์•ผ ํ•ด. ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐฐ์†ก๋น„๋ฅผ ์กฐ์ž‘ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ›ก๏ธ

โš ๏ธ ์ฃผ์š” ๋ณด์•ˆ ์œ„ํ˜‘

1. ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์กฐ์ž‘ ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋กœ JavaScript ๋ณ€์ˆ˜๋‚˜ HTML ๊ฐ’์„ ์กฐ์ž‘ํ•ด์„œ ๋ฐฐ์†ก๋น„๋ฅผ 0์›์œผ๋กœ ๋งŒ๋“ค๋ ค๋Š” ์‹œ๋„
2. API ์ง์ ‘ ํ˜ธ์ถœ ์ •์ƒ์ ์ธ ์ฃผ๋ฌธ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ๊ฒฐ์ œ API๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด์„œ ๋ฐฐ์†ก๋น„๋ฅผ ์ž„์˜๋กœ ์„ค์ •
3. ์„ธ์…˜ ํ•˜์ด์žฌํ‚น ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ์„ธ์…˜์„ ํƒˆ์ทจํ•ด์„œ ํ• ์ธ ํ˜œํƒ์ด๋‚˜ ๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด์„ ์•…์šฉ

๐Ÿ”’ ๋ณด์•ˆ ๋Œ€์ฑ…

/**
 * ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ฐฐ์†ก๋น„ ๊ฒ€์ฆ
 */
public function validateDeliveryFee($orderData) {
    // 1. ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „์†ก๋œ ๋ฐฐ์†ก๋น„
    $clientDeliveryFee = $orderData['deliveryFee'];
    
    // 2. ์„œ๋ฒ„์—์„œ ์žฌ๊ณ„์‚ฐํ•œ ๋ฐฐ์†ก๋น„
    $serverDeliveryFee = $this->recalculateDeliveryFee($orderData);
    
    // 3. ๋‘ ๊ฐ’์ด ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆ
    if ($clientDeliveryFee != $serverDeliveryFee) {
        // ๋ถˆ์ผ์น˜ ์‹œ ๋กœ๊ทธ ๊ธฐ๋ก
        $this->logSecurityAlert([
            'type' => 'delivery_fee_mismatch',
            'client_fee' => $clientDeliveryFee,
            'server_fee' => $serverDeliveryFee,
            'order_data' => $orderData,
            'user_ip' => $_SERVER['REMOTE_ADDR'],
            'timestamp' => date('Y-m-d H:i:s')
        ]);
        
        // ์„œ๋ฒ„ ๊ณ„์‚ฐ ๊ฐ’์œผ๋กœ ๊ฐ•์ œ ์ ์šฉ
        $orderData['deliveryFee'] = $serverDeliveryFee;
        
        // ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ
        throw new Exception('๋ฐฐ์†ก๋น„๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ํ™•์ธํ•ด์ฃผ์„ธ์š”.');
    }
    
    return true;
}

/**
 * ์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ
 */
public function verifyOrderIntegrity($orderData) {
    // ํ•ด์‹œ ๊ธฐ๋ฐ˜ ๋ฌด๊ฒฐ์„ฑ ์ฒดํฌ
    $expectedHash = $this->generateOrderHash($orderData);
    $receivedHash = $orderData['integrity_hash'] ?? '';
    
    if ($expectedHash !== $receivedHash) {
        $this->logSecurityAlert([
            'type' => 'order_data_tampering',
            'expected_hash' => $expectedHash,
            'received_hash' => $receivedHash,
            'user_ip' => $_SERVER['REMOTE_ADDR']
        ]);
        
        throw new Exception('์ฃผ๋ฌธ ์ •๋ณด๊ฐ€ ๋ณ€์กฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
    }
    
    return true;
}

/**
 * ์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ ํ•ด์‹œ ์ƒ์„ฑ
 */
private function generateOrderHash($orderData) {
    // ์ค‘์š” ํ•„๋“œ๋งŒ ์ถ”์ถœ
    $hashData = [
        'goods' => $orderData['goods'],
        'delivery_fee' => $orderData['deliveryFee'],
        'total_price' => $orderData['totalPrice'],
        'timestamp' => $orderData['timestamp']
    ];
    
    // ์ •๋ ฌ ํ›„ JSON ์ธ์ฝ”๋”ฉ
    ksort($hashData);
    $jsonData = json_encode($hashData);
    
    // HMAC ํ•ด์‹œ ์ƒ์„ฑ (๋น„๋ฐ€ํ‚ค ์‚ฌ์šฉ)
    $secretKey = \Config::get('security.order_hash_key');
    return hash_hmac('sha256', $jsonData, $secretKey);
}

/**
 * Rate Limiting (API ํ˜ธ์ถœ ์ œํ•œ)
 */
public function checkRateLimit($userId) {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    
    $key = 'rate_limit:delivery_calc:' . $userId;
    $limit = 30; // 1๋ถ„๋‹น 30ํšŒ
    $window = 60; // 60์ดˆ
    
    $current = $redis->incr($key);
    
    if ($current === 1) {
        $redis->expire($key, $window);
    }
    
    if ($current > $limit) {
        throw new Exception('์š”์ฒญ์ด ๋„ˆ๋ฌด ๋งŽ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.');
    }
    
    return true;
}
๐Ÿ’ก ๋ณด์•ˆ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค โ€ข ๋ชจ๋“  ๊ธˆ์•ก ๊ณ„์‚ฐ์€ ์„œ๋ฒ„์—์„œ ์ˆ˜ํ–‰ํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ ๊ฐ’์€ ์ฐธ๊ณ ์šฉ์œผ๋กœ๋งŒ ์‚ฌ์šฉ
โ€ข HTTPS ํ•„์ˆ˜ ์‚ฌ์šฉ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ „์†ก ์•”ํ˜ธํ™”
โ€ข CSRF ํ† ํฐ์œผ๋กœ ์œ„์กฐ ์š”์ฒญ ๋ฐฉ์ง€
โ€ข ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ ์„ค์ •์œผ๋กœ ํ•˜์ด์žฌํ‚น ์œ„ํ—˜ ๊ฐ์†Œ
โ€ข ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™์€ ์ž๋™์œผ๋กœ ๋กœ๊ทธ ๊ธฐ๋ก ๋ฐ ์•Œ๋ฆผ

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์—ฌ๊ธฐ๊นŒ์ง€ ๊ณ ๋„๋ชฐ์—์„œ ์ƒํ’ˆ ๋ฌถ์Œ ๋ฐฐ์†ก ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ดค์–ด! ์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋‹จ๊ณ„๋ณ„๋กœ ์ฐจ๊ทผ์ฐจ๊ทผ ๊ตฌํ˜„ํ•˜๋‹ค ๋ณด๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ต์ง€ ์•Š์•„. ๐ŸŽ‰

ํ•ต์‹ฌ์„ ์ •๋ฆฌํ•˜์ž๋ฉด:

๐ŸŽฏ ํ•ต์‹ฌ ํฌ์ธํŠธ ์š”์•ฝ 1. ๊ทธ๋ฃนํ•‘์ด ๊ฐ€์žฅ ์ค‘์š”ํ•ด
๋ฐฐ์†ก ์ •์ฑ…, ์ถœ๋ฐœ์ง€, ์ƒํ’ˆ ํŠน์„ฑ์— ๋”ฐ๋ผ ์ •ํ™•ํ•˜๊ฒŒ ๊ทธ๋ฃน์„ ๋‚˜๋ˆ„๋Š” ๊ฒŒ ๊ธฐ๋ณธ์ด์•ผ.

2. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๊ผผ๊ผผํ•˜๊ฒŒ
๋ฌด๋ฃŒ๋ฐฐ์†ก ์กฐ๊ฑด, ์ง€์—ญ๋ณ„ ์ถ”๊ฐ€ ๋ฐฐ์†ก๋น„, ํŠน์ˆ˜ ์ƒํ’ˆ ๋“ฑ ๋‹ค์–‘ํ•œ ์ผ€์ด์Šค๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ด.

3. ์„ฑ๋Šฅ ์ตœ์ ํ™”๋Š” ํ•„์ˆ˜
์บ์‹ฑ, ์ฟผ๋ฆฌ ์ตœ์ ํ™”, ํ”„๋ก ํŠธ์—”๋“œ ์ตœ์ ํ™”๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•ด์•ผ ํ•ด.

4. ๋ณด์•ˆ์„ ์ ˆ๋Œ€ ๊ฐ„๊ณผํ•˜์ง€ ๋งˆ
์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๊ฒ€์ฆ, ๋ฌด๊ฒฐ์„ฑ ์ฒดํฌ, Rate Limiting์œผ๋กœ ์•…์˜์ ์ธ ์‹œ๋„๋ฅผ ์ฐจ๋‹จํ•ด์•ผ ํ•ด.

5. ํ…Œ์ŠคํŠธ๋Š” ์ฒ ์ €ํ•˜๊ฒŒ
๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ , ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋‘๋Š” ๊ฒŒ ์ข‹์•„.

๋ฐฐ์†ก๋น„ ๋กœ์ง์€ ํ•œ ๋ฒˆ ์ œ๋Œ€๋กœ ๊ตฌํ˜„ํ•ด๋‘๋ฉด ์‡ผํ•‘๋ชฐ ์šด์˜์ด ์ •๋ง ํŽธํ•ด์ ธ. ๊ณ ๊ฐ๋“ค๋„ ๋ช…ํ™•ํ•œ ๋ฐฐ์†ก๋น„ ์•ˆ๋‚ด๋ฅผ ๋ฐ›์œผ๋ฉด ์‹ ๋ขฐ๋„๊ฐ€ ๋†’์•„์ง€๊ณ , ๊ฒฐ๊ณผ์ ์œผ๋กœ ๊ตฌ๋งค ์ „ํ™˜์œจ๋„ ์˜ฌ๋ผ๊ฐ€๊ฑฐ๋“ ! ๐Ÿ’ช

ํ˜น์‹œ ๊ตฌํ˜„ํ•˜๋‹ค๊ฐ€ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๊ฑฐ๋‚˜, ํŠน์ˆ˜ํ•œ ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ๋‹ค๋ฉด ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‡ผํ•‘๋ชฐ ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ถ”์ฒœํ•ด!

์ž, ์ด์ œ ๋‹น์‹ ์˜ ์‡ผํ•‘๋ชฐ์— ์™„๋ฒฝํ•œ ๋ฌถ์Œ ๋ฐฐ์†ก ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด๋ณด์ž! ํ™”์ดํŒ…! ๐Ÿš€โœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.