tosspayments2-rails 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd88d1af83f21b3f11746c4825425fd7c9bfee9aa793bd0c6adfde623b25a8c2
4
- data.tar.gz: 780b41fdccead7e7b6e26beb5f0ad701f756cc19036c7a713aa1b6c978cbbe19
3
+ metadata.gz: 39febb97b720a3f4b5b8c17869e56d30f512708c553079e158108be18b243856
4
+ data.tar.gz: d1d2cb235b61caa90e9bd1477894eed5b5322b7eb497f2f3bf48a5423371401d
5
5
  SHA512:
6
- metadata.gz: 8804a4ec3d7f980fca6cdba867592f73a763ec933bf963c90bb52e30837be9b59fc4e0cabf320e4915da4eaa9e8fc475e52419620db1565a52d0a714e85eafac
7
- data.tar.gz: fe9a02353d00025acb304f67ff7b6e90d2b5bedd6e69760921be3352eb9e06608ce2f073cf24934191550d123561f0102a754f0de0e169dd73a576f06c7d97c2
6
+ metadata.gz: 5fe2a225d5d062c68d6078136a77cc0e431e99e8bd22925c7ae86e8489cae598e674a0913fde2fc1ba6a13bebd547e216f14447a7750c2afe7eb34264d0e9281
7
+ data.tar.gz: 111e60a9b9339dc92457af01b3749f9c56a6a1a798383ce114e72b4653fa8733464e73a3916783310d0e723df0d36cce93cca48499bb51c9b6d2941aa2ee98cc
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: spec-design
3
3
  description: use PROACTIVELY to create/refine the spec design document in a spec development process/workflow. MUST BE USED AFTER spec requirements document is approved.
4
+ model: inherit
4
5
  ---
5
6
 
6
7
  You are a professional spec design document expert. Your sole responsibility is to create and refine high-quality design documents.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: spec-impl
3
3
  description: Coding implementation expert. Use PROACTIVELY when specific coding tasks need to be executed. Specializes in implementing functional code according to task lists.
4
+ model: inherit
4
5
  ---
5
6
 
6
7
  You are a coding implementation expert. Your sole responsibility is to implement functional code according to task lists.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: spec-judge
3
3
  description: use PROACTIVELY to evaluate spec documents (requirements, design, tasks) in a spec development process/workflow
4
+ model: inherit
4
5
  ---
5
6
 
6
7
  You are a professional spec document evaluator. Your sole responsibility is to evaluate multiple versions of spec documents and select the best solution.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: spec-requirements
3
3
  description: use PROACTIVELY to create/refine the spec requirements document in a spec development process/workflow
4
+ model: inherit
4
5
  ---
5
6
 
6
7
  You are an EARS (Easy Approach to Requirements Syntax) requirements document expert. Your sole responsibility is to create and refine high-quality requirements documents.
@@ -2,6 +2,7 @@
2
2
  name: spec-system-prompt-loader
3
3
  description: a spec workflow system prompt loader. MUST BE CALLED FIRST when user wants to start a spec process/workflow. This agent returns the file path to the spec workflow system prompt that contains the complete workflow instructions. Call this before any spec-related agents if the prompt is not loaded yet. Input: the type of spec workflow requested. Output: file path to the appropriate workflow prompt file. The returned path should be read to get the full workflow instructions.
4
4
  tools:
5
+ model: inherit
5
6
  ---
6
7
 
7
8
  You are a prompt path mapper. Your ONLY job is to generate and return a file path.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: spec-tasks
3
3
  description: use PROACTIVELY to create/refine the spec tasks document in a spec development process/workflow. MUST BE USED AFTER spec design document is approved.
4
+ model: inherit
4
5
  ---
5
6
 
6
7
  You are a spec tasks document expert. Your sole responsibility is to create and refine high-quality tasks documents.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: spec-test
3
3
  description: use PROACTIVELY to create test documents and test code in spec development workflows. MUST BE USED when users need testing solutions. Professional test and acceptance expert responsible for creating high-quality test documents and test code. Creates comprehensive test case documentation (.md) and corresponding executable test code (.test.ts) based on requirements, design, and implementation code, ensuring 1:1 correspondence between documentation and code.
4
+ model: inherit
4
5
  ---
5
6
 
6
7
  You are a professional test and acceptance expert. Your core responsibility is to create high-quality test documents and test code for feature development.
data/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  _No changes yet._
4
4
 
5
+ ## [0.5.0] - 2025-08-21
6
+ ### Added
7
+ - Complete PaymentsController with full CRUD operations (index, show, new, create, checkout, success, fail, cancel)
8
+ - Enhanced Payment model with validations, scopes, and helper methods
9
+ - New view templates: new.html.erb, checkout.html.erb with TossPayments v2 widget integration
10
+ - PaymentsHelper with status and currency formatting utilities
11
+ - Improved route configuration with collection and member routes
12
+ - Rails credentials support in initializer (fallback to environment variables)
13
+ - Comprehensive checkout flow with actual TossPayments widget implementation
14
+
15
+ ### Changed
16
+ - Updated Rails migration version from 6.0 to 7.0 for Rails 7/8 compatibility
17
+ - Enhanced generator to create complete payment system structure
18
+ - Improved view templates with better styling and user experience
19
+ - Refactored PaymentsController to reduce complexity and improve maintainability
20
+ - Enhanced error handling and payment status management
21
+
22
+ ### Fixed
23
+ - RuboCop violations in generated templates
24
+ - Missing routes for payment workflow
25
+ - Incomplete payment flow implementation
26
+
27
+ ## [0.4.0] - 2025-08-20
28
+ ### Changed
29
+ - Add frozen_string_literal magic comments to generator templates
30
+ - Update .gitignore to ignore built gem artifacts (*.gem)
31
+ - RuboCop clean; RSpec green
32
+
5
33
  ## [0.3.0] - 2025-08-21
