tosspayments2-rails 0.4.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 +4 -4
- data/.claude/agents/kfc/spec-design.md +1 -0
- data/.claude/agents/kfc/spec-impl.md +1 -0
- data/.claude/agents/kfc/spec-judge.md +1 -0
- data/.claude/agents/kfc/spec-requirements.md +1 -0
- data/.claude/agents/kfc/spec-system-prompt-loader.md +1 -0
- data/.claude/agents/kfc/spec-tasks.md +1 -0
- data/.claude/agents/kfc/spec-test.md +1 -0
- data/CHANGELOG.md +22 -0
- data/CLAUDE.md +124 -0
- data/lib/generators/tosspayments2/install/install_generator.rb +16 -2
- data/lib/generators/tosspayments2/install/templates/checkout.html.erb +115 -0
- data/lib/generators/tosspayments2/install/templates/index.html.erb +77 -9
- data/lib/generators/tosspayments2/install/templates/initializer.rb +19 -2
- data/lib/generators/tosspayments2/install/templates/migration.rb +1 -1
- data/lib/generators/tosspayments2/install/templates/new.html.erb +42 -0
- data/lib/generators/tosspayments2/install/templates/payment.rb +28 -2
- data/lib/generators/tosspayments2/install/templates/payments_controller.rb +101 -9
- data/lib/generators/tosspayments2/install/templates/payments_helper.rb +37 -0
- data/lib/generators/tosspayments2/install/templates/show.html.erb +149 -8
- data/lib/tosspayments2/rails/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39febb97b720a3f4b5b8c17869e56d30f512708c553079e158108be18b243856
|
4
|
+
data.tar.gz: d1d2cb235b61caa90e9bd1477894eed5b5322b7eb497f2f3bf48a5423371401d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,28 @@
|
|
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
|
+
|
5
27
|
## [0.4.0] - 2025-08-20
|
6
28
|
### Changed
|
7
29
|
- Add frozen_string_literal magic comments to generator templates
|
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` 사용
|
@@ -11,13 +11,14 @@ module Tosspayments2
|
|
11
11
|
desc: 'Generate Payment model, migration, and views'
|
12
12
|
|
13
13
|
def create_initializer
|
14
|
-
template 'initializer.rb', 'config/initializers/
|
14
|
+
template 'initializer.rb', 'config/initializers/tosspayments.rb'
|
15
15
|
end
|
16
16
|
|
17
17
|
def create_controller
|
18
18
|
return unless options[:controller]
|
19
19
|
|
20
20
|
template 'payments_controller.rb', 'app/controllers/payments_controller.rb'
|
21
|
+
template 'payments_helper.rb', 'app/helpers/payments_helper.rb'
|
21
22
|
end
|
22
23
|
|
23
24
|
def create_payment_model_and_migration
|
@@ -35,10 +36,23 @@ module Tosspayments2
|
|
35
36
|
empty_directory 'app/views/payments'
|
36
37
|
template 'index.html.erb', 'app/views/payments/index.html.erb'
|
37
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'
|
38
41
|
end
|
39
42
|
|
40
43
|
def add_payments_route
|
41
|
-
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
|
42
56
|
end
|
43
57
|
|
44
58
|
# 제너레이터 실행 시 자동으로 모델/마이그레이션/뷰 생성
|
@@ -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>
|
@@ -1,5 +1,10 @@
|
|
1
1
|
<h1>결제 내역</h1>
|
2
|
-
|
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">
|
3
8
|
<thead>
|
4
9
|
<tr>
|
5
10
|
<th>주문번호</th>
|
@@ -7,19 +12,82 @@
|
|
7
12
|
<th>상태</th>
|
8
13
|
<th>거래ID</th>
|
9
14
|
<th>생성일</th>
|
10
|
-
<th
|
15
|
+
<th>액션</th>
|
11
16
|
</tr>
|
12
17
|
</thead>
|
13
18
|
<tbody>
|
14
|
-
<% @payments.
|
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 %>
|
15
42
|
<tr>
|
16
|
-
<td
|
17
|
-
<td><%= payment.amount %></td>
|
18
|
-
<td><%= payment.status %></td>
|
19
|
-
<td><%= payment.transaction_id %></td>
|
20
|
-
<td><%= payment.created_at.strftime('%Y-%m-%d %H:%M') %></td>
|
21
|
-
<td><%= link_to '상세', payment_path(payment) %></td>
|
43
|
+
<td colspan="6" class="text-center">결제 내역이 없습니다.</td>
|
22
44
|
</tr>
|
23
45
|
<% end %>
|
24
46
|
</tbody>
|
25
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
|
-
|
6
|
-
c.
|
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,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>
|
@@ -2,6 +2,32 @@
|
|
2
2
|
|
3
3
|
# 결제 정보를 저장하는 Payment 모델
|
4
4
|
class Payment < ApplicationRecord
|
5
|
-
|
6
|
-
|
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
|
7
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
|
-
|
39
|
+
process_payment_confirmation
|
40
|
+
redirect_to @payment, notice: '결제가 성공적으로 완료되었습니다.'
|
10
41
|
rescue Tosspayments2::Rails::VerificationError, Tosspayments2::Rails::APIError => e
|
11
|
-
|
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 |
|
30
|
-
#
|
31
|
-
|
32
|
-
|
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
|
@@ -1,9 +1,150 @@
|
|
1
1
|
<h1>결제 상세</h1>
|
2
|
-
|
3
|
-
|
4
|
-
<
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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>
|
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.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lucius Choi
|
@@ -52,16 +52,20 @@ 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
|
60
62
|
- lib/generators/tosspayments2/install/templates/index.html.erb
|
61
63
|
- lib/generators/tosspayments2/install/templates/initializer.rb
|
62
64
|
- lib/generators/tosspayments2/install/templates/migration.rb
|
65
|
+
- lib/generators/tosspayments2/install/templates/new.html.erb
|
63
66
|
- lib/generators/tosspayments2/install/templates/payment.rb
|
64
67
|
- lib/generators/tosspayments2/install/templates/payments_controller.rb
|
68
|
+
- lib/generators/tosspayments2/install/templates/payments_helper.rb
|
65
69
|
- lib/generators/tosspayments2/install/templates/show.html.erb
|
66
70
|
- lib/tosspayments2/rails.rb
|
67
71
|
- lib/tosspayments2/rails/callback_verifier.rb
|