kcppayments_rails 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +49 -0
- data/Rakefile +4 -0
- data/lib/generators/kcppayments_rails/install/install_generator.rb +21 -0
- data/lib/generators/kcppayments_rails/install/templates/kcp_controller.js +54 -0
- data/lib/generators/kcppayments_rails/install/templates/kcppayments_rails.rb +14 -0
- data/lib/kcppayments_rails/client.rb +49 -0
- data/lib/kcppayments_rails/configuration.rb +24 -0
- data/lib/kcppayments_rails/engine.rb +21 -0
- data/lib/kcppayments_rails/helpers/kcp_helper.rb +33 -0
- data/lib/kcppayments_rails/version.rb +5 -0
- data/lib/kcppayments_rails.rb +26 -0
- data/sig/kcppayments_rails.rbs +4 -0
- data/test_app/.dockerignore +51 -0
- data/test_app/.github/dependabot.yml +12 -0
- data/test_app/.github/workflows/ci.yml +55 -0
- data/test_app/.gitignore +87 -0
- data/test_app/.kamal/hooks/docker-setup.sample +3 -0
- data/test_app/.kamal/hooks/post-app-boot.sample +3 -0
- data/test_app/.kamal/hooks/post-deploy.sample +14 -0
- data/test_app/.kamal/hooks/post-proxy-reboot.sample +3 -0
- data/test_app/.kamal/hooks/pre-app-boot.sample +3 -0
- data/test_app/.kamal/hooks/pre-build.sample +51 -0
- data/test_app/.kamal/hooks/pre-connect.sample +47 -0
- data/test_app/.kamal/hooks/pre-deploy.sample +122 -0
- data/test_app/.kamal/hooks/pre-proxy-reboot.sample +3 -0
- data/test_app/.kamal/secrets +17 -0
- data/test_app/.rubocop.yml +8 -0
- data/test_app/.ruby-version +1 -0
- data/test_app/Dockerfile +72 -0
- data/test_app/Gemfile +60 -0
- data/test_app/Gemfile.lock +370 -0
- data/test_app/README.md +181 -0
- data/test_app/Rakefile +6 -0
- data/test_app/app/assets/images/.keep +0 -0
- data/test_app/app/assets/stylesheets/application.css +10 -0
- data/test_app/app/controllers/application_controller.rb +4 -0
- data/test_app/app/controllers/concerns/.keep +0 -0
- data/test_app/app/controllers/orders_controller.rb +31 -0
- data/test_app/app/controllers/payments_controller.rb +120 -0
- data/test_app/app/helpers/application_helper.rb +2 -0
- data/test_app/app/helpers/orders_helper.rb +2 -0
- data/test_app/app/helpers/payments_helper.rb +2 -0
- data/test_app/app/javascript/application.js +3 -0
- data/test_app/app/javascript/controllers/application.js +9 -0
- data/test_app/app/javascript/controllers/hello_controller.js +7 -0
- data/test_app/app/javascript/controllers/index.js +4 -0
- data/test_app/app/javascript/controllers/kcp_controller.js +310 -0
- data/test_app/app/jobs/application_job.rb +7 -0
- data/test_app/app/mailers/application_mailer.rb +4 -0
- data/test_app/app/models/application_record.rb +3 -0
- data/test_app/app/models/concerns/.keep +0 -0
- data/test_app/app/models/order.rb +67 -0
- data/test_app/app/views/layouts/application.html.erb +28 -0
- data/test_app/app/views/layouts/mailer.html.erb +13 -0
- data/test_app/app/views/layouts/mailer.text.erb +1 -0
- data/test_app/app/views/orders/create.html.erb +2 -0
- data/test_app/app/views/orders/index.html.erb +41 -0
- data/test_app/app/views/orders/new.html.erb +44 -0
- data/test_app/app/views/orders/show.html.erb +36 -0
- data/test_app/app/views/payments/_receipt_links.html.erb +31 -0
- data/test_app/app/views/payments/callback.html.erb +2 -0
- data/test_app/app/views/payments/create.html.erb +2 -0
- data/test_app/app/views/payments/failure.html.erb +20 -0
- data/test_app/app/views/payments/new.html.erb +145 -0
- data/test_app/app/views/payments/receipt.html.erb +156 -0
- data/test_app/app/views/payments/success.html.erb +34 -0
- data/test_app/app/views/pwa/manifest.json.erb +22 -0
- data/test_app/app/views/pwa/service-worker.js +26 -0
- data/test_app/bin/brakeman +7 -0
- data/test_app/bin/dev +2 -0
- data/test_app/bin/docker-entrypoint +14 -0
- data/test_app/bin/importmap +4 -0
- data/test_app/bin/jobs +6 -0
- data/test_app/bin/kamal +16 -0
- data/test_app/bin/rails +4 -0
- data/test_app/bin/rake +4 -0
- data/test_app/bin/rubocop +8 -0
- data/test_app/bin/setup +65 -0
- data/test_app/bin/thrust +5 -0
- data/test_app/config/application.rb +42 -0
- data/test_app/config/boot.rb +4 -0
- data/test_app/config/cable.yml +17 -0
- data/test_app/config/cache.yml +16 -0
- data/test_app/config/credentials.yml.enc +1 -0
- data/test_app/config/database.yml +41 -0
- data/test_app/config/deploy.yml +116 -0
- data/test_app/config/environment.rb +5 -0
- data/test_app/config/environments/development.rb +74 -0
- data/test_app/config/environments/production.rb +90 -0
- data/test_app/config/environments/test.rb +53 -0
- data/test_app/config/importmap.rb +7 -0
- data/test_app/config/initializers/assets.rb +7 -0
- data/test_app/config/initializers/content_security_policy.rb +25 -0
- data/test_app/config/initializers/filter_parameter_logging.rb +8 -0
- data/test_app/config/initializers/inflections.rb +16 -0
- data/test_app/config/initializers/kcppayments_rails.rb +29 -0
- data/test_app/config/locales/en.yml +31 -0
- data/test_app/config/master.key +1 -0
- data/test_app/config/puma.rb +41 -0
- data/test_app/config/queue.yml +18 -0
- data/test_app/config/recurring.yml +15 -0
- data/test_app/config/routes.rb +29 -0
- data/test_app/config/storage.yml +34 -0
- data/test_app/config.ru +6 -0
- data/test_app/db/cable_schema.rb +11 -0
- data/test_app/db/cache_schema.rb +14 -0
- data/test_app/db/migrate/20250827075913_create_orders.rb +16 -0
- data/test_app/db/migrate/20250827121258_add_kcp_fields_to_orders.rb +6 -0
- data/test_app/db/queue_schema.rb +129 -0
- data/test_app/db/schema.rb +28 -0
- data/test_app/db/seeds.rb +80 -0
- data/test_app/lib/tasks/.keep +0 -0
- data/test_app/log/.keep +0 -0
- data/test_app/public/.well-known/appspecific/com.chrome.devtools.json +1 -0
- data/test_app/public/400.html +114 -0
- data/test_app/public/404.html +114 -0
- data/test_app/public/406-unsupported-browser.html +114 -0
- data/test_app/public/422.html +114 -0
- data/test_app/public/500.html +114 -0
- data/test_app/public/icon.png +0 -0
- data/test_app/public/icon.svg +3 -0
- data/test_app/public/robots.txt +1 -0
- data/test_app/script/.keep +0 -0
- data/test_app/tmp/.keep +0 -0
- data/test_app/tmp/restart.txt +0 -0
- data/test_app/vendor/.keep +0 -0
- data/test_app/vendor/javascript/.keep +0 -0
- metadata +184 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// data-controller="kcp"
|
|
4
|
+
// Values:
|
|
5
|
+
// data-kcp-order-id-value
|
|
6
|
+
// data-kcp-amount-value
|
|
7
|
+
// data-kcp-buyer-name-value
|
|
8
|
+
// data-kcp-buyer-email-value
|
|
9
|
+
// data-kcp-product-name-value
|
|
10
|
+
// data-kcp-return-url-value
|
|
11
|
+
// data-kcp-escrow-value
|
|
12
|
+
// data-kcp-tax-free-amount-value
|
|
13
|
+
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
static values = {
|
|
16
|
+
orderId: String,
|
|
17
|
+
amount: Number,
|
|
18
|
+
buyerName: String,
|
|
19
|
+
buyerEmail: String,
|
|
20
|
+
productName: String,
|
|
21
|
+
returnUrl: String,
|
|
22
|
+
escrow: Boolean,
|
|
23
|
+
taxFreeAmount: Number,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
connect() {
|
|
27
|
+
// KCP 스크립트는 서버에서 include되어야 함 (kcp_script_tag)
|
|
28
|
+
console.log("KCP Controller connected");
|
|
29
|
+
|
|
30
|
+
// 폼 존재 확인
|
|
31
|
+
const form = document.forms["kcp_pay_form"];
|
|
32
|
+
if (form) {
|
|
33
|
+
console.log("KCP form found on page load");
|
|
34
|
+
this.validateFormFields(form);
|
|
35
|
+
|
|
36
|
+
// KCP 결제 완료 후 폼 submit을 가로채기 위한 리스너 추가
|
|
37
|
+
form.addEventListener('submit', this.handleFormSubmit.bind(this));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// KCP 콜백 처리를 위한 전역 함수 등록
|
|
41
|
+
window.kcp_payment_complete = this.handleKcpCallback.bind(this);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
handleFormSubmit(event) {
|
|
45
|
+
console.log("=== Form Submit Intercepted ===");
|
|
46
|
+
console.log("Form data:", new FormData(event.target));
|
|
47
|
+
|
|
48
|
+
// KCP에서 결과가 있는지 확인
|
|
49
|
+
const form = event.target;
|
|
50
|
+
const resCode = form.elements['res_cd']?.value;
|
|
51
|
+
|
|
52
|
+
if (resCode) {
|
|
53
|
+
console.log("KCP result detected:", resCode);
|
|
54
|
+
// res_cd가 있으면 KCP에서 결과를 받은 것이므로 정상 진행
|
|
55
|
+
return true;
|
|
56
|
+
} else {
|
|
57
|
+
console.log("No KCP result - preventing default submit");
|
|
58
|
+
// 아직 KCP 처리가 안 되었으면 submit 방지
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handleKcpCallback(result) {
|
|
65
|
+
console.log("=== KCP Callback Received ===");
|
|
66
|
+
console.log("Result:", result);
|
|
67
|
+
|
|
68
|
+
const form = document.forms["kcp_pay_form"];
|
|
69
|
+
if (form && result) {
|
|
70
|
+
// 결과를 폼에 설정
|
|
71
|
+
if (form.elements['res_cd']) form.elements['res_cd'].value = result.res_cd || '';
|
|
72
|
+
if (form.elements['res_msg']) form.elements['res_msg'].value = result.res_msg || '';
|
|
73
|
+
if (form.elements['tno']) form.elements['tno'].value = result.tno || '';
|
|
74
|
+
|
|
75
|
+
// 폼을 서버로 제출
|
|
76
|
+
form.submit();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
validateFormFields(form) {
|
|
81
|
+
console.log("=== Form Validation ===");
|
|
82
|
+
|
|
83
|
+
// KCP 필수 필드 목록 (KCP 문서 기준)
|
|
84
|
+
const requiredFields = {
|
|
85
|
+
// 가맹점 정보
|
|
86
|
+
'site_cd': 'Site code (가맹점 코드)',
|
|
87
|
+
'site_name': 'Site name (가맹점명)',
|
|
88
|
+
|
|
89
|
+
// 주문 정보
|
|
90
|
+
'ordr_idxx': 'Order ID (주문번호)',
|
|
91
|
+
'good_name': 'Product name (상품명)',
|
|
92
|
+
'good_mny': 'Amount (결제금액)',
|
|
93
|
+
|
|
94
|
+
// 구매자 정보
|
|
95
|
+
'buyr_name': 'Buyer name (구매자명)',
|
|
96
|
+
'buyr_mail': 'Buyer email (구매자 이메일)',
|
|
97
|
+
'buyr_tel1': 'Buyer phone (구매자 전화)',
|
|
98
|
+
'buyr_tel2': 'Buyer phone (구매자 전화)',
|
|
99
|
+
|
|
100
|
+
// 결제 설정
|
|
101
|
+
'pay_method': 'Payment method (결제수단)',
|
|
102
|
+
'curr_cd': 'Currency code (통화코드)',
|
|
103
|
+
'eng_flag': 'Language flag (언어)',
|
|
104
|
+
|
|
105
|
+
// 추가 필수 필드
|
|
106
|
+
'req_tx': 'Request type (요청구분)',
|
|
107
|
+
'encoding_trans': 'Encoding (인코딩)'
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let missingFields = [];
|
|
111
|
+
let emptyFields = [];
|
|
112
|
+
|
|
113
|
+
for (const [fieldName, description] of Object.entries(requiredFields)) {
|
|
114
|
+
const field = form.elements[fieldName];
|
|
115
|
+
if (!field) {
|
|
116
|
+
missingFields.push(`${fieldName} (${description})`);
|
|
117
|
+
} else if (!field.value || field.value.trim() === '') {
|
|
118
|
+
emptyFields.push(`${fieldName} (${description})`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (missingFields.length > 0) {
|
|
123
|
+
console.error("MISSING FIELDS:", missingFields);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (emptyFields.length > 0) {
|
|
127
|
+
console.warn("EMPTY FIELDS:", emptyFields);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (missingFields.length === 0 && emptyFields.length === 0) {
|
|
131
|
+
console.log("✓ All required fields present and populated");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return missingFields.length === 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
requestPayment(event) {
|
|
138
|
+
event?.preventDefault();
|
|
139
|
+
|
|
140
|
+
// 폼 찾기
|
|
141
|
+
const form = document.forms["kcp_pay_form"] || document.getElementById("kcp_pay_form");
|
|
142
|
+
if (!form) {
|
|
143
|
+
console.error("Form 'kcp_pay_form' not found");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// KCP 스크립트 로드 확인
|
|
148
|
+
console.log("=== KCP Payment Debug ===");
|
|
149
|
+
console.log("Form found:", form.name || form.id);
|
|
150
|
+
console.log("KCP_Pay_Execute type:", typeof window.KCP_Pay_Execute);
|
|
151
|
+
console.log("Form action:", form.action);
|
|
152
|
+
console.log("Form method:", form.method);
|
|
153
|
+
|
|
154
|
+
// 모든 폼 필드 확인
|
|
155
|
+
console.log("All form elements:");
|
|
156
|
+
for (let i = 0; i < form.elements.length; i++) {
|
|
157
|
+
const element = form.elements[i];
|
|
158
|
+
if (element.name && element.type === 'hidden') {
|
|
159
|
+
console.log(`${element.name}: "${element.value}"`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// KCP가 일반적으로 확인하는 필드들
|
|
164
|
+
const kcpFields = [
|
|
165
|
+
'site_cd', 'ordr_idxx', 'good_mny', 'buyr_name', 'pay_method', 'curr_cd',
|
|
166
|
+
'buyr_tel1', 'buyr_tel2', 'buyr_tel3', 'buyr_mail', 'good_name',
|
|
167
|
+
'Ret_URL', 'mod_type', 'eng_flag', 'shop_user_id'
|
|
168
|
+
];
|
|
169
|
+
console.log("\nKCP critical fields:");
|
|
170
|
+
kcpFields.forEach(fieldName => {
|
|
171
|
+
const field = form.elements[fieldName];
|
|
172
|
+
if (!field) {
|
|
173
|
+
console.error(`MISSING REQUIRED FIELD: ${fieldName}`);
|
|
174
|
+
} else if (!field.value || field.value.trim() === '') {
|
|
175
|
+
console.error(`EMPTY REQUIRED FIELD: ${fieldName}`);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`${fieldName}: "${field.value}"`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// KCP JavaScript 함수 직접 확인
|
|
182
|
+
console.log("KCP functions available:");
|
|
183
|
+
console.log("KCP_Pay_Execute:", typeof window.KCP_Pay_Execute);
|
|
184
|
+
|
|
185
|
+
if (typeof window.KCP_Pay_Execute !== "function") {
|
|
186
|
+
console.warn("KCP JavaScript not loaded properly. Running demo mode...");
|
|
187
|
+
this.runDemoPayment(form);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// KCP JavaScript가 로드된 경우 실제 호출
|
|
192
|
+
try {
|
|
193
|
+
console.log("\n=== Attempting KCP_Pay_Execute ===");
|
|
194
|
+
|
|
195
|
+
// KCP 함수가 폼 객체를 기대하는 경우를 위한 시도
|
|
196
|
+
if (form && typeof window.KCP_Pay_Execute === "function") {
|
|
197
|
+
// 다양한 호출 방식 시도
|
|
198
|
+
try {
|
|
199
|
+
console.log("Trying: KCP_Pay_Execute with form name string");
|
|
200
|
+
window.KCP_Pay_Execute("kcp_pay_form");
|
|
201
|
+
} catch (e1) {
|
|
202
|
+
console.error("Method 1 failed:", e1.message);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
console.log("Trying: KCP_Pay_Execute with form object");
|
|
206
|
+
window.KCP_Pay_Execute(form);
|
|
207
|
+
} catch (e2) {
|
|
208
|
+
console.error("Method 2 failed:", e2.message);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
console.log("Trying: KCP_Pay_Execute with no arguments");
|
|
212
|
+
window.KCP_Pay_Execute();
|
|
213
|
+
} catch (e3) {
|
|
214
|
+
console.error("Method 3 failed:", e3.message);
|
|
215
|
+
throw e1; // Re-throw original error
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
console.warn("KCP JavaScript not properly loaded");
|
|
221
|
+
this.runDemoPayment(form);
|
|
222
|
+
}
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.error("KCP_Pay_Execute failed:", e);
|
|
225
|
+
console.error("Error stack:", e.stack);
|
|
226
|
+
console.warn("\nFalling back to demo mode...");
|
|
227
|
+
this.runDemoPayment(form);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
runDemoPayment(form) {
|
|
232
|
+
console.log("=== KCP 결제 데모 모드 ===");
|
|
233
|
+
|
|
234
|
+
// 폼 데이터 추출
|
|
235
|
+
const orderData = {
|
|
236
|
+
orderId: form.elements['ordr_idxx'].value,
|
|
237
|
+
productName: form.elements['good_name'].value,
|
|
238
|
+
amount: form.elements['good_mny'].value,
|
|
239
|
+
buyerName: form.elements['buyr_name'].value,
|
|
240
|
+
buyerEmail: form.elements['buyr_mail'].value,
|
|
241
|
+
payMethod: form.elements['pay_method'].value
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
console.log("결제 정보:", orderData);
|
|
245
|
+
|
|
246
|
+
// 결제 수단별 메시지
|
|
247
|
+
let payMethodName = "알 수 없음";
|
|
248
|
+
if (orderData.payMethod === "100000000000") payMethodName = "신용카드";
|
|
249
|
+
else if (orderData.payMethod === "010000000000") payMethodName = "계좌이체";
|
|
250
|
+
else if (orderData.payMethod === "001000000000") payMethodName = "가상계좌";
|
|
251
|
+
|
|
252
|
+
// 결제 진행 시뮬레이션
|
|
253
|
+
const message = `
|
|
254
|
+
데모 결제를 진행합니다:
|
|
255
|
+
|
|
256
|
+
• 주문번호: ${orderData.orderId}
|
|
257
|
+
• 상품명: ${orderData.productName}
|
|
258
|
+
• 결제금액: ${parseInt(orderData.amount).toLocaleString()}원
|
|
259
|
+
• 구매자: ${orderData.buyerName}
|
|
260
|
+
• 결제수단: ${payMethodName}
|
|
261
|
+
|
|
262
|
+
실제 KCP 계정이 설정되면 실제 결제창이 열립니다.
|
|
263
|
+
데모로 결제 성공 처리하시겠습니까?
|
|
264
|
+
`.trim();
|
|
265
|
+
|
|
266
|
+
if (confirm(message)) {
|
|
267
|
+
// 결제 성공 시뮬레이션
|
|
268
|
+
this.simulatePaymentSuccess(orderData);
|
|
269
|
+
} else {
|
|
270
|
+
console.log("데모 결제 취소됨");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
simulatePaymentSuccess(orderData) {
|
|
275
|
+
console.log("결제 성공 시뮬레이션 중...");
|
|
276
|
+
|
|
277
|
+
// 2초 후 성공 페이지로 이동 (실제 KCP 콜백 시뮬레이션)
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
const successUrl = `/payments/success?order_no=${orderData.orderId}&demo=true`;
|
|
280
|
+
window.location.href = successUrl;
|
|
281
|
+
}, 2000);
|
|
282
|
+
|
|
283
|
+
// 로딩 표시
|
|
284
|
+
document.body.style.opacity = "0.7";
|
|
285
|
+
document.body.style.pointerEvents = "none";
|
|
286
|
+
|
|
287
|
+
const loading = document.createElement('div');
|
|
288
|
+
loading.innerHTML = `
|
|
289
|
+
<div style="
|
|
290
|
+
position: fixed;
|
|
291
|
+
top: 50%;
|
|
292
|
+
left: 50%;
|
|
293
|
+
transform: translate(-50%, -50%);
|
|
294
|
+
background: white;
|
|
295
|
+
padding: 30px;
|
|
296
|
+
border-radius: 10px;
|
|
297
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
298
|
+
z-index: 9999;
|
|
299
|
+
text-align: center;
|
|
300
|
+
">
|
|
301
|
+
<div style="margin-bottom: 15px;">⏳</div>
|
|
302
|
+
<div>데모 결제 처리 중...</div>
|
|
303
|
+
<div style="font-size: 12px; color: #666; margin-top: 10px;">
|
|
304
|
+
실제 환경에서는 KCP 결제창이 열립니다
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
`;
|
|
308
|
+
document.body.appendChild(loading);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class ApplicationJob < ActiveJob::Base
|
|
2
|
+
# Automatically retry jobs that encountered a deadlock
|
|
3
|
+
# retry_on ActiveRecord::Deadlocked
|
|
4
|
+
|
|
5
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
|
6
|
+
# discard_on ActiveJob::DeserializationError
|
|
7
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class Order < ApplicationRecord
|
|
2
|
+
# KCP 공식 영수증 URL 생성
|
|
3
|
+
def kcp_receipt_url
|
|
4
|
+
return nil unless kcp_transaction_no.present?
|
|
5
|
+
|
|
6
|
+
# 결제 수단에 따른 cmd 파라미터 결정
|
|
7
|
+
cmd = case payment_method_type
|
|
8
|
+
when 'credit_card'
|
|
9
|
+
'card_bill'
|
|
10
|
+
when 'bank_transfer'
|
|
11
|
+
'acnt_bill'
|
|
12
|
+
when 'virtual_account'
|
|
13
|
+
'vcnt_bill'
|
|
14
|
+
when 'mobile_cash'
|
|
15
|
+
'mcash_bill'
|
|
16
|
+
else
|
|
17
|
+
'card_bill' # 기본값
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
base_url = "https://admin8.kcp.co.kr/assist/bill.BillActionNew.do"
|
|
21
|
+
params = {
|
|
22
|
+
cmd: cmd,
|
|
23
|
+
tno: kcp_transaction_no,
|
|
24
|
+
order_no: order_no,
|
|
25
|
+
trade_mony: amount
|
|
26
|
+
# 주의: site_cd 파라미터는 실제 KCP 가맹점 코드가 필요합니다
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
"#{base_url}?#{params.to_query}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 영수증 사용 가능 여부 확인
|
|
33
|
+
def kcp_receipt_available?
|
|
34
|
+
status == "paid" && kcp_transaction_no.present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 실제 KCP 거래인지 확인 (개발/데모 환경 구분)
|
|
38
|
+
def real_kcp_transaction?
|
|
39
|
+
return false unless kcp_transaction_no.present?
|
|
40
|
+
|
|
41
|
+
# 데모 거래번호 패턴 확인
|
|
42
|
+
is_demo = kcp_transaction_no.match?(/^(DEMO|KCP)/) ||
|
|
43
|
+
created_at > 1.day.ago && Rails.env.development?
|
|
44
|
+
|
|
45
|
+
!is_demo
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# 결제 수단 타입 결정 (payment_method에서 추출)
|
|
51
|
+
def payment_method_type
|
|
52
|
+
return 'credit_card' unless payment_method.present?
|
|
53
|
+
|
|
54
|
+
case payment_method.downcase
|
|
55
|
+
when /credit|card|신용|카드/
|
|
56
|
+
'credit_card'
|
|
57
|
+
when /bank|transfer|계좌|이체/
|
|
58
|
+
'bank_transfer'
|
|
59
|
+
when /virtual|account|가상|계좌/
|
|
60
|
+
'virtual_account'
|
|
61
|
+
when /mobile|cash|모바일|현금/
|
|
62
|
+
'mobile_cash'
|
|
63
|
+
else
|
|
64
|
+
'credit_card'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= content_for(:title) || "Test App" %></title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
8
|
+
<%= csrf_meta_tags %>
|
|
9
|
+
<%= csp_meta_tag %>
|
|
10
|
+
|
|
11
|
+
<%= yield :head %>
|
|
12
|
+
|
|
13
|
+
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
|
14
|
+
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
|
15
|
+
|
|
16
|
+
<link rel="icon" href="/icon.png" type="image/png">
|
|
17
|
+
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
|
18
|
+
<link rel="apple-touch-icon" href="/icon.png">
|
|
19
|
+
|
|
20
|
+
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
|
21
|
+
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
|
22
|
+
<%= javascript_importmap_tags %>
|
|
23
|
+
</head>
|
|
24
|
+
|
|
25
|
+
<body>
|
|
26
|
+
<%= yield %>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= yield %>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<h1>주문 목록</h1>
|
|
2
|
+
|
|
3
|
+
<%= link_to "새 주문 생성", new_order_path, class: "btn btn-primary" %>
|
|
4
|
+
|
|
5
|
+
<table style="margin-top: 20px; border-collapse: collapse; width: 100%;">
|
|
6
|
+
<thead>
|
|
7
|
+
<tr style="background-color: #f0f0f0;">
|
|
8
|
+
<th style="padding: 10px; border: 1px solid #ddd;">주문번호</th>
|
|
9
|
+
<th style="padding: 10px; border: 1px solid #ddd;">상품명</th>
|
|
10
|
+
<th style="padding: 10px; border: 1px solid #ddd;">금액</th>
|
|
11
|
+
<th style="padding: 10px; border: 1px solid #ddd;">구매자</th>
|
|
12
|
+
<th style="padding: 10px; border: 1px solid #ddd;">상태</th>
|
|
13
|
+
<th style="padding: 10px; border: 1px solid #ddd;">액션</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody>
|
|
17
|
+
<% @orders.each do |order| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td style="padding: 10px; border: 1px solid #ddd;"><%= order.order_no %></td>
|
|
20
|
+
<td style="padding: 10px; border: 1px solid #ddd;"><%= order.product_name %></td>
|
|
21
|
+
<td style="padding: 10px; border: 1px solid #ddd;"><%= number_to_currency(order.amount, unit: "₩", format: "%u%n", delimiter: ",", precision: 0) %></td>
|
|
22
|
+
<td style="padding: 10px; border: 1px solid #ddd;"><%= order.buyer_name %></td>
|
|
23
|
+
<td style="padding: 10px; border: 1px solid #ddd;">
|
|
24
|
+
<% if order.status == "paid" %>
|
|
25
|
+
<span style="color: green;">결제완료</span>
|
|
26
|
+
<% elsif order.status == "failed" %>
|
|
27
|
+
<span style="color: red;">결제실패</span>
|
|
28
|
+
<% else %>
|
|
29
|
+
<span style="color: orange;">대기중</span>
|
|
30
|
+
<% end %>
|
|
31
|
+
</td>
|
|
32
|
+
<td style="padding: 10px; border: 1px solid #ddd;">
|
|
33
|
+
<%= link_to "상세보기", order_path(order) %>
|
|
34
|
+
<% if order.status == "pending" %>
|
|
35
|
+
| <%= link_to "결제하기", new_payment_path(order_id: order.id), data: { turbo: false } %>
|
|
36
|
+
<% end %>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% end %>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<h1>새 주문 생성</h1>
|
|
2
|
+
|
|
3
|
+
<%= form_with model: @order do |form| %>
|
|
4
|
+
<% if @order.errors.any? %>
|
|
5
|
+
<div id="error_explanation">
|
|
6
|
+
<h2><%= @order.errors.count %> 개의 오류가 발생했습니다:</h2>
|
|
7
|
+
<ul>
|
|
8
|
+
<% @order.errors.full_messages.each do |message| %>
|
|
9
|
+
<li><%= message %></li>
|
|
10
|
+
<% end %>
|
|
11
|
+
</ul>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<div style="margin-bottom: 15px;">
|
|
16
|
+
<%= form.label :product_name, "상품명" %>
|
|
17
|
+
<%= form.text_field :product_name, style: "width: 300px; padding: 5px;" %>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div style="margin-bottom: 15px;">
|
|
21
|
+
<%= form.label :amount, "금액" %>
|
|
22
|
+
<%= form.number_field :amount, style: "width: 200px; padding: 5px;" %>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div style="margin-bottom: 15px;">
|
|
26
|
+
<%= form.label :buyer_name, "구매자명" %>
|
|
27
|
+
<%= form.text_field :buyer_name, style: "width: 200px; padding: 5px;" %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div style="margin-bottom: 15px;">
|
|
31
|
+
<%= form.label :buyer_email, "구매자 이메일" %>
|
|
32
|
+
<%= form.email_field :buyer_email, style: "width: 300px; padding: 5px;" %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div style="margin-bottom: 15px;">
|
|
36
|
+
<%= form.label :tax_free_amount, "면세 금액 (선택사항)" %>
|
|
37
|
+
<%= form.number_field :tax_free_amount, style: "width: 200px; padding: 5px;" %>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div>
|
|
41
|
+
<%= form.submit "주문 생성", style: "padding: 10px 20px; background-color: #4CAF50; color: white; border: none; cursor: pointer;" %>
|
|
42
|
+
<%= link_to "취소", orders_path, style: "margin-left: 10px;" %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<h1>주문 상세</h1>
|
|
2
|
+
|
|
3
|
+
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
|
4
|
+
<p><strong>주문번호:</strong> <%= @order.order_no %></p>
|
|
5
|
+
<p><strong>상품명:</strong> <%= @order.product_name %></p>
|
|
6
|
+
<p><strong>금액:</strong> <%= number_to_currency(@order.amount, unit: "₩", format: "%u%n", delimiter: ",", precision: 0) %></p>
|
|
7
|
+
<% if @order.tax_free_amount.present? && @order.tax_free_amount > 0 %>
|
|
8
|
+
<p><strong>면세금액:</strong> <%= number_to_currency(@order.tax_free_amount, unit: "₩", format: "%u%n", delimiter: ",", precision: 0) %></p>
|
|
9
|
+
<% end %>
|
|
10
|
+
<p><strong>구매자명:</strong> <%= @order.buyer_name %></p>
|
|
11
|
+
<p><strong>구매자 이메일:</strong> <%= @order.buyer_email %></p>
|
|
12
|
+
<p><strong>상태:</strong>
|
|
13
|
+
<% if @order.status == "paid" %>
|
|
14
|
+
<span style="color: green;">결제완료</span>
|
|
15
|
+
<% elsif @order.status == "failed" %>
|
|
16
|
+
<span style="color: red;">결제실패</span>
|
|
17
|
+
<% else %>
|
|
18
|
+
<span style="color: orange;">대기중</span>
|
|
19
|
+
<% end %>
|
|
20
|
+
</p>
|
|
21
|
+
<% if @order.payment_method.present? %>
|
|
22
|
+
<p><strong>결제수단:</strong> <%= @order.payment_method %></p>
|
|
23
|
+
<% end %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div style="margin-top: 20px;">
|
|
27
|
+
<% if @order.status == "pending" %>
|
|
28
|
+
<%= link_to "결제하기", new_payment_path(order_id: @order.id), data: { turbo: false },
|
|
29
|
+
style: "padding: 10px 20px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 3px;" %>
|
|
30
|
+
<% else %>
|
|
31
|
+
<%= render 'payments/receipt_links', order: @order %>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
<div style="margin-top: 20px;">
|
|
35
|
+
<%= link_to "목록으로", orders_path, style: "margin-left: 10px;" %>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<% if order.status == "paid" %>
|
|
2
|
+
<%= link_to "📄 자체 영수증", receipt_payment_path(order),
|
|
3
|
+
target: "_blank",
|
|
4
|
+
style: "margin-right: 10px; padding: 10px 20px; background-color: #2196F3; color: white; text-decoration: none; border-radius: 3px;" %>
|
|
5
|
+
|
|
6
|
+
<% if order.kcp_receipt_available? %>
|
|
7
|
+
<% if order.real_kcp_transaction? %>
|
|
8
|
+
<%= link_to "🏦 KCP 공식 영수증", order.kcp_receipt_url,
|
|
9
|
+
target: "_blank",
|
|
10
|
+
style: "padding: 10px 20px; background-color: #FF9800; color: white; text-decoration: none; border-radius: 3px;" %>
|
|
11
|
+
<% else %>
|
|
12
|
+
<%= link_to "🏦 KCP 공식 영수증 (데모)", order.kcp_receipt_url,
|
|
13
|
+
target: "_blank",
|
|
14
|
+
onclick: "alert('이는 데모/테스트 거래로, KCP 서버에 실제 거래 내역이 없어 \"해당거래 내역이 없다\" 메시지가 표시될 수 있습니다. 📄 자체 영수증을 이용해주세요.'); return true;",
|
|
15
|
+
style: "padding: 10px 20px; background-color: #6c757d; color: white; text-decoration: none; border-radius: 3px;" %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% if order.kcp_receipt_available? %>
|
|
20
|
+
<div style="margin-top: 10px; padding: 10px; background-color: #f8f9fa; border-radius: 5px; font-size: 14px; color: #6c757d;">
|
|
21
|
+
💡 <strong>영수증 안내:</strong><br>
|
|
22
|
+
• <strong>📄 자체 영수증</strong>: 즉시 확인 가능한 상세 영수증 ⭐<strong>추천</strong><br>
|
|
23
|
+
<% if order.real_kcp_transaction? %>
|
|
24
|
+
• <strong>🏦 KCP 공식 영수증</strong>: KCP에서 제공하는 공식 매출전표
|
|
25
|
+
<% else %>
|
|
26
|
+
• <strong>🏦 KCP 공식 영수증 (데모)</strong>: 테스트 환경용 (실제 거래 내역 없음)<br>
|
|
27
|
+
<small style="color: #dc3545;">⚠️ 데모/테스트 거래로 인해 "해당거래 내역이 없다" 메시지가 표시될 수 있습니다.</small>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% end %>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<h1 style="color: red;">✗ 결제 실패</h1>
|
|
2
|
+
|
|
3
|
+
<div style="background-color: #ffebee; padding: 20px; border-radius: 5px; border: 1px solid #f44336;">
|
|
4
|
+
<h2>결제가 실패했습니다.</h2>
|
|
5
|
+
|
|
6
|
+
<p>결제 처리 중 문제가 발생했습니다. 다시 시도해 주세요.</p>
|
|
7
|
+
|
|
8
|
+
<div style="margin-top: 20px;">
|
|
9
|
+
<p><strong>주문번호:</strong> <%= @order.order_no %></p>
|
|
10
|
+
<p><strong>상품명:</strong> <%= @order.product_name %></p>
|
|
11
|
+
<p><strong>결제금액:</strong> <%= number_to_currency(@order.amount, unit: "₩", format: "%u%n", delimiter: ",", precision: 0) %></p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div style="margin-top: 20px;">
|
|
16
|
+
<%= link_to "다시 결제하기", new_payment_path(order_id: @order.id),
|
|
17
|
+
style: "padding: 10px 20px; background-color: #FF5722; color: white; text-decoration: none; border-radius: 3px;" %>
|
|
18
|
+
<%= link_to "주문 상세보기", order_path(@order), style: "margin-left: 10px;" %>
|
|
19
|
+
<%= link_to "주문 목록", orders_path, style: "margin-left: 10px;" %>
|
|
20
|
+
</div>
|