결제 기능 구현
결제 시스템 개요
- PG사 연동: 직접 결제 처리 X, 전문 업체 활용
- 결제 대행사: 다날, 이니시스, KG이니시스 등
- 통합 솔루션: 포트원(구 아임포트) 활용 권장
포트원(PortOne) 결제 구현
1. 포트원 설정
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
const IMP = window.IMP;
IMP.init('imp_code');
2. 결제 요청
function requestPay() {
IMP.request_pay({
pg: "html5_inicis",
pay_method: "card",
merchant_uid: "order_" + new Date().getTime(),
name: "상품명",
amount: 1000,
buyer_email: "buyer@example.com",
buyer_name: "구매자명",
buyer_tel: "010-1234-5678",
buyer_addr: "서울특별시",
buyer_postcode: "123-456"
}, function(rsp) {
if (rsp.success) {
fetch('/payment/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
imp_uid: rsp.imp_uid,
merchant_uid: rsp.merchant_uid,
amount: rsp.paid_amount
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('결제가 완료되었습니다.');
location.href = '/payment/success';
}
});
} else {
alert('결제에 실패하였습니다. ' + rsp.error_msg);
}
});
}
3. 서버에서 결제 검증
app.post('/payment/complete', async (요청, 응답) => {
try {
const { imp_uid, merchant_uid, amount } = 요청.body;
const getToken = await fetch('https://api.iamport.kr/users/getToken', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
imp_key: 'your_api_key',
imp_secret: 'your_api_secret'
})
});
const { access_token } = await getToken.json();
const getPaymentData = await fetch(`https://api.iamport.kr/payments/${imp_uid}`, {
headers: { 'Authorization': access_token }
});
const paymentData = await getPaymentData.json();
const { amount: paidAmount, status } = paymentData.response;
if (paidAmount === amount && status === 'paid') {
await db.collection('payments').insertOne({
imp_uid: imp_uid,
merchant_uid: merchant_uid,
amount: amount,
status: 'completed',
user_id: 요청.user._id,
created_at: new Date()
});
응답.json({ success: true, message: '결제가 완료되었습니다.' });
} else {
응답.json({ success: false, message: '결제 검증 실패' });
}
} catch (error) {
console.error('결제 처리 오류:', error);
응답.status(500).json({ success: false, message: '서버 오류' });
}
});
해외 결제 솔루션
Stripe 결제 구현
<script src="https://js.stripe.com/v3/"></script>
const stripe = Stripe('pk_test_your_publishable_key');
async function createPayment() {
const response = await fetch('/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 1000 })
});
const { client_secret } = await response.json();
const result = await stripe.confirmCardPayment(client_secret, {
payment_method: {
card: cardElement,
billing_details: {
name: 'Customer Name'
}
}
});
if (result.error) {
console.error(result.error.message);
} else {
console.log('결제 성공!');
}
}
PayPal 결제 구현
paypal.Buttons({
createOrder: function(data, actions) {
return actions.order.create({
purchase_units: [{
amount: {
value: '10.00'
}
}]
});
},
onApprove: function(data, actions) {
return actions.order.capture().then(function(details) {
alert('결제가 완료되었습니다. ' + details.payer.name.given_name);
});
}
}).render('#paypal-button-container');
리치 텍스트 에디터 구현
에디터의 역할
- WYSIWYG: What You See Is What You Get
- 기능: 텍스트 서식, 이미지 삽입, 링크 생성 등
- 데이터 변환: 사용자 입력 → HTML/JSON 형태로 변환
Quill.js 에디터 구현
1. 기본 설정
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
<div id="editor">
<p>여기에 내용을 입력하세요...</p>
</div>
2. 에디터 초기화
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
['link', 'image', 'video'],
['clean']
]
}
});
function getEditorContent() {
const htmlContent = quill.root.innerHTML;
const deltaContent = quill.getContents();
return {
html: htmlContent,
delta: deltaContent
};
}
document.querySelector('form').addEventListener('submit', function(e) {
const content = getEditorContent();
document.querySelector('input[name="content"]').value = content.html;
document.querySelector('input[name="contentDelta"]').value = JSON.stringify(content.delta);
});
3. 서버에서 에디터 데이터 처리
app.post('/add-post', async (요청, 응답) => {
try {
const { title, content, contentDelta } = 요청.body;
const cleanContent = sanitizeHtml(content, {
allowedTags: ['h1', 'h2', 'h3', 'p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li', 'a', 'img'],
allowedAttributes: {
'a': ['href'],
'img': ['src', 'alt']
}
});
await db.collection('posts').insertOne({
title: title,
content: cleanContent,
contentDelta: JSON.parse(contentDelta),
author: 요청.user._id,
createdAt: new Date()
});
응답.redirect('/posts');
} catch (error) {
console.error('게시글 저장 오류:', error);
응답.status(500).send('서버 오류');
}
});
Toast UI Editor 구현
1. 기본 설정
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
<div id="editor"></div>
2. 에디터 초기화
const editor = new toastui.Editor({
el: document.querySelector('#editor'),
height: '500px',
initialEditType: 'wysiwyg',
previewStyle: 'vertical',
placeholder: '내용을 입력하세요...',
hooks: {
addImageBlobHook: (blob, callback) => {
uploadImage(blob).then(imageUrl => {
callback(imageUrl, 'alt text');
});
}
}
});
function getEditorContent() {
const htmlContent = editor.getHTML();
const markdownContent = editor.getMarkdown();
return {
html: htmlContent,
markdown: markdownContent
};
}
async function uploadImage(blob) {
const formData = new FormData();
formData.append('image', blob);
const response = await fetch('/upload-image', {
method: 'POST',
body: formData
});
const result = await response.json();
return result.imageUrl;
}
에디터 보안 처리
HTML 검증 및 정제
npm install sanitize-html
const sanitizeHtml = require('sanitize-html');
function sanitizeContent(content) {
return sanitizeHtml(content, {
allowedTags: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'strong', 'em', 'u', 's', 'sup', 'sub',
'ol', 'ul', 'li',
'a', 'img',
'blockquote', 'code', 'pre',
'table', 'thead', 'tbody', 'tr', 'th', 'td'
],
allowedAttributes: {
'a': ['href', 'target'],
'img': ['src', 'alt', 'width', 'height'],
'table': ['border', 'cellpadding', 'cellspacing'],
'td': ['colspan', 'rowspan'],
'th': ['colspan', 'rowspan']
},
allowedSchemes: ['http', 'https', 'mailto']
});
}
파일 업로드 에디터 연동
이미지 업로드 서버 구현
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/editor-images/');
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'image-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: function (req, file, cb) {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('이미지 파일만 업로드 가능합니다.'));
}
}
});
app.post('/upload-image', upload.single('image'), (요청, 응답) => {
if (!요청.file) {
return 응답.status(400).json({ error: '파일이 업로드되지 않았습니다.' });
}
const imageUrl = `/uploads/editor-images/${요청.file.filename}`;
응답.json({ imageUrl: imageUrl });
});
app.use('/uploads', express.static('uploads'));
에디터 고급 기능
1. 자동 저장 기능
let autoSaveInterval;
function startAutoSave() {
autoSaveInterval = setInterval(() => {
const content = getEditorContent();
const postId = getCurrentPostId();
fetch('/auto-save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
postId: postId,
content: content.html,
contentDelta: content.delta
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAutoSaveIndicator();
}
});
}, 30000);
}
function showAutoSaveIndicator() {
const indicator = document.querySelector('#auto-save-indicator');
indicator.textContent = '자동 저장됨 ' + new Date().toLocaleTimeString();
indicator.style.opacity = '1';
setTimeout(() => {
indicator.style.opacity = '0';
}, 3000);
}
2. 글자 수 제한
quill.on('text-change', function() {
const text = quill.getText();
const maxLength = 10000;
if (text.length > maxLength) {
quill.deleteText(maxLength, text.length);
alert(`최대 ${maxLength}자까지 입력 가능합니다.`);
}
document.querySelector('#char-count').textContent =
`${text.length} / ${maxLength}`;
});
3. 에디터 테마 커스터마이징
.ql-editor {
min-height: 300px;
font-size: 16px;
line-height: 1.6;
}
.ql-toolbar {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
}
.ql-container {
border-bottom: 1px solid #ccc;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
}
.dark-theme .ql-editor {
background-color: #2d3748;
color: #e2e8f0;
}
.dark-theme .ql-toolbar {
background-color: #4a5568;
border-color: #718096;
}
실전 통합 예시
완전한 게시글 작성 폼
<form id="post-form" method="POST" action="/posts">
<div class="form-group">
<label for="title">제목</label>
<input type="text" id="title" name="title" required>
</div>
<div class="form-group">
<label for="category">카테고리</label>
<select id="category" name="category">
<option value="notice">공지사항</option>
<option value="free">자유게시판</option>
<option value="qna">Q&A</option>
</select>
</div>
<div class="form-group">
<label>내용</label>
<div id="editor"></div>
<input type="hidden" name="content" id="content-input">
<input type="hidden" name="contentDelta" id="content-delta-input">
</div>
<div class="form-group">
<div id="char-count">0 / 10000</div>
<div id="auto-save-indicator"></div>
</div>
<div class="form-actions">
<button type="button" onclick="saveDraft()">임시저장</button>
<button type="submit">발행하기</button>
</div>
</form>
<script>
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'image'],
['clean']
]
}
});
document.getElementById('post-form').addEventListener('submit', function(e) {
const content = quill.root.innerHTML;
const delta = quill.getContents();
document.getElementById('content-input').value = content;
document.getElementById('content-delta-input').value = JSON.stringify(delta);
});
startAutoSave();
</script>
결제 및 에디터 비교표
결제 솔루션 비교
| 구분 | 포트원 | Stripe | PayPal |
|---|
| 대상 지역 | 한국 | 전세계 | 전세계 |
| 수수료 | 2.9%~ | 2.9%~ | 3.4%~ |
| 구현 난이도 | 쉬움 | 보통 | 쉬움 |
| 지원 결제수단 | 카드, 계좌이체, 간편결제 | 카드 중심 | PayPal 계정, 카드 |
| 개발자 친화성 | 우수 | 매우 우수 | 우수 |
에디터 라이브러리 비교
| 구분 | Quill.js | Toast UI Editor | TinyMCE |
|---|
| 크기 | 작음 | 보통 | 큼 |
| 기능 | 기본적 | 풍부 | 매우 풍부 |
| 커스터마이징 | 쉬움 | 보통 | 어려움 |
| 마크다운 지원 | X | O | 플러그인 |
| 무료 사용 | O | O | 제한적 |
실전 체크리스트
결제 기능
에디터 기능
보안 고려사항