Ver3.5 ๐๏ธ ๋นํ์ ์ฃผ๋ฌธ & ๊ฒ์คํธ ์ฒดํฌ์์ ์์ ์ ๋ณตํ์๊ฐ์ ์์ด๋ ์ฒ์ฒ! ์ผํ๋ชฐ ์ ํ์จ์ ๋์ด๋ ๊ตฌํ ์ ๋ต

๐๏ธ ๋นํ์ ์ฃผ๋ฌธ & ๊ฒ์คํธ ์ฒดํฌ์์ ์์ ์ ๋ณต
ํ์๊ฐ์
์์ด๋ ์ฒ์ฒ! ์ผํ๋ชฐ ์ ํ์จ์ ๋์ด๋ ๊ตฌํ ์ ๋ต
์ผ, ์์งํ ๋งํด๋ด. ๐ค
์ผํ๋ชฐ์์ ๋ญ๊ฐ ์ฌ๋ ค๊ณ ํ๋๋ฐ "ํ์๊ฐ์
๋จผ์ ํ์ธ์!" ๋ผ๋ ๋ฌธ๊ตฌ ๋ณด๊ณ ๊ทธ๋ฅ ๋ค๋ก๊ฐ๊ธฐ ๋๋ฅธ ์ ์์ง ์์?
๋๋ง ๊ทธ๋ฐ ๊ฑฐ ์๋์ผ. ํต๊ณ์ ์ผ๋ก ์ผํ๋ชฐ ์ดํ๋ฅ ์ ์ฝ 23~35%๊ฐ ๋ฐ๋ก ์ด '๊ฐ์ ํ์๊ฐ์
' ๋๋ฌธ์ ๋ฐ์ํ๋ค๊ณ ํด.
๊ทธ๋์ ์์ฆ ์ ๋๊ฐ๋ ์ผํ๋ชฐ๋ค์ ๊ฒ์คํธ ์ฒดํฌ์์(Guest Checkout), ์ฆ ๋นํ์ ์ฃผ๋ฌธ ๊ธฐ๋ฅ์ ํ์๋ก ๋ฃ๊ณ ์์ด.
์ด ๊ธ์์๋ ๋นํ์ ์ฃผ๋ฌธ์ด ์ ์ค์ํ์ง๋ถํฐ, ์ค์ ๋ก ์ด๋ป๊ฒ ๊ตฌํํ๋์ง๊น์ง ๊ฐ๋ฐ์ ๊ด์ ์์ ํฉํธ ์์ฃผ๋ก ์ซ ํ์ด๋ณผ๊ฒ.
๋ฐฑ์๋ ๋ก์ง, ์ธ์
๊ด๋ฆฌ, ๋ณด์ ์ฒ๋ฆฌ, ์ฃผ๋ฌธ ์กฐํ๊น์ง โ ํ๋๋ ๋น ์ง์์ด ๋ค ๋ค๋ฃฐ ๊ฑฐ์ผ. ์ค๋น๋์ง? ๐ฅ
๐ ์ ๊ฒ์คํธ ์ฒดํฌ์์์ด ํ์์ธ๊ฐ? โ ์ซ์๋ก ๋ณด๋ ํ์ค
๊ฐ๋ฐ์ ์
์ฅ์์ "๊ทธ๋ฅ ํ์๊ฐ์
์ํค๋ฉด ๋์ง ์๋?" ์ถ์ ์ ์์ด.
๊ทผ๋ฐ ๋ง์ผํ
ยทUX ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ฉด ์๊ฐ์ด ๋ฌ๋ผ์ง ๊ฑฐ์ผ.
์ผํ๋ชฐ ์ฅ๋ฐ๊ตฌ๋ ์ดํ ์์ธ 1์: "์ฌ์ดํธ๊ฐ ๊ณ์ ์์ฑ์ ๊ฐ์ํด์" (24%)
๊ฒ์คํธ ์ฒดํฌ์์ ์ ๊ณต ์ ์ ํ์จ ํ๊ท 35% ํฅ์
Amazon, Shopify, WooCommerce ๋ฑ ์ฃผ์ ํ๋ซํผ ๋ชจ๋ ๊ธฐ๋ณธ ์ ๊ณต
ํนํ ๋ชจ๋ฐ์ผ ์ผํ ํ๊ฒฝ์์๋ ๋ ์ฌ๊ฐํด.
์์ ํ๋ฉด์์ ํ์๊ฐ์
ํผ ๋ค ์ฑ์ฐ๋ ๊ฑฐ ์ง์ง ๊ท์ฐฎ๊ฑฐ๋ .
๊ทธ๋์ ๋ชจ๋ฐ์ผ ์ ํ์จ์ ๋์ด๋ ค๋ฉด ๊ฒ์คํธ ์ฒดํฌ์์์ ์ ํ์ด ์๋๋ผ ํ์์ผ.
๐๏ธ ๊ฒ์คํธ ์ฒดํฌ์์ ์ค๊ณ์ ํต์ฌ ๊ฐ๋
์, ์ด์ ์ง์ง ๊ฐ๋ฐ ์๊ธฐ ๋ค์ด๊ฐ๋ณด์.
๊ฒ์คํธ ์ฒดํฌ์์์ ๊ตฌํํ ๋ ๊ฐ์ฅ ๋จผ์ ์ดํดํด์ผ ํ ๊ฑด "๋นํ์์ ์ด๋ป๊ฒ ์๋ณํ๊ณ ์ถ์ ํ๋๋"์ผ.
๐ ๋นํ์ ์๋ณ ์ ๋ต 3๊ฐ์ง
์๋ฒ ์ธก ์ธ์ ์ ์์ ์ฅ๋ฐ๊ตฌ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋ฐฉ์.
๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์ผ๋ฉด ๋ฐ์ดํฐ๊ฐ ์ฌ๋ผ์ง๋ ๋จ์ ์ด ์์ด.
PHPNode.jsDjango
UUID ๊ฐ์ ๊ณ ์ ํ ํฐ์ ์์ฑํด์ ์ฟ ํค๋ localStorage์ ์ ์ฅ.
๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์๋ ์ ์ง๋๊ณ , ์๋ฒ์์ ์ฃผ๋ฌธ ์ ๋ณด์ ์ฐ๊ฒฐ ๊ฐ๋ฅ.
JWTUUIDCookie
์ฒดํฌ์์ ์ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ๋ฐ์์ ์ฃผ๋ฌธ์ ํค(key)๋ก ์ฌ์ฉ.
์ฃผ๋ฌธ ์กฐํ, ๋ฐฐ์ก ์๋ฆผ ๋ฑ์ ํ์ฉ ๊ฐ๋ฅ.
EmailOrder Token
ํ ํฐ์ผ๋ก ์ธ์ ์ ์ ์งํ๊ณ , ์ด๋ฉ์ผ๋ก ์ฃผ๋ฌธ ํ์ธ ๋ฐ ์กฐํ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด ๊ฐ์ฅ ์์ ์ ์ด์ผ.
๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ โ ๋นํ์ ์ฃผ๋ฌธ์ ์ด๋ป๊ฒ ์ ์ฅํ ๊น?
ํ์ ์ฃผ๋ฌธ๊ณผ ๋นํ์ ์ฃผ๋ฌธ์ ๊ฐ์ ํ
์ด๋ธ์์ ๊ด๋ฆฌํ๋ ๊ฒ ์ผ๋ฐ์ ์ด์ผ.
ํต์ฌ์ user_id๋ฅผ NULL ํ์ฉ์ผ๋ก ์ค๊ณํ๋ ๊ฑฐ์ผ.
-- ์ฃผ๋ฌธ ํ
์ด๋ธ ์ค๊ณ ์์ (MySQL)
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_number VARCHAR(30) NOT NULL UNIQUE, -- ์ฃผ๋ฌธ๋ฒํธ
user_id BIGINT UNSIGNED NULL, -- ํ์์ด๋ฉด FK, ๋นํ์์ด๋ฉด NULL
guest_token VARCHAR(64) NULL, -- ๋นํ์ ์๋ณ ํ ํฐ
guest_email VARCHAR(255) NULL, -- ๋นํ์ ์ด๋ฉ์ผ
guest_name VARCHAR(100) NULL, -- ๋นํ์ ์ด๋ฆ
guest_phone VARCHAR(20) NULL, -- ๋นํ์ ์ฐ๋ฝ์ฒ
status ENUM('pending','paid','shipping','done','cancel') DEFAULT 'pending',
total_price DECIMAL(12,2) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
-- ์ฃผ๋ฌธ ์ํ ํ
์ด๋ธ
CREATE TABLE order_items (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
quantity INT NOT NULL,
price DECIMAL(12,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
-- ๋ฐฐ์ก์ง ํ
์ด๋ธ
CREATE TABLE order_shipping (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL UNIQUE,
recipient VARCHAR(100) NOT NULL,
address1 VARCHAR(255) NOT NULL,
address2 VARCHAR(255),
zip_code VARCHAR(10) NOT NULL,
phone VARCHAR(20) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
โข
user_id๋ NULL ํ์ฉ โ ๋นํ์ ์ฃผ๋ฌธ ์ NULLโข
guest_token: UUID๋ก ์์ฑํ ๊ณ ์ ํ ํฐ (์ฃผ๋ฌธ ์กฐํ์ ์ฌ์ฉ)โข
guest_email: ์ฃผ๋ฌธ ํ์ธ ๋ฉ์ผ ๋ฐ์ก ๋ฐ ์กฐํ ํคโข ํ์/๋นํ์ ์ฃผ๋ฌธ์ ํ๋์ ํ ์ด๋ธ์์ ํตํฉ ๊ด๋ฆฌ โ ๊ด๋ฆฌ ํจ์จ โ
โ๏ธ ๋ฐฑ์๋ ๊ตฌํ โ Node.js(Express) ๊ธฐ์ค ์ค์ ์ฝ๋
์, ์ด์ ์ง์ง ์ฝ๋ ๋ค์ด๊ฐ๋ณด์! ๐ฅ
Node.js + Express ๊ธฐ์ค์ผ๋ก ์ค๋ช
ํ ๊ฒ. ๋ค๋ฅธ ์ธ์ด๋ ๋ก์ง์ ๋๊ฐ์ผ๋๊น ์ฐธ๊ณ ํด๋ด.
1๏ธโฃ ๊ฒ์คํธ ํ ํฐ ์์ฑ ๋ฐ ์ฅ๋ฐ๊ตฌ๋ ๊ด๋ฆฌ
// utils/guestToken.js
const { v4: uuidv4 } = require('uuid');
// ๊ฒ์คํธ ํ ํฐ ์์ฑ ๋๋ ๊ธฐ์กด ํ ํฐ ๋ฐํ
function getOrCreateGuestToken(req, res) {
let token = req.cookies.guest_token;
if (!token) {
token = uuidv4(); // ์: "550e8400-e29b-41d4-a716-446655440000"
// httpOnly ์ฟ ํค๋ก ์ค์ (JS์์ ์ ๊ทผ ๋ถ๊ฐ โ ๋ณด์ ๊ฐํ)
res.cookie('guest_token', token, {
maxAge: 30 * 24 * 60 * 60 * 1000, // 30์ผ
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // HTTPS์์๋ง
sameSite: 'lax'
});
}
return token;
}
module.exports = { getOrCreateGuestToken };
// routes/cart.js
const express = require('express');
const router = express.Router();
const { getOrCreateGuestToken } = require('../utils/guestToken');
const db = require('../db');
// ์ฅ๋ฐ๊ตฌ๋์ ์ํ ์ถ๊ฐ
router.post('/add', async (req, res) => {
const { product_id, quantity } = req.body;
// ๋ก๊ทธ์ธ ์ฌ๋ถ ํ์ธ
const userId = req.session?.user?.id || null;
const guestToken = userId ? null : getOrCreateGuestToken(req, res);
// ์ฅ๋ฐ๊ตฌ๋ ํ
์ด๋ธ์ ์ ์ฅ
await db.query(
`INSERT INTO cart_items (user_id, guest_token, product_id, quantity)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE quantity = quantity + ?`,
[userId, guestToken, product_id, quantity, quantity]
);
res.json({ success: true, message: '์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ๋์์ต๋๋ค.' });
});
// ์ฅ๋ฐ๊ตฌ๋ ์กฐํ
router.get('/', async (req, res) => {
const userId = req.session?.user?.id || null;
const guestToken = req.cookies.guest_token || null;
let items;
if (userId) {
items = await db.query(
`SELECT ci.*, p.name, p.price, p.image
FROM cart_items ci JOIN products p ON ci.product_id = p.id
WHERE ci.user_id = ?`, [userId]
);
} else if (guestToken) {
items = await db.query(
`SELECT ci.*, p.name, p.price, p.image
FROM cart_items ci JOIN products p ON ci.product_id = p.id
WHERE ci.guest_token = ?`, [guestToken]
);
} else {
items = [];
}
res.json({ items });
});
2๏ธโฃ ๊ฒ์คํธ ์ฒดํฌ์์ โ ์ฃผ๋ฌธ ์์ฑ API
// routes/checkout.js
const express = require('express');
const router = express.Router();
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const mailer = require('../utils/mailer');
// ๊ฒ์คํธ ์ฃผ๋ฌธ ์์ฑ
router.post('/guest', async (req, res) => {
const {
guest_email,
guest_name,
guest_phone,
recipient,
address1,
address2,
zip_code,
shipping_phone,
payment_method
} = req.body;
// ์
๋ ฅ๊ฐ ๊ฒ์ฆ
if (!guest_email || !guest_name || !address1) {
return res.status(400).json({ error: 'ํ์ ์ ๋ณด๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.' });
}
const guestToken = req.cookies.guest_token;
if (!guestToken) {
return res.status(400).json({ error: '์ฅ๋ฐ๊ตฌ๋ ์ ๋ณด๊ฐ ์์ต๋๋ค.' });
}
// ์ฅ๋ฐ๊ตฌ๋ ์กฐํ
const cartItems = await db.query(
`SELECT ci.quantity, p.price, p.id as product_id, p.stock
FROM cart_items ci JOIN products p ON ci.product_id = p.id
WHERE ci.guest_token = ?`, [guestToken]
);
if (!cartItems.length) {
return res.status(400).json({ error: '์ฅ๋ฐ๊ตฌ๋๊ฐ ๋น์ด์์ต๋๋ค.' });
}
// ์ฌ๊ณ ํ์ธ
for (const item of cartItems) {
if (item.stock < item.quantity) {
return res.status(400).json({ error: `์ฌ๊ณ ๊ฐ ๋ถ์กฑํ ์ํ์ด ์์ต๋๋ค.` });
}
}
// ์ด ๊ธ์ก ๊ณ์ฐ
const totalPrice = cartItems.reduce(
(sum, item) => sum + (item.price * item.quantity), 0
);
// ์ฃผ๋ฌธ๋ฒํธ ์์ฑ (์: ORD-20240115-A3F2)
const orderNumber = `ORD-${Date.now()}-${Math.random().toString(36).substr(2,4).toUpperCase()}`;
// ์ฃผ๋ฌธ ๊ณ ์ ์กฐํ ํ ํฐ (์ด๋ฉ์ผ์ ํฌํจ๋ ๋งํฌ์ฉ)
const orderToken = uuidv4();
// ํธ๋์ญ์
์์
const conn = await db.getConnection();
await conn.beginTransaction();
try {
// 1. ์ฃผ๋ฌธ ์์ฑ
const [orderResult] = await conn.query(
`INSERT INTO orders
(order_number, user_id, guest_token, guest_email, guest_name, guest_phone, total_price, order_token)
VALUES (?, NULL, ?, ?, ?, ?, ?, ?)`,
[orderNumber, guestToken, guest_email, guest_name, guest_phone, totalPrice, orderToken]
);
const orderId = orderResult.insertId;
// 2. ์ฃผ๋ฌธ ์ํ ์ ์ฅ
for (const item of cartItems) {
await conn.query(
`INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (?, ?, ?, ?)`,
[orderId, item.product_id, item.quantity, item.price]
);
// ์ฌ๊ณ ์ฐจ๊ฐ
await conn.query(
`UPDATE products SET stock = stock - ? WHERE id = ?`,
[item.quantity, item.product_id]
);
}
// 3. ๋ฐฐ์ก์ง ์ ์ฅ
await conn.query(
`INSERT INTO order_shipping (order_id, recipient, address1, address2, zip_code, phone)
VALUES (?, ?, ?, ?, ?, ?)`,
[orderId, recipient, address1, address2, zip_code, shipping_phone]
);
// 4. ์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ
await conn.query(
`DELETE FROM cart_items WHERE guest_token = ?`, [guestToken]
);
await conn.commit();
// 5. ์ฃผ๋ฌธ ํ์ธ ์ด๋ฉ์ผ ๋ฐ์ก
const orderViewUrl = `${process.env.BASE_URL}/orders/guest?token=${orderToken}&email=${guest_email}`;
await mailer.sendOrderConfirmation({
to: guest_email,
name: guest_name,
orderNumber,
totalPrice,
orderViewUrl
});
res.json({
success: true,
orderNumber,
message: '์ฃผ๋ฌธ์ด ์๋ฃ๋์์ต๋๋ค. ํ์ธ ์ด๋ฉ์ผ์ ๋ฐ์กํ์ต๋๋ค.'
});
} catch (err) {
await conn.rollback();
console.error('์ฃผ๋ฌธ ์์ฑ ์ค๋ฅ:', err);
res.status(500).json({ error: '์ฃผ๋ฌธ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.' });
} finally {
conn.release();
}
});
์ฃผ๋ฌธ ์์ฑ, ์ฌ๊ณ ์ฐจ๊ฐ, ๋ฐฐ์ก์ง ์ ์ฅ์ ๋ฐ๋์ ํ๋์ ํธ๋์ญ์ ์ผ๋ก ๋ฌถ์ด์ผ ํด.
์ค๊ฐ์ ์ค๋ฅ๊ฐ ๋๋ฉด ์ ์ฒด ๋กค๋ฐฑํด์ ๋ฐ์ดํฐ ๋ถ์ผ์น๋ฅผ ๋ฐฉ์งํด์ผ ํ๊ฑฐ๋ .
3๏ธโฃ ๋นํ์ ์ฃผ๋ฌธ ์กฐํ API
// routes/orders.js
// ๋นํ์ ์ฃผ๋ฌธ ์กฐํ (์ด๋ฉ์ผ + ์ฃผ๋ฌธ ํ ํฐ์ผ๋ก ์ธ์ฆ)
router.get('/guest', async (req, res) => {
const { token, email } = req.query;
if (!token || !email) {
return res.status(400).json({ error: '์ฃผ๋ฌธ ์กฐํ ์ ๋ณด๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.' });
}
// ์ด๋ฉ์ผ + ํ ํฐ ๋์ ๊ฒ์ฆ (๋ ๋ค ๋ง์์ผ ์กฐํ ๊ฐ๋ฅ)
const [order] = await db.query(
`SELECT o.*, os.recipient, os.address1, os.address2, os.zip_code
FROM orders o
LEFT JOIN order_shipping os ON o.id = os.order_id
WHERE o.order_token = ? AND o.guest_email = ?`,
[token, email]
);
if (!order) {
return res.status(404).json({ error: '์ฃผ๋ฌธ์ ์ฐพ์ ์ ์์ต๋๋ค.' });
}
// ์ฃผ๋ฌธ ์ํ ์กฐํ
const items = await db.query(
`SELECT oi.*, p.name, p.image
FROM order_items oi JOIN products p ON oi.product_id = p.id
WHERE oi.order_id = ?`,
[order.id]
);
res.json({ order, items });
});
order_token๋ง์ผ๋ก๋ ์กฐํ ๋ถ๊ฐ๋ฅํ๊ฒ ํด์ผ ํด.๋ฐ๋์ ์ด๋ฉ์ผ + ํ ํฐ ๋ ๊ฐ์ง๋ฅผ ๋์์ ๊ฒ์ฆํด์ผ ๋ค๋ฅธ ์ฌ๋์ด ํ ํฐ์ ์ถ์ธกํด์ ์กฐํํ๋ ๊ฑธ ๋ง์ ์ ์์ด.
๐จ ํ๋ก ํธ์๋ ๊ตฌํ โ ๊ฒ์คํธ ์ฒดํฌ์์ UI/UX
๋ฐฑ์๋๋ง ์ ๋ง๋ค๋ฉด ๋ญํด, ํ๋ก ํธ๋ ์ ๋ง๋ค์ด์ผ์ง! ๐
๊ฒ์คํธ ์ฒดํฌ์์ UI์์ ๊ฐ์ฅ ์ค์ํ ๊ฑด "๋ง์ฐฐ์ ์ต์ํ"ํ๋ ๊ฑฐ์ผ.
๐ฅ๏ธ ์ฒดํฌ์์ ๋ฐฉ์ ์ ํ ํ๋ฉด (React ์์)
// components/CheckoutOptions.jsx
import React, { useState } from 'react';
function CheckoutOptions({ onSelectMethod }) {
const [email, setEmail] = useState('');
return (
<div className="checkout-options">
<h2>์ฃผ๋ฌธ ๋ฐฉ์์ ์ ํํด์ฃผ์ธ์</h2>
{/* ํ์ ๋ก๊ทธ์ธ ์ต์
*/}
<div className="option-card">
<h3>๐ค ํ์์ผ๋ก ์ฃผ๋ฌธ</h3>
<p>๋ก๊ทธ์ธํ๋ฉด ์ฃผ๋ฌธ ์ด๋ ฅ ๊ด๋ฆฌ, ํฌ์ธํธ ์ ๋ฆฝ์ด ๊ฐ๋ฅํด์</p>
<button onClick={() => onSelectMethod('login')}>
๋ก๊ทธ์ธํ๊ธฐ
</button>
</div>
{/* ๊ฒ์คํธ ์ฃผ๋ฌธ ์ต์
*/}
<div className="option-card guest">
<h3>๐ ๋นํ์์ผ๋ก ๋น ๋ฅด๊ฒ ์ฃผ๋ฌธ</h3>
<p>ํ์๊ฐ์
์์ด ์ด๋ฉ์ผ๋ง์ผ๋ก ์ฃผ๋ฌธํ ์ ์์ด์</p>
<input
type="email"
placeholder="์ด๋ฉ์ผ ์ฃผ์ ์
๋ ฅ"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button
onClick={() => onSelectMethod('guest', email)}
disabled={!email.includes('@')}
>
๋นํ์์ผ๋ก ๊ณ์ํ๊ธฐ โ
</button>
</div>
{/* ์ฃผ๋ฌธ ํ ํ์๊ฐ์
์ ๋ ๋ฌธ๊ตฌ */}
<p className="hint">
๐ก ์ฃผ๋ฌธ ์๋ฃ ํ ํ์๊ฐ์
ํ๋ฉด ์ด ์ฃผ๋ฌธ๋ ์ด๋ ฅ์ ์ฐ๊ฒฐ๋ผ์!
</p>
</div>
);
}
๐ ๊ฒ์คํธ ์ ๋ณด ์ ๋ ฅ ํผ
// components/GuestCheckoutForm.jsx
function GuestCheckoutForm({ guestEmail, onSubmit }) {
const [form, setForm] = useState({
guest_name: '',
guest_phone: '',
recipient: '',
address1: '',
address2: '',
zip_code: '',
shipping_phone: ''
});
const handleSubmit = async (e) => {
e.preventDefault();
// ์ ํจ์ฑ ๊ฒ์ฌ
if (!form.guest_name || !form.guest_phone) {
alert('์ด๋ฆ๊ณผ ์ฐ๋ฝ์ฒ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.');
return;
}
if (!form.address1 || !form.zip_code) {
alert('๋ฐฐ์ก์ง๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.');
return;
}
try {
const response = await fetch('/api/checkout/guest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
guest_email: guestEmail,
...form
})
});
const data = await response.json();
if (data.success) {
// ์ฃผ๋ฌธ ์๋ฃ ํ์ด์ง๋ก ์ด๋
window.location.href = `/order-complete?orderNumber=${data.orderNumber}`;
}
} catch (err) {
alert('์ฃผ๋ฌธ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
}
};
return (
<form onSubmit={handleSubmit}>
<h3>์ฃผ๋ฌธ์ ์ ๋ณด</h3>
<input placeholder="์ด๋ฆ" value={form.guest_name}
onChange={e => setForm({...form, guest_name: e.target.value})} />
<input placeholder="์ฐ๋ฝ์ฒ" value={form.guest_phone}
onChange={e => setForm({...form, guest_phone: e.target.value})} />
<h3>๋ฐฐ์ก์ง ์ ๋ณด</h3>
<input placeholder="์ฐํธ๋ฒํธ" value={form.zip_code}
onChange={e => setForm({...form, zip_code: e.target.value})} />
<input placeholder="๊ธฐ๋ณธ ์ฃผ์" value={form.address1}
onChange={e => setForm({...form, address1: e.target.value})} />
<input placeholder="์์ธ ์ฃผ์" value={form.address2}
onChange={e => setForm({...form, address2: e.target.value})} />
<button type="submit">๊ฒฐ์ ํ๊ธฐ ๐ณ</button>
</form>
);
}
โข ์ฃผ์ ์ ๋ ฅ์ ์นด์นด์ค ์ฐํธ๋ฒํธ API ์ฐ๋์ผ๋ก ์๋ํ
โข ์ฐ๋ฝ์ฒ ์ ๋ ฅ ์ ์๋ ํ์ดํ ํฌ๋งทํ (010-1234-5678)
โข ํ์ ํญ๋ชฉ์ * ํ์ + ์ค์๊ฐ ์ ํจ์ฑ ๊ฒ์ฌ
โข ๋ชจ๋ฐ์ผ์์๋ ์ ์ ํ
inputmode ์์ฑ ์ค์ (์ซ์ ํคํจ๋ ๋ฑ)
๐ ๋ณด์ ์ฒ๋ฆฌ โ ๋นํ์ ์ฃผ๋ฌธ์ ์ทจ์ฝ์ ๊ณผ ๋์
๊ฒ์คํธ ์ฒดํฌ์์์ ํธ๋ฆฌํ์ง๋ง, ๋ณด์ ํ์ ์ด ์๊ธฐ๊ธฐ ์ฌ์.
์ด๋ค ๊ณต๊ฒฉ ๋ฒกํฐ๊ฐ ์๊ณ ์ด๋ป๊ฒ ๋ง์์ผ ํ๋์ง ์์๋ณด์.
๐ด ํ ํฐ ์ถ์ธก ๊ณต๊ฒฉ
๐ด ์ฃผ๋ฌธ ์กฐํ ๋ฌด์ฐจ๋ณ ๋์
๐ด CSRF ๊ณต๊ฒฉ
๐ด ์ด๋ฉ์ผ ์คํธ ์ฃผ๋ฌธ
๐ด ์ฌ๊ณ ์์ง ๊ณต๊ฒฉ
โ UUID v4 (128bit ๋๋ค)
โ Rate Limiting ์ ์ฉ
โ CSRF ํ ํฐ ์ฌ์ฉ
โ reCAPTCHA ์ฐ๋
โ ๊ฒฐ์ ์๋ฃ ํ ์ฌ๊ณ ์ฐจ๊ฐ
๐ก๏ธ Rate Limiting ์ ์ฉ (Express)
// middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
// ์ฃผ๋ฌธ API Rate Limiting
const orderLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 5, // ์ต๋ 5ํ ์ฃผ๋ฌธ ์๋
message: { error: '๋๋ฌด ๋ง์ ์ฃผ๋ฌธ ์๋์
๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.' },
standardHeaders: true,
legacyHeaders: false,
});
// ์ฃผ๋ฌธ ์กฐํ Rate Limiting
const orderViewLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5๋ถ
max: 20, // ์ต๋ 20ํ ์กฐํ
message: { error: '์กฐํ ํ์๋ฅผ ์ด๊ณผํ์ต๋๋ค.' }
});
module.exports = { orderLimiter, orderViewLimiter };
// app.js์์ ์ ์ฉ
app.use('/api/checkout/guest', orderLimiter);
app.use('/api/orders/guest', orderViewLimiter);
๐ CSRF ๋ณดํธ
// CSRF ํ ํฐ ๋ฏธ๋ค์จ์ด ์ ์ฉ (csurf ํจํค์ง)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
// ์ฒดํฌ์์ ํผ ๋ ๋๋ง ์ CSRF ํ ํฐ ์ ๋ฌ
app.get('/checkout', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// ์ฃผ๋ฌธ API์ CSRF ๊ฒ์ฆ ์ ์ฉ
app.post('/api/checkout/guest', csrfProtection, orderLimiter, async (req, res) => {
// ... ์ฃผ๋ฌธ ์ฒ๋ฆฌ ๋ก์ง
});
๊ฒฐ์ ์๋ฃ ์ ์ ์ฌ๊ณ ๋ฅผ ์ฐจ๊ฐํ๋ฉด ๊ฒฐ์ ์คํจ ์ ์ฌ๊ณ ๊ฐ ์๋ชป ๊ด๋ฆฌ๋ ์ ์์ด.
PG์ฌ ๊ฒฐ์ ์๋ฃ ์นํ (Webhook)์ ๋ฐ์ ํ์ ์ฌ๊ณ ๋ฅผ ์ฐจ๊ฐํ๋ ๊ฒ ์์ ํด.
๋๋ ์ฃผ๋ฌธ ์์ฑ ์ ์ฌ๊ณ ๋ฅผ ์์ ์์ฝ(hold)ํ๊ณ , ๊ฒฐ์ ์๋ฃ ์ ํ์ ํ๋ ๋ฐฉ์๋ ์ข์.
๐ง ์ด๋ฉ์ผ ์ธ์ฆ์ผ๋ก ์คํธ ์ฃผ๋ฌธ ๋ฐฉ์ง
// ์ด๋ฉ์ผ ์ธ์ฆ ์ฝ๋ ๋ฐ์ก (์ ํ์ ์ ์ฉ)
router.post('/verify-email', async (req, res) => {
const { email } = req.body;
// 6์๋ฆฌ ์ธ์ฆ ์ฝ๋ ์์ฑ
const verifyCode = Math.floor(100000 + Math.random() * 900000).toString();
// Redis์ 5๋ถ๊ฐ ์ ์ฅ
await redis.setex(`email_verify:${email}`, 300, verifyCode);
// ์ธ์ฆ ๋ฉ์ผ ๋ฐ์ก
await mailer.sendVerifyCode({ to: email, code: verifyCode });
res.json({ success: true, message: '์ธ์ฆ ์ฝ๋๋ฅผ ๋ฐ์กํ์ต๋๋ค.' });
});
// ์ธ์ฆ ์ฝ๋ ํ์ธ
router.post('/confirm-email', async (req, res) => {
const { email, code } = req.body;
const savedCode = await redis.get(`email_verify:${email}`);
if (!savedCode || savedCode !== code) {
return res.status(400).json({ error: '์ธ์ฆ ์ฝ๋๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.' });
}
// ์ธ์ฆ ์๋ฃ ํ์
await redis.setex(`email_verified:${email}`, 3600, '1'); // 1์๊ฐ ์ ํจ
await redis.del(`email_verify:${email}`);
res.json({ success: true });
});
๐ ์ฃผ๋ฌธ ํ ํ์ ์ ํ โ ๋นํ์์ ํ์์ผ๋ก ๋ง๋๋ ์ ๋ต
๊ฒ์คํธ ์ฒดํฌ์์์ ์ง์ง ๋ชฉ์ ์ ๋จ์ํ ์ฃผ๋ฌธ์ ์๋ฃ์ํค๋ ๊ฒ ์๋์ผ.
์ฃผ๋ฌธ ์๋ฃ ํ ์์ฐ์ค๋ฝ๊ฒ ํ์๊ฐ์
์ ์ ๋ํด์ ์ถฉ์ฑ ๊ณ ๊ฐ์ผ๋ก ๋ง๋๋ ๊ฒ ์ต์ข
๋ชฉํ์ผ! ๐ฏ
"์ด ์ด๋ฉ์ผ๋ก ๊ฐ์ ํ๋ฉด ์ด ์ฃผ๋ฌธ์ด ์๋์ผ๋ก ์ฐ๊ฒฐ๋ผ์!" ๋ผ๋ ๋ฌธ๊ตฌ์ ํจ๊ป
์ํด๋ฆญ ํ์๊ฐ์ ๋ฒํผ ์ ๊ณต. ์ด๋ฏธ ์ด๋ฉ์ผ์ ์ ๋ ฅํ์ผ๋ ๋น๋ฐ๋ฒํธ๋ง ์ค์ ํ๋ฉด ๋ผ.
๋นํ์์ด ๊ฐ์ ์
guest_email์ด ๊ฐ์ ์ฃผ๋ฌธ๋ค์ ์๋์ผ๋ก user_id์ ์ฐ๊ฒฐ.
์ฃผ๋ฌธ ํ์ธ ์ด๋ฉ์ผ์ "ํ์๊ฐ์ ํ๊ณ ํฌ์ธํธ ๋ฐ๊ธฐ" ๋ฒํผ์ ๋ฃ์ด์ ์ฌ๋ฐฉ๋ฌธ ์ ๋.
// ๋นํ์ ์ฃผ๋ฌธ์ ํ์ ๊ณ์ ์ ์ฐ๊ฒฐํ๋ ๋ก์ง
router.post('/link-guest-orders', async (req, res) => {
const userId = req.session.user.id;
const userEmail = req.session.user.email;
// ๊ฐ์ ์ด๋ฉ์ผ๋ก ๋ ๋นํ์ ์ฃผ๋ฌธ์ ํ์ ๊ณ์ ์ ์ฐ๊ฒฐ
const result = await db.query(
`UPDATE orders
SET user_id = ?, guest_token = NULL
WHERE guest_email = ? AND user_id IS NULL`,
[userId, userEmail]
);
res.json({
success: true,
linkedOrders: result.affectedRows,
message: `${result.affectedRows}๊ฐ์ ์ฃผ๋ฌธ์ด ๊ณ์ ์ ์ฐ๊ฒฐ๋์์ต๋๋ค.`
});
});
์๋น์ค๋ฅผ ์ฒ์ ์ด์ฉํ๋ ์ฌ๋์ด ๋ถ๋ด ์์ด ์์ํ ์ ์๋๋ก ์ง์ ์ฅ๋ฒฝ์ ๋ฎ์ถ๊ณ ,
์ด์ฉ ๊ฒฝํ์ด ์ข์ผ๋ฉด ์์ฐ์ค๋ฝ๊ฒ ํ์์ผ๋ก ์ ํ๋๋ ํ๋ฆ์ด ํต์ฌ์ด์ผ.
๐ ์ฅ๋ฐ๊ตฌ๋ ๋ณํฉ โ ๋นํ์ โ ํ์ ๋ก๊ทธ์ธ ์ ์ฒ๋ฆฌ
๋นํ์์ผ๋ก ์ฅ๋ฐ๊ตฌ๋์ ๋ด์๋๋ค๊ฐ ๋ก๊ทธ์ธํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
์ด ์ํฉ์ ์ ๋๋ก ์ฒ๋ฆฌ ์ ํ๋ฉด ์ฅ๋ฐ๊ตฌ๋๊ฐ ๋ ์๊ฐ๋ ์ต์
์ UX๊ฐ ๋ฐ์ํด. ๐ฑ
// ๋ก๊ทธ์ธ ์ฑ๊ณต ํ ์ฅ๋ฐ๊ตฌ๋ ๋ณํฉ ์ฒ๋ฆฌ
async function mergeGuestCart(userId, guestToken) {
if (!guestToken) return;
// ๊ฒ์คํธ ์ฅ๋ฐ๊ตฌ๋ ์กฐํ
const guestItems = await db.query(
`SELECT * FROM cart_items WHERE guest_token = ?`, [guestToken]
);
for (const item of guestItems) {
// ํ์ ์ฅ๋ฐ๊ตฌ๋์ ๊ฐ์ ์ํ์ด ์๋์ง ํ์ธ
const [existing] = await db.query(
`SELECT * FROM cart_items WHERE user_id = ? AND product_id = ?`,
[userId, item.product_id]
);
if (existing) {
// ์๋ ํฉ์ฐ
await db.query(
`UPDATE cart_items SET quantity = quantity + ?
WHERE user_id = ? AND product_id = ?`,
[item.quantity, userId, item.product_id]
);
} else {
// ํ์ ์ฅ๋ฐ๊ตฌ๋๋ก ์ด์
await db.query(
`UPDATE cart_items SET user_id = ?, guest_token = NULL
WHERE id = ?`,
[userId, item.id]
);
}
}
// ๊ฒ์คํธ ์ฅ๋ฐ๊ตฌ๋ ์ ๋ฆฌ
await db.query(
`DELETE FROM cart_items WHERE guest_token = ?`, [guestToken]
);
}
// ๋ก๊ทธ์ธ ๋ผ์ฐํฐ์์ ํธ์ถ
router.post('/login', async (req, res) => {
// ... ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ...
// ๋ก๊ทธ์ธ ์ฑ๊ณต ํ ์ฅ๋ฐ๊ตฌ๋ ๋ณํฉ
const guestToken = req.cookies.guest_token;
await mergeGuestCart(user.id, guestToken);
// ๊ฒ์คํธ ์ฟ ํค ์ญ์
res.clearCookie('guest_token');
res.json({ success: true });
});
โข ์๋ ํฉ์ฐ: ๊ฐ์ ์ํ์ด๋ฉด ์๋์ ๋ํจ (๊ฐ์ฅ ์ผ๋ฐ์ )
โข ๊ฒ์คํธ ์ฐ์ : ๊ฒ์คํธ ์ฅ๋ฐ๊ตฌ๋๋ก ๋ฎ์ด์
โข ํ์ ์ฐ์ : ๊ธฐ์กด ํ์ ์ฅ๋ฐ๊ตฌ๋ ์ ์ง
โข ์ฌ์ฉ์ ์ ํ: "๊ธฐ์กด ์ฅ๋ฐ๊ตฌ๋์ ํฉ์น ๊น์?" ํ์ ์ ๊ณต
๋๋ถ๋ถ์ ์ผํ๋ชฐ์ ์๋ ํฉ์ฐ ๋ฐฉ์์ ์ฌ์ฉํด.
โ๏ธ ์ฃผ๋ฌธ ํ์ธ ์ด๋ฉ์ผ โ ๋นํ์ ์ํต์ ํต์ฌ
๋นํ์์ ๋ง์ดํ์ด์ง๊ฐ ์์ด. ๊ทธ๋์ ์ด๋ฉ์ผ์ด ์ ์ผํ ์ํต ์ฑ๋์ด์ผ.
์ฃผ๋ฌธ ํ์ธ ์ด๋ฉ์ผ์ ์ ๋ง๋๋ ๊ฒ ์ ๋ง ์ค์ํด.
// utils/mailer.js (Nodemailer ์ฌ์ฉ)
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
service: 'gmail', // ๋๋ AWS SES, SendGrid ๋ฑ
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
async function sendOrderConfirmation({ to, name, orderNumber, totalPrice, orderViewUrl }) {
const html = `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1a1a2e;">๐ ์ฃผ๋ฌธ์ด ์๋ฃ๋์์ต๋๋ค!</h2>
<p>์๋
ํ์ธ์, <strong>${name}</strong>๋!</p>
<p>์ฃผ๋ฌธ๋ฒํธ: <strong>${orderNumber}</strong></p>
<p>๊ฒฐ์ ๊ธ์ก: <strong>${totalPrice.toLocaleString()}์</strong></p>
<a href="${orderViewUrl}"
style="display:inline-block; background:#0f3460; color:#fff;
padding:12px 24px; border-radius:8px; text-decoration:none; margin:16px 0;">
๐ฆ ์ฃผ๋ฌธ ์์ธ ๋ณด๊ธฐ
</a>
<hr/>
<p style="color:#888; font-size:12px;">
์ด ๋งํฌ๋ ๋ณด์์ ์ํด 30์ผ ํ ๋ง๋ฃ๋ฉ๋๋ค.<br>
ํ์๊ฐ์
ํ์๋ฉด ์ธ์ ๋ ์ง ์ฃผ๋ฌธ ์ด๋ ฅ์ ํ์ธํ ์ ์์ด์!
</p>
</div>
`;
await transporter.sendMail({
from: `"์ผํ๋ชฐ" <${process.env.EMAIL_USER}>`,
to,
subject: `[์ฃผ๋ฌธ์๋ฃ] ${orderNumber} ์ฃผ๋ฌธ์ด ์ ์๋์์ต๋๋ค`,
html
});
}
module.exports = { sendOrderConfirmation };
โข SendGrid: ๋ฌด๋ฃ 100๊ฑด/์ผ, ๋์ฉ๋ ๋ฐ์ก์ ์ ํฉ
โข AWS SES: ์ ๋ ดํ ๋น์ฉ, AWS ์ธํ๋ผ ์ฌ์ฉ ์ ์ถ์ฒ
โข Mailgun: ๊ฐ๋ฐ์ ์นํ์ API
โข Nodemailer + Gmail: ์๊ท๋ชจ ํ๋ก์ ํธ์ ์ ํฉ (์ผ์ผ ํ๋ ์์)
๐ ๏ธ ์ฃผ์ ํ๋ซํผ๋ณ ๊ฒ์คํธ ์ฒดํฌ์์ ๊ตฌํ
์ง์ ๊ฐ๋ฐํ๋ ๊ฒฝ์ฐ ์ธ์๋, ๊ธฐ์กด ํ๋ซํผ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๋ ๋ง์ง?
์ฃผ์ ํ๋ซํผ์์ ๊ฒ์คํธ ์ฒดํฌ์์์ ์ด๋ป๊ฒ ์ค์ ํ๋์ง ์์๋ณด์.
WooCommerce โ ์ค์ โ ๊ณ์ ๋ฐ ๊ฐ์ธ์ ๋ณด์์"๊ฒ์คํธ ์ฒดํฌ์์ ํ์ฉ" ์ต์ ํ์ฑํ.
์ถ๊ฐ ์ปค์คํฐ๋ง์ด์ง์
woocommerce_checkout_fields ํํฐ ํ
์ฌ์ฉ.
๊ธฐ๋ณธ์ ์ผ๋ก ๊ฒ์คํธ ์ฒดํฌ์์ ์ง์.
Settings โ Checkout โ Customer accounts์์"Accounts are optional" ์ ํํ๋ฉด ๊ฒ์คํธ ์ฃผ๋ฌธ ๊ฐ๋ฅ.
Stores โ Configuration โ Sales โ Checkout โ Checkout Options์์"Allow Guest Checkout" = Yes ์ค์ .
@IsOptional() ๋ฐ์ฝ๋ ์ดํฐ๋ก user_id ํ๋๋ฅผ ์ ํ์ ์ผ๋ก ์ฒ๋ฆฌ.Guard์์
@Public() ๋ฐ์ฝ๋ ์ดํฐ๋ก ์ธ์ฆ ์ฐํ ์ฒ๋ฆฌ.
๐ Django ๊ธฐ๋ฐ ๊ตฌํ ์์
# views.py - Django ๊ฒ์คํธ ์ฃผ๋ฌธ ๋ทฐ
from django.views import View
from django.http import JsonResponse
import uuid
import json
class GuestCheckoutView(View):
def post(self, request):
data = json.loads(request.body)
# ๊ฒ์คํธ ํ ํฐ ํ์ธ
guest_token = request.COOKIES.get('guest_token')
if not guest_token:
return JsonResponse({'error': '์ฅ๋ฐ๊ตฌ๋ ์ ๋ณด๊ฐ ์์ต๋๋ค.'}, status=400)
# ์ฃผ๋ฌธ ์์ฑ
order = Order.objects.create(
user=None, # ๋นํ์
guest_token=guest_token,
guest_email=data.get('guest_email'),
guest_name=data.get('guest_name'),
guest_phone=data.get('guest_phone'),
order_token=str(uuid.uuid4()),
total_price=calculate_total(guest_token)
)
# ์ฅ๋ฐ๊ตฌ๋ โ ์ฃผ๋ฌธ ์ํ ์ด์
cart_items = CartItem.objects.filter(guest_token=guest_token)
for item in cart_items:
OrderItem.objects.create(
order=order,
product=item.product,
quantity=item.quantity,
price=item.product.price
)
# ์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ
cart_items.delete()
# ํ์ธ ์ด๋ฉ์ผ ๋ฐ์ก (Celery ๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ถ์ฅ)
send_order_confirmation.delay(order.id)
return JsonResponse({
'success': True,
'order_number': order.order_number
})
โก ์ฑ๋ฅ ์ต์ ํ โ ๊ฒ์คํธ ์ฒดํฌ์์์ ๋น ๋ฅด๊ฒ
๊ฒ์คํธ ์ฒดํฌ์์์ ํต์ฌ์ ์๋์ผ.
๋๋ฆฌ๋ฉด ์ดํํ๊ฑฐ๋ . ์ต์ ํ ํฌ์ธํธ๋ฅผ ์ง์ด๋ณด์.
DB ๋์ Redis์ ์ฅ๋ฐ๊ตฌ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ฉด ์ฝ๊ธฐ ์๋๊ฐ 10๋ฐฐ ์ด์ ๋นจ๋ผ์ ธ.
cart:{guest_token} ํค๋ก JSON ์ ์ฅ, TTL 30์ผ ์ค์ .
์ด๋ฉ์ผ ๋ฐ์ก์ ๋๊ธฐ๋ก ์ฒ๋ฆฌํ๋ฉด ์๋ต์ด ๋๋ ค์ ธ.
๋ฉ์์ง ํ(RabbitMQ, Bull, Celery)๋ฅผ ์ฌ์ฉํด์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํด.
PG์ฌ ์นํ ์ ๋น ๋ฅด๊ฒ 200 ์๋ต ํ ์ค์ ์ฒ๋ฆฌ๋ ๋น๋๊ธฐ๋ก.
์นํ ์ฒ๋ฆฌ ์คํจ ์ ์ฌ์๋ ๋ก์ง ํ์.
guest_token, guest_email, order_token ์ปฌ๋ผ์ ์ธ๋ฑ์ค ์ถ๊ฐ.์ฃผ๋ฌธ ์กฐํ ์ฟผ๋ฆฌ ์ฑ๋ฅ์ด ํฌ๊ฒ ํฅ์๋ผ.
-- ์ธ๋ฑ์ค ์ถ๊ฐ
ALTER TABLE orders ADD INDEX idx_guest_token (guest_token);
ALTER TABLE orders ADD INDEX idx_guest_email (guest_email);
ALTER TABLE orders ADD INDEX idx_order_token (order_token);
ALTER TABLE cart_items ADD INDEX idx_cart_guest_token (guest_token);
๐ ์ค์ ํ โ ํ์ ์์ ์์ฃผ ๋ง์ฃผ์น๋ ์ํฉ๋ค
๐ค Q. ๋นํ์ ์ฅ๋ฐ๊ตฌ๋๋ฅผ DB์ ์ ์ฅํด์ผ ํ ๊น, ์ฟ ํค/localStorage์ ์ ์ฅํด์ผ ํ ๊น?
(์ฟ ํค/localStorage)
โ ์๋ฒ ๋ถํ ์์
โ ๊ตฌํ ๊ฐ๋จ
โ ๋ธ๋ผ์ฐ์ ์ญ์ ์ ์์ค
โ ๋ค๋ฅธ ๊ธฐ๊ธฐ์์ ์ ๊ทผ ๋ถ๊ฐ
โ ์ฌ๊ณ ์ค์๊ฐ ๋ฐ์ ์ด๋ ค์
(guest_token ๊ธฐ๋ฐ)
โ ๊ธฐ๊ธฐ ๊ฐ ๊ณต์ ๊ฐ๋ฅ
โ ์ฌ๊ณ ์ค์๊ฐ ์ฒดํฌ
โ ๋ฐ์ดํฐ ์์
โ ์๋ฒ ๋ถํ ์ฆ๊ฐ
โ ์ฃผ๊ธฐ์ ์ ๋ฆฌ ํ์
DB ์ ์ฅ ์ 30์ผ ์ด์ ๋ ๋นํ์ ์ฅ๋ฐ๊ตฌ๋๋ ์ฃผ๊ธฐ์ ์ผ๋ก ์ญ์ ํ๋ ๋ฐฐ์น ์์ ํ์.
๐ค Q. PG์ฌ ๊ฒฐ์ ์ฐ๋ ์ ๋นํ์ ์ฒ๋ฆฌ๋?
๋๋ถ๋ถ์ PG์ฌ(ํ ์คํ์ด๋จผ์ธ , ์์ํฌํธ/ํฌํธ์, KG์ด๋์์ค ๋ฑ)๋
๊ฒฐ์ ์์ฒญ ์ ์ฃผ๋ฌธ์ ์ ๋ณด๋ฅผ ์ง์ ์ ๋ฌํ๋ ๋ฐฉ์์ด๋ผ ํ์/๋นํ์ ๊ตฌ๋ถ์ด ์์ด.
์ค์ํ ๊ฑด ๊ฒฐ์ ์๋ฃ ํ ์นํ
(Webhook)์ผ๋ก ์ฃผ๋ฌธ ์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ ๊ฑฐ์ผ.
// ํ ์คํ์ด๋จผ์ธ ๊ฒฐ์ ์๋ฃ ์นํ
์ฒ๋ฆฌ
router.post('/webhook/toss', async (req, res) => {
const { paymentKey, orderId, status, amount } = req.body;
// ์นํ
์๋ช
๊ฒ์ฆ (๋ณด์ ํ์!)
const isValid = verifyTossWebhook(req.headers, req.body);
if (!isValid) return res.status(401).end();
if (status === 'DONE') {
// ๊ฒฐ์ ์๋ฃ ์ฒ๋ฆฌ
await db.query(
`UPDATE orders SET status = 'paid', payment_key = ?
WHERE order_number = ?`,
[paymentKey, orderId]
);
// ์ฌ๊ณ ์ฐจ๊ฐ (๊ฒฐ์ ์๋ฃ ํ ์ฒ๋ฆฌ)
await decreaseStock(orderId);
// ๋ฐฐ์ก ์ค๋น ์๋ฆผ (๋นํ์์ด๋ฉด ์ด๋ฉ์ผ๋ก)
await notifyOrderPaid(orderId);
}
// ์นํ
์ ๋น ๋ฅด๊ฒ 200 ์๋ต
res.status(200).end();
});
๐ค Q. ๋นํ์ ์ฃผ๋ฌธ ๋ฐ์ดํฐ ๋ณด์กด ๊ธฐ๊ฐ์?
โข ๊ณ์ฝยท์ฒญ์ฝ ์ฒ ํ ๊ธฐ๋ก: 5๋
โข ๋๊ธ ๊ฒฐ์ ๊ธฐ๋ก: 5๋
โข ์๋น์ ๋ถ๋งยท๋ถ์ ์ฒ๋ฆฌ ๊ธฐ๋ก: 3๋
๋นํ์ ์ฃผ๋ฌธ๋ ๋์ผํ๊ฒ ์ ์ฉ๋ผ. ์์๋ก ์ญ์ ํ๋ฉด ์ ๋ผ!
๋จ, ์ฅ๋ฐ๊ตฌ๋ ๋ฐ์ดํฐ(๋ฏธ์๋ฃ ์ฃผ๋ฌธ)๋ 30~90์ผ ํ ์ญ์ ๊ฐ๋ฅ.
๐ค Q. ์ฌ๋ฅ๋ท ๊ฐ์ ์๋น์ค ํ๋ซํผ์์์ ๋นํ์ ์ฒ๋ฆฌ
์ฌ๋ฅ๋ท์ฒ๋ผ ์ฌ๋ฅ์ ๊ฑฐ๋ํ๋ ํ๋ซํผ์์๋ ๋น์ทํ ๊ฐ๋
์ด ์ ์ฉ๋ผ.
์๋น์ค๋ฅผ ์ฒ์ ํ์ํ๋ ์ฌ๋์ด ํ์๊ฐ์
์์ด ๊ฒฌ์ ๋ฌธ์๋ ๊ฐ๋จํ ์๋ด์ ํ ์ ์๊ฒ ํ๋ฉด
์ง์
์ฅ๋ฒฝ์ด ๋ฎ์์ง๊ณ ์ ํ์จ์ด ๋์์ง๋ ํจ๊ณผ๊ฐ ์์ด.
ํต์ฌ์ "์ผ๋จ ๊ฒฝํํ๊ฒ ํ๊ณ , ๊ทธ ๋ค์์ ๊ฐ์
์ ์ ๋"ํ๋ ํ๋ฆ์ด์ผ.
๐ฏ ๋ง๋ฌด๋ฆฌ โ ๊ฒ์คํธ ์ฒดํฌ์์ ๊ตฌํ ํต์ฌ ์์ฝ
์, ์ฌ๊ธฐ๊น์ง ์์ผ๋ฉด ์ด์ ๊ฒ์คํธ ์ฒดํฌ์์์ ๋ํด ๊ฝค ์ ์๊ฒ ๋์ ๊ฑฐ์ผ! ๐
ํต์ฌ๋ง ๋ค์ ํ๋ฒ ์ ๋ฆฌํด๋ณผ๊ฒ.
๊ฒ์คํธ ์ฒดํฌ์์์ ๋จ์ํ "ํ์๊ฐ์ ์์ด ์ฃผ๋ฌธ"์ด ์๋์ผ.
๊ณ ๊ฐ์ ์ฒซ ๊ฒฝํ์ ์ต๋ํ ๋ถ๋๋ฝ๊ฒ ๋ง๋ค๊ณ , ๊ทธ ๊ฒฝํ์ ๋ฐํ์ผ๋ก ์ฅ๊ธฐ ๊ณ ๊ฐ์ผ๋ก ์ ํ์ํค๋ ์ ๋ต์ด์ผ.
์ ๊ตฌํ๋ ๊ฒ์คํธ ์ฒดํฌ์์ ํ๋๊ฐ ์ผํ๋ชฐ ๋งค์ถ์ 30% ์ด์ ์ฌ๋ฆด ์ ์์ด.
์ด์ ์ง์ ๊ตฌํํด๋ด! ๋ ํ ์ ์์ด ๐ช
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.