Ver3.0 ๐ณ PHP ๊ฒฐ์ ์์คํ ๊ณผ Stripe PayPal API ํตํฉ ๊ตฌํ

๐ณ PHP ๊ฒฐ์ ์์คํ ๊ณผ Stripe PayPal API ํตํฉ ๊ตฌํ
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ๋ฐฐ์ฐ๋ ๊ฒฐ์ ์์คํ ์์ ์ ๋ณต!
๐ฏ ์ ๊ฒฐ์ ์์คํ ํตํฉ์ด ์ค์ํ ๊น?
์ผ, ์น๊ตฌ! ์จ๋ผ์ธ ์ผํ๋ชฐ์ด๋ ์๋น์ค ํ๋ซํผ์ ๋ง๋ค๊ณ ์๋ค๋ฉด ๊ฒฐ์ ์์คํ ์ ์ ๋ง ํต์ฌ ์ค์ ํต์ฌ์ด์ผ. ๐ ์๋ฌด๋ฆฌ ๋ฉ์ง ์ํ์ ํ์๋ ๊ฒฐ์ ๊ฐ ์ ๋๋ฉด ๋ง์งฑ ๋๋ฃจ๋ฌต์ด์์? ๊ทธ๋์ ์ค๋์ PHP๋ก Stripe์ PayPal ๊ฐ์ ๊ธ๋ก๋ฒ ๊ฒฐ์ ์์คํ ์ ์ด๋ป๊ฒ ํตํฉํ๋์ง ์์ ์ ๋ณตํด๋ณผ ๊ฑฐ์ผ!
์์ฆ ์ฌ์ฉ์๋ค์ ๋ค์ํ ๊ฒฐ์ ์๋จ์ ์ํด. ์ด๋ค ์ฌ๋์ ์ ์ฉ์นด๋๋ฅผ ์ ํธํ๊ณ , ์ด๋ค ์ฌ๋์ PayPal ๊ณ์ ์ผ๋ก ๊ฐํธํ๊ฒ ๊ฒฐ์ ํ๊ณ ์ถ์ด ํ์ง. ๋ค์ํ ๊ฒฐ์ ์ต์ ์ ์ ๊ณตํ๋ฉด ์ ํ์จ์ด ์ต๋ 30%๊น์ง ์ฌ๋ผ๊ฐ๋ค๋ ์ฐ๊ตฌ ๊ฒฐ๊ณผ๋ ์์ด! ๐ฐ
๐ Stripe vs PayPal: ๋ญ๊ฐ ๋ค๋ฅผ๊น?
๋ณธ๊ฒฉ์ ์ผ๋ก ์ฝ๋ฉ์ ๋ค์ด๊ฐ๊ธฐ ์ ์, ๋ ๊ฒฐ์ ์์คํ ์ ํน์ง์ ๋จผ์ ์์๋ณด์! ๊ฐ๊ฐ ์ฅ๋จ์ ์ด ์๊ฑฐ๋ . ๐
| ํน์ง | Stripe ๐ | PayPal ๐ |
|---|---|---|
| ๊ฐ๋ฐ์ ์นํ์ฑ | ๋งค์ฐ ์ฐ์ (RESTful API, ํ๋ถํ ๋ฌธ์) | ์ข์ (๋ค์ ๋ณต์กํ ๊ตฌ์กฐ) |
| ์์๋ฃ | 2.9% + $0.30 (๋ฏธ๊ตญ ๊ธฐ์ค) | 2.9% + $0.30 (์ ์ฌ) |
| ์ฌ์ฉ์ ์ธ์ง๋ | ๊ฐ๋ฐ์/์คํํธ์ ์์ ์ธ๊ธฐ | ์ผ๋ฐ ์ฌ์ฉ์์๊ฒ ๋งค์ฐ ๋์ |
| ์ ์ฐ ์๋ | 2-7์ผ | ์ฆ์ ๊ฐ๋ฅ (PayPal ์์ก) |
| ๊ตญ์ ์ง์ | 46๊ฐ๊ตญ ์ด์ | 200๊ฐ๊ตญ ์ด์ |
| ์ปค์คํฐ๋ง์ด์ง | ๋งค์ฐ ์์ ๋ก์ | ์ ํ์ |
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ค๋นํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ธํ ํด์ผ ํด. ๊ฑฑ์ ๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์๋ ค์ค๊ฒ! ๐
PHP 7.4 ์ด์์ด ํ์ํด. ํฐ๋ฏธ๋์์ ํ์ธํด๋ณด์:
php -v
๋ง์ฝ ๋ฒ์ ์ด ๋ฎ๋ค๋ฉด ์ ๋ฐ์ดํธ๊ฐ ํ์ํด!
PHP์ ํจํค์ง ๊ด๋ฆฌ์์ธ Composer๊ฐ ํ์์ผ. ์ด๋ฏธ ์ค์น๋์ด ์๋์ง ํ์ธ:
composer --version
์๋ค๋ฉด getcomposer.org์์ ๋ค์ด๋ก๋ํ์!
๊น๋ํ๊ฒ ํ๋ก์ ํธ ํด๋๋ฅผ ๋ง๋ค์ด๋ณด์:
mkdir payment-integration
cd payment-integration
composer init
Stripe์ PayPal SDK๋ฅผ ํ ๋ฒ์ ์ค์น!
composer require stripe/stripe-php
composer require paypal/rest-api-sdk-php
๐ API ํค ๋ฐ๊ธ๋ฐ๊ธฐ
๊ฒฐ์ ์์คํ ์ ์ฌ์ฉํ๋ ค๋ฉด ๊ฐ ์๋น์ค์์ API ํค๋ฅผ ๋ฐ๊ธ๋ฐ์์ผ ํด. ์ด๊ฑด ๋ง์น ์ง ์ด์ ๊ฐ์ ๊ฑฐ์ผ! ๐
Stripe API ํค ๋ฐ๊ธ
stripe.com์ ์ ์ํด์ ๋ฌด๋ฃ ๊ณ์ ์ ๋ง๋ค์ด. ์ด๋ฉ์ผ ์ธ์ฆ๋ง ํ๋ฉด ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅํด!
๋ก๊ทธ์ธ ํ ์ผ์ชฝ ๋ฉ๋ด์์ "Developers" โ "API keys"๋ฅผ ํด๋ฆญํด.
๋ ๊ฐ์ง ํค๊ฐ ๋ณด์ผ ๊ฑฐ์ผ:
- Publishable key: ํด๋ผ์ด์ธํธ ์ธก์์ ์ฌ์ฉ (๊ณต๊ฐ ๊ฐ๋ฅ)
- Secret key: ์๋ฒ ์ธก์์ ์ฌ์ฉ (์ ๋ ๊ณต๊ฐ ๊ธ์ง!)
4242 4242 4242 4242๋ฅผ ์ฌ์ฉํ๋ฉด ๋ผ.
PayPal API ํค ๋ฐ๊ธ
developer.paypal.com์ ์ ์ํด์ ๋ก๊ทธ์ธํด. ์ผ๋ฐ PayPal ๊ณ์ ์ผ๋ก๋ ๋ก๊ทธ์ธ ๊ฐ๋ฅํด!
"My Apps & Credentials"๋ก ๊ฐ์ "Create App" ๋ฒํผ์ ํด๋ฆญํด.
PayPal์ Sandbox(ํ ์คํธ ํ๊ฒฝ)๋ฅผ ์ ๊ณตํด. ๊ฐ์์ ๊ตฌ๋งค์์ ํ๋งค์ ๊ณ์ ์ ๋ง๋ค์ด์ ํ ์คํธํ ์ ์์ด!
์ฑ์ ๋ง๋ค๋ฉด 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
.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 ๊ฒฐ์ ๊ตฌํํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก 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;
}
}
}
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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.