์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ’ณ PHP ๊ฒฐ์ œ ์‹œ์Šคํ…œ๊ณผ Stripe PayPal API ํ†ตํ•ฉ ๊ตฌํ˜„

๐Ÿ’ณ PHP ๊ฒฐ์ œ ์‹œ์Šคํ…œ๊ณผ Stripe PayPal API ํ†ตํ•ฉ ๊ตฌํ˜„

์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์™„์ „ ์ •๋ณต!

PHP Payment Stripe API PayPal API ํ†ตํ•ฉ ๊ฒฐ์ œ ์•ˆ์ „ํ•˜๊ณ  ๋น ๋ฅธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ

๐ŸŽฏ ์™œ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ํ†ตํ•ฉ์ด ์ค‘์š”ํ• ๊นŒ?

์•ผ, ์นœ๊ตฌ! ์˜จ๋ผ์ธ ์‡ผํ•‘๋ชฐ์ด๋‚˜ ์„œ๋น„์Šค ํ”Œ๋žซํผ์„ ๋งŒ๋“ค๊ณ  ์žˆ๋‹ค๋ฉด ๊ฒฐ์ œ ์‹œ์Šคํ…œ์€ ์ •๋ง ํ•ต์‹ฌ ์ค‘์˜ ํ•ต์‹ฌ์ด์•ผ. ๐Ÿ›’ ์•„๋ฌด๋ฆฌ ๋ฉ‹์ง„ ์ƒํ’ˆ์„ ํŒ”์•„๋„ ๊ฒฐ์ œ๊ฐ€ ์•ˆ ๋˜๋ฉด ๋ง์งฑ ๋„๋ฃจ๋ฌต์ด์ž–์•„? ๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ PHP๋กœ Stripe์™€ PayPal ๊ฐ™์€ ๊ธ€๋กœ๋ฒŒ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ์–ด๋–ป๊ฒŒ ํ†ตํ•ฉํ•˜๋Š”์ง€ ์™„์ „ ์ •๋ณตํ•ด๋ณผ ๊ฑฐ์•ผ!

์š”์ฆ˜ ์‚ฌ์šฉ์ž๋“ค์€ ๋‹ค์–‘ํ•œ ๊ฒฐ์ œ ์ˆ˜๋‹จ์„ ์›ํ•ด. ์–ด๋–ค ์‚ฌ๋žŒ์€ ์‹ ์šฉ์นด๋“œ๋ฅผ ์„ ํ˜ธํ•˜๊ณ , ์–ด๋–ค ์‚ฌ๋žŒ์€ PayPal ๊ณ„์ •์œผ๋กœ ๊ฐ„ํŽธํ•˜๊ฒŒ ๊ฒฐ์ œํ•˜๊ณ  ์‹ถ์–ด ํ•˜์ง€. ๋‹ค์–‘ํ•œ ๊ฒฐ์ œ ์˜ต์…˜์„ ์ œ๊ณตํ•˜๋ฉด ์ „ํ™˜์œจ์ด ์ตœ๋Œ€ 30%๊นŒ์ง€ ์˜ฌ๋ผ๊ฐ„๋‹ค๋Š” ์—ฐ๊ตฌ ๊ฒฐ๊ณผ๋„ ์žˆ์–ด! ๐Ÿ’ฐ

