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.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/README.md +49 -0
  4. data/Rakefile +4 -0
  5. data/lib/generators/kcppayments_rails/install/install_generator.rb +21 -0
  6. data/lib/generators/kcppayments_rails/install/templates/kcp_controller.js +54 -0
  7. data/lib/generators/kcppayments_rails/install/templates/kcppayments_rails.rb +14 -0
  8. data/lib/kcppayments_rails/client.rb +49 -0
  9. data/lib/kcppayments_rails/configuration.rb +24 -0
  10. data/lib/kcppayments_rails/engine.rb +21 -0
  11. data/lib/kcppayments_rails/helpers/kcp_helper.rb +33 -0
  12. data/lib/kcppayments_rails/version.rb +5 -0
  13. data/lib/kcppayments_rails.rb +26 -0
  14. data/sig/kcppayments_rails.rbs +4 -0
  15. data/test_app/.dockerignore +51 -0
  16. data/test_app/.github/dependabot.yml +12 -0
  17. data/test_app/.github/workflows/ci.yml +55 -0
  18. data/test_app/.gitignore +87 -0
  19. data/test_app/.kamal/hooks/docker-setup.sample +3 -0
  20. data/test_app/.kamal/hooks/post-app-boot.sample +3 -0
  21. data/test_app/.kamal/hooks/post-deploy.sample +14 -0
  22. data/test_app/.kamal/hooks/post-proxy-reboot.sample +3 -0
  23. data/test_app/.kamal/hooks/pre-app-boot.sample +3 -0
  24. data/test_app/.kamal/hooks/pre-build.sample +51 -0
  25. data/test_app/.kamal/hooks/pre-connect.sample +47 -0
  26. data/test_app/.kamal/hooks/pre-deploy.sample +122 -0
  27. data/test_app/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  28. data/test_app/.kamal/secrets +17 -0
  29. data/test_app/.rubocop.yml +8 -0
  30. data/test_app/.ruby-version +1 -0
  31. data/test_app/Dockerfile +72 -0
  32. data/test_app/Gemfile +60 -0
  33. data/test_app/Gemfile.lock +370 -0
  34. data/test_app/README.md +181 -0
  35. data/test_app/Rakefile +6 -0
  36. data/test_app/app/assets/images/.keep +0 -0
  37. data/test_app/app/assets/stylesheets/application.css +10 -0
  38. data/test_app/app/controllers/application_controller.rb +4 -0
  39. data/test_app/app/controllers/concerns/.keep +0 -0
  40. data/test_app/app/controllers/orders_controller.rb +31 -0
  41. data/test_app/app/controllers/payments_controller.rb +120 -0
  42. data/test_app/app/helpers/application_helper.rb +2 -0
  43. data/test_app/app/helpers/orders_helper.rb +2 -0
  44. data/test_app/app/helpers/payments_helper.rb +2 -0
  45. data/test_app/app/javascript/application.js +3 -0
  46. data/test_app/app/javascript/controllers/application.js +9 -0
  47. data/test_app/app/javascript/controllers/hello_controller.js +7 -0
  48. data/test_app/app/javascript/controllers/index.js +4 -0
  49. data/test_app/app/javascript/controllers/kcp_controller.js +310 -0
  50. data/test_app/app/jobs/application_job.rb +7 -0
  51. data/test_app/app/mailers/application_mailer.rb +4 -0
  52. data/test_app/app/models/application_record.rb +3 -0
  53. data/test_app/app/models/concerns/.keep +0 -0
  54. data/test_app/app/models/order.rb +67 -0
  55. data/test_app/app/views/layouts/application.html.erb +28 -0
  56. data/test_app/app/views/layouts/mailer.html.erb +13 -0
  57. data/test_app/app/views/layouts/mailer.text.erb +1 -0
  58. data/test_app/app/views/orders/create.html.erb +2 -0
  59. data/test_app/app/views/orders/index.html.erb +41 -0
  60. data/test_app/app/views/orders/new.html.erb +44 -0
  61. data/test_app/app/views/orders/show.html.erb +36 -0
  62. data/test_app/app/views/payments/_receipt_links.html.erb +31 -0
  63. data/test_app/app/views/payments/callback.html.erb +2 -0
  64. data/test_app/app/views/payments/create.html.erb +2 -0
  65. data/test_app/app/views/payments/failure.html.erb +20 -0
  66. data/test_app/app/views/payments/new.html.erb +145 -0
  67. data/test_app/app/views/payments/receipt.html.erb +156 -0
  68. data/test_app/app/views/payments/success.html.erb +34 -0
  69. data/test_app/app/views/pwa/manifest.json.erb +22 -0
  70. data/test_app/app/views/pwa/service-worker.js +26 -0
  71. data/test_app/bin/brakeman +7 -0
  72. data/test_app/bin/dev +2 -0
  73. data/test_app/bin/docker-entrypoint +14 -0
  74. data/test_app/bin/importmap +4 -0
  75. data/test_app/bin/jobs +6 -0
  76. data/test_app/bin/kamal +16 -0
  77. data/test_app/bin/rails +4 -0
  78. data/test_app/bin/rake +4 -0
  79. data/test_app/bin/rubocop +8 -0
  80. data/test_app/bin/setup +65 -0
  81. data/test_app/bin/thrust +5 -0
  82. data/test_app/config/application.rb +42 -0
  83. data/test_app/config/boot.rb +4 -0
  84. data/test_app/config/cable.yml +17 -0
  85. data/test_app/config/cache.yml +16 -0
  86. data/test_app/config/credentials.yml.enc +1 -0
  87. data/test_app/config/database.yml +41 -0
  88. data/test_app/config/deploy.yml +116 -0
  89. data/test_app/config/environment.rb +5 -0
  90. data/test_app/config/environments/development.rb +74 -0
  91. data/test_app/config/environments/production.rb +90 -0
  92. data/test_app/config/environments/test.rb +53 -0
  93. data/test_app/config/importmap.rb +7 -0
  94. data/test_app/config/initializers/assets.rb +7 -0
  95. data/test_app/config/initializers/content_security_policy.rb +25 -0
  96. data/test_app/config/initializers/filter_parameter_logging.rb +8 -0
  97. data/test_app/config/initializers/inflections.rb +16 -0
  98. data/test_app/config/initializers/kcppayments_rails.rb +29 -0
  99. data/test_app/config/locales/en.yml +31 -0
  100. data/test_app/config/master.key +1 -0
  101. data/test_app/config/puma.rb +41 -0
  102. data/test_app/config/queue.yml +18 -0
  103. data/test_app/config/recurring.yml +15 -0
  104. data/test_app/config/routes.rb +29 -0
  105. data/test_app/config/storage.yml +34 -0
  106. data/test_app/config.ru +6 -0
  107. data/test_app/db/cable_schema.rb +11 -0
  108. data/test_app/db/cache_schema.rb +14 -0
  109. data/test_app/db/migrate/20250827075913_create_orders.rb +16 -0
  110. data/test_app/db/migrate/20250827121258_add_kcp_fields_to_orders.rb +6 -0
  111. data/test_app/db/queue_schema.rb +129 -0
  112. data/test_app/db/schema.rb +28 -0
  113. data/test_app/db/seeds.rb +80 -0
  114. data/test_app/lib/tasks/.keep +0 -0
  115. data/test_app/log/.keep +0 -0
  116. data/test_app/public/.well-known/appspecific/com.chrome.devtools.json +1 -0
  117. data/test_app/public/400.html +114 -0
  118. data/test_app/public/404.html +114 -0
  119. data/test_app/public/406-unsupported-browser.html +114 -0
  120. data/test_app/public/422.html +114 -0
  121. data/test_app/public/500.html +114 -0
  122. data/test_app/public/icon.png +0 -0
  123. data/test_app/public/icon.svg +3 -0
  124. data/test_app/public/robots.txt +1 -0
  125. data/test_app/script/.keep +0 -0
  126. data/test_app/tmp/.keep +0 -0
  127. data/test_app/tmp/restart.txt +0 -0
  128. data/test_app/vendor/.keep +0 -0
  129. data/test_app/vendor/javascript/.keep +0 -0
  130. 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
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: "from@example.com"
3
+ layout "mailer"
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ primary_abstract_class
3
+ 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,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1,2 @@
1
+ <h1>Orders#create</h1>
2
+ <p>Find me in app/views/orders/create.html.erb</p>
@@ -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,2 @@
1
+ <h1>Payments#callback</h1>
2
+ <p>Find me in app/views/payments/callback.html.erb</p>
@@ -0,0 +1,2 @@
1
+ <h1>Payments#create</h1>
2
+ <p>Find me in app/views/payments/create.html.erb</p>
@@ -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>