6
34
  ### Changed
7
35
  - General maintenance and improvements for release process
data/CLAUDE.md ADDED
@@ -0,0 +1,124 @@
1
+ # CLAUDE.md
2
+
3
+ 이 파일은 Claude Code (claude.ai/code)가 이 저장소에서 작업할 때 필요한 가이드를 제공합니다.
4
+
5
+ ## 개발 명령어
6
+
7
+ ### 테스트
8
+ ```bash
9
+ bundle exec rspec # 모든 테스트 실행
10
+ bundle exec rspec spec/client_spec.rb # 특정 테스트 파일 실행
11
+ ```
12
+
13
+ ### 코드 품질
14
+ ```bash
15
+ bundle exec rubocop # RuboCop 린터 실행
16
+ bundle exec rubocop --auto-correct # RuboCop 이슈 자동 수정
17
+ rake quality # RuboCop과 테스트 모두 실행
18
+ ```
19
+
20
+ ### 문서화
21
+ ```bash
22
+ bundle exec yard doc # YARD 문서 생성
23
+ open doc/index.html # 생성된 문서 보기
24
+ ```
25
+
26
+ ### 콘솔 및 개발
27
+ ```bash
28
+ bin/console # 젬이 로드된 인터랙티브 콘솔
29
+ bin/setup # 개발을 위한 초기 설정
30
+ ```
31
+
32
+ ### 젬 관리
33
+ ```bash
34
+ rake build # 젬을 로컬에서 빌드
35
+ rake install # 젬을 로컬에 설치
36
+ rake release # 젬 배포 (사전 검사 포함)
37
+ rake release:check # 배포 전 검증 실행
38
+ ```
39
+
40
+ ## 아키텍처 개요
41
+
42
+ ### 핵심 구성 요소
43
+
44
+ Rails 7 & 8 애플리케이션을 위한 TossPayments v2 Payment Widget 통합을 제공하는 Rails 엔진입니다.
45
+
46
+ **메인 진입점**: `lib/tosspayments2/rails.rb`
47
+ - 모든 구성 요소를 로드하고 모듈 레벨 `configure` 메서드 제공
48
+ - Rails가 있을 때만 Rails 엔진을 조건부로 로드
49
+
50
+ **Rails 엔진**: `lib/tosspayments2/rails/engine.rb`
51
+ - Rails 앱 설정이나 환경 변수에서 자동 설정
52
+ - 뷰 헬퍼와 컨트롤러 관심사를 자동 로드
53
+ - 격리된 네임스페이스 패턴 사용
54
+
55
+ **설정**: `lib/tosspayments2/rails/configuration.rb`
56
+ - 전역 설정을 위한 싱글톤 패턴
57
+ - 이니셜라이저와 Rails 앱 설정 패턴 모두 지원
58
+ - 기본값: widget_version='v2', api_base='https://api.tosspayments.com', timeout=10
59
+
60
+ ### 주요 구성 요소
61
+
62
+ **Client** (`client.rb`): TossPayments API용 HTTP 클라이언트
63
+ - 결제 승인과 취소 처리
64
+ - 일시적 실패를 위한 내장 재시도 로직 (500+ 상태 코드)
65
+ - 비밀 키를 사용한 Base64 Basic Auth
66
+ - 심볼화된 키로 파싱된 JSON 반환
67
+
68
+ **Controller Concern** (`controller_concern.rb`): Rails 컨트롤러 믹스인
69
+ - `toss_client` 헬퍼 메서드 제공
70
+ - 요청당 클라이언트 인스턴스 메모이제이션
71
+
72
+ **Script Tag Helper** (`script_tag_helper.rb`): 뷰 헬퍼
73
+ - TossPayments SDK용 `<script>` 태그 생성
74
+ - 버전과 HTML 속성 커스터마이징 지원
75
+ - 메서드명: `tosspayments_script_tag`
76
+
77
+ **Verifiers**: 보안 검증 유틸리티
78
+ - `CallbackVerifier`: 콜백 파라미터 검증 (order_id, amount)
79
+ - `WebhookVerifier`: HMAC-SHA256 웹훅 서명 검증
80
+
81
+ **Generator** (`generators/tosspayments2/install/`): Rails 제너레이터
82
+ - 환경 변수 설정으로 이니셜라이저 생성
83
+ - Payment 모델, 마이그레이션, 컨트롤러, 뷰 선택적 생성
84
+ - 명령어: `rails generate tosspayments2:install`
85
+
86
+ ### 에러 처리
87
+
88
+ **커스텀 예외** (`errors.rb`):
89
+ - `ConfigurationError`: 필수 설정 누락
90
+ - `APIError`: status, body, request_id를 포함한 TossPayments API 에러
91
+ - `VerificationError`: 콜백/웹훅 검증 실패
92
+
93
+ ### 개발 패턴
94
+
95
+ **젬 구조**: 표준 Ruby 젬 레이아웃
96
+ - `lib/`에 네임스페이스별로 구성된 메인 코드 포함
97
+ - `spec/`에 HTTP 모킹을 위한 WebMock을 사용한 RSpec 테스트 포함
98
+ - `sig/`에 RBS 타입 시그니처 포함
99
+ - `bin/`에 개발 유틸리티 포함
100
+
101
+ **Rails 통합**: 엔진 패턴
102
+ - 깔끔한 분리를 위해 `isolate_namespace` 사용
103
+ - Rails 훅을 통한 헬퍼 자동 로드
104
+ - 이니셜라이저와 앱 설정 모두 지원
105
+
106
+ **설정 우선순위**:
107
+ 1. 클래스에 대한 명시적 파라미터
108
+ 2. `Tosspayments2::Rails.configure`를 통한 전역 설정
109
+ 3. Rails 앱 설정 (`config.tosspayments2.*`)
110
+ 4. 환경 변수 (`TOSSPAYMENTS_CLIENT_KEY`, `TOSSPAYMENTS_SECRET_KEY`)
111
+
112
+ **테스트**: 광범위한 모킹을 사용한 RSpec
113
+ - HTTP 요청을 위한 WebMock
114
+ - 커버리지 보고를 위한 SimpleCov
115
+ - 설명적인 이름으로 구성 요소별로 구성된 테스트
116
+
117
+ ### 배포 프로세스
118
+
119
+ 젬에는 자동화된 배포 검증이 포함됩니다:
120
+ - Git 작업 디렉토리가 깨끗해야 함
121
+ - RuboCop이 통과해야 함
122
+ - 모든 테스트가 통과해야 함
123
+ - CHANGELOG.md에 현재 버전 항목이 있어야 함
124
+ - 배포 전 `rake release:check` 사용
@@ -5,16 +5,65 @@ module Tosspayments2
5
5
  module Generators