๐ŸŽจ ์‹ค์ œ ์‚ฌ๋ก€: ์žฌ๋Šฅ๋„ท(https://www.jaenung.net) ๊ฐ™์€ ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์—์„œ๋„ ๋‹ค์–‘ํ•œ ๊ฒฐ์ œ ์ˆ˜๋‹จ์„ ์ œ๊ณตํ•ด์„œ ์‚ฌ์šฉ์ž๋“ค์ด ํŽธํ•˜๊ฒŒ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ณ  ์žˆ์–ด. ๊ฒฐ์ œ ์‹œ์Šคํ…œ์ด ์–ผ๋งˆ๋‚˜ ์ค‘์š”ํ•œ์ง€ ์•Œ ์ˆ˜ ์žˆ์ง€?

๐Ÿ” Stripe vs PayPal: ๋ญ๊ฐ€ ๋‹ค๋ฅผ๊นŒ?

๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋”ฉ์— ๋“ค์–ด๊ฐ€๊ธฐ ์ „์—, ๋‘ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์˜ ํŠน์ง•์„ ๋จผ์ € ์•Œ์•„๋ณด์ž! ๊ฐ๊ฐ ์žฅ๋‹จ์ ์ด ์žˆ๊ฑฐ๋“ . ๐Ÿ˜Š

ํŠน์ง• Stripe ๐Ÿ’œ PayPal ๐Ÿ’›
๊ฐœ๋ฐœ์ž ์นœํ™”์„ฑ ๋งค์šฐ ์šฐ์ˆ˜ (RESTful API, ํ’๋ถ€ํ•œ ๋ฌธ์„œ) ์ข‹์Œ (๋‹ค์†Œ ๋ณต์žกํ•œ ๊ตฌ์กฐ)
์ˆ˜์ˆ˜๋ฃŒ 2.9% + $0.30 (๋ฏธ๊ตญ ๊ธฐ์ค€) 2.9% + $0.30 (์œ ์‚ฌ)
์‚ฌ์šฉ์ž ์ธ์ง€๋„ ๊ฐœ๋ฐœ์ž/์Šคํƒ€ํŠธ์—…์—์„œ ์ธ๊ธฐ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋งค์šฐ ๋†’์Œ
์ •์‚ฐ ์†๋„ 2-7์ผ ์ฆ‰์‹œ ๊ฐ€๋Šฅ (PayPal ์ž”์•ก)
๊ตญ์ œ ์ง€์› 46๊ฐœ๊ตญ ์ด์ƒ 200๊ฐœ๊ตญ ์ด์ƒ
์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๋งค์šฐ ์ž์œ ๋กœ์›€ ์ œํ•œ์ 
๐Ÿ’ก ํ”„๋กœ ํŒ: ๋‘ ๊ฐœ ๋‹ค ํ†ตํ•ฉํ•˜๋Š” ๊ฒŒ ์ตœ์„ ์ด์•ผ! ์‚ฌ์šฉ์ž์—๊ฒŒ ์„ ํƒ๊ถŒ์„ ์ฃผ๋ฉด ์ „ํ™˜์œจ์ด ํ™•์‹คํžˆ ์˜ฌ๋ผ๊ฐ€๊ฑฐ๋“ . ์ฝ”๋“œ๋„ ์ƒ๊ฐ๋ณด๋‹ค ๋ณต์žกํ•˜์ง€ ์•Š์•„!

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ์„ธํŒ…ํ•ด์•ผ ํ•ด. ๊ฑฑ์ • ๋งˆ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์•Œ๋ ค์ค„๊ฒŒ! ๐Ÿš€

1 PHP ํ™˜๊ฒฝ ํ™•์ธ

PHP 7.4 ์ด์ƒ์ด ํ•„์š”ํ•ด. ํ„ฐ๋ฏธ๋„์—์„œ ํ™•์ธํ•ด๋ณด์ž:

php -v

๋งŒ์•ฝ ๋ฒ„์ „์ด ๋‚ฎ๋‹ค๋ฉด ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•ด!

2 Composer ์„ค์น˜

PHP์˜ ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž์ธ Composer๊ฐ€ ํ•„์ˆ˜์•ผ. ์ด๋ฏธ ์„ค์น˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ:

composer --version

์—†๋‹ค๋ฉด getcomposer.org์—์„œ ๋‹ค์šด๋กœ๋“œํ•˜์ž!

3 ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ

๊น”๋”ํ•˜๊ฒŒ ํ”„๋กœ์ ํŠธ ํด๋”๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

mkdir payment-integration
cd payment-integration
composer init
4 ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

Stripe์™€ PayPal SDK๋ฅผ ํ•œ ๋ฒˆ์— ์„ค์น˜!

composer require stripe/stripe-php
composer require paypal/rest-api-sdk-php
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: SSL ์ธ์ฆ์„œ๊ฐ€ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•ด! ๊ฒฐ์ œ ์ •๋ณด๋Š” ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋‹ˆ๊นŒ HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ์šด์˜ํ•ด์•ผ ํ•ด. ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ๋Š” localhost๋„ ๊ดœ์ฐฎ์ง€๋งŒ, ์‹ค์ œ ๋ฐฐํฌ ์‹œ์—๋Š” ๊ผญ SSL์„ ์ ์šฉํ•˜์ž!

๐Ÿ”‘ API ํ‚ค ๋ฐœ๊ธ‰๋ฐ›๊ธฐ

๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๊ฐ ์„œ๋น„์Šค์—์„œ API ํ‚ค๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์•„์•ผ ํ•ด. ์ด๊ฑด ๋งˆ์น˜ ์ง‘ ์—ด์‡  ๊ฐ™์€ ๊ฑฐ์•ผ! ๐Ÿ”

Stripe API ํ‚ค ๋ฐœ๊ธ‰

1 Stripe ๊ณ„์ • ์ƒ์„ฑ

stripe.com์— ์ ‘์†ํ•ด์„œ ๋ฌด๋ฃŒ ๊ณ„์ •์„ ๋งŒ๋“ค์–ด. ์ด๋ฉ”์ผ ์ธ์ฆ๋งŒ ํ•˜๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด!

2 ๋Œ€์‹œ๋ณด๋“œ ์ ‘์†

๋กœ๊ทธ์ธ ํ›„ ์™ผ์ชฝ ๋ฉ”๋‰ด์—์„œ "Developers" โ†’ "API keys"๋ฅผ ํด๋ฆญํ•ด.

3 ํ‚ค ๋ณต์‚ฌ

๋‘ ๊ฐ€์ง€ ํ‚ค๊ฐ€ ๋ณด์ผ ๊ฑฐ์•ผ:

  • Publishable key: ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์‚ฌ์šฉ (๊ณต๊ฐœ ๊ฐ€๋Šฅ)
  • Secret key: ์„œ๋ฒ„ ์ธก์—์„œ ์‚ฌ์šฉ (์ ˆ๋Œ€ ๊ณต๊ฐœ ๊ธˆ์ง€!)
๐ŸŽฏ ํ…Œ์ŠคํŠธ ๋ชจ๋“œ: Stripe๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ ๋ชจ๋“œ๋กœ ์‹œ์ž‘ํ•ด. ์‹ค์ œ ๋ˆ์ด ๊ฑฐ๋ž˜๋˜์ง€ ์•Š์œผ๋‹ˆ๊นŒ ๋งˆ์Œ๊ป ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด! ํ…Œ์ŠคํŠธ ์นด๋“œ ๋ฒˆํ˜ธ๋Š” 4242 4242 4242 4242๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ.

PayPal API ํ‚ค ๋ฐœ๊ธ‰

1 PayPal Developer ๊ณ„์ •

developer.paypal.com์— ์ ‘์†ํ•ด์„œ ๋กœ๊ทธ์ธํ•ด. ์ผ๋ฐ˜ PayPal ๊ณ„์ •์œผ๋กœ๋„ ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅํ•ด!

2 ์•ฑ ์ƒ์„ฑ

"My Apps & Credentials"๋กœ ๊ฐ€์„œ "Create App" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด.

3 Sandbox ๊ณ„์ • ์„ค์ •

PayPal์€ Sandbox(ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ)๋ฅผ ์ œ๊ณตํ•ด. ๊ฐ€์ƒ์˜ ๊ตฌ๋งค์ž์™€ ํŒ๋งค์ž ๊ณ„์ •์„ ๋งŒ๋“ค์–ด์„œ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด!

4 Client ID์™€ Secret ๋ณต์‚ฌ

์•ฑ์„ ๋งŒ๋“ค๋ฉด Client ID์™€ Secret์ด ์ƒ์„ฑ๋ผ. ์ด ๋‘ ๊ฐœ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•˜์ž!

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์„ค๊ณ„ํ•˜๊ธฐ

์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ์ž˜ ์งœ๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ๋‚˜์ค‘์— ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ํ›จ์”ฌ ํŽธํ•˜๊ฑฐ๋“ ! ๐Ÿ˜Ž

payment-integration/
โ”‚
โ”œโ”€โ”€ config/
โ”‚   โ”œโ”€โ”€ stripe.php          # Stripe ์„ค์ •
โ”‚   โ””โ”€โ”€ paypal.php          # PayPal ์„ค์ •
โ”‚
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ Payment/
โ”‚   โ”‚   โ”œโ”€โ”€ StripePayment.php
โ”‚   โ”‚   โ”œโ”€โ”€ PayPalPayment.php
โ”‚   โ”‚   โ””โ”€โ”€ PaymentInterface.php
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ Database/
โ”‚       โ””โ”€โ”€ TransactionLogger.php
โ”‚
โ”œโ”€โ”€ public/
โ”‚   โ”œโ”€โ”€ index.php           # ๋ฉ”์ธ ํŽ˜์ด์ง€
โ”‚   โ”œโ”€โ”€ checkout.php        # ๊ฒฐ์ œ ํŽ˜์ด์ง€
โ”‚   โ”œโ”€โ”€ success.php         # ์„ฑ๊ณต ํŽ˜์ด์ง€
โ”‚   โ””โ”€โ”€ cancel.php          # ์ทจ์†Œ ํŽ˜์ด์ง€
โ”‚
โ”œโ”€โ”€ vendor/                 # Composer ํŒจํ‚ค์ง€
โ”œโ”€โ”€ .env                    # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ (API ํ‚ค ์ €์žฅ)
โ”œโ”€โ”€ .gitignore
โ””โ”€โ”€ composer.json
๐Ÿ’ก ๋ณด์•ˆ ํŒ: API ํ‚ค๋Š” ์ ˆ๋Œ€ ์ฝ”๋“œ์— ์ง์ ‘ ๋„ฃ์ง€ ๋งˆ! .env ํŒŒ์ผ์„ ์‚ฌ์šฉํ•ด์„œ ๊ด€๋ฆฌํ•˜๊ณ , ์ด ํŒŒ์ผ์€ .gitignore์— ์ถ”๊ฐ€ํ•ด์„œ Git์— ์˜ฌ๋ผ๊ฐ€์ง€ ์•Š๋„๋ก ํ•˜์ž!

๐ŸŽจ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •ํ•˜๊ธฐ

๋จผ์ € ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก vlucas/phpdotenv ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜์ž:

composer require vlucas/phpdotenv

๊ทธ๋ฆฌ๊ณ  ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

# Stripe ์„ค์ •
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here

# PayPal ์„ค์ •
PAYPAL_CLIENT_ID=your_client_id_here
PAYPAL_CLIENT_SECRET=your_client_secret_here
PAYPAL_MODE=sandbox  # ์‹ค์ œ ์šด์˜ ์‹œ 'live'๋กœ ๋ณ€๊ฒฝ

# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ •
DB_HOST=localhost
DB_NAME=payment_db
DB_USER=root
DB_PASS=your_password

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •
APP_URL=http://localhost:8000
CURRENCY=USD

์ด์ œ ์„ค์ • ํŒŒ์ผ๋“ค์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๋จผ์ € config/stripe.php:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Dotenv\Dotenv;

$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();

return [
    'publishable_key' => $_ENV['STRIPE_PUBLISHABLE_KEY'],
    'secret_key' => $_ENV['STRIPE_SECRET_KEY'],
    'currency' => $_ENV['CURRENCY'] ?? 'USD',
    'api_version' => '2023-10-16'
];

๋‹ค์Œ์€ config/paypal.php:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Dotenv\Dotenv;

$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();

return [
    'client_id' => $_ENV['PAYPAL_CLIENT_ID'],
    'client_secret' => $_ENV['PAYPAL_CLIENT_SECRET'],
    'mode' => $_ENV['PAYPAL_MODE'] ?? 'sandbox',
    'currency' => $_ENV['CURRENCY'] ?? 'USD',
    'return_url' => $_ENV['APP_URL'] . '/success.php',
    'cancel_url' => $_ENV['APP_URL'] . '/cancel.php'
];

๐Ÿ—๏ธ ๊ฒฐ์ œ ์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„ํ•˜๊ธฐ

๊ฐ์ฒด์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ์žฅ์ ์„ ์‚ด๋ ค์„œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋จผ์ € ์ •์˜ํ•˜์ž! ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‚˜์ค‘์— ๋‹ค๋ฅธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ๋„ ์‰ฌ์›Œ์ ธ. ๐ŸŽฏ

src/Payment/PaymentInterface.php ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

<?php

namespace Payment;

interface PaymentInterface
{
    /**
     * ๊ฒฐ์ œ ์ดˆ๊ธฐํ™”
     * 
     * @param array $data ๊ฒฐ์ œ ์ •๋ณด (๊ธˆ์•ก, ์ƒํ’ˆ๋ช… ๋“ฑ)
     * @return mixed ๊ฒฐ์ œ ์„ธ์…˜ ๋˜๋Š” ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URL
     */
    public function createPayment(array $data);
    
    /**
     * ๊ฒฐ์ œ ์‹คํ–‰
     * 
     * @param string $paymentId ๊ฒฐ์ œ ID
     * @param array $data ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ
     * @return array ๊ฒฐ์ œ ๊ฒฐ๊ณผ
     */
    public function executePayment(string $paymentId, array $data = []);
    
    /**
     * ๊ฒฐ์ œ ์ทจ์†Œ
     * 
     * @param string $paymentId ๊ฒฐ์ œ ID
     * @return bool ์ทจ์†Œ ์„ฑ๊ณต ์—ฌ๋ถ€
     */
    public function cancelPayment(string $paymentId): bool;
    
    /**
     * ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
     * 
     * @param string $paymentId ๊ฒฐ์ œ ID
     * @param float $amount ํ™˜๋ถˆ ๊ธˆ์•ก
     * @return array ํ™˜๋ถˆ ๊ฒฐ๊ณผ
     */
    public function refundPayment(string $paymentId, float $amount);
    
    /**
     * ๊ฒฐ์ œ ์ƒํƒœ ์กฐํšŒ
     * 
     * @param string $paymentId ๊ฒฐ์ œ ID
     * @return array ๊ฒฐ์ œ ์ƒํƒœ ์ •๋ณด
     */
    public function getPaymentStatus(string $paymentId): array;
}
๐ŸŽ“ ์™œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ• ๊นŒ? ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ์˜ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์–ด. Stripe๋“  PayPal์ด๋“  ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋กœ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์ง€์ง€! ๋‚˜์ค‘์— Toss๋‚˜ ์นด์นด์˜คํŽ˜์ด๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋„ ๊ฐ™์€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด ๋ผ.

๐Ÿ’œ Stripe ๊ฒฐ์ œ ๊ตฌํ˜„ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Stripe ๊ฒฐ์ œ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž! Stripe๋Š” ์ •๋ง ๊ฐœ๋ฐœ์ž ์นœํ™”์ ์ด๋ผ์„œ ๊ตฌํ˜„ํ•˜๊ธฐ ํŽธํ•ด. ๐Ÿ˜Š

src/Payment/StripePayment.php ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

<?php

namespace Payment;

use Stripe\Stripe;
use Stripe\Checkout\Session;
use Stripe\PaymentIntent;
use Stripe\Refund;
use Exception;

class StripePayment implements PaymentInterface
{
    private $secretKey;
    private $publishableKey;
    private $currency;
    
    public function __construct(array $config)
    {
        $this->secretKey = $config['secret_key'];
        $this->publishableKey = $config['publishable_key'];
        $this->currency = $config['currency'];
        
        // Stripe API ์ดˆ๊ธฐํ™”
        Stripe::setApiKey($this->secretKey);
    }
    
    /**
     * Checkout Session ์ƒ์„ฑ (ํ˜ธ์ŠคํŒ…๋œ ๊ฒฐ์ œ ํŽ˜์ด์ง€)
     */
    public function createPayment(array $data)
    {
        try {
            $session = Session::create([
                'payment_method_types' => ['card'],
                'line_items' => [[
                    'price_data' => [
                        'currency' => $this->currency,
                        'product_data' => [
                            'name' => $data['product_name'],
                            'description' => $data['description'] ?? '',
                        ],
                        'unit_amount' => $data['amount'] * 100, // ์„ผํŠธ ๋‹จ์œ„
                    ],
                    'quantity' => $data['quantity'] ?? 1,
                ]],
                'mode' => 'payment',
                'success_url' => $data['success_url'] . '?session_id={CHECKOUT_SESSION_ID}',
                'cancel_url' => $data['cancel_url'],
                'customer_email' => $data['customer_email'] ?? null,
                'metadata' => $data['metadata'] ?? [],
            ]);
            
            return [
                'success' => true,
                'session_id' => $session->id,
                'url' => $session->url,
                'publishable_key' => $this->publishableKey
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * Payment Intent ์ƒ์„ฑ (์ปค์Šคํ…€ ๊ฒฐ์ œ ํผ)
     */
    public function createPaymentIntent(array $data)
    {
        try {
            $paymentIntent = PaymentIntent::create([
                'amount' => $data['amount'] * 100,
                'currency' => $this->currency,
                'description' => $data['description'] ?? '',
                'metadata' => $data['metadata'] ?? [],
            ]);
            
            return [
                'success' => true,
                'client_secret' => $paymentIntent->client_secret,
                'payment_intent_id' => $paymentIntent->id
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * ๊ฒฐ์ œ ์‹คํ–‰ (Checkout Session ํ™•์ธ)
     */
    public function executePayment(string $sessionId, array $data = [])
    {
        try {
            $session = Session::retrieve($sessionId);
            
            return [
                'success' => true,
                'payment_status' => $session->payment_status,
                'amount_total' => $session->amount_total / 100,
                'customer_email' => $session->customer_details->email ?? null,
                'payment_intent' => $session->payment_intent
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * ๊ฒฐ์ œ ์ทจ์†Œ
     */
    public function cancelPayment(string $paymentId): bool
    {
        try {
            $paymentIntent = PaymentIntent::retrieve($paymentId);
            $paymentIntent->cancel();
            return true;
            
        } catch (Exception $e) {
            return false;
        }
    }
    
    /**
     * ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
     */
    public function refundPayment(string $paymentId, float $amount)
    {
        try {
            $refund = Refund::create([
                'payment_intent' => $paymentId,
                'amount' => $amount * 100, // ๋ถ€๋ถ„ ํ™˜๋ถˆ ๊ฐ€๋Šฅ
            ]);
            
            return [
                'success' => true,
                'refund_id' => $refund->id,
                'status' => $refund->status,
                'amount' => $refund->amount / 100
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * ๊ฒฐ์ œ ์ƒํƒœ ์กฐํšŒ
     */
    public function getPaymentStatus(string $paymentId): array
    {
        try {
            $paymentIntent = PaymentIntent::retrieve($paymentId);
            
            return [
                'success' => true,
                'status' => $paymentIntent->status,
                'amount' => $paymentIntent->amount / 100,
                'currency' => $paymentIntent->currency,
                'created' => date('Y-m-d H:i:s', $paymentIntent->created)
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * Webhook ์„œ๋ช… ๊ฒ€์ฆ
     */
    public function verifyWebhook(string $payload, string $signature, string $secret): bool
    {
        try {
            \Stripe\Webhook::constructEvent($payload, $signature, $secret);
            return true;
        } catch (Exception $e) {
            return false;
        }
    }
}
๐Ÿ’ก Stripe์˜ ๋‘ ๊ฐ€์ง€ ๊ฒฐ์ œ ๋ฐฉ์‹: 1. Checkout Session: Stripe๊ฐ€ ํ˜ธ์ŠคํŒ…ํ•˜๋Š” ๊ฒฐ์ œ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ. ๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  PCI ์ปดํ”Œ๋ผ์ด์–ธ์Šค ๊ฑฑ์ • ์—†์Œ!
2. Payment Intent: ์ž์ฒด ๊ฒฐ์ œ ํผ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ. ๋” ๋งŽ์€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๋ณด์•ˆ ์ฑ…์ž„์ด ์ปค์ง.

Stripe ๊ฒฐ์ œ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

์ด์ œ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ๊ฒฐ์ œ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! public/stripe-checkout.php:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Payment\StripePayment;

$config = require __DIR__ . '/../config/stripe.php';
$stripe = new StripePayment($config);

// POST ์š”์ฒญ ์ฒ˜๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $paymentData = [
        'product_name' => $_POST['product_name'],
        'description' => $_POST['description'] ?? '',
        'amount' => floatval($_POST['amount']),
        'quantity' => intval($_POST['quantity'] ?? 1),
        'customer_email' => $_POST['email'] ?? '',
        'success_url' => $_ENV['APP_URL'] . '/success.php',
        'cancel_url' => $_ENV['APP_URL'] . '/cancel.php',
        'metadata' => [
            'order_id' => uniqid('order_'),
            'user_id' => $_SESSION['user_id'] ?? 'guest'
        ]
    ];
    
    $result = $stripe->createPayment($paymentData);
    
    if ($result['success']) {
        // Stripe Checkout ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
        header('Location: ' . $result['url']);
        exit;
    } else {
        $error = $result['error'];
    }
}

?>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Stripe ๊ฒฐ์ œ</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 500px; margin: 50px auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h2 { color: #6c5ce7; margin-bottom: 20px; }
        .form-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 5px; color: #2d3436; font-weight: 600; }
        input, textarea { width: 100%; padding: 12px; border: 2px solid #dfe6e9; border-radius: 5px; font-size: 14px; }
        input:focus, textarea:focus { outline: none; border-color: #6c5ce7; }
        button { width: 100%; padding: 15px; background: #6c5ce7; color: white; border: none; border-radius: 5px; font-size: 16px; font-weight: 600; cursor: pointer; transition: 0.3s; }
        button:hover { background: #5f4dd1; }
        .error { background: #ff7675; color: white; padding: 10px; border-radius: 5px; margin-bottom: 20px; }
    </style>
</head>
<body>
    <div class="container">
        <h2>๐Ÿ’ณ Stripe ๊ฒฐ์ œ</h2>
        
        <?php if (isset($error)): ?>
            <div class="error">โŒ <!--?= htmlspecialchars($error) ?></div>
        <?php endif; ?>
        
        <form method="POST">
            <div class="form-group">
                <label>์ƒํ’ˆ๋ช…</label>
                <input type="text" name="product_name" required placeholder="์˜ˆ: ํ”„๋ฆฌ๋ฏธ์—„ ๋ฉค๋ฒ„์‹ญ">
            </div>
            
            <div class="form-group">
                <label>์ƒํ’ˆ ์„ค๋ช…</label>
                <textarea name="description" rows="3" placeholder="์ƒํ’ˆ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ์„ค๋ช…"></textarea>
            </div>
            
            <div class="form-group">
                <label>๊ธˆ์•ก (USD)</label>
                <input type="number" name="amount" step="0.01" required placeholder="29.99">
            </div>
            
            <div class="form-group">
                <label>์ˆ˜๋Ÿ‰</label>
                <input type="number" name="quantity" value="1" min="1">
            </div>
            
            <div class="form-group">
                <label>์ด๋ฉ”์ผ</label>
                <input type="email" name="email" placeholder="your@email.com">
            </div>
            
            <button type="submit">๐Ÿš€ Stripe๋กœ ๊ฒฐ์ œํ•˜๊ธฐ</button>
        </form>
    </div>
</body>
</html></code-->


๐Ÿ’› PayPal ๊ฒฐ์ œ ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ PayPal ์ฐจ๋ก€์•ผ! PayPal์€ ์ „ ์„ธ๊ณ„์ ์œผ๋กœ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์ค‘ ํ•˜๋‚˜๋ผ์„œ ๊ผญ ํ†ตํ•ฉํ•ด์•ผ ํ•ด. ๐ŸŒ

src/Payment/PayPalPayment.php ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด:

<?php

namespace Payment;

use PayPal\Rest\ApiContext;
use PayPal\Auth\OAuthTokenCredential;
use PayPal\Api\Amount;
use PayPal\Api\Payer;
use PayPal\Api\Payment;
use PayPal\Api\PaymentExecution;
use PayPal\Api\RedirectUrls;
use PayPal\Api\Transaction;
use PayPal\Api\RefundRequest;
use PayPal\Api\Sale;
use Exception;

class PayPalPayment implements PaymentInterface
{
    private $apiContext;
    private $currency;
    private $returnUrl;
    private $cancelUrl;
    
    public function __construct(array $config)
    {
        $this->currency = $config['currency'];
        $this->returnUrl = $config['return_url'];
        $this->cancelUrl = $config['cancel_url'];
        
        // PayPal API Context ์ดˆ๊ธฐํ™”
        $this->apiContext = new ApiContext(
            new OAuthTokenCredential(
                $config['client_id'],
                $config['client_secret']
            )
        );
        
        $this->apiContext->setConfig([
            'mode' => $config['mode'],
            'log.LogEnabled' => true,
            'log.FileName' => __DIR__ . '/../../logs/paypal.log',
            'log.LogLevel' => 'INFO',
            'cache.enabled' => true,
        ]);
    }
    
    /**
     * PayPal ๊ฒฐ์ œ ์ƒ์„ฑ
     */
    public function createPayment(array $data)
    {
        try {
            // Payer ์„ค์ •
            $payer = new Payer();
            $payer->setPaymentMethod('paypal');
            
            // ๊ธˆ์•ก ์„ค์ •
            $amount = new Amount();
            $amount->setCurrency($this->currency)
                   ->setTotal($data['amount']);
            
            // ๊ฑฐ๋ž˜ ์ •๋ณด ์„ค์ •
            $transaction = new Transaction();
            $transaction->setAmount($amount)
                       ->setDescription($data['description'] ?? '')
                       ->setInvoiceNumber($data['invoice_number'] ?? uniqid('INV-'));
            
            // ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ URL ์„ค์ •
            $redirectUrls = new RedirectUrls();
            $redirectUrls->setReturnUrl($this->returnUrl)
                        ->setCancelUrl($this->cancelUrl);
            
            // Payment ๊ฐ์ฒด ์ƒ์„ฑ
            $payment = new Payment();
            $payment->setIntent('sale')
                   ->setPayer($payer)
                   ->setRedirectUrls($redirectUrls)
                   ->setTransactions([$transaction]);
            
            // ๊ฒฐ์ œ ์ƒ์„ฑ
            $payment->create($this->apiContext);
            
            // ์Šน์ธ URL ์ฐพ๊ธฐ
            $approvalUrl = null;
            foreach ($payment->getLinks() as $link) {
                if ($link->getRel() === 'approval_url') {
                    $approvalUrl = $link->getHref();
                    break;
                }
            }
            
            return [
                'success' => true,
                'payment_id' => $payment->getId(),
                'approval_url' => $approvalUrl,
                'state' => $payment->getState()
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * ๊ฒฐ์ œ ์‹คํ–‰
     */
    public function executePayment(string $paymentId, array $data = [])
    {
        try {
            $payment = Payment::get($paymentId, $this->apiContext);
            
            $execution = new PaymentExecution();
            $execution->setPayerId($data['payer_id']);
            
            $result = $payment->execute($execution, $this->apiContext);
            
            return [
                'success' => true,
                'payment_id' => $result->getId(),
                'state' => $result->getState(),
                'payer_email' => $result->getPayer()->getPayerInfo()->getEmail(),
                'amount' => $result->getTransactions()[0]->getAmount()->getTotal(),
                'currency' => $result->getTransactions()[0]->getAmount()->getCurrency()
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * ๊ฒฐ์ œ ์ทจ์†Œ
     */
    public function cancelPayment(string $paymentId): bool
    {
        // PayPal์€ ์Šน์ธ ์ „ ๊ฒฐ์ œ๋Š” ์ž๋™์œผ๋กœ ๋งŒ๋ฃŒ๋จ
        // ์Šน์ธ ํ›„ ๊ฒฐ์ œ๋Š” ํ™˜๋ถˆ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ
        return true;
    }
    
    /**
     * ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
     */
    public function refundPayment(string $saleId, float $amount)
    {
        try {
            $sale = Sale::get($saleId, $this->apiContext);
            
            $refundRequest = new RefundRequest();
            $refundAmount = new Amount();
            $refundAmount->setCurrency($this->currency)
                        ->setTotal($amount);
            $refundRequest->setAmount($refundAmount);
            
            $refund = $sale->refundSale($refundRequest, $this->apiContext);
            
            return [
                'success' => true,
                'refund_id' => $refund->getId(),
                'state' => $refund->getState(),
                'amount' => $refund->getAmount()->getTotal()
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * ๊ฒฐ์ œ ์ƒํƒœ ์กฐํšŒ
     */
    public function getPaymentStatus(string $paymentId): array
    {
        try {
            $payment = Payment::get($paymentId, $this->apiContext);
            
            return [
                'success' => true,
                'payment_id' => $payment->getId(),
                'state' => $payment->getState(),
                'create_time' => $payment->getCreateTime(),
                'update_time' => $payment->getUpdateTime()
            ];
            
        } catch (Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }
    
    /**
     * Webhook ๊ฒ€์ฆ
     */
    public function verifyWebhook(array $headers, string $payload, string $webhookId): bool
    {
        try {
            // PayPal Webhook ๊ฒ€์ฆ ๋กœ์ง
            // ์‹ค์ œ ๊ตฌํ˜„ ์‹œ PayPal์˜ Webhook ๊ฒ€์ฆ API ์‚ฌ์šฉ
            return true;
        } catch (Exception $e) {
            return false;
        }
    }
}

PayPal ๊ฒฐ์ œ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

public/paypal-checkout.php:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Payment\PayPalPayment;

$config = require __DIR__ . '/../config/paypal.php';
$paypal = new PayPalPayment($config);

// POST ์š”์ฒญ ์ฒ˜๋ฆฌ
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $paymentData = [
        'amount' => floatval($_POST['amount']),
        'description' => $_POST['description'] ?? '',
        'invoice_number' => 'INV-' . time()
    ];
    
    $result = $paypal->createPayment($paymentData);
    
    if ($result['success']) {
        // PayPal ์Šน์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
        header('Location: ' . $result['approval_url']);
        exit;
    } else {
        $error = $result['error'];
    }
}

?>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PayPal ๊ฒฐ์ œ</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 500px; margin: 50px auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h2 { color: #0070ba; margin-bottom: 20px; }
        .form-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 5px; color: #2d3436; font-weight: 600; }
        input, textarea { width: 100%; padding: 12px; border: 2px solid #dfe6e9; border-radius: 5px; font-size: 14px; }
        input:focus, textarea:focus { outline: none; border-color: #0070ba; }
        button { width: 100%; padding: 15px; background: #0070ba; color: white; border: none; border-radius: 5px; font-size: 16px; font-weight: 600; cursor: pointer; transition: 0.3s; }
        button:hover { background: #005a94; }
        .error { background: #ff7675; color: white; padding: 10px; border-radius: 5px; margin-bottom: 20px; }
    </style>
</head>
<body>
    <div class="container">
        <h2>๐Ÿ’› PayPal ๊ฒฐ์ œ</h2>
        
        <?php if (isset($error)): ?>
            <div class="error">โŒ <!--?= htmlspecialchars($error) ?></div>
        <?php endif; ?>
        
        <form method="POST">
            <div class="form-group">
                <label>๊ฒฐ์ œ ์„ค๋ช…</label>
                <textarea name="description" rows="3" required placeholder="๊ฒฐ์ œ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”"></textarea>
            </div>
            
            <div class="form-group">
                <label>๊ธˆ์•ก (USD)</label>
                <input type="number" name="amount" step="0.01" required placeholder="29.99">
            </div>
            
            <button type="submit">๐Ÿš€ PayPal๋กœ ๊ฒฐ์ œํ•˜๊ธฐ</button>
        </form>
    </div>
</body>
</html></code-->

๊ทธ๋ฆฌ๊ณ  PayPal ๊ฒฐ์ œ ์™„๋ฃŒ ํ›„ ๋Œ์•„์˜ค๋Š” ํŽ˜์ด์ง€ public/success.php:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Payment\PayPalPayment;
use Payment\StripePayment;

// Stripe ๊ฒฐ์ œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
if (isset($_GET['session_id'])) {
    $config = require __DIR__ . '/../config/stripe.php';
    $stripe = new StripePayment($config);
    
    $result = $stripe->executePayment($_GET['session_id']);
    
    if ($result['success']) {
        $paymentInfo = $result;
        $paymentMethod = 'Stripe';
    }
}

// PayPal ๊ฒฐ์ œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
if (isset($_GET['paymentId']) && isset($_GET['PayerID'])) {
    $config = require __DIR__ . '/../config/paypal.php';
    $paypal = new PayPalPayment($config);
    
    $result = $paypal->executePayment($_GET['paymentId'], [
        'payer_id' => $_GET['PayerID']
    ]);
    
    if ($result['success']) {
        $paymentInfo = $result;
        $paymentMethod = 'PayPal';
    }
}

?>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>๊ฒฐ์ œ ์™„๋ฃŒ</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
        .success-box { background: white; padding: 40px; border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); max-width: 500px; text-align: center; }
        .success-icon { font-size: 80px; margin-bottom: 20px; }
        h1 { color: #00b894; margin-bottom: 10px; }
        .info-box { background: #f8f9fa; padding: 20px; border-radius: 10px; margin: 20px 0; text-align: left; }
        .info-row { display: flex; justify-content: space-between; margin: 10px 0; padding: 10px 0; border-bottom: 1px solid #dfe6e9; }
        .info-row:last-child { border-bottom: none; }
        .label { font-weight: 600; color: #636e72; }
        .value { color: #2d3436; }
        .btn { display: inline-block; padding: 12px 30px; background: #6c5ce7; color: white; text-decoration: none; border-radius: 5px; margin-top: 20px; transition: 0.3s; }
        .btn:hover { background: #5f4dd1; }
    </style>
</head>
<body>
    <div class="success-box">
        <div class="success-icon">โœ…</div>
        <h1>๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!</h1>
        <p>๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ์ œ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</p>
        
        <?php if (isset($paymentInfo)): ?>
        <div class="info-box">
            <div class="info-row">
                <span class="label">๊ฒฐ์ œ ๋ฐฉ๋ฒ•</span>
                <span class="value"><!--?= $paymentMethod ?></span>
            </div>
            <div class="info-row">
                <span class="label">๊ฒฐ์ œ ID</span>
                <span class="value"><?= htmlspecialchars($paymentInfo['payment_id'] ?? $paymentInfo['payment_intent']) ?></span>
            </div>
            <div class="info-row">
                <span class="label">๊ธˆ์•ก</span>
                <span class="value">$<?= number_format($paymentInfo['amount'] ?? $paymentInfo['amount_total'], 2) ?></span>
            </div>
            <div class="info-row">
                <span class="label">์ƒํƒœ</span>
                <span class="value"><?= $paymentInfo['payment_status'] ?? $paymentInfo['state'] ?></span>
            </div>
        </div>
        <?php endif; ?>
        
        <a href="/" class="btn">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</a>
    </div>
</body>
</html></code-->


๐ŸŽญ ํ†ตํ•ฉ ๊ฒฐ์ œ ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

์ด์ œ Stripe์™€ PayPal์„ ํ•œ ํŽ˜์ด์ง€์—์„œ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ํ†ตํ•ฉ ๊ฒฐ์ œ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์งˆ ๊ฑฐ์•ผ. ๐ŸŽจ

public/checkout.php:

<?php
session_start();
require_once __DIR__ . '/../vendor/autoload.php';

// ์žฅ๋ฐ”๊ตฌ๋‹ˆ๋‚˜ ์ฃผ๋ฌธ ์ •๋ณด (์‹ค์ œ๋กœ๋Š” DB์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•จ)
$orderInfo = [
    'product_name' => 'ํ”„๋ฆฌ๋ฏธ์—„ ๋ฉค๋ฒ„์‹ญ',
    'description' => '1๋…„ ๊ตฌ๋…',
    'amount' => 99.99,
    'quantity' => 1
];
?>

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ํ†ตํ•ฉ ๊ฒฐ์ œ</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 800px; margin: 30px auto; }
        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px 15px 0 0; text-align: center; }
        .content { background: white; padding: 30px; border-radius: 0 0 15px 15px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); }
        .order-summary { background: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 30px; }
        .order-item { display: flex; justify-content: space-between; margin: 10px 0; padding: 10px 0; border-bottom: 1px solid #dfe6e9; }
        .total { font-size: 1.5em; font-weight: bold; color: #2d3436; margin-top: 15px; }
        .payment-methods { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 30px; }
        .payment-btn { padding: 30px; border: 2px solid #dfe6e9; border-radius: 10px; text-align: center; cursor: pointer; transition: 0.3s; text-decoration: none; display: block; }
        .payment-btn:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
        .stripe-btn { border-color: #6c5ce7; }
        .stripe-btn:hover { background: #f0eeff; border-color: #5f4dd1; }
        .paypal-btn { border-color: #0070ba; }
        .paypal-btn:hover { background: #e6f3ff; border-color: #005a94; }
        .payment-icon { font-size: 50px; margin-bottom: 10px; }
        .payment-name { font-size: 1.2em; font-weight: 600; color: #2d3436; }
        .payment-desc { color: #636e72; font-size: 0.9em; margin-top: 5px; }
        
        @media (max-width: 600px) {
            .payment-methods { grid-template-columns: 1fr; }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>๐Ÿ’ณ ๊ฒฐ์ œํ•˜๊ธฐ</h1>
            <p>์•ˆ์ „ํ•˜๊ณ  ๋น ๋ฅธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ</p>
        </div>
        
        <div class="content">
            <div class="order-summary">
                <h3 style="margin-bottom: 15px; color: #2d3436;">๐Ÿ“ฆ ์ฃผ๋ฌธ ๋‚ด์—ญ</h3>
                
                <div class="order-item">
                    <span>์ƒํ’ˆ๋ช…</span>
                    <strong><!--?= htmlspecialchars($orderInfo['product_name']) ?></strong>
                </div>
                
                <div class="order-item">
                    <span>์„ค๋ช…</span>
                    <span><?= htmlspecialchars($orderInfo['description']) ?></span>
                </div>
                
                <div class="order-item">
                    <span>์ˆ˜๋Ÿ‰</span>
                    <span><?= $orderInfo['quantity'] ?></span>
                </div>
                
                <div class="order-item">
                    <span>๋‹จ๊ฐ€</span>
                    <span>$<?= number_format($orderInfo['amount'], 2) ?></span>
                </div>
                
                <div class="total">
                    <span>์ด ๊ฒฐ์ œ ๊ธˆ์•ก</span>
                    <span style="color: #6c5ce7;">$<?= number_format($orderInfo['amount'] * $orderInfo['quantity'], 2) ?></span>
                </div>
            </div>
            
            <h3 style="margin-bottom: 20px; color: #2d3436;">๐Ÿ’ฐ ๊ฒฐ์ œ ์ˆ˜๋‹จ ์„ ํƒ</h3>
            
            <div class="payment-methods">
                <form action="stripe-checkout.php" method="POST" style="margin: 0;">
                    <input type="hidden" name="product_name" value="<?= htmlspecialchars($orderInfo['product_name']) ?>">
                    <input type="hidden" name="description" value="<?= htmlspecialchars($orderInfo['description']) ?>">
                    <input type="hidden" name="amount" value="<?= $orderInfo['amount'] ?>">
                    <input type="hidden" name="quantity" value="<?= $orderInfo['quantity'] ?>">
                    
                    <button type="submit" class="payment-btn stripe-btn" style="border: 2px solid #6c5ce7; background: white;">
                        <div class="payment-icon">๐Ÿ’œ</div>
                        <div class="payment-name">Stripe</div>
                        <div class="payment-desc">์‹ ์šฉ์นด๋“œ / ์ฒดํฌ์นด๋“œ</div>
                    </button>
                </form>
                
                <form action="paypal-checkout.php" method="POST" style="margin: 0;">
                    <input type="hidden" name="description" value="<?= htmlspecialchars($orderInfo['product_name'] . ' - ' . $orderInfo['description']) ?>">
                    <input type="hidden" name="amount" value="<?= $orderInfo['amount'] * $orderInfo['quantity'] ?>">
                    
                    <button type="submit" class="payment-btn paypal-btn" style="border: 2px solid #0070ba; background: white;">
                        <div class="payment-icon">๐Ÿ’›</div>
                        <div class="payment-name">PayPal</div>
                        <div class="payment-desc">PayPal ๊ณ„์ • ๊ฒฐ์ œ</div>
                    </button>
                </form>
            </div>
            
            <div style="margin-top: 30px; padding: 15px; background: #e8f5e9; border-radius: 8px; text-align: center;">
                <p style="color: #27ae60; margin: 0;">๐Ÿ”’ ๋ชจ๋“  ๊ฒฐ์ œ๋Š” SSL๋กœ ์•”ํ˜ธํ™”๋˜์–ด ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค</p>
            </div>
        </div>
    </div>
</body>
</html></code-->


๐ŸŽจ UX ๋””์ž์ธ ํŒ: ๊ฒฐ์ œ ํŽ˜์ด์ง€๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€์žฅ ์‹ ๊ฒฝ ์“ฐ๋Š” ๋ถ€๋ถ„์ด์•ผ. ๊น”๋”ํ•˜๊ณ  ์‹ ๋ขฐ๊ฐ ์žˆ๋Š” ๋””์ž์ธ์ด ์ค‘์š”ํ•ด! ๋ณด์•ˆ ๋ฐฐ์ง€๋‚˜ SSL ์ธ์ฆ ํ‘œ์‹œ๋ฅผ ๋„ฃ์œผ๋ฉด ์ „ํ™˜์œจ์ด ์˜ฌ๋ผ๊ฐ€๋”๋ผ๊ณ . ๐Ÿ˜Š


๐Ÿ”” Webhook์œผ๋กœ ์‹ค์‹œ๊ฐ„ ๊ฒฐ์ œ ์•Œ๋ฆผ ๋ฐ›๊ธฐ

๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์„œ๋ฒ„์—์„œ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ์ผ๋“ค์ด ๋งŽ์•„. ์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ, ์ด๋ฉ”์ผ ๋ฐœ์†ก, ์žฌ๊ณ  ๊ด€๋ฆฌ ๋“ฑ๋“ฑ... ์ด๋Ÿด ๋•Œ Webhook์„ ์‚ฌ์šฉํ•˜๋ฉด ์™„์ „ ํŽธํ•ด! ๐ŸŽฏ

Stripe Webhook ๊ตฌํ˜„

public/webhooks/stripe.php:

<?php

require_once __DIR__ . '/../../vendor/autoload.php';

use Stripe\Stripe;
use Stripe\Webhook;

$config = require __DIR__ . '/../../config/stripe.php';
Stripe::setApiKey($config['secret_key']);

// Webhook Secret (Stripe ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ™•์ธ)
$webhookSecret = 'whsec_your_webhook_secret_here';

// ์š”์ฒญ ๋ณธ๋ฌธ ๊ฐ€์ ธ์˜ค๊ธฐ
$payload = @file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';

try {
    // Webhook ์„œ๋ช… ๊ฒ€์ฆ
    $event = Webhook::constructEvent($payload, $sigHeader, $webhookSecret);
    
    // ๋กœ๊ทธ ๊ธฐ๋ก
    file_put_contents(
        __DIR__ . '/../../logs/stripe-webhook.log',
        date('Y-m-d H:i:s') . ' - ' . $event->type . PHP_EOL,
        FILE_APPEND
    );
    
    // ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ
    switch ($event->type) {
        case 'checkout.session.completed':
            $session = $event->data->object;
            
            // ๊ฒฐ์ œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
            handlePaymentSuccess($session);
            break;
            
        case 'payment_intent.succeeded':
            $paymentIntent = $event->data->object;
            
            // Payment Intent ์„ฑ๊ณต ์ฒ˜๋ฆฌ
            handlePaymentIntentSuccess($paymentIntent);
            break;
            
        case 'payment_intent.payment_failed':
            $paymentIntent = $event->data->object;
            
            // ๊ฒฐ์ œ ์‹คํŒจ ์ฒ˜๋ฆฌ
            handlePaymentFailure($paymentIntent);
            break;
            
        case 'charge.refunded':
            $charge = $event->data->object;
            
            // ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
            handleRefund($charge);
            break;
            
        default:
            // ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๋Š” ์ด๋ฒคํŠธ
            break;
    }
    
    // 200 ์‘๋‹ต ๋ฐ˜ํ™˜ (์ค‘์š”!)
    http_response_code(200);
    echo json_encode(['status' => 'success']);
    
} catch (\UnexpectedValueException $e) {
    // ์ž˜๋ชป๋œ ํŽ˜์ด๋กœ๋“œ
    http_response_code(400);
    echo json_encode(['error' => 'Invalid payload']);
    exit;
    
} catch (\Stripe\Exception\SignatureVerificationException $e) {
    // ์„œ๋ช… ๊ฒ€์ฆ ์‹คํŒจ
    http_response_code(400);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

/**
 * ๊ฒฐ์ œ ์„ฑ๊ณต ์ฒ˜๋ฆฌ
 */
function handlePaymentSuccess($session)
{
    // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ฃผ๋ฌธ ์ •๋ณด ์ €์žฅ
    $orderId = $session->metadata->order_id ?? null;
    $amount = $session->amount_total / 100;
    $customerEmail = $session->customer_details->email ?? '';
    
    // TODO: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—…๋ฐ์ดํŠธ
    // - ์ฃผ๋ฌธ ์ƒํƒœ๋ฅผ '๊ฒฐ์ œ ์™„๋ฃŒ'๋กœ ๋ณ€๊ฒฝ
    // - ๊ฒฐ์ œ ์ •๋ณด ์ €์žฅ
    // - ๊ณ ๊ฐ์—๊ฒŒ ํ™•์ธ ์ด๋ฉ”์ผ ๋ฐœ์†ก
    
    // ๋กœ๊ทธ ๊ธฐ๋ก
    file_put_contents(
        __DIR__ . '/../../logs/orders.log',
        sprintf(
            "[%s] Order %s completed - Amount: $%.2f - Email: %s\n",
            date('Y-m-d H:i:s'),
            $orderId,
            $amount,
            $customerEmail
        ),
        FILE_APPEND
    );
}

/**
 * Payment Intent ์„ฑ๊ณต ์ฒ˜๋ฆฌ
 */
function handlePaymentIntentSuccess($paymentIntent)
{
    // Payment Intent ๋ฐฉ์‹์˜ ๊ฒฐ์ œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
    $amount = $paymentIntent->amount / 100;
    
    // TODO: ํ•„์š”ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ
}

/**
 * ๊ฒฐ์ œ ์‹คํŒจ ์ฒ˜๋ฆฌ
 */
function handlePaymentFailure($paymentIntent)
{
    // ๊ฒฐ์ œ ์‹คํŒจ ์•Œ๋ฆผ
    $errorMessage = $paymentIntent->last_payment_error->message ?? 'Unknown error';
    
    // TODO: ๊ณ ๊ฐ์—๊ฒŒ ์‹คํŒจ ์•Œ๋ฆผ ์ด๋ฉ”์ผ ๋ฐœ์†ก
    
    file_put_contents(
        __DIR__ . '/../../logs/payment-failures.log',
        sprintf(
            "[%s] Payment failed - Error: %s\n",
            date('Y-m-d H:i:s'),
            $errorMessage
        ),
        FILE_APPEND
    );
}

/**
 * ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
 */
function handleRefund($charge)
{
    $refundAmount = $charge->amount_refunded / 100;
    
    // TODO: 
    // - ์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
    // - ์žฌ๊ณ  ๋ณต๊ตฌ
    // - ํ™˜๋ถˆ ์™„๋ฃŒ ์ด๋ฉ”์ผ ๋ฐœ์†ก
    
    file_put_contents(
        __DIR__ . '/../../logs/refunds.log',
        sprintf(
            "[%s] Refund processed - Amount: $%.2f\n",
            date('Y-m-d H:i:s'),
            $refundAmount
        ),
        FILE_APPEND
    );
}

PayPal Webhook ๊ตฌํ˜„

public/webhooks/paypal.php:

<?php

require_once __DIR__ . '/../../vendor/autoload.php';

use PayPal\Rest\ApiContext;
use PayPal\Auth\OAuthTokenCredential;

$config = require __DIR__ . '/../../config/paypal.php';

// PayPal API Context
$apiContext = new ApiContext(
    new OAuthTokenCredential(
        $config['client_id'],
        $config['client_secret']
    )
);
$apiContext->setConfig(['mode' => $config['mode']]);

// Webhook ๋ฐ์ดํ„ฐ ๋ฐ›๊ธฐ
$payload = @file_get_contents('php://input');
$data = json_decode($payload, true);

// ๋กœ๊ทธ ๊ธฐ๋ก
file_put_contents(
    __DIR__ . '/../../logs/paypal-webhook.log',
    date('Y-m-d H:i:s') . ' - ' . ($data['event_type'] ?? 'unknown') . PHP_EOL,
    FILE_APPEND
);

// ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ
if (isset($data['event_type'])) {
    switch ($data['event_type']) {
        case 'PAYMENT.SALE.COMPLETED':
            // ๊ฒฐ์ œ ์™„๋ฃŒ
            handlePayPalPaymentCompleted($data);
            break;
            
        case 'PAYMENT.SALE.REFUNDED':
            // ํ™˜๋ถˆ ์™„๋ฃŒ
            handlePayPalRefund($data);
            break;
            
        case 'PAYMENT.SALE.REVERSED':
            // ๊ฒฐ์ œ ์ทจ์†Œ
            handlePayPalReversal($data);
            break;
            
        default:
            // ๊ธฐํƒ€ ์ด๋ฒคํŠธ
            break;
    }
}

// 200 ์‘๋‹ต ๋ฐ˜ํ™˜
http_response_code(200);
echo json_encode(['status' => 'success']);

/**
 * PayPal ๊ฒฐ์ œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
 */
function handlePayPalPaymentCompleted($data)
{
    $resource = $data['resource'] ?? [];
    $amount = $resource['amount']['total'] ?? 0;
    $currency = $resource['amount']['currency'] ?? 'USD';
    $saleId = $resource['id'] ?? '';
    
    // TODO: ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ๋กœ์ง
    
    file_put_contents(
        __DIR__ . '/../../logs/orders.log',
        sprintf(
            "[%s] PayPal payment completed - Sale ID: %s - Amount: %s %s\n",
            date('Y-m-d H:i:s'),
            $saleId,
            $amount,
            $currency
        ),
        FILE_APPEND
    );
}

/**
 * PayPal ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
 */
function handlePayPalRefund($data)
{
    $resource = $data['resource'] ?? [];
    $refundAmount = $resource['amount']['total'] ?? 0;
    
    // TODO: ํ™˜๋ถˆ ์ฒ˜๋ฆฌ ๋กœ์ง
    
    file_put_contents(
        __DIR__ . '/../../logs/refunds.log',
        sprintf(
            "[%s] PayPal refund processed - Amount: %s\n",
            date('Y-m-d H:i:s'),
            $refundAmount
        ),
        FILE_APPEND
    );
}

/**
 * PayPal ๊ฒฐ์ œ ์ทจ์†Œ ์ฒ˜๋ฆฌ
 */
function handlePayPalReversal($data)
{
    // ๊ฒฐ์ œ ์ทจ์†Œ ์ฒ˜๋ฆฌ
    file_put_contents(
        __DIR__ . '/../../logs/reversals.log',
        sprintf(
            "[%s] PayPal payment reversed\n",
            date('Y-m-d H:i:s')
        ),
        FILE_APPEND
    );
}
โš ๏ธ Webhook ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ:
1. ๋ฐ˜๋“œ์‹œ ์„œ๋ช… ๊ฒ€์ฆ์„ ํ•ด์•ผ ํ•ด! ๋ˆ„๊ตฌ๋‚˜ Webhook URL์— ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฑฐ๋“ .
2. Webhook URL์€ HTTPS์—ฌ์•ผ ํ•ด.
3. IP ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋ฅผ ์„ค์ •ํ•˜๋ฉด ๋” ์•ˆ์ „ํ•ด.
4. ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์ด๋ฒคํŠธ ID๋ฅผ ์ €์žฅํ•˜๊ณ  ํ™•์ธํ•˜์ž!

Webhook ํ…Œ์ŠคํŠธํ•˜๊ธฐ

๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ Webhook์„ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด ngrok ๊ฐ™์€ ํ„ฐ๋„๋ง ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด:

# ngrok ์„ค์น˜ ํ›„
ngrok http 8000

# ์ƒ์„ฑ๋œ HTTPS URL์„ Stripe/PayPal ๋Œ€์‹œ๋ณด๋“œ์— ๋“ฑ๋ก
# ์˜ˆ: https://abc123.ngrok.io/webhooks/stripe.php

๐Ÿ’พ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ํ•˜๊ธฐ

๊ฒฐ์ œ ์ •๋ณด๋ฅผ ์ œ๋Œ€๋กœ ๊ด€๋ฆฌํ•˜๋ ค๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„๊ฐ€ ์ค‘์š”ํ•ด! ๊ฐ„๋‹จํ•˜์ง€๋งŒ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ๐Ÿ“Š

ํ…Œ์ด๋ธ” ๊ตฌ์กฐ

-- ์ฃผ๋ฌธ ํ…Œ์ด๋ธ”
CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    order_number VARCHAR(50) UNIQUE NOT NULL,
    total_amount DECIMAL(10, 2) NOT NULL,
    currency VARCHAR(3) DEFAULT 'USD',
    status ENUM('pending', 'processing', 'completed', 'cancelled', 'refunded') DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_order_number (order_number),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ๊ฒฐ์ œ ํ…Œ์ด๋ธ”
CREATE TABLE payments (
    id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT NOT NULL,
    payment_method ENUM('stripe', 'paypal') NOT NULL,
    payment_id VARCHAR(255) NOT NULL,
    transaction_id VARCHAR(255),
    amount DECIMAL(10, 2) NOT NULL,
    currency VARCHAR(3) DEFAULT 'USD',
    status ENUM('pending', 'completed', 'failed', 'refunded') DEFAULT 'pending',
    payer_email VARCHAR(255),
    payment_data JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    INDEX idx_order_id (order_id),
    INDEX idx_payment_id (payment_id),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ํ™˜๋ถˆ ํ…Œ์ด๋ธ”
CREATE TABLE refunds (
    id INT AUTO_INCREMENT PRIMARY KEY,
    payment_id INT NOT NULL,
    refund_id VARCHAR(255) NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    reason TEXT,
    status ENUM('pending', 'completed', 'failed') DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (payment_id) REFERENCES payments(id) ON DELETE CASCADE,
    INDEX idx_payment_id (payment_id),
    INDEX idx_refund_id (refund_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Webhook ๋กœ๊ทธ ํ…Œ์ด๋ธ”
CREATE TABLE webhook_logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    provider ENUM('stripe', 'paypal') NOT NULL,
    event_id VARCHAR(255) UNIQUE NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    payload JSON NOT NULL,
    processed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_event_id (event_id),
    INDEX idx_processed (processed)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํด๋ž˜์Šค

src/Database/Connection.php:

<?php

namespace Database;

use PDO;
use PDOException;

class Connection
{
    private static $instance = null;
    private $pdo;
    
    private function __construct()
    {
        $host = $_ENV['DB_HOST'];
        $dbname = $_ENV['DB_NAME'];
        $user = $_ENV['DB_USER'];
        $pass = $_ENV['DB_PASS'];
        
        try {
            $this->pdo = new PDO(
                "mysql:host=$host;dbname=$dbname;charset=utf8mb4",
                $user,
                $pass,
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ]
            );
        } catch (PDOException $e) {
            die("Database connection failed: " . $e->getMessage());
        }
    }
    
    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    public function getConnection()
    {
        return $this->pdo;
    }
    
    // Singleton ํŒจํ„ด ๋ณดํ˜ธ
    private function __clone() {}
    public function __wakeup() {}
}

ํŠธ๋žœ์žญ์…˜ ๋กœ๊ฑฐ ํด๋ž˜์Šค

src/Database/TransactionLogger.php:

<?php

namespace Database;

use PDO;

class TransactionLogger
{
    private $pdo;
    
    public function __construct()
    {
        $this->pdo = Connection::getInstance()->getConnection();
    }
    
    /**
     * ์ฃผ๋ฌธ ์ƒ์„ฑ
     */
    public function createOrder(array $data)
    {
        $sql = "INSERT INTO orders (user_id, order_number, total_amount, currency, status) 
                VALUES (:user_id, :order_number, :total_amount, :currency, :status)";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'user_id' => $data['user_id'],
            'order_number' => $data['order_number'],
            'total_amount' => $data['total_amount'],
            'currency' => $data['currency'] ?? 'USD',
            'status' => $data['status'] ?? 'pending'
        ]);
        
        return $this->pdo->lastInsertId();
    }
    
    /**
     * ๊ฒฐ์ œ ์ •๋ณด ์ €์žฅ
     */
    public function logPayment(array $data)
    {
        $sql = "INSERT INTO payments 
                (order_id, payment_method, payment_id, transaction_id, amount, currency, status, payer_email, payment_data) 
                VALUES (:order_id, :payment_method, :payment_id, :transaction_id, :amount, :currency, :status, :payer_email, :payment_data)";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'order_id' => $data['order_id'],
            'payment_method' => $data['payment_method'],
            'payment_id' => $data['payment_id'],
            'transaction_id' => $data['transaction_id'] ?? null,
            'amount' => $data['amount'],
            'currency' => $data['currency'] ?? 'USD',
            'status' => $data['status'] ?? 'pending',
            'payer_email' => $data['payer_email'] ?? null,
            'payment_data' => json_encode($data['payment_data'] ?? [])
        ]);
        
        return $this->pdo->lastInsertId();
    }
    
    /**
     * ์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
     */
    public function updateOrderStatus(int $orderId, string $status)
    {
        $sql = "UPDATE orders SET status = :status WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute(['status' => $status, 'id' => $orderId]);
    }
    
    /**
     * ๊ฒฐ์ œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
     */
    public function updatePaymentStatus(string $paymentId, string $status)
    {
        $sql = "UPDATE payments SET status = :status WHERE payment_id = :payment_id";
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute(['status' => $status, 'payment_id' => $paymentId]);
    }
    
    /**
     * ํ™˜๋ถˆ ๊ธฐ๋ก
     */
    public function logRefund(array $data)
    {
        $sql = "INSERT INTO refunds (payment_id, refund_id, amount, reason, status) 
                VALUES (:payment_id, :refund_id, :amount, :reason, :status)";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'payment_id' => $data['payment_id'],
            'refund_id' => $data['refund_id'],
            'amount' => $data['amount'],
            'reason' => $data['reason'] ?? null,
            'status' => $data['status'] ?? 'pending'
        ]);
        
        return $this->pdo->lastInsertId();
    }
    
    /**
     * Webhook ์ด๋ฒคํŠธ ๋กœ๊ทธ
     */
    public function logWebhookEvent(string $provider, string $eventId, string $eventType, array $payload)
    {
        $sql = "INSERT INTO webhook_logs (provider, event_id, event_type, payload) 
                VALUES (:provider, :event_id, :event_type, :payload)
                ON DUPLICATE KEY UPDATE event_id = event_id"; // ์ค‘๋ณต ๋ฐฉ์ง€
        
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute([
            'provider' => $provider,
            'event_id' => $eventId,
            'event_type' => $eventType,
            'payload' => json_encode($payload)
        ]);
    }
    
    /**
     * Webhook ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ‘œ์‹œ
     */
    public function markWebhookProcessed(string $eventId)
    {
        $sql = "UPDATE webhook_logs SET processed = TRUE WHERE event_id = :event_id";
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute(['event_id' => $eventId]);
    }
    
    /**
     * ์ฃผ๋ฌธ ์กฐํšŒ
     */
    public function getOrder(int $orderId)
    {
        $sql = "SELECT * FROM orders WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => $orderId]);
        return $stmt->fetch();
    }
    
    /**
     * ๊ฒฐ์ œ ์ •๋ณด ์กฐํšŒ
     */
    public function getPaymentByPaymentId(string $paymentId)
    {
        $sql = "SELECT * FROM payments WHERE payment_id = :payment_id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['payment_id' => $paymentId]);
        return $stmt->fetch();
    }
}
๐Ÿ’ก ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒ:
1. ์ธ๋ฑ์Šค: ์ž์ฃผ ์กฐํšŒํ•˜๋Š” ์ปฌ๋Ÿผ์—๋Š” ์ธ๋ฑ์Šค๋ฅผ ๊ฑธ์–ด์ฃผ์ž. ์„ฑ๋Šฅ์ด ํ™• ๋‹ฌ๋ผ์ ธ!
2. JSON ์ปฌ๋Ÿผ: ์œ ์—ฐํ•œ ๋ฐ์ดํ„ฐ ์ €์žฅ์— ์œ ์šฉํ•ด. ํ•˜์ง€๋งŒ ๋„ˆ๋ฌด ๋‚จ์šฉํ•˜์ง€๋Š” ๋ง์ž.
3. ํŠธ๋žœ์žญ์…˜: ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์„ ๋™์‹œ์— ์—…๋ฐ์ดํŠธํ•  ๋•Œ๋Š” ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜์ž!

๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™”ํ•˜๊ธฐ

๊ฒฐ์ œ ์‹œ์Šคํ…œ์—์„œ ๋ณด์•ˆ์€ ์ •๋ง์ •๋ง ์ค‘์š”ํ•ด! ํ•œ ๋ฒˆ์˜ ๋ณด์•ˆ ์‚ฌ๊ณ ๋กœ ํšŒ์‚ฌ๊ฐ€ ๋งํ•  ์ˆ˜๋„ ์žˆ๊ฑฐ๋“ . ๐Ÿ˜ฑ ๊ธฐ๋ณธ์ ์ธ ๋ณด์•ˆ ์กฐ์น˜๋“ค์„ ์•Œ์•„๋ณด์ž!

1. CSRF ํ† ํฐ ์‚ฌ์šฉ

src/Security/CSRF.php:

<?php

namespace Security;

class CSRF
{
    /**
     * CSRF ํ† ํฐ ์ƒ์„ฑ
     */
    public static function generateToken()
    {
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    
    /**
     * CSRF ํ† ํฐ ๊ฒ€์ฆ
     */
    public static function validateToken($token)
    {
        if (!isset($_SESSION['csrf_token'])) {
            return false;
        }
        return hash_equals($_SESSION['csrf_token'], $token);
    }
    
    /**
     * HTML ํผ์— ์‚ฝ์ž…ํ•  hidden input ์ƒ์„ฑ
     */
    public static function getTokenField()
    {
        $token = self::generateToken();
        return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token) . '">';
    }
}

๊ฒฐ์ œ ํผ์— CSRF ํ† ํฐ ์ถ”๊ฐ€:

<form method="POST" action="process-payment.php">
    <?php echo \Security\CSRF::getTokenField(); ?>
    
    <!-- ๋‚˜๋จธ์ง€ ํผ ํ•„๋“œ๋“ค -->
</form>

2. ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ์‚ด๊ท 

src/Security/Validator.php:

<?php

namespace Security;

class Validator
{
    /**
     * ์ด๋ฉ”์ผ ๊ฒ€์ฆ
     */
    public static function validateEmail($email)
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
    
    /**
     * ๊ธˆ์•ก ๊ฒ€์ฆ
     */
    public static function validateAmount($amount)
    {
        if (!is_numeric($amount)) {
            return false;
        }
        
        $amount = floatval($amount);
        return $amount > 0 && $amount <= 999999.99;
    }
    
    /**
     * ๋ฌธ์ž์—ด ์‚ด๊ท 
     */
    public static function sanitizeString($string)
    {
        return htmlspecialchars(strip_tags(trim($string)), ENT_QUOTES, 'UTF-8');
    }
    
    /**
     * ์ˆซ์ž๋งŒ ์ถ”์ถœ
     */
    public static function sanitizeNumber($value)
    {
        return preg_replace('/[^0-9.]/', '', $value);
    }
    
    /**
     * ๊ฒฐ์ œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ
     */
    public static function validatePaymentData(array $data)
    {
        $errors = [];
        
        // ํ•„์ˆ˜ ํ•„๋“œ ํ™•์ธ
        $required = ['amount', 'currency'];
        foreach ($required as $field) {
            if (!isset($data[$field]) || empty($data[$field])) {
                $errors[] = "ํ•„๋“œ '$field'๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.";
            }
        }
        
        // ๊ธˆ์•ก ๊ฒ€์ฆ
        if (isset($data['amount']) && !self::validateAmount($data['amount'])) {
            $errors[] = "์œ ํšจํ•˜์ง€ ์•Š์€ ๊ธˆ์•ก์ž…๋‹ˆ๋‹ค.";
        }
        
        // ์ด๋ฉ”์ผ ๊ฒ€์ฆ (์„ ํƒ์ )
        if (isset($data['email']) && !empty($data['email']) && !self::validateEmail($data['email'])) {
            $errors[] = "์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค.";
        }
        
        return [
            'valid' => empty($errors),
            'errors' => $errors
        ];
    }
}

3. Rate Limiting (์š”์ฒญ ์ œํ•œ)

src/Security/RateLimiter.php:

<?php

namespace Security;

class RateLimiter
{
    private $redis;
    private $maxAttempts;
    private $decayMinutes;
    
    public function __construct($maxAttempts = 5, $decayMinutes = 1)
    {
        // Redis ์—ฐ๊ฒฐ (๋˜๋Š” ํŒŒ์ผ ๊ธฐ๋ฐ˜ ์ €์žฅ์†Œ)
        $this->maxAttempts = $maxAttempts;
        $this->decayMinutes = $decayMinutes;
    }
    
    /**
     * ์š”์ฒญ ์ œํ•œ ํ™•์ธ
     */
    public function tooManyAttempts($key)
    {
        $attempts = $this->getAttempts($key);
        return $attempts >= $this->maxAttempts;
    }
    
    /**
     * ์‹œ๋„ ํšŸ์ˆ˜ ์ฆ๊ฐ€
     */
    public function hit($key)
    {
        $cacheKey = $this->getCacheKey($key);
        $attempts = $this->getAttempts($key);
        
        if ($attempts === 0) {
            // ์ฒซ ์‹œ๋„
            $_SESSION[$cacheKey] = [
                'attempts' => 1,
                'expires_at' => time() + ($this->decayMinutes * 60)
            ];
        } else {
            $_SESSION[$cacheKey]['attempts']++;
        }
    }
    
    /**
     * ์‹œ๋„ ํšŸ์ˆ˜ ์กฐํšŒ
     */
    private function getAttempts($key)
    {
        $cacheKey = $this->getCacheKey($key);
        
        if (!isset($_SESSION[$cacheKey])) {
            return 0;
        }
        
        $data = $_SESSION[$cacheKey];
        
        // ๋งŒ๋ฃŒ ํ™•์ธ
        if (time() > $data['expires_at']) {
            unset($_SESSION[$cacheKey]);
            return 0;
        }
        
        return $data['attempts'];
    }
    
    /**
     * ์บ์‹œ ํ‚ค ์ƒ์„ฑ
     */
    private function getCacheKey($key)
    {
        return 'rate_limit_' . md5($key);
    }
    
    /**
     * ๋‚จ์€ ์‹œ๊ฐ„ (์ดˆ)
     */
    public function availableIn($key)
    {
        $cacheKey = $this->getCacheKey($key);
        
        if (!isset($_SESSION[$cacheKey])) {
            return 0;
        }
        
        $expiresAt = $_SESSION[$cacheKey]['expires_at'];
        return max(0, $expiresAt - time());
    }
}

์‚ฌ์šฉ ์˜ˆ์‹œ:

<?php

use Security\RateLimiter;

$rateLimiter = new RateLimiter(5, 1); // 1๋ถ„์— 5๋ฒˆ๊นŒ์ง€
$key = $_SERVER['REMOTE_ADDR']; // IP ์ฃผ์†Œ ๊ธฐ๋ฐ˜

if ($rateLimiter->tooManyAttempts($key)) {
    $seconds = $rateLimiter->availableIn($key);
    die("๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์ž…๋‹ˆ๋‹ค. {$seconds}์ดˆ ํ›„์— ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
}

$rateLimiter->hit($key);

// ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ๊ณ„์†...

4. SSL/TLS ๊ฐ•์ œ

.htaccess ํŒŒ์ผ์— ์ถ”๊ฐ€:

# HTTPS ๊ฐ•์ œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

# ๋ณด์•ˆ ํ—ค๋”
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://js.stripe.com; frame-src https://js.stripe.com https://www.paypal.com;"
โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง์•„์•ผ ํ•  ๊ฒƒ๋“ค:
1. ์‹ ์šฉ์นด๋“œ ์ •๋ณด๋ฅผ ์ง์ ‘ ์ €์žฅํ•˜์ง€ ๋งˆ! (PCI DSS ์œ„๋ฐ˜)
2. API ํ‚ค๋ฅผ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋งˆ!
3. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์ง€ ๋งˆ!
4. SQL ์ฟผ๋ฆฌ์— ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ง์ ‘ ๋„ฃ์ง€ ๋งˆ! (ํ•ญ์ƒ Prepared Statement ์‚ฌ์šฉ)
5. ๊ฒฐ์ œ ๊ธˆ์•ก์„ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ›์ง€ ๋งˆ! (์„œ๋ฒ„์—์„œ ๊ณ„์‚ฐ)

๐Ÿงช ํ…Œ์ŠคํŠธํ•˜๊ธฐ

๊ฒฐ์ œ ์‹œ์Šคํ…œ์€ ์ฒ ์ €ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์ˆ˜์•ผ! ์‹ค์ œ ๋ˆ์ด ์˜ค๊ฐ€๋Š” ๋ถ€๋ถ„์ด๋‹ˆ๊นŒ ๋”๋”์šฑ ์กฐ์‹ฌํ•ด์•ผ ํ•ด. ๐ŸŽฏ

ํ…Œ์ŠคํŠธ ์นด๋“œ ๋ฒˆํ˜ธ

์นด๋“œ์‚ฌ ์นด๋“œ ๋ฒˆํ˜ธ ์šฉ๋„
Visa 4242 4242 4242 4242 ์„ฑ๊ณต ํ…Œ์ŠคํŠธ
Visa (๊ฑฐ์ ˆ) 4000 0000 0000 0002 ์นด๋“œ ๊ฑฐ์ ˆ ํ…Œ์ŠคํŠธ
Visa (์ž”์•ก ๋ถ€์กฑ) 4000 0000 0000 9995 ์ž”์•ก ๋ถ€์กฑ ํ…Œ์ŠคํŠธ
Mastercard 5555 5555 5555 4444 ์„ฑ๊ณต ํ…Œ์ŠคํŠธ
American Express 3782 822463 10005 ์„ฑ๊ณต ํ…Œ์ŠคํŠธ

ํ…Œ์ŠคํŠธ ์‹œ ์‚ฌ์šฉํ•  ์ •๋ณด:

  • ๋งŒ๋ฃŒ์ผ: ๋ฏธ๋ž˜์˜ ์•„๋ฌด ๋‚ ์งœ (์˜ˆ: 12/25)
  • CVC: ์•„๋ฌด 3์ž๋ฆฌ ์ˆซ์ž (์˜ˆ: 123)
  • ์šฐํŽธ๋ฒˆํ˜ธ: ์•„๋ฌด 5์ž๋ฆฌ ์ˆซ์ž (์˜ˆ: 12345)

PayPal Sandbox ๊ณ„์ •

PayPal Developer ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ…Œ์ŠคํŠธ ๊ณ„์ •์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด:

1 Sandbox ๊ณ„์ • ์ƒ์„ฑ

developer.paypal.com โ†’ Sandbox โ†’ Accounts์—์„œ "Create Account" ํด๋ฆญ

2 ๊ณ„์ • ํƒ€์ž… ์„ ํƒ

Personal (๊ตฌ๋งค์ž)์™€ Business (ํŒ๋งค์ž) ๊ณ„์ •์„ ๊ฐ๊ฐ ๋งŒ๋“ค์–ด

3 ํ…Œ์ŠคํŠธ ๊ฒฐ์ œ

์ƒ์„ฑ๋œ ํ…Œ์ŠคํŠธ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•ด์„œ ๊ฒฐ์ œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด

์ž๋™ํ™” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

PHPUnit์„ ์‚ฌ์šฉํ•œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์˜ˆ์‹œ:

<?php

use PHPUnit\Framework\TestCase;
use Payment\StripePayment;
use Payment\PayPalPayment;

class PaymentTest extends TestCase
{
    private $stripeConfig;
    private $paypalConfig;
    
    protected function setUp(): void
    {
        $this->stripeConfig = require __DIR__ . '/../config/stripe.php';
        $this->paypalConfig = require __DIR__ . '/../config/paypal.php';
    }
    
    /**
     * Stripe ๊ฒฐ์ œ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ
     */
    public function testStripePaymentCreation()
    {
        $stripe = new StripePayment($this->stripeConfig);
        
        $result = $stripe->createPayment([
            'product_name' => 'Test Product',
            'description' => 'Test Description',
            'amount' => 10.00,
            'quantity' => 1,
            'success_url' => 'http://localhost/success',
            'cancel_url' => 'http://localhost/cancel'
        ]);
        
        $this->assertTrue($result['success']);
        $this->assertArrayHasKey('session_id', $result);
        $this->assertArrayHasKey('url', $result);
    }
    
    /**
     * PayPal ๊ฒฐ์ œ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ
     */
    public function testPayPalPaymentCreation()
    {
        $paypal = new PayPalPayment($this->paypalConfig);
        
        $result = $paypal->createPayment([
            'amount' => 10.00,
            'description' => 'Test Payment'
        ]);
        
        $this->assertTrue($result['success']);
        $this->assertArrayHasKey('payment_id', $result);
        $this->assertArrayHasKey('approval_url', $result);
    }
    
    /**
     * ๊ธˆ์•ก ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ
     */
    public function testAmountValidation()
    {
        $this->assertTrue(\Security\Validator::validateAmount(10.00));
        $this->assertTrue(\Security\Validator::validateAmount(0.01));
        $this->assertFalse(\Security\Validator::validateAmount(-10.00));
        $this->assertFalse(\Security\Validator::validateAmount(0));
        $this->assertFalse(\Security\Validator::validateAmount('invalid'));
    }
    
    /**
     * CSRF ํ† ํฐ ํ…Œ์ŠคํŠธ
     */
    public function testCSRFToken()
    {
        session_start();
        
        $token = \Security\CSRF::generateToken();
        $this->assertNotEmpty($token);
        
        $this->assertTrue(\Security\CSRF::validateToken($token));
        $this->assertFalse(\Security\CSRF::validateToken('invalid_token'));
    }
}

ํ…Œ์ŠคํŠธ ์‹คํ–‰:

composer require --dev phpunit/phpunit
./vendor/bin/phpunit tests/PaymentTest.php

๐Ÿš€ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๋“œ๋””์–ด ์‹ค์ œ ์„œ๋น„์Šค์— ๋ฐฐํฌํ•  ์ค€๋น„๊ฐ€ ๋์–ด! ํ•˜์ง€๋งŒ ์ž ๊น, ์ด๊ฒƒ๋“ค์€ ๊ผญ ํ™•์ธํ•˜๊ณ  ๊ฐ€์ž! โœ…

1 ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ „ํ™˜

.env ํŒŒ์ผ์—์„œ ํ…Œ์ŠคํŠธ ํ‚ค๋ฅผ ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ‚ค๋กœ ๋ณ€๊ฒฝ:

# Stripe
STRIPE_PUBLISHABLE_KEY=pk_live_your_real_key
STRIPE_SECRET_KEY=sk_live_your_real_key

# PayPal
PAYPAL_MODE=live  # sandbox์—์„œ live๋กœ ๋ณ€๊ฒฝ
2 SSL ์ธ์ฆ์„œ ํ™•์ธ

HTTPS๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธ. Let's Encrypt๋กœ ๋ฌด๋ฃŒ SSL ์ธ์ฆ์„œ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด!

3 Webhook URL ๋“ฑ๋ก

Stripe์™€ PayPal ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ”„๋กœ๋•์…˜ Webhook URL์„ ๋“ฑ๋ก:

  • Stripe: https://yourdomain.com/webhooks/stripe.php
  • PayPal: https://yourdomain.com/webhooks/paypal.php
4 ์—๋Ÿฌ ๋กœ๊น… ์„ค์ •

ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์—๋Ÿฌ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ฃผ์ง€ ๋ง๊ณ  ๋กœ๊ทธ ํŒŒ์ผ์— ๊ธฐ๋ก:

// php.ini ๋˜๋Š” .htaccess
display_errors = Off
log_errors = On
error_log = /path/to/logs/php-errors.log
5 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—…

์ •๊ธฐ์ ์ธ ์ž๋™ ๋ฐฑ์—… ์„ค์ •. ๊ฒฐ์ œ ๋ฐ์ดํ„ฐ๋Š” ์ ˆ๋Œ€ ์žƒ์–ด๋ฒ„๋ฆฌ๋ฉด ์•ˆ ๋ผ!

6 ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •

๊ฒฐ์ œ ์‹คํŒจ์œจ, ์‘๋‹ต ์‹œ๊ฐ„ ๋“ฑ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ ๊ตฌ์ถ•

7 ๋ณด์•ˆ ํ—ค๋” ํ™•์ธ

์•ž์„œ ์„ค์ •ํ•œ ๋ณด์•ˆ ํ—ค๋”๋“ค์ด ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ

8 ํ…Œ์ŠคํŠธ ๊ฒฐ์ œ ์ง„ํ–‰

์‹ค์ œ ์†Œ์•ก์œผ๋กœ ํ…Œ์ŠคํŠธ ๊ฒฐ์ œ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด๊ณ  ํ™˜๋ถˆ๊นŒ์ง€ ํ…Œ์ŠคํŠธ

9 ๋ฒ•์  ๋ฌธ์„œ ์ค€๋น„

์ด์šฉ์•ฝ๊ด€, ๊ฐœ์ธ์ •๋ณด์ฒ˜๋ฆฌ๋ฐฉ์นจ, ํ™˜๋ถˆ ์ •์ฑ… ๋“ฑ์„ ๋ช…ํ™•ํžˆ ์ž‘์„ฑ

10 ๊ณ ๊ฐ ์ง€์› ์ค€๋น„

๊ฒฐ์ œ ๊ด€๋ จ ๋ฌธ์˜์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ ๊ตฌ์ถ•

๐Ÿ’ก ์‹ค์ „ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์‹ค์ œ๋กœ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ์šด์˜ํ•˜๋ฉด์„œ ๋ฐฐ์šด ๋…ธํ•˜์šฐ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ! ๐ŸŽ“

1. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ตœ์ ํ™”

๐ŸŽจ ๊ฒฐ์ œ ํŽ˜์ด์ง€ ๋””์ž์ธ:
- ๋ถˆํ•„์š”ํ•œ ํ•„๋“œ๋Š” ์ตœ์†Œํ™” (์ด๋ฉ”์ผ, ๊ธˆ์•ก ์ •๋„๋งŒ)
- ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋ช…ํ™•ํžˆ ํ‘œ์‹œ (1๋‹จ๊ณ„/3๋‹จ๊ณ„ ๋“ฑ)
- ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ํ‘œ์‹œ (์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ)
- ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”๋Š” ํ•„์ˆ˜! (๋ชจ๋ฐ”์ผ ๊ฒฐ์ œ๊ฐ€ 50% ์ด์ƒ)

2. ์—๋Ÿฌ ์ฒ˜๋ฆฌ

๐Ÿšจ ์นœ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€:
- "๊ฒฐ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค" โŒ
- "์นด๋“œ๊ฐ€ ๊ฑฐ์ ˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์นด๋“œ๋ฅผ ์‹œ๋„ํ•˜๊ฑฐ๋‚˜ ์€ํ–‰์— ๋ฌธ์˜ํ•ด์ฃผ์„ธ์š”" โœ…

์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์Œ์— ๋ฌด์—‡์„ ํ•ด์•ผ ํ• ์ง€ ๋ช…ํ™•ํžˆ ์•Œ๋ ค์ฃผ์ž!

3. ์„ฑ๋Šฅ ์ตœ์ ํ™”

โšก ๋น ๋ฅธ ๊ฒฐ์ œ ๊ฒฝํ—˜:
- API ํ˜ธ์ถœ์€ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ
- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์ตœ์ ํ™” (์ธ๋ฑ์Šค ํ™œ์šฉ)
- ์บ์‹ฑ ํ™œ์šฉ (Redis, Memcached)
- CDN ์‚ฌ์šฉ (์ •์  ํŒŒ์ผ ๋กœ๋”ฉ ์†๋„ ํ–ฅ์ƒ)

4. ์žฌ์‹œ๋„ ๋กœ์ง

๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋‚˜ ์ผ์‹œ์ ์ธ ๋ฌธ์ œ๋กœ ๊ฒฐ์ œ๊ฐ€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์–ด. ์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์ž:

<?php

function retryPayment(callable $paymentFunction, $maxAttempts = 3)
{
    $attempt = 0;
    $lastError = null;
    
    while ($attempt < $maxAttempts) {
        try {
            $result = $paymentFunction();
            
            if ($result['success']) {
                return $result;
            }
            
            $lastError = $result['error'] ?? 'Unknown error';
            
        } catch (Exception $e) {
            $lastError = $e->getMessage();
        }
        
        $attempt++;
        
        if ($attempt < $maxAttempts) {
            // ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ (1์ดˆ, 2์ดˆ, 4์ดˆ...)
            sleep(pow(2, $attempt - 1));
        }
    }
    
    return [
        'success' => false,
        'error' => "๊ฒฐ์ œ ์‹คํŒจ (์‹œ๋„ {$maxAttempts}ํšŒ): " . $lastError
    ];
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
$result = retryPayment(function() use ($stripe, $paymentData) {
    return $stripe->createPayment($paymentData);
});

5. ์ด๋ฉ”์ผ ์•Œ๋ฆผ

๊ฒฐ์ œ ์™„๋ฃŒ, ์‹คํŒจ, ํ™˜๋ถˆ ๋“ฑ์˜ ์ƒํ™ฉ์—์„œ ๊ณ ๊ฐ์—๊ฒŒ ์ด๋ฉ”์ผ์„ ๋ณด๋‚ด์ž:

<?php

function sendPaymentConfirmationEmail($orderData)
{
    $to = $orderData['customer_email'];
    $subject = '๊ฒฐ์ œ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค - ์ฃผ๋ฌธ๋ฒˆํ˜ธ: ' . $orderData['order_number'];
    
    $message = "
    
    
        <style>
            body { font-family: Arial, sans-serif; line-height: 1.6; }
            .container { max-width: 600px; margin: 0 auto; padding: 20px; }
            .header { background: #6c5ce7; color: white; padding: 20px; text-align: center; }
            .content { padding: 20px; background: #f8f9fa; }
            .footer { text-align: center; padding: 20px; color: #636e72; }
        </style>
    
    
        <div class="container">
            <div class="header">
                <h1>๊ฒฐ์ œ ์™„๋ฃŒ</h1>
            </div>
            <div class="content">
                <p>์•ˆ๋…•ํ•˜์„ธ์š”,</p>
                <p>์ฃผ๋ฌธ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</p>
                <p><strong>์ฃผ๋ฌธ๋ฒˆํ˜ธ:</strong> {$orderData['order_number']}</p>
                <p><strong>๊ฒฐ์ œ๊ธˆ์•ก:</strong> \${$orderData['amount']}</p>
                <p><strong>๊ฒฐ์ œ๋ฐฉ๋ฒ•:</strong> {$orderData['payment_method']}</p>
                <p>๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!</p>
            </div>
            <div class="footer">
                <p>๋ฌธ์˜์‚ฌํ•ญ์ด ์žˆ์œผ์‹œ๋ฉด support@yourdomain.com์œผ๋กœ ์—ฐ๋ฝ์ฃผ์„ธ์š”.</p>
            </div>
        </div>
    
    
    ";
    
    $headers = "MIME-Version: 1.0" . "\r\n";
    $headers .= "Content-type:text/html;charset=UTF-8" . "\r\n";
    $headers .= "From: noreply@yourdomain.com" . "\r\n";
    
    mail($to, $subject, $message, $headers);
}

6. ๋ถ„์„ ๋ฐ ๋ฆฌํฌํŒ…

๊ฒฐ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•ด์„œ ๋น„์ฆˆ๋‹ˆ์Šค ์ธ์‚ฌ์ดํŠธ๋ฅผ ์–ป์ž:

<?php

class PaymentAnalytics
{
    private $pdo;
    
    public function __construct()
    {
        $this->pdo = \Database\Connection::getInstance()->getConnection();
    }
    
    /**
     * ์ผ๋ณ„ ๋งค์ถœ ์กฐํšŒ
     */
    public function getDailySales($startDate, $endDate)
    {
        $sql = "SELECT 
                    DATE(created_at) as date,
                    COUNT(*) as transaction_count,
                    SUM(amount) as total_amount,
                    payment_method
                FROM payments
                WHERE status = 'completed'
                AND created_at BETWEEN :start_date AND :end_date
                GROUP BY DATE(created_at), payment_method
                ORDER BY date DESC";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'start_date' => $startDate,
            'end_date' => $endDate
        ]);
        
        return $stmt->fetchAll();
    }
    
    /**
     * ๊ฒฐ์ œ ์„ฑ๊ณต๋ฅ  ๊ณ„์‚ฐ
     */
    public function getSuccessRate($period = '30 days')
    {
        $sql = "SELECT 
                    COUNT(*) as total,
                    SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful
                FROM payments
                WHERE created_at >= DATE_SUB(NOW(), INTERVAL $period)";
        
        $stmt = $this->pdo->query($sql);
        $result = $stmt->fetch();
        
        if ($result['total'] > 0) {
            return ($result['successful'] / $result['total']) * 100;
        }
        
        return 0;
    }
    
    /**
     * ์ธ๊ธฐ ๊ฒฐ์ œ ์ˆ˜๋‹จ
     */
    public function getPopularPaymentMethods()
    {
        $sql = "SELECT 
                    payment_method,
                    COUNT(*) as count,
                    SUM(amount) as total_amount
                FROM payments
                WHERE status = 'completed'
                AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
                GROUP BY payment_method
                ORDER BY count DESC";
        
        return $this->pdo->query($sql)->fetchAll();
    }
}

๐ŸŒŸ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

๊ธฐ๋ณธ์ ์ธ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ๋„˜์–ด์„œ ๋” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž! ๐Ÿš€

1. ์ •๊ธฐ ๊ฒฐ์ œ (๊ตฌ๋…)

Stripe Subscription์„ ์‚ฌ์šฉํ•œ ์ •๊ธฐ ๊ฒฐ์ œ:

<?php

namespace Payment;

use Stripe\Stripe;
use Stripe\Customer;
use Stripe\Subscription;
use Stripe\Price;

class StripeSubscription
{
    private $secretKey;
    
    public function __construct($secretKey)
    {
        $this->secretKey = $secretKey;
        Stripe::setApiKey($secretKey);
    }
    
    /**
     * ๊ตฌ๋… ์ƒํ’ˆ ์ƒ์„ฑ
     */
    public function createSubscriptionPrice($productName, $amount, $interval = 'month')
    {
        $price = Price::create([
            'product_data' => [
                'name' => $productName,
            ],
            'unit_amount' => $amount * 100,
            'currency' => 'usd',
            'recurring' => [
                'interval' => $interval, // day, week, month, year
            ],
        ]);
        
        return $price->id;
    }
    
    /**
     * ๊ณ ๊ฐ ์ƒ์„ฑ
     */
    public function createCustomer($email, $paymentMethodId)
    {
        $customer = Customer::create([
            'email' => $email,
            'payment_method' => $paymentMethodId,
            'invoice_settings' => [
                'default_payment_method' => $paymentMethodId,
            ],
        ]);
        
        return $customer->id;
    }
    
    /**
     * ๊ตฌ๋… ์ƒ์„ฑ
     */
    public function createSubscription($customerId, $priceId)
    {
        $subscription = Subscription::create([
            'customer' => $customerId,
            'items' => [
                ['price' => $priceId],
            ],
            'expand' => ['latest_invoice.payment_intent'],
        ]);
        
        return [
            'subscription_id' => $subscription->id,
            'status' => $subscription->status,
            'current_period_end' => $subscription->current_period_end,
        ];
    }
    
    /**
     * ๊ตฌ๋… ์ทจ์†Œ
     */
    public function cancelSubscription($subscriptionId)
    {
        $subscription = Subscription::retrieve($subscriptionId);
        $subscription->cancel();
        
        return true;
    }
}

2. ์ฟ ํฐ ๋ฐ ํ• ์ธ ์ฝ”๋“œ

<?php

class CouponManager
{
    private $pdo;
    
    public function __construct()
    {
        $this->pdo = \Database\Connection::getInstance()->getConnection();
    }
    
    /**
     * ์ฟ ํฐ ๊ฒ€์ฆ
     */
    public function validateCoupon($code)
    {
        $sql = "SELECT * FROM coupons 
                WHERE code = :code 
                AND active = 1 
                AND (expires_at IS NULL OR expires_at > NOW())
                AND (usage_limit IS NULL OR usage_count < usage_limit)";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['code' => $code]);
        $coupon = $stmt->fetch();
        
        if (!$coupon) {
            return [
                'valid' => false,
                'message' => '์œ ํšจํ•˜์ง€ ์•Š์€ ์ฟ ํฐ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.'
            ];
        }
        
        return [
            'valid' => true,
            'coupon' => $coupon
        ];
    }
    
    /**
     * ํ• ์ธ ๊ธˆ์•ก ๊ณ„์‚ฐ
     */
    public function calculateDiscount($amount, $coupon)
    {
        if ($coupon['type'] === 'percentage') {
            $discount = $amount * ($coupon['value'] / 100);
        } else {
            $discount = $coupon['value'];
        }
        
        // ์ตœ๋Œ€ ํ• ์ธ ๊ธˆ์•ก ์ œํ•œ
        if (isset($coupon['max_discount']) && $discount > $coupon['max_discount']) {
            $discount = $coupon['max_discount'];
        }
        
        return [
            'original_amount' => $amount,
            'discount' => $discount,
            'final_amount' => max(0, $amount - $discount)
        ];
    }
    
    /**
     * ์ฟ ํฐ ์‚ฌ์šฉ ๊ธฐ๋ก
     */
    public function useCoupon($couponId, $userId)
    {
        // ์‚ฌ์šฉ ํšŸ์ˆ˜ ์ฆ๊ฐ€
        $sql = "UPDATE coupons SET usage_count = usage_count + 1 WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => $couponId]);
        
        // ์‚ฌ์šฉ ๊ธฐ๋ก ์ €์žฅ
        $sql = "INSERT INTO coupon_usage (coupon_id, user_id, used_at) VALUES (:coupon_id, :user_id, NOW())";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            'coupon_id' => $couponId,
            'user_id' => $userId
        ]);
    }
}

3. ๋‹ค์ค‘ ํ†ตํ™” ์ง€์›

<?php

class CurrencyConverter
{
    private $apiKey;
    private $baseCurrency = 'USD';
    
    public function __construct($apiKey)
    {
        $this->apiKey = $apiKey;
    }
    
    /**
     * ํ™˜์œจ ์กฐํšŒ (์บ์‹ฑ ํฌํ•จ)
     */
    public function getExchangeRate($fromCurrency, $toCurrency)
    {
        $cacheKey = "exchange_rate_{$fromCurrency}_{$toCurrency}";
        
        // ์บ์‹œ ํ™•์ธ (1์‹œ๊ฐ„ ์œ ํšจ)
        if (isset($_SESSION[$cacheKey]) && 
            $_SESSION[$cacheKey]['expires'] > time()) {
            return $_SESSION[$cacheKey]['rate'];
        }
        
        // API ํ˜ธ์ถœ (์˜ˆ: exchangerate-api.com)
        $url = "https://api.exchangerate-api.com/v4/latest/{$fromCurrency}";
        $response = file_get_contents($url);
        $data = json_decode($response, true);
        
        $rate = $data['rates'][$toCurrency] ?? 1;
        
        // ์บ์‹œ ์ €์žฅ
        $_SESSION[$cacheKey] = [
            'rate' => $rate,
            'expires' => time() + 3600 // 1์‹œ๊ฐ„
        ];
        
        return $rate;
    }
    
    /**
     * ๊ธˆ์•ก ๋ณ€ํ™˜
     */
    public function convert($amount, $fromCurrency, $toCurrency)
    {
        if ($fromCurrency === $toCurrency) {
            return $amount;
        }
        
        $rate = $this->getExchangeRate($fromCurrency, $toCurrency);
        return round($amount * $rate, 2);
    }
    
    /**
     * ํ†ตํ™” ๊ธฐํ˜ธ ๊ฐ€์ ธ์˜ค๊ธฐ
     */
    public function getCurrencySymbol($currency)
    {
        $symbols = [
            'USD' => '$',
            'EUR' => 'โ‚ฌ',
            'GBP' => 'ยฃ',
            'JPY' => 'ยฅ',
            'KRW' => 'โ‚ฉ',
            'CNY' => 'ยฅ',
        ];
        
        return $symbols[$currency] ?? $currency;
    }
}

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

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์™”๋‹ค๋‹ˆ ์ •๋ง ๋Œ€๋‹จํ•ด! ๐ŸŽ‰ ์šฐ๋ฆฌ๊ฐ€ ํ•จ๊ป˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด์ž:

โœจ ํ•ต์‹ฌ ์š”์•ฝ

โœ… Stripe์™€ PayPal API ํ†ตํ•ฉ ๋ฐฉ๋ฒ•

โœ… ์•ˆ์ „ํ•œ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

โœ… Webhook์„ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ

โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ

โœ… ๋ณด์•ˆ ๊ฐ•ํ™” ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

โœ… ์ •๊ธฐ ๊ฒฐ์ œ, ์ฟ ํฐ, ๋‹ค์ค‘ ํ†ตํ™” ๋“ฑ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

๊ฒฐ์ œ ์‹œ์Šคํ…œ์€ ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด๋†“์œผ๋ฉด ๊ณ„์† ๋ฐœ์ „์‹œ์ผœ ๋‚˜๊ฐˆ ์ˆ˜ ์žˆ์–ด. ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์•„์„œ ๊ฐœ์„ ํ•˜๊ณ , ์ƒˆ๋กœ์šด ๊ฒฐ์ œ ์ˆ˜๋‹จ์„ ์ถ”๊ฐ€ํ•˜๊ณ , ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ๊ฑฐ์ง€! ๐Ÿ’ช

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์ด ํ•ต์‹ฌ์ด์•ผ. ์žฌ๋Šฅ์„ ์‚ฌ๊ณ ํŒŒ๋Š” ์‚ฌ๋žŒ๋“ค์ด ์•ˆ์ „ํ•˜๊ณ  ํŽธ๋ฆฌํ•˜๊ฒŒ ๊ฑฐ๋ž˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ง์ด์•ผ. ๋„ˆ๋„ ์ด์ œ ๋ฉ‹์ง„ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด!

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„:
1. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด๊ธฐ
2. ์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ ์ง„ํ–‰ํ•˜๊ธฐ
3. ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ตœ์ ํ™”
4. ์ƒˆ๋กœ์šด ๊ฒฐ์ œ ์ˆ˜๋‹จ ์ถ”๊ฐ€ (Toss, ์นด์นด์˜คํŽ˜์ด ๋“ฑ)
5. ๊ตญ์ œ ๊ฒฐ์ œ ์ง€์› ํ™•๋Œ€

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋„์›€์ด ํ•„์š”ํ•˜๋ฉด ์–ธ์ œ๋“  Stripe์™€ PayPal์˜ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์ž. ๋‘ ์„œ๋น„์Šค ๋ชจ๋‘ ์ •๋ง ํ›Œ๋ฅญํ•œ ๋ฌธ์„œ์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด! ๐Ÿ“š

์ž, ์ด์ œ ๋‚˜๊ฐ€์„œ ๋ฉ‹์ง„ ๊ฒฐ์ œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ํ™”์ดํŒ…! ๐ŸŽฏโœจ

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

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

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

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