[Node.js-10] 웹 서비스 추가 기능

Comely·2025년 6월 6일

Node.js

목록 보기
10/14

결제 기능 구현

결제 시스템 개요

  • 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",              // PG사 코드
    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. 서버에서 결제 검증

// 결제 완료 API
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') {
      // DB에 결제 정보 저장
      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 결제 구현

// Stripe 라이브러리 로드
<script src="https://js.stripe.com/v3/"></script>

// Stripe 초기화
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 버튼 생성
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. 기본 설정

<!-- Quill 라이브러리 로드 -->
<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. 에디터 초기화

// Quill 에디터 초기화
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();
  
  // 숨겨진 input에 내용 저장
  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;
    
    // HTML 내용 검증 및 정제 (XSS 방지)
    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. 기본 설정

<!-- Toast UI Editor 라이브러리 -->
<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. 에디터 초기화

// Toast UI Editor 초기화
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 검증 및 정제

// HTML 정제 라이브러리 설치
npm install sanitize-html

// 서버에서 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 }, // 5MB 제한
  fileFilter: function (req, file, cb) {
    // 이미지 파일만 허용
    if (file.mimetype.startsWith('image/')) {
      cb(null, true);
    } else {
      cb(new Error('이미지 파일만 업로드 가능합니다.'));
    }
  }
});

// 이미지 업로드 API
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); // 30초마다 자동 저장
}

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 에디터 글자 수 제한
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. 에디터 테마 커스터마이징

/* Quill 에디터 커스텀 스타일 */
.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>

결제 및 에디터 비교표

결제 솔루션 비교

구분포트원StripePayPal
대상 지역한국전세계전세계
수수료2.9%~2.9%~3.4%~
구현 난이도쉬움보통쉬움
지원 결제수단카드, 계좌이체, 간편결제카드 중심PayPal 계정, 카드
개발자 친화성우수매우 우수우수

에디터 라이브러리 비교

구분Quill.jsToast UI EditorTinyMCE
크기작음보통
기능기본적풍부매우 풍부
커스터마이징쉬움보통어려움
마크다운 지원XO플러그인
무료 사용OO제한적

실전 체크리스트

결제 기능

  • PG사 계약 및 테스트 계정 발급
  • 결제 라이브러리 연동
  • 결제 검증 로직 구현
  • 결제 내역 DB 저장
  • 환불 처리 로직

에디터 기능

  • 에디터 라이브러리 선택 및 연동
  • 이미지 업로드 기능
  • HTML 보안 처리
  • 자동 저장 기능
  • 반응형 디자인

보안 고려사항

  • XSS 공격 방지
  • 파일 업로드 보안
  • 결제 데이터 암호화
  • API 보안 인증
profile
App, Web Developer

0개의 댓글