6
6
  class InstallGenerator < ::Rails::Generators::Base
7
7
  source_root File.expand_path('templates', __dir__)
8
+
8
9
  class_option :controller, type: :boolean, default: false, desc: 'Generate example payments controller'
10
+ class_option :with_model, type: :boolean, default: true,
11
+ desc: 'Generate Payment model, migration, and views'
9
12
 
10
13
  def create_initializer
11
- template 'initializer.rb', 'config/initializers/tosspayments2.rb'
14
+ template 'initializer.rb', 'config/initializers/tosspayments.rb'
12
15
  end
13
16
 
14
17
  def create_controller
15
18
  return unless options[:controller]
16
19
 
17
20
  template 'payments_controller.rb', 'app/controllers/payments_controller.rb'
21
+ template 'payments_helper.rb', 'app/helpers/payments_helper.rb'
22
+ end
23
+
24
+ def create_payment_model_and_migration
25
+ return unless options[:with_model]
26
+
27
+ # Payment 모델 생성
28
+ template 'payment.rb', 'app/models/payment.rb'
29
+
30
+ # 마이그레이션 파일명에 타임스탬프 추가
31
+ timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
32
+ migration_file = "db/migrate/#{timestamp}_create_payments.rb"
33
+ template 'migration.rb', migration_file
34
+
35
+ # 뷰 파일 생성
36
+ empty_directory 'app/views/payments'
37
+ template 'index.html.erb', 'app/views/payments/index.html.erb'
38
+ template 'show.html.erb', 'app/views/payments/show.html.erb'
39
+ template 'new.html.erb', 'app/views/payments/new.html.erb'
40
+ template 'checkout.html.erb', 'app/views/payments/checkout.html.erb'
41
+ end
42
+
43
+ def add_payments_route
44
+ route <<~RUBY
45
+ resources :payments do
46
+ collection do
47
+ get :checkout
48
+ get :success
49
+ get :fail
50
+ end
51
+ member do
52
+ patch :cancel
53
+ end
54
+ end
55
+ RUBY
56
+ end
57
+
58
+ # 제너레이터 실행 시 자동으로 모델/마이그레이션/뷰 생성
59
+ def install
60
+ create_payment_model_and_migration
61
+ add_payments_route
62
+ end
63
+
64
+ # Thor의 hook으로 install 메서드가 자동 실행되도록 설정
65
+ def self.default_task
66
+ :install
18
67
  end
19
68
  end
20
69
  end
