bootpay 1.2.0 → 3.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 +4 -4
- data/.gitignore +8 -9
- data/.rspec +2 -0
- data/CHANGELOG.md +107 -21
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile +1 -3
- data/LICENSE.txt +1 -1
- data/README.md +309 -196
- data/Rakefile +3 -3
- data/bin/console +1 -1
- data/bootpay.gemspec +22 -20
- data/lib/bootpay/bootpay-rest-client.rb +51 -0
- data/lib/bootpay/concern/authenticate.rb +69 -0
- data/lib/bootpay/concern/cash_receipt.rb +83 -0
- data/lib/bootpay/concern/easy.rb +18 -0
- data/lib/bootpay/concern/escrow.rb +25 -0
- data/lib/bootpay/concern/payment.rb +126 -0
- data/lib/bootpay/concern/reseller.rb +97 -0
- data/lib/bootpay/concern/rest.rb +56 -0
- data/lib/bootpay/concern/sdk.rb +52 -0
- data/lib/bootpay/concern/seller.rb +15 -0
- data/lib/bootpay/concern/service.rb +15 -0
- data/lib/bootpay/concern/subscription.rb +211 -0
- data/lib/bootpay/concern/token.rb +33 -0
- data/lib/bootpay/concern/user_token.rb +23 -0
- data/lib/bootpay/concern/wallet.rb +20 -0
- data/lib/bootpay/concern/webhook.rb +18 -0
- data/lib/bootpay/concern.rb +35 -0
- data/lib/bootpay-backend-ruby.rb +6 -0
- data/lib/bootpay.rb +10 -66
- data/lib/bootpay_storage/bootpay-storage-rest-client.rb +46 -0
- data/lib/bootpay_storage/concern/csv.rb +18 -0
- data/lib/bootpay_storage/concern/image.rb +30 -0
- data/lib/bootpay_storage/concern/rest.rb +176 -0
- data/lib/bootpay_storage/concern/token.rb +9 -0
- data/lib/bootpay_storage/concern.rb +12 -0
- data/lib/bootpay_storage/response.rb +14 -0
- data/lib/bootpay_store/bootpay-store-rest-client.rb +62 -0
- data/lib/bootpay_store/concern/alimtalk_message.rb +67 -0
- data/lib/bootpay_store/concern/alimtalk_official.rb +63 -0
- data/lib/bootpay_store/concern/alimtalk_optout.rb +72 -0
- data/lib/bootpay_store/concern/alimtalk_send.rb +92 -0
- data/lib/bootpay_store/concern/alimtalk_sender.rb +105 -0
- data/lib/bootpay_store/concern/alimtalk_template.rb +215 -0
- data/lib/bootpay_store/concern/alimtalk_webhook.rb +84 -0
- data/lib/bootpay_store/concern/faq.rb +97 -0
- data/lib/bootpay_store/concern/inquiry.rb +138 -0
- data/lib/bootpay_store/concern/invoice.rb +102 -0
- data/lib/bootpay_store/concern/mall_setting.rb +385 -0
- data/lib/bootpay_store/concern/notice.rb +100 -0
- data/lib/bootpay_store/concern/order.rb +74 -0
- data/lib/bootpay_store/concern/order_subscription.rb +391 -0
- data/lib/bootpay_store/concern/payment.rb +119 -0
- data/lib/bootpay_store/concern/product.rb +175 -0
- data/lib/bootpay_store/concern/product_qna.rb +149 -0
- data/lib/bootpay_store/concern/product_review.rb +145 -0
- data/lib/bootpay_store/concern/rest.rb +139 -0
- data/lib/bootpay_store/concern/store.rb +31 -0
- data/lib/bootpay_store/concern/supervisor.rb +211 -0
- data/lib/bootpay_store/concern/token.rb +24 -0
- data/lib/bootpay_store/concern/user.rb +325 -0
- data/lib/bootpay_store/concern/user_group.rb +221 -0
- data/lib/bootpay_store/concern/webhook.rb +17 -0
- data/lib/bootpay_store/concern.rb +55 -0
- data/lib/bootpay_store/response.rb +14 -0
- data/lib/version.rb +9 -0
- metadata +82 -49
- data/.DS_Store +0 -0
- data/.env.example +0 -23
- data/lib/.DS_Store +0 -0
- data/lib/bootpay/.DS_Store +0 -0
- data/lib/bootpay/authentication.rb +0 -61
- data/lib/bootpay/automatic_transfer.rb +0 -56
- data/lib/bootpay/billing.rb +0 -157
- data/lib/bootpay/cancel.rb +0 -29
- data/lib/bootpay/cash_receipt.rb +0 -77
- data/lib/bootpay/commerce/cart.rb +0 -20
- data/lib/bootpay/commerce/category.rb +0 -39
- data/lib/bootpay/commerce/commerce_resource.rb +0 -196
- data/lib/bootpay/commerce/coupon.rb +0 -41
- data/lib/bootpay/commerce/invoice.rb +0 -46
- data/lib/bootpay/commerce/order.rb +0 -61
- data/lib/bootpay/commerce/order_cancel.rb +0 -52
- data/lib/bootpay/commerce/order_subscription.rb +0 -117
- data/lib/bootpay/commerce/order_subscription_adjustment.rb +0 -29
- data/lib/bootpay/commerce/order_subscription_bill.rb +0 -50
- data/lib/bootpay/commerce/order_subscription_request.rb +0 -60
- data/lib/bootpay/commerce/point.rb +0 -36
- data/lib/bootpay/commerce/product.rb +0 -63
- data/lib/bootpay/commerce/store.rb +0 -21
- data/lib/bootpay/commerce/user.rb +0 -75
- data/lib/bootpay/commerce/user_group.rb +0 -70
- data/lib/bootpay/easy.rb +0 -25
- data/lib/bootpay/escrow.rb +0 -24
- data/lib/bootpay/link.rb +0 -60
- data/lib/bootpay/naverpay.rb +0 -17
- data/lib/bootpay/payment_resource.rb +0 -31
- data/lib/bootpay/reseller.rb +0 -12
- data/lib/bootpay/rest.rb +0 -46
- data/lib/bootpay/submit.rb +0 -20
- data/lib/bootpay/token.rb +0 -33
- data/lib/bootpay/verification.rb +0 -55
- data/lib/bootpay/version.rb +0 -25
- data/lib/bootpay/wallet.rb +0 -56
- data/lib/bootpay_commerce.rb +0 -152
- /data/lib/{response.rb → bootpay/response.rb} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
module BootpayStore::Concern::Product
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
# 상품 목록을 조회한다 (V1 Mall API)
|
|
6
|
+
# Comment by Codex
|
|
7
|
+
# @date: 2026-02-23
|
|
8
|
+
# @date: 26-08-14 ⚠️ keyword 는 서버(v1/products_controller#index)가 읽지 않는다.
|
|
9
|
+
# 컨트롤러는 page/limit/category_id/ex_uid/sort 만 사용 — keyword 를 보내도 조용히 무시된다.
|
|
10
|
+
# 하위호환 때문에 인자는 남겨두되, 검색이 필요하면 서버 지원 추가가 선행되어야 한다.
|
|
11
|
+
# @date: 26-08-26 ex_uid 인자 추가. 컨트롤러가 params[:ex_uid] 를 읽는데 SDK 에 인자가 없어
|
|
12
|
+
# 외부 UID 로 상품을 찾을 수 없었다.
|
|
13
|
+
def products(page: 1, limit: 20, category_id: nil, ex_uid: nil, sort: nil, keyword: nil,
|
|
14
|
+
user_jwt: nil, idempotency_key: nil)
|
|
15
|
+
request(
|
|
16
|
+
uri: 'products',
|
|
17
|
+
method: :get,
|
|
18
|
+
headers: {
|
|
19
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
20
|
+
'Bootpay-User-JWT' => user_jwt
|
|
21
|
+
}.compact,
|
|
22
|
+
params: {
|
|
23
|
+
page: page,
|
|
24
|
+
limit: limit,
|
|
25
|
+
category_id: category_id,
|
|
26
|
+
ex_uid: ex_uid,
|
|
27
|
+
sort: sort,
|
|
28
|
+
keyword: keyword
|
|
29
|
+
}.compact
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 상품 상세를 조회한다 (V1 Mall API)
|
|
34
|
+
# Comment by Codex
|
|
35
|
+
# @date: 2026-02-23
|
|
36
|
+
def product_detail(product_id:, user_jwt: nil, idempotency_key: nil)
|
|
37
|
+
request(
|
|
38
|
+
uri: "products/#{product_id}",
|
|
39
|
+
method: :get,
|
|
40
|
+
headers: {
|
|
41
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
42
|
+
'Bootpay-User-JWT' => user_jwt
|
|
43
|
+
}.compact
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 상품 정보를 가져온다
|
|
48
|
+
# Comment by GOSOMI
|
|
49
|
+
# @date: 2025-10-10
|
|
50
|
+
# @date: 26-08-14 product_detail 과 uri·동작이 같다(차이는 user_jwt 인자 유무).
|
|
51
|
+
# 중복이지만 기존 사용자가 있을 수 있어 제거하지 않는다 — 신규 코드는 product_detail 을 쓸 것.
|
|
52
|
+
# @date: 26-08-26 user_jwt 를 여기에도 추가했다. 매뉴얼이 GET /v1/products/:id 에 user_jwt 를
|
|
53
|
+
# 안내하는데 이 메서드만 헤더를 안 보내 회원 컨텍스트 조회가 안 됐다. 이제 product_detail 과 동작이 같다.
|
|
54
|
+
def lookup_product(product_id:, user_jwt: nil, idempotency_key: nil)
|
|
55
|
+
request(
|
|
56
|
+
uri: "products/#{product_id}",
|
|
57
|
+
method: :get,
|
|
58
|
+
headers: {
|
|
59
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
60
|
+
'Bootpay-User-JWT' => user_jwt
|
|
61
|
+
}.compact
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 상품을 등록한다 (POST /v1/products)
|
|
66
|
+
# @comment_by Claude (alfred)
|
|
67
|
+
# @date: 26-08-14
|
|
68
|
+
# images 가 있으면 multipart, 없으면 JSON 으로 보낸다.
|
|
69
|
+
# 컨트롤러가 읽는 필드는 _product_params 참조 — 여기 명시하지 않은 값도 attrs 로 그대로 전달된다.
|
|
70
|
+
def product_create(name:, display_price: nil, desc: nil, content: nil, category_id: nil,
|
|
71
|
+
type: nil, stock: nil, status_sale: nil, status_display: nil,
|
|
72
|
+
use_subscription: nil, subscription_setting_id: nil,
|
|
73
|
+
images: nil, save_by: nil, idempotency_key: nil, **attrs)
|
|
74
|
+
payload = {
|
|
75
|
+
name: name,
|
|
76
|
+
display_price: display_price,
|
|
77
|
+
desc: desc,
|
|
78
|
+
content: content,
|
|
79
|
+
category_id: category_id,
|
|
80
|
+
type: type,
|
|
81
|
+
stock: stock,
|
|
82
|
+
status_sale: status_sale,
|
|
83
|
+
status_display: status_display,
|
|
84
|
+
use_subscription: use_subscription,
|
|
85
|
+
subscription_setting_id: subscription_setting_id,
|
|
86
|
+
save_by: save_by
|
|
87
|
+
}.merge(attrs).compact
|
|
88
|
+
|
|
89
|
+
headers = {
|
|
90
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
91
|
+
'Bootpay-Role' => 'manager'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if images.present?
|
|
95
|
+
form = payload.each_with_object({}) { |(k, v), h| h[k.to_s] = multipart_value(v) }
|
|
96
|
+
Array(images).each_with_index { |image, i| form["images[#{i}]"] = multipart_file(image) }
|
|
97
|
+
post_multipart(uri: 'products', form: form, headers: headers)
|
|
98
|
+
else
|
|
99
|
+
request(uri: 'products', method: :post, headers: headers, payload: payload)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 상품을 수정한다 (PUT /v1/products/:id)
|
|
104
|
+
# @comment_by Claude (alfred)
|
|
105
|
+
# @date: 26-08-14
|
|
106
|
+
# 바뀐 값만 보내면 된다. category_id 는 키 존재 여부로 '해제 의사'를 판별하므로 주의.
|
|
107
|
+
def product_update(product_id:, name: nil, display_price: nil, desc: nil, content: nil,
|
|
108
|
+
category_id: nil, stock: nil, status_sale: nil, status_display: nil,
|
|
109
|
+
save_by: nil, idempotency_key: nil, **attrs)
|
|
110
|
+
request(
|
|
111
|
+
uri: "products/#{product_id}",
|
|
112
|
+
method: :put,
|
|
113
|
+
headers: {
|
|
114
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
115
|
+
'Bootpay-Role' => 'manager'
|
|
116
|
+
},
|
|
117
|
+
payload: {
|
|
118
|
+
name: name,
|
|
119
|
+
display_price: display_price,
|
|
120
|
+
desc: desc,
|
|
121
|
+
content: content,
|
|
122
|
+
category_id: category_id,
|
|
123
|
+
stock: stock,
|
|
124
|
+
status_sale: status_sale,
|
|
125
|
+
status_display: status_display,
|
|
126
|
+
save_by: save_by
|
|
127
|
+
}.merge(attrs).compact
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# 상품을 삭제한다 (DELETE /v1/products/:id)
|
|
132
|
+
# @comment_by Claude (alfred)
|
|
133
|
+
# @date: 26-08-14
|
|
134
|
+
def product_delete(product_id:, idempotency_key: nil)
|
|
135
|
+
request(
|
|
136
|
+
uri: "products/#{product_id}",
|
|
137
|
+
method: :delete,
|
|
138
|
+
headers: {
|
|
139
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
140
|
+
'Bootpay-Role' => 'manager'
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# 상품 판매/노출 상태를 변경한다 (PUT /v1/products/:id/status)
|
|
146
|
+
# @comment_by Claude (alfred)
|
|
147
|
+
# @date: 26-08-14
|
|
148
|
+
# 컨트롤러 _status_params 기준. 재고(stock)는 여기가 아니라 product_update 로 바꾼다.
|
|
149
|
+
def product_status(product_id:, status_sale: nil, status_display: nil, status_frozen: nil,
|
|
150
|
+
status_review: nil, use_display_period: nil, display_start_at: nil, display_end_at: nil,
|
|
151
|
+
use_sale_period: nil, sale_start_at: nil, sale_end_at: nil,
|
|
152
|
+
idempotency_key: nil, **attrs)
|
|
153
|
+
request(
|
|
154
|
+
uri: "products/#{product_id}/status",
|
|
155
|
+
method: :put,
|
|
156
|
+
headers: {
|
|
157
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
158
|
+
'Bootpay-Role' => 'manager'
|
|
159
|
+
},
|
|
160
|
+
payload: {
|
|
161
|
+
status_sale: status_sale,
|
|
162
|
+
status_display: status_display,
|
|
163
|
+
status_frozen: status_frozen,
|
|
164
|
+
status_review: status_review,
|
|
165
|
+
use_display_period: use_display_period,
|
|
166
|
+
display_start_at: display_start_at,
|
|
167
|
+
display_end_at: display_end_at,
|
|
168
|
+
use_sale_period: use_sale_period,
|
|
169
|
+
sale_start_at: sale_start_at,
|
|
170
|
+
sale_end_at: sale_end_at
|
|
171
|
+
}.merge(attrs).compact
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
module BootpayStore::Concern::ProductQna
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
# 상품문의 목록 (GET /v1/product-qnas)
|
|
6
|
+
# @comment_by Claude
|
|
7
|
+
# @date: 26-09-29
|
|
8
|
+
# 고객 모드는 product_id 필수. 다른 사람의 비밀글은 is_viewable: false 로 본문 없이 온다.
|
|
9
|
+
# 회원(user_id·login_id·user_jwt)을 보내면 그 회원이 쓴 비밀글 본문이 보인다.
|
|
10
|
+
# supervisor: true + view: 'all' 이면 몰 전체(숨김 포함) — 이때 product_id 는 선택 필터다.
|
|
11
|
+
def product_qnas(product_id: nil, view: nil, page: 1, limit: 20, user_id: nil, login_id: nil, user_jwt: nil,
|
|
12
|
+
supervisor: false, idempotency_key: nil)
|
|
13
|
+
request(
|
|
14
|
+
uri: 'product-qnas',
|
|
15
|
+
method: :get,
|
|
16
|
+
headers: {
|
|
17
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
18
|
+
'Bootpay-Role' => supervisor ? 'supervisor' : 'user',
|
|
19
|
+
'Bootpay-User-JWT' => user_jwt
|
|
20
|
+
}.compact,
|
|
21
|
+
params: {
|
|
22
|
+
product_id: product_id,
|
|
23
|
+
view: view,
|
|
24
|
+
page: page,
|
|
25
|
+
limit: limit,
|
|
26
|
+
user_id: user_id,
|
|
27
|
+
login_id: login_id
|
|
28
|
+
}.compact
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 상품문의 단건 (GET /v1/product-qnas/:id)
|
|
33
|
+
# @comment_by Claude
|
|
34
|
+
# @date: 26-09-29
|
|
35
|
+
# 다른 사람의 비밀글은 403 PRODUCT_QNA_SECRET_FORBIDDEN. supervisor: true 면 비밀글·숨김 글도 본다.
|
|
36
|
+
def product_qna_detail(product_qna_id:, user_id: nil, login_id: nil, user_jwt: nil, supervisor: false,
|
|
37
|
+
idempotency_key: nil)
|
|
38
|
+
request(
|
|
39
|
+
uri: "product-qnas/#{product_qna_id}",
|
|
40
|
+
method: :get,
|
|
41
|
+
headers: {
|
|
42
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
43
|
+
'Bootpay-Role' => supervisor ? 'supervisor' : 'user',
|
|
44
|
+
'Bootpay-User-JWT' => user_jwt
|
|
45
|
+
}.compact,
|
|
46
|
+
params: {
|
|
47
|
+
user_id: user_id,
|
|
48
|
+
login_id: login_id
|
|
49
|
+
}.compact
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 상품문의 작성 (POST /v1/product-qnas)
|
|
54
|
+
# @comment_by Claude
|
|
55
|
+
# @date: 26-09-29
|
|
56
|
+
# 회원(user_id·login_id·user_jwt) 또는 비회원(guest_name + guest_password, 몰이 비회원 작성을 허용할 때).
|
|
57
|
+
# 회원 문의면 guest_* 는 무시된다.
|
|
58
|
+
def product_qna_create(product_id:, content:, title: nil, product_option_id: nil, option_text: nil,
|
|
59
|
+
is_secret: nil, guest_name: nil, guest_password: nil,
|
|
60
|
+
user_id: nil, login_id: nil, user_jwt: nil, idempotency_key: nil)
|
|
61
|
+
request(
|
|
62
|
+
uri: 'product-qnas',
|
|
63
|
+
method: :post,
|
|
64
|
+
headers: {
|
|
65
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
66
|
+
'Bootpay-Role' => 'user',
|
|
67
|
+
'Bootpay-User-JWT' => user_jwt
|
|
68
|
+
}.compact,
|
|
69
|
+
payload: {
|
|
70
|
+
product_id: product_id,
|
|
71
|
+
title: title,
|
|
72
|
+
content: content,
|
|
73
|
+
product_option_id: product_option_id,
|
|
74
|
+
option_text: option_text,
|
|
75
|
+
is_secret: is_secret,
|
|
76
|
+
guest_name: guest_name,
|
|
77
|
+
guest_password: guest_password,
|
|
78
|
+
user_id: user_id,
|
|
79
|
+
login_id: login_id
|
|
80
|
+
}.compact
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# 상품문의 수정 (PUT /v1/product-qnas/:id) — 작성 회원 본인 또는 비회원 비밀번호, 답변 전만
|
|
85
|
+
# @comment_by Claude
|
|
86
|
+
# @date: 26-09-29
|
|
87
|
+
# 답변이 달린 문의는 409 PRODUCT_QNA_ALREADY_ANSWERED, 비회원 비밀번호가 틀리면 403 PRODUCT_QNA_GUEST_PASSWORD_INVALID.
|
|
88
|
+
def product_qna_update(product_qna_id:, title: nil, content: nil, is_secret: nil, guest_password: nil,
|
|
89
|
+
user_id: nil, login_id: nil, user_jwt: nil, idempotency_key: nil)
|
|
90
|
+
request(
|
|
91
|
+
uri: "product-qnas/#{product_qna_id}",
|
|
92
|
+
method: :put,
|
|
93
|
+
headers: {
|
|
94
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
95
|
+
'Bootpay-Role' => 'user',
|
|
96
|
+
'Bootpay-User-JWT' => user_jwt
|
|
97
|
+
}.compact,
|
|
98
|
+
payload: {
|
|
99
|
+
title: title,
|
|
100
|
+
content: content,
|
|
101
|
+
is_secret: is_secret,
|
|
102
|
+
guest_password: guest_password,
|
|
103
|
+
user_id: user_id,
|
|
104
|
+
login_id: login_id
|
|
105
|
+
}.compact
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 상품문의 삭제 (DELETE /v1/product-qnas/:id) — 작성 회원 본인·비회원 비밀번호 또는 supervisor
|
|
110
|
+
# @comment_by Claude
|
|
111
|
+
# @date: 26-09-29
|
|
112
|
+
# guest_password 는 URL 쿼리가 아닌 본문으로 보낸다(서버는 둘 다 받는다).
|
|
113
|
+
def product_qna_delete(product_qna_id:, guest_password: nil, user_id: nil, login_id: nil, user_jwt: nil,
|
|
114
|
+
supervisor: false, idempotency_key: nil)
|
|
115
|
+
request(
|
|
116
|
+
uri: "product-qnas/#{product_qna_id}",
|
|
117
|
+
method: :delete,
|
|
118
|
+
headers: {
|
|
119
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
120
|
+
'Bootpay-Role' => supervisor ? 'supervisor' : 'user',
|
|
121
|
+
'Bootpay-User-JWT' => user_jwt
|
|
122
|
+
}.compact,
|
|
123
|
+
payload: {
|
|
124
|
+
guest_password: guest_password,
|
|
125
|
+
user_id: user_id,
|
|
126
|
+
login_id: login_id
|
|
127
|
+
}.compact
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# 상품문의 답변 등록·수정 (PUT /v1/product-qnas/:id/answer) — supervisor 전용
|
|
132
|
+
# @comment_by Claude
|
|
133
|
+
# @date: 26-09-29
|
|
134
|
+
# 답변이 없으면 만들고, 있으면 내용을 바꾼다. 응답은 답변이 반영된 상품문의 객체다.
|
|
135
|
+
def product_qna_answer(product_qna_id:, content:, idempotency_key: nil)
|
|
136
|
+
request(
|
|
137
|
+
uri: "product-qnas/#{product_qna_id}/answer",
|
|
138
|
+
method: :put,
|
|
139
|
+
headers: {
|
|
140
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
141
|
+
'Bootpay-Role' => 'supervisor'
|
|
142
|
+
},
|
|
143
|
+
payload: {
|
|
144
|
+
content: content
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module BootpayStore::Concern::ProductReview
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
# 상품평 목록 (GET /v1/reviews)
|
|
6
|
+
# @comment_by Claude
|
|
7
|
+
# @date: 26-09-29
|
|
8
|
+
# 회원 모드: user_id·login_id·user_jwt 로 정한 회원이 쓴 상품평만 본다.
|
|
9
|
+
# supervisor: true 면 몰 전체 상품평 — 이때 user_id 는 작성 회원 필터(선택)다.
|
|
10
|
+
# 상품 상세의 공개 상품평 목록은 기존 GET /v1/products/:id/reviews 를 쓴다.
|
|
11
|
+
def reviews(page: 1, limit: 20, product_id: nil, view: nil, user_id: nil, login_id: nil, user_jwt: nil,
|
|
12
|
+
supervisor: false, idempotency_key: nil)
|
|
13
|
+
request(
|
|
14
|
+
uri: 'reviews',
|
|
15
|
+
method: :get,
|
|
16
|
+
headers: {
|
|
17
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
18
|
+
'Bootpay-Role' => supervisor ? 'supervisor' : 'user',
|
|
19
|
+
'Bootpay-User-JWT' => user_jwt
|
|
20
|
+
}.compact,
|
|
21
|
+
params: {
|
|
22
|
+
page: page,
|
|
23
|
+
limit: limit,
|
|
24
|
+
product_id: product_id,
|
|
25
|
+
view: view,
|
|
26
|
+
user_id: user_id,
|
|
27
|
+
login_id: login_id
|
|
28
|
+
}.compact
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 상품평 단건 (GET /v1/reviews/:id)
|
|
33
|
+
# @comment_by Claude
|
|
34
|
+
# @date: 26-09-29
|
|
35
|
+
# 공개 상품평은 누구나, 숨김 상품평은 작성 회원 본인 또는 supervisor 만 본다.
|
|
36
|
+
def review_detail(product_review_id:, user_id: nil, login_id: nil, user_jwt: nil, supervisor: false,
|
|
37
|
+
idempotency_key: nil)
|
|
38
|
+
request(
|
|
39
|
+
uri: "reviews/#{product_review_id}",
|
|
40
|
+
method: :get,
|
|
41
|
+
headers: {
|
|
42
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
43
|
+
'Bootpay-Role' => supervisor ? 'supervisor' : 'user',
|
|
44
|
+
'Bootpay-User-JWT' => user_jwt
|
|
45
|
+
}.compact,
|
|
46
|
+
params: {
|
|
47
|
+
user_id: user_id,
|
|
48
|
+
login_id: login_id
|
|
49
|
+
}.compact
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 상품평 작성 (POST /v1/reviews) — 회원 전용
|
|
54
|
+
# @comment_by Claude
|
|
55
|
+
# @date: 26-09-29
|
|
56
|
+
# order_id 는 회원의 구매확정 주문, product_id(·product_option_id)는 그 주문에 담긴 상품이어야 한다.
|
|
57
|
+
# images 는 사진 URL 최대 5개(문자열 또는 { url: } 해시). 파일 업로드는 지원하지 않는다.
|
|
58
|
+
def review_create(order_id:, product_id:, rating:, content:, product_option_id: nil, images: nil,
|
|
59
|
+
user_id: nil, login_id: nil, user_jwt: nil, idempotency_key: nil)
|
|
60
|
+
request(
|
|
61
|
+
uri: 'reviews',
|
|
62
|
+
method: :post,
|
|
63
|
+
headers: {
|
|
64
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
65
|
+
'Bootpay-Role' => 'user',
|
|
66
|
+
'Bootpay-User-JWT' => user_jwt
|
|
67
|
+
}.compact,
|
|
68
|
+
payload: {
|
|
69
|
+
user_id: user_id,
|
|
70
|
+
login_id: login_id,
|
|
71
|
+
order_id: order_id,
|
|
72
|
+
product_id: product_id,
|
|
73
|
+
product_option_id: product_option_id,
|
|
74
|
+
rating: rating,
|
|
75
|
+
content: content,
|
|
76
|
+
images: images
|
|
77
|
+
}.compact
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 상품평 수정 (PUT /v1/reviews/:id) — 작성 회원 본인, 작성 후 7일 이내
|
|
82
|
+
# @comment_by Claude
|
|
83
|
+
# @date: 26-09-29
|
|
84
|
+
# images 는 보내면 통째로 교체한다([] 면 모두 삭제). 7일이 지나면 REVIEW_EDIT_TIME_EXPIRED.
|
|
85
|
+
def review_update(product_review_id:, rating: nil, content: nil, images: nil,
|
|
86
|
+
user_id: nil, login_id: nil, user_jwt: nil, idempotency_key: nil)
|
|
87
|
+
request(
|
|
88
|
+
uri: "reviews/#{product_review_id}",
|
|
89
|
+
method: :put,
|
|
90
|
+
headers: {
|
|
91
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
92
|
+
'Bootpay-Role' => 'user',
|
|
93
|
+
'Bootpay-User-JWT' => user_jwt
|
|
94
|
+
}.compact,
|
|
95
|
+
payload: {
|
|
96
|
+
user_id: user_id,
|
|
97
|
+
login_id: login_id,
|
|
98
|
+
rating: rating,
|
|
99
|
+
content: content,
|
|
100
|
+
images: images
|
|
101
|
+
}.compact
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# 상품평 삭제 (DELETE /v1/reviews/:id) — 작성 회원 본인 또는 supervisor
|
|
106
|
+
# @comment_by Claude
|
|
107
|
+
# @date: 26-09-29
|
|
108
|
+
# 지급된 적립금 회수·상품 통계 차감이 함께 일어난다. reason 은 운영자 모드의 삭제 사유.
|
|
109
|
+
def review_delete(product_review_id:, reason: nil, user_id: nil, login_id: nil, user_jwt: nil,
|
|
110
|
+
supervisor: false, idempotency_key: nil)
|
|
111
|
+
request(
|
|
112
|
+
uri: "reviews/#{product_review_id}",
|
|
113
|
+
method: :delete,
|
|
114
|
+
headers: {
|
|
115
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
116
|
+
'Bootpay-Role' => supervisor ? 'supervisor' : 'user',
|
|
117
|
+
'Bootpay-User-JWT' => user_jwt
|
|
118
|
+
}.compact,
|
|
119
|
+
params: {
|
|
120
|
+
user_id: user_id,
|
|
121
|
+
login_id: login_id,
|
|
122
|
+
reason: reason
|
|
123
|
+
}.compact
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# 상품평 판매자 답글 등록·수정 (PUT /v1/reviews/:id/reply) — supervisor 전용
|
|
128
|
+
# @comment_by Claude
|
|
129
|
+
# @date: 26-09-29
|
|
130
|
+
# 답글이 없으면 만들고, 있으면 내용을 바꾼다(10자 이상). 응답은 답글이 반영된 상품평 객체다.
|
|
131
|
+
def review_reply(product_review_id:, content:, idempotency_key: nil)
|
|
132
|
+
request(
|
|
133
|
+
uri: "reviews/#{product_review_id}/reply",
|
|
134
|
+
method: :put,
|
|
135
|
+
headers: {
|
|
136
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid,
|
|
137
|
+
'Bootpay-Role' => 'supervisor'
|
|
138
|
+
},
|
|
139
|
+
payload: {
|
|
140
|
+
content: content
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module BootpayStore::Concern::Rest
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# 요청 경로에 맞는 API 기본 주소.
|
|
8
|
+
# @comment_by Claude (alfred)
|
|
9
|
+
# @date: 26-09-18
|
|
10
|
+
# 알림톡 API 는 26-09-18 부터 커머스 API(api.bootapi.com/v1)가 아니라 메시지 API(message.bootapi.com, /v1 없음)가 받는다.
|
|
11
|
+
# 경로·파라미터·응답은 그대로이고 호스트만 다르다 — 그래서 알림톡 메서드는 고치지 않고 여기서 주소만 가른다.
|
|
12
|
+
# 옛 주소(/alimtalk/*)는 410 으로 응답한다.
|
|
13
|
+
def api_base_url(uri)
|
|
14
|
+
table = uri.to_s.sub(%r{\A/}, '').start_with?('alimtalk') ? BootpayStore::RestClient::MESSAGE_API : BootpayStore::RestClient::API
|
|
15
|
+
table[@mode.to_sym]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# HTTP Request 기본 Method
|
|
19
|
+
# Comment by Gosomi
|
|
20
|
+
# Date: 2021-05-21
|
|
21
|
+
def request(method: :post, uri:, payload: {}, headers: {}, params: nil)
|
|
22
|
+
response = HTTP.headers(
|
|
23
|
+
{
|
|
24
|
+
Authorization: @token.present? ? "Bearer #{@token}" : "Basic #{basic_authentification}",
|
|
25
|
+
content_type: 'application/json',
|
|
26
|
+
accept: 'application/json',
|
|
27
|
+
bootpay_api_version: @api_version,
|
|
28
|
+
bootpay_sdk_version: Bootpay::V2_VERSION,
|
|
29
|
+
bootpay_sdk_type: '300'
|
|
30
|
+
}.merge!(headers).compact
|
|
31
|
+
).send(
|
|
32
|
+
method.to_sym,
|
|
33
|
+
[api_base_url(uri), uri].join('/'),
|
|
34
|
+
json: payload,
|
|
35
|
+
params: params
|
|
36
|
+
)
|
|
37
|
+
BootpayStore::Response.new(
|
|
38
|
+
response.status.to_i == 200,
|
|
39
|
+
JSON.parse(response.body.to_s, symbolize_names: true)
|
|
40
|
+
)
|
|
41
|
+
rescue Exception => e
|
|
42
|
+
BootpayStore::Response.new(
|
|
43
|
+
false,
|
|
44
|
+
message: "부트페이 API 서버와의 통신이 실패하였습니다. 오류 메세지: #{e.message}",
|
|
45
|
+
backtrace: e.backtrace.join("\n")
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# JSON 이 아닌 본문(CSV 등)을 파싱하지 않고 그대로 받는다.
|
|
50
|
+
# @comment_by Claude (alfred)
|
|
51
|
+
# @date: 26-08-27
|
|
52
|
+
# ⚠️ 공용 request 는 응답을 무조건 JSON.parse 하므로, CSV 를 돌려주는 엔드포인트
|
|
53
|
+
# (알림톡 템플릿 내보내기 format=csv)에서는 파싱 실패가 rescue 로 흘러
|
|
54
|
+
# **"부트페이 API 서버와의 통신이 실패하였습니다"** 라는 틀린 메시지로 보고된다.
|
|
55
|
+
# 실제로는 성공한 요청이라 그대로 두면 원인을 못 찾는다. 그래서 원문 경로를 따로 둔다.
|
|
56
|
+
# 성공 시 data 는 { body: '<원문 문자열>', content_type: '...' } 다.
|
|
57
|
+
def request_raw(uri:, method: :get, params: nil, headers: {})
|
|
58
|
+
response = HTTP.headers(
|
|
59
|
+
{
|
|
60
|
+
Authorization: @token.present? ? "Bearer #{@token}" : "Basic #{basic_authentification}",
|
|
61
|
+
accept: '*/*',
|
|
62
|
+
bootpay_api_version: @api_version,
|
|
63
|
+
bootpay_sdk_version: Bootpay::V2_VERSION,
|
|
64
|
+
bootpay_sdk_type: '300'
|
|
65
|
+
}.merge!(headers).compact
|
|
66
|
+
).send(
|
|
67
|
+
method.to_sym,
|
|
68
|
+
[api_base_url(uri), uri].join('/'),
|
|
69
|
+
params: params
|
|
70
|
+
)
|
|
71
|
+
BootpayStore::Response.new(
|
|
72
|
+
response.status.to_i == 200,
|
|
73
|
+
body: response.body.to_s,
|
|
74
|
+
content_type: response.headers['Content-Type'].to_s
|
|
75
|
+
)
|
|
76
|
+
rescue Exception => e
|
|
77
|
+
BootpayStore::Response.new(
|
|
78
|
+
false,
|
|
79
|
+
message: "부트페이 API 서버와의 통신이 실패하였습니다. 오류 메세지: #{e.message}",
|
|
80
|
+
backtrace: e.backtrace.join("\n")
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# multipart/form-data 전송 (파일 업로드용)
|
|
85
|
+
# @comment_by Claude (alfred)
|
|
86
|
+
# @date: 26-08-14
|
|
87
|
+
# ⚠️ content_type 을 명시하지 않는다. 명시하면 HTTP.rb 가 붙이는 boundary 가 사라져 본문이 깨진다.
|
|
88
|
+
# (Node SDK 는 인터셉터가 Content-Type 을 덮어써 본문이 null 로 전송되는 버그가 있다 — 같은 함정)
|
|
89
|
+
# 기존 request 는 json: 고정이라 손대지 않고 별도 메서드로 둔다.
|
|
90
|
+
def post_multipart(uri:, form: {}, headers: {})
|
|
91
|
+
response = HTTP.headers(
|
|
92
|
+
{
|
|
93
|
+
Authorization: @token.present? ? "Bearer #{@token}" : "Basic #{basic_authentification}",
|
|
94
|
+
accept: 'application/json',
|
|
95
|
+
bootpay_api_version: @api_version,
|
|
96
|
+
bootpay_sdk_version: Bootpay::V2_VERSION,
|
|
97
|
+
bootpay_sdk_type: '300'
|
|
98
|
+
}.merge!(headers).compact
|
|
99
|
+
).post(
|
|
100
|
+
[api_base_url(uri), uri].join('/'),
|
|
101
|
+
form: form
|
|
102
|
+
)
|
|
103
|
+
BootpayStore::Response.new(
|
|
104
|
+
response.status.to_i == 200,
|
|
105
|
+
JSON.parse(response.body.to_s, symbolize_names: true)
|
|
106
|
+
)
|
|
107
|
+
rescue Exception => e
|
|
108
|
+
BootpayStore::Response.new(
|
|
109
|
+
false,
|
|
110
|
+
message: "부트페이 API 서버와의 통신이 실패하였습니다. 오류 메세지: #{e.message}",
|
|
111
|
+
backtrace: e.backtrace.join("\n")
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# multipart form 값 정규화 — 파일은 FormData::File 로, 나머지는 문자열로.
|
|
116
|
+
# 파일은 경로(String)·IO·HTTP::FormData::File 셋 다 받는다.
|
|
117
|
+
# @comment_by Claude (alfred)
|
|
118
|
+
# @date: 26-08-14
|
|
119
|
+
def multipart_file(value)
|
|
120
|
+
return value if value.is_a?(HTTP::FormData::File)
|
|
121
|
+
HTTP::FormData::File.new(value)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def multipart_value(value)
|
|
125
|
+
case value
|
|
126
|
+
when Array, Hash then value.to_json
|
|
127
|
+
when TrueClass, FalseClass then value.to_s
|
|
128
|
+
else value.to_s
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# basic authenticate 추가
|
|
133
|
+
# Comment by GOSOMI
|
|
134
|
+
# @date: 2026-02-20
|
|
135
|
+
def basic_authentification
|
|
136
|
+
Base64.strict_encode64("#{@client_key}:#{@secret_key}")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module BootpayStore::Concern::Store
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
# 가맹점 기본 정보를 조회한다 (/v1/store)
|
|
6
|
+
# Comment by Codex
|
|
7
|
+
# @date: 2026-02-23
|
|
8
|
+
def get_store(idempotency_key: nil)
|
|
9
|
+
request(
|
|
10
|
+
uri: 'store',
|
|
11
|
+
method: :get,
|
|
12
|
+
headers: {
|
|
13
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 가맹점 상세 정보를 조회한다 (/v1/store/detail)
|
|
19
|
+
# Comment by Codex
|
|
20
|
+
# @date: 2026-02-23
|
|
21
|
+
def get_store_detail(idempotency_key: nil)
|
|
22
|
+
request(
|
|
23
|
+
uri: 'store/detail',
|
|
24
|
+
method: :get,
|
|
25
|
+
headers: {
|
|
26
|
+
'Idempotency-Key' => idempotency_key.presence || SecureRandom.uuid
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|