@@ -0,0 +1,115 @@
1
+ <h1>결제 진행</h1>
2
+
3
+ <div class="payment-info">
4
+ <h3>결제 정보</h3>
5
+ <p><strong>주문번호:</strong> <%= @payment.order_id %></p>
6
+ <p><strong>결제금액:</strong> <%= @payment.formatted_amount %></p>
7
+ </div>
8
+
9
+ <!-- TossPayments 결제 위젯 영역 -->
10
+ <div id="payment-methods"></div>
11
+ <div id="agreement"></div>
12
+ <button id="payment-button" class="btn btn-primary">결제하기</button>
13
+
14
+ <%= tosspayments_script_tag %>
15
+
16
+ <script>
17
+ document.addEventListener('DOMContentLoaded', async () => {
18
+ // TossPayments 설정
19
+ const clientKey = '<%= Rails.application.credentials.dig(:tosspayments, :client_key) || ENV["TOSSPAYMENTS_CLIENT_KEY"] %>';
20
+ const customerKey = 'customer_<%= Time.current.to_i %>'; // 구매자 식별값
21
+
22
+ if (!clientKey) {
23
+ console.error('TossPayments 클라이언트 키가 설정되지 않았습니다.');
24
+ alert('결제 서비스를 초기화할 수 없습니다. 관리자에게 문의하세요.');
25
+ return;
26
+ }
27
+
28
+ try {
29
+ // TossPayments 위젯 초기화
30
+ const tosspayments = TossPayments(clientKey);
31
+ const widgets = tosspayments.widgets({ customerKey: customerKey });
32
+
33
+ // 결제 수단 렌더링
34
+ await widgets.renderPaymentMethods({
35
+ selector: '#payment-methods',
36
+ variantKey: 'DEFAULT'
37
+ });
38
+
39
+ // 약관 동의 렌더링
40
+ await widgets.renderAgreement({
41
+ selector: '#agreement'
42
+ });
43
+
44
+ // 결제 버튼 이벤트 리스너
45
+ document.getElementById('payment-button').addEventListener('click', async () => {
46
+ try {
47
+ await widgets.requestPayment({
48
+ orderId: '<%= @payment.order_id %>',
49
+ orderName: '결제 테스트 상품',
50
+ customerName: '고객',
51
+ amount: <%= @payment.amount %>,
52
+ successUrl: '<%= success_payments_url %>',
53
+ failUrl: '<%= fail_payments_url %>'
54
+ });
55
+ } catch (error) {
56
+ console.error('결제 요청 실패:', error);
57
+ alert('결제 요청에 실패했습니다: ' + (error.message || error.toString()));
58
+ }
59
+ });
60
+ } catch (error) {
61
+ console.error('TossPayments 초기화 실패:', error);
62
+ alert('결제 서비스 초기화에 실패했습니다: ' + (error.message || error.toString()));
63
+ }
64
+ });
65
+ </script>
66
+
67
+ <style>
68
+ .payment-info {
69
+ background-color: #f8f9fa;
70
+ padding: 1rem;
71
+ border-radius: 0.25rem;
72
+ margin-bottom: 2rem;
73
+ }
74
+
75
+ #payment-methods {
76
+ margin-bottom: 1rem;
77
+ }
78
+
79
+ #agreement {
80
+ margin-bottom: 1rem;
81
+ }
82
+
83
+ #payment-button {
84
+ width: 100%;
85
+ padding: 0.75rem;
86
+ font-size: 1.1rem;
87
+ font-weight: bold;
88
+ }
89
+
90
+ .btn {
91
+ display: inline-block;
92
+ padding: 0.375rem 0.75rem;
93
+ margin-bottom: 0;
94
+ font-size: 1rem;
95
+ font-weight: 400;
96
+ line-height: 1.5;
97
+ text-align: center;
98
+ text-decoration: none;
99
+ vertical-align: middle;
100
+ cursor: pointer;
101
+ border: 1px solid transparent;
102
+ border-radius: 0.25rem;
103
+ }
104
+
105
+ .btn-primary {
106
+ color: #fff;
107
+ background-color: #007bff;
108
+ border-color: #007bff;
109
+ }
110
+
111
+ .btn-primary:hover {
112
+ background-color: #0056b3;
113
+ border-color: #004085;
114
+ }
115
+ </style>
@@ -0,0 +1,93 @@
1
+ <h1>결제 내역</h1>
2
+
3
+ <div class="mb-3">
4
+ <%= link_to '새 결제', new_payment_path, class: 'btn btn-primary' %>
5
+ </div>
6
+
7
+ <table class="table table-striped">
8
+ <thead>
9
+ <tr>
10
+ <th>주문번호</th>
11
+ <th>금액</th>
12
+ <th>상태</th>
13
+ <th>거래ID</th>
14
+ <th>생성일</th>
15
+ <th>액션</th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% if @payments.any? %>
20
+ <% @payments.each do |payment| %>
21
+ <tr>
22
+ <td><%= payment.order_id %></td>
23
+ <td><%= payment.formatted_amount %></td>
24
+ <td>
25
+ <span class="badge <%= payment_status_class(payment.status) %>">
26
+ <%= payment_status_text(payment.status) %>
27
+ </span>
28
+ </td>
29
+ <td><%= payment.transaction_id %></td>
30
+ <td><%= payment.created_at.strftime('%Y-%m-%d %H:%M') %></td>
31
+ <td>
32
+ <%= link_to '상세', payment_path(payment), class: 'btn btn-sm btn-outline-primary' %>
33
+ <% if payment.confirmed? %>
34
+ <%= link_to '취소', cancel_payment_path(payment), method: :patch,
35
+ confirm: '결제를 취소하시겠습니까?',
36
+ class: 'btn btn-sm btn-outline-danger' %>
37
+ <% end %>
38
+ </td>
39
+ </tr>
40
+ <% end %>
41
+ <% else %>
42
+ <tr>
43
+ <td colspan="6" class="text-center">결제 내역이 없습니다.</td>
44
+ </tr>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+
49
+ <script>
50
+ function payment_status_class(status) {
51
+ switch(status) {
52
+ case 'confirmed': return 'bg-success';
53
+ case 'pending': return 'bg-warning';
54
+ case 'cancelled': return 'bg-secondary';
55
+ case 'failed': return 'bg-danger';
56
+ default: return 'bg-light';
57
+ }
58
+ }
59
+
60
+ function payment_status_text(status) {
61
+ switch(status) {
62
+ case 'confirmed': return '완료';
63
+ case 'pending': return '대기중';
64
+ case 'cancelled': return '취소됨';
65
+ case 'failed': return '실패';
66
+ default: return status;
67
+ }
68
+ }
69
+ </script>
70
+
71
+ <%# Bootstrap CSS가 없다면 기본 스타일링 %>
72
+ <style>
73
+ .table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
74
+ .table th, .table td { padding: 0.75rem; border-bottom: 1px solid #dee2e6; text-align: left; }
75
+ .table thead th { border-bottom: 2px solid #dee2e6; }
76
+ .table-striped tbody tr:nth-of-type(odd) { background-color: rgba(0, 0, 0, 0.05); }
77
+ .btn { display: inline-block; padding: 0.375rem 0.75rem; margin-bottom: 0; font-size: 1rem;
78
+ font-weight: 400; line-height: 1.5; text-align: center; text-decoration: none;
79
+ vertical-align: middle; cursor: pointer; border: 1px solid transparent; border-radius: 0.25rem; }
80
+ .btn-primary { color: #fff; background-color: #007bff; border-color: #007bff; }
81
+ .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
82
+ .btn-outline-primary { color: #007bff; border-color: #007bff; }
83
+ .btn-outline-danger { color: #dc3545; border-color: #dc3545; }
84
+ .badge { display: inline-block; padding: 0.25em 0.4em; font-size: 75%; font-weight: 700;
85
+ line-height: 1; text-align: center; white-space: nowrap; vertical-align: baseline;
86
+ border-radius: 0.25rem; }
87
+ .bg-success { background-color: #28a745 !important; color: white; }
88
+ .bg-warning { background-color: #ffc107 !important; color: black; }
89
+ .bg-secondary { background-color: #6c757d !important; color: white; }
90
+ .bg-danger { background-color: #dc3545 !important; color: white; }
91
+ .mb-3 { margin-bottom: 1rem; }
92
+ .text-center { text-align: center; }
93
+ </style>
@@ -2,8 +2,25 @@
2
2
 
3
3
  # TossPayments2 configuration
4
4
  Tosspayments2::Rails.configure do |c|
5
- c.client_key = ENV.fetch('TOSSPAYMENTS_CLIENT_KEY', nil)
6
- c.secret_key = ENV.fetch('TOSSPAYMENTS_SECRET_KEY', nil)
5
+ # Rails credentials 우선, 없으면 환경 변수 사용
6
+ c.client_key = Rails.application.credentials.dig(:tosspayments, :client_key) ||
7
+ ENV.fetch('TOSSPAYMENTS_CLIENT_KEY', nil)
8
+ c.secret_key = Rails.application.credentials.dig(:tosspayments, :secret_key) ||
9
+ ENV.fetch('TOSSPAYMENTS_SECRET_KEY', nil)
10
+
11
+ # 선택적 설정들 (기본값 사용 가능)
7
12
  # c.widget_version = 'v2'
8
13
  # c.api_base = 'https://api.tosspayments.com'
14
+ # c.timeout = 10
9
15
  end
16
+
17
+ # credentials.yml.enc 파일 설정 예시:
18
+ # $ EDITOR="nano" bin/rails credentials:edit
19
+ #
20
+ # tosspayments:
21
+ # client_key: test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoqo56A
22
+ # secret_key: test_sk_zXLkKEypNArWmo50nX3lmeaxYG5vSSu7
23
+ #
24
+ # 또는 환경 변수로 설정:
25
+ # export TOSSPAYMENTS_CLIENT_KEY=test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoqo56A
26
+ # export TOSSPAYMENTS_SECRET_KEY=test_sk_zXLkKEypNArWmo50nX3lmeaxYG5vSSu7
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreatePayments < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :payments do |t|
6
+ t.string :order_id, null: false
7
+ t.integer :amount, null: false
8
+ t.string :status, null: false
9
+ t.string :transaction_id
10
+ t.timestamps
11
+ end
12
+ add_index :payments, :order_id, unique: true
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ <h1>새 결제</h1>
2
+
3
+ <%= form_with(model: @payment, local: true) do |form| %>
4
+ <% if @payment.errors.any? %>
5
+ <div class="alert alert-danger">
6
+ <h4><%= pluralize(@payment.errors.count, "error") %> 발생:</h4>
7
+ <ul>
8
+ <% @payment.errors.full_messages.each do |message| %>
9
+ <li><%= message %></li>
10
+ <% end %>
11
+ </ul>
12
+ </div>
13
+ <% end %>
14
+
15
+ <div class="form-group">
16
+ <%= form.label :amount, '결제 금액' %>
17
+ <%= form.number_field :amount, class: 'form-control', placeholder: '예: 10000', required: true %>
18
+ <small class="form-text text-muted">원 단위로 입력해주세요</small>
19
+ </div>
20
+
21
+ <div class="actions">
22
+ <%= form.submit '결제하기', class: 'btn btn-primary' %>
23
+ <%= link_to '취소', payments_path, class: 'btn btn-secondary' %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <style>
28
+ .form-group { margin-bottom: 1rem; }
29
+ .form-control { display: block; width: 100%; padding: 0.375rem 0.75rem; font-size: 1rem;
30
+ line-height: 1.5; color: #495057; background-color: #fff; background-clip: padding-box;
31
+ border: 1px solid #ced4da; border-radius: 0.25rem; }
32
+ .form-text { display: block; margin-top: 0.25rem; font-size: 0.875em; color: #6c757d; }
33
+ .alert { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 0.25rem; }
34
+ .alert-danger { color: #721c24; background-color: #f8d7da; border-color: #f5c6cb; }
35
+ .actions { margin-top: 1rem; }
36
+ .btn { display: inline-block; padding: 0.375rem 0.75rem; margin-bottom: 0; font-size: 1rem;
37
+ font-weight: 400; line-height: 1.5; text-align: center; text-decoration: none;
38
+ vertical-align: middle; cursor: pointer; border: 1px solid transparent; border-radius: 0.25rem; }
39
+ .btn-primary { color: #fff; background-color: #007bff; border-color: #007bff; }
40
+ .btn-secondary { color: #fff; background-color: #6c757d; border-color: #6c757d; }
41
+ label { display: inline-block; margin-bottom: 0.5rem; font-weight: bold; }
42
+ </style>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 결제 정보를 저장하는 Payment 모델
4
+ class Payment < ApplicationRecord
5
+ validates :order_id, presence: true, uniqueness: true
6
+ validates :amount, presence: true, numericality: { greater_than: 0 }
7
+ validates :status, presence: true, inclusion: { in: %w[pending confirmed cancelled failed] }
8
+
9
+ scope :confirmed, -> { where(status: 'confirmed') }
10
+ scope :pending, -> { where(status: 'pending') }
11
+ scope :cancelled, -> { where(status: 'cancelled') }
12
+ scope :failed, -> { where(status: 'failed') }
13
+
14
+ def confirmed?
15
+ status == 'confirmed'
16
+ end
17
+
18
+ def pending?
19
+ status == 'pending'
20
+ end
21
+
22
+ def cancelled?
23
+ status == 'cancelled'
24
+ end
25
+
26
+ def failed?
27
+ status == 'failed'
28
+ end
29
+
30
+ def formatted_amount
31
+ "#{amount.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}원"
32
+ end
33
+ end
@@ -3,22 +3,79 @@
3
3
  class PaymentsController < ApplicationController
4
4
  include Tosspayments2::Rails::ControllerConcern
5
5
 
6
+ before_action :set_payment, only: %i[show cancel]
7
+
8
+ def index
9
+ @payments = Payment.order(created_at: :desc)
10
+ end
11
+
12
+ def show; end
13
+
14
+ def new
15
+ @payment = Payment.new
16
+ @order_id = "ORDER-#{Time.current.strftime('%Y%m%d%H%M%S')}-#{SecureRandom.hex(4)}"
17
+ end
18
+
19
+ def create
20
+ @payment = Payment.new(payment_params)
21
+ @payment.status = 'pending'
22
+ @payment.order_id = "ORDER-#{Time.current.strftime('%Y%m%d%H%M%S')}-#{SecureRandom.hex(4)}"
23
+
24
+ if @payment.save
25
+ redirect_to checkout_payments_path(order_id: @payment.order_id)
26
+ else
27
+ render :new, status: :unprocessable_entity
28
+ end
29
+ end
30
+
31
+ def checkout
32
+ @order_id = params[:order_id]
33
+ @payment = Payment.find_by!(order_id: @order_id)
34
+ end
35
+
6
36
  def success
7
37
  load_params
8
38
  verify_amount!
9
- @payment = toss_client.confirm(payment_key: @payment_key, order_id: @order_id, amount: @amount)
39
+ process_payment_confirmation
40
+ redirect_to @payment, notice: '결제가 성공적으로 완료되었습니다.'
10
41
  rescue Tosspayments2::Rails::VerificationError, Tosspayments2::Rails::APIError => e
11
- Rails.logger.error("Payment error: #{e.class} #{e.message}")
12
- redirect_to root_path, alert: '결제 승인 실패'
42
+ handle_payment_error(e)
13
43
  end
14
44
 
15
45
  def fail
46
+ order_id = params[:orderId]
47
+
48
+ # 실패 시 Payment 상태 업데이트
49
+ if order_id && (payment = Payment.find_by(order_id: order_id))
50
+ payment.update(status: 'failed')
51
+ end
52
+
16
53
  flash[:alert] = "결제 실패: #{params[:message] || params[:errorMessage]}"
17
54
  redirect_to root_path
18
55
  end
19
56
 
57
+ def cancel
58
+ toss_client.cancel(
59
+ payment_key: @payment.transaction_id,
60
+ cancel_reason: params[:cancel_reason] || '고객 요청에 의한 취소'
61
+ )
62
+ @payment.update!(status: 'cancelled')
63
+ redirect_to @payment, notice: '결제가 취소되었습니다.'
64
+ rescue Tosspayments2::Rails::APIError => e
65
+ Rails.logger.error("Payment cancel error: #{e.message}")
66
+ redirect_to @payment, alert: '결제 취소에 실패했습니다.'
67
+ end
68
+
20
69
  private
21
70
 
71
+ def set_payment
72
+ @payment = Payment.find(params[:id])
73
+ end
74
+
75
+ def payment_params
76
+ params.require(:payment).permit(:amount)
77
+ end
78
+
22
79
  def load_params
23
80
  @payment_key = params[:paymentKey]
24
81
  @order_id = params[:orderId]
@@ -26,12 +83,47 @@ class PaymentsController < ApplicationController
26
83
  end
27
84
 
28
85
  def verify_amount!
29
- Tosspayments2::Rails::CallbackVerifier.new.match_amount?(order_id: @order_id, amount: @amount) do |_oid|
30
- # Lookup the expected amount for the given order id in your domain model.
31
- # Example:
32
- # Order.find_by!(uuid: _oid).amount
33
- # For demo purposes we return a fixed integer:
34
- 1000
86
+ Tosspayments2::Rails::CallbackVerifier.new.match_amount?(order_id: @order_id, amount: @amount) do |oid|
87
+ # 저장된 Payment 레코드에서 금액 확인
88
+ payment = Payment.find_by(order_id: oid)
89
+ payment&.amount || 0
35
90
  end
36
91
  end
92
+
93
+ def process_payment_confirmation
94
+ # 결제 승인 API 호출
95
+ toss_response = toss_client.confirm(payment_key: @payment_key, order_id: @order_id, amount: @amount)
96
+
97
+ # Payment 레코드 생성 또는 업데이트
98
+ @payment = find_or_create_payment
99
+
100
+ # 결제 승인 성공 시 상태 업데이트
101
+ @payment.update!(
102
+ status: 'confirmed',
103
+ transaction_id: toss_response[:paymentKey] || toss_response['paymentKey']
104
+ )
105
+ end
106
+
107
+ def find_or_create_payment
108
+ Payment.find_or_create_by(order_id: @order_id) do |payment|
109
+ payment.amount = @amount
110
+ payment.status = 'pending'
111
+ end
112
+ end
113
+
114
+ def handle_payment_error(error)
115
+ Rails.logger.error("Payment error: #{error.class} #{error.message}")
116
+
117
+ # 실패 시 Payment 상태 업데이트
118
+ update_payment_status_on_failure
119
+
120
+ redirect_to root_path, alert: '결제 승인에 실패했습니다.'
121
+ end
122
+
123
+ def update_payment_status_on_failure
124
+ return unless @order_id
125
+
126
+ payment = Payment.find_by(order_id: @order_id)
127
+ payment&.update(status: 'failed')
128
+ end
37
129
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaymentsHelper
4
+ def payment_status_class(status)
5
+ case status
6
+ when 'confirmed'
7
+ 'bg-success'
8
+ when 'pending'
9
+ 'bg-warning'
10
+ when 'cancelled'
11
+ 'bg-secondary'
12
+ when 'failed'
13
+ 'bg-danger'
14
+ else
15
+ 'bg-light'
16
+ end
17
+ end
18
+
19
+ def payment_status_text(status)
20
+ case status
21
+ when 'confirmed'
22
+ '결제완료'
23
+ when 'pending'
24
+ '결제대기'
25
+ when 'cancelled'
26
+ '결제취소'
27
+ when 'failed'
28
+ '결제실패'
29
+ else
30
+ status
31
+ end
32
+ end
33
+
34
+ def format_currency(amount)
35
+ "#{amount.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}원"
36
+ end
37
+ end
@@ -0,0 +1,150 @@
1
+ <h1>결제 상세</h1>
2
+
3
+ <div class="payment-detail">
4
+ <div class="card">
5
+ <div class="card-header">
6
+ <h3>결제 정보</h3>
7
+ <span class="badge <%= payment_status_class(@payment.status) %>">
8
+ <%= payment_status_text(@payment.status) %>
9
+ </span>
10
+ </div>
11
+
12
+ <div class="card-body">
13
+ <dl class="row">
14
+ <dt class="col-sm-3">주문번호</dt>
15
+ <dd class="col-sm-9"><%= @payment.order_id %></dd>
16
+
17
+ <dt class="col-sm-3">결제금액</dt>
18
+ <dd class="col-sm-9"><%= @payment.formatted_amount %></dd>
19
+
20
+ <dt class="col-sm-3">결제상태</dt>
21
+ <dd class="col-sm-9">
22
+ <span class="badge <%= payment_status_class(@payment.status) %>">
23
+ <%= payment_status_text(@payment.status) %>
24
+ </span>
25
+ </dd>
26
+
27
+ <% if @payment.transaction_id.present? %>
28
+ <dt class="col-sm-3">거래ID</dt>
29
+ <dd class="col-sm-9"><%= @payment.transaction_id %></dd>
30
+ <% end %>
31
+
32
+ <dt class="col-sm-3">생성일시</dt>
33
+ <dd class="col-sm-9"><%= @payment.created_at.strftime('%Y-%m-%d %H:%M:%S') %></dd>
34
+
35
+ <dt class="col-sm-3">최종 업데이트</dt>
36
+ <dd class="col-sm-9"><%= @payment.updated_at.strftime('%Y-%m-%d %H:%M:%S') %></dd>
37
+ </dl>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="actions">
43
+ <%= link_to '목록으로', payments_path, class: 'btn btn-secondary' %>
44
+
45
+ <% if @payment.confirmed? %>
46
+ <%= link_to '결제 취소', cancel_payment_path(@payment), method: :patch,
47
+ confirm: '정말로 이 결제를 취소하시겠습니까?',
48
+ class: 'btn btn-danger' %>
49
+ <% elsif @payment.pending? %>
50
+ <%= link_to '결제 계속하기', checkout_payments_path(order_id: @payment.order_id),
51
+ class: 'btn btn-primary' %>
52
+ <% end %>
53
+ </div>
54
+
55
+ <script>
56
+ function payment_status_class(status) {
57
+ switch(status) {
58
+ case 'confirmed': return 'bg-success';
59
+ case 'pending': return 'bg-warning';
60
+ case 'cancelled': return 'bg-secondary';
61
+ case 'failed': return 'bg-danger';
62
+ default: return 'bg-light';
63
+ }
64
+ }
65
+
66
+ function payment_status_text(status) {
67
+ switch(status) {
68
+ case 'confirmed': return '결제완료';
69
+ case 'pending': return '결제대기';
70
+ case 'cancelled': return '결제취소';
71
+ case 'failed': return '결제실패';
72
+ default: return status;
73
+ }
74
+ }
75
+ </script>
76
+
77
+ <style>
78
+ .payment-detail { margin-bottom: 2rem; }
79
+
80
+ .card {
81
+ position: relative;
82
+ display: flex;
83
+ flex-direction: column;
84
+ min-width: 0;
85
+ word-wrap: break-word;
86
+ background-color: #fff;
87
+ background-clip: border-box;
88
+ border: 1px solid rgba(0,0,0,.125);
89
+ border-radius: 0.25rem;
90
+ }
91
+
92
+ .card-header {
93
+ padding: 0.75rem 1.25rem;
94
+ margin-bottom: 0;
95
+ background-color: rgba(0,0,0,.03);
96
+ border-bottom: 1px solid rgba(0,0,0,.125);
97
+ display: flex;
98
+ justify-content: space-between;
99
+ align-items: center;
100
+ }
101
+
102
+ .card-body { padding: 1.25rem; }
103
+
104
+ .row { display: flex; flex-wrap: wrap; margin-right: -15px; margin-left: -15px; }
105
+ .col-sm-3 { flex: 0 0 25%; max-width: 25%; padding-right: 15px; padding-left: 15px; }
106
+ .col-sm-9 { flex: 0 0 75%; max-width: 75%; padding-right: 15px; padding-left: 15px; }
107
+
108
+ dl { margin-bottom: 1rem; }
109
+ dt { font-weight: 700; }
110
+ dd { margin-bottom: 0.5rem; margin-left: 0; }
111
+
112
+ .badge {
113
+ display: inline-block;
114
+ padding: 0.25em 0.4em;
115
+ font-size: 75%;
116
+ font-weight: 700;
117
+ line-height: 1;
118
+ text-align: center;
119
+ white-space: nowrap;
120
+ vertical-align: baseline;
121
+ border-radius: 0.25rem;
122
+ }
123
+
124
+ .bg-success { background-color: #28a745 !important; color: white; }
125
+ .bg-warning { background-color: #ffc107 !important; color: black; }
126
+ .bg-secondary { background-color: #6c757d !important; color: white; }
127
+ .bg-danger { background-color: #dc3545 !important; color: white; }
128
+
129
+ .actions { margin-top: 2rem; }
130
+
131
+ .btn {
132
+ display: inline-block;
133
+ padding: 0.375rem 0.75rem;
134
+ margin-right: 0.5rem;
135
+ margin-bottom: 0;
136
+ font-size: 1rem;
137
+ font-weight: 400;
138
+ line-height: 1.5;
139
+ text-align: center;
140
+ text-decoration: none;
141
+ vertical-align: middle;
142
+ cursor: pointer;
143
+ border: 1px solid transparent;
144
+ border-radius: 0.25rem;
145
+ }
146
+
147
+ .btn-primary { color: #fff; background-color: #007bff; border-color: #007bff; }
148
+ .btn-secondary { color: #fff; background-color: #6c757d; border-color: #6c757d; }
149
+ .btn-danger { color: #fff; background-color: #dc3545; border-color: #dc3545; }
150
+ </style>
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Tosspayments2
4
4
  module Rails
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tosspayments2-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucius Choi
@@ -52,13 +52,21 @@ files:
52
52
  - ".vscode/mcp.json"
53
53
  - ".yardopts"
54
54
  - CHANGELOG.md
55
+ - CLAUDE.md
55
56
  - LICENSE.txt
56
57
  - README.md
57
58
  - RELEASE_NOTES_v0.2.0.md
58
59
  - Rakefile
59
60
  - lib/generators/tosspayments2/install/install_generator.rb
61
+ - lib/generators/tosspayments2/install/templates/checkout.html.erb
62
+ - lib/generators/tosspayments2/install/templates/index.html.erb
60
63
  - lib/generators/tosspayments2/install/templates/initializer.rb
64
+ - lib/generators/tosspayments2/install/templates/migration.rb
65
+ - lib/generators/tosspayments2/install/templates/new.html.erb
66
+ - lib/generators/tosspayments2/install/templates/payment.rb
61
67
  - lib/generators/tosspayments2/install/templates/payments_controller.rb
68
+ - lib/generators/tosspayments2/install/templates/payments_helper.rb
69
+ - lib/generators/tosspayments2/install/templates/show.html.erb
62
70
  - lib/tosspayments2/rails.rb
63
71
  - lib/tosspayments2/rails/callback_verifier.rb
64
72
  - lib/tosspayments2/rails/client